feat(db): drizzle schema, migrations, and seed
This commit is contained in:
10
packages/backend/drizzle.config.ts
Normal file
10
packages/backend/drizzle.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'drizzle-kit';
|
||||
|
||||
export default defineConfig({
|
||||
schema: './src/db/schema.ts',
|
||||
out: './drizzle',
|
||||
dialect: 'sqlite',
|
||||
dbCredentials: {
|
||||
url: process.env.DB_PATH ?? '../../data/flashcard.db',
|
||||
},
|
||||
});
|
||||
66
packages/backend/drizzle/0000_exotic_wrecker.sql
Normal file
66
packages/backend/drizzle/0000_exotic_wrecker.sql
Normal file
@@ -0,0 +1,66 @@
|
||||
CREATE TABLE `attempts` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`session_id` integer NOT NULL,
|
||||
`card_id` integer NOT NULL,
|
||||
`direction` text NOT NULL,
|
||||
`shown_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||
`result` text NOT NULL,
|
||||
`time_to_answer_ms` integer,
|
||||
FOREIGN KEY (`session_id`) REFERENCES `sessions`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`card_id`) REFERENCES `cards`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `card_progress` (
|
||||
`card_id` integer NOT NULL,
|
||||
`direction` text NOT NULL,
|
||||
`box` integer DEFAULT 1 NOT NULL,
|
||||
`correct_count` integer DEFAULT 0 NOT NULL,
|
||||
`incorrect_count` integer DEFAULT 0 NOT NULL,
|
||||
`last_shown_at` integer,
|
||||
`next_due_at` integer DEFAULT 0 NOT NULL,
|
||||
FOREIGN KEY (`card_id`) REFERENCES `cards`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `cards` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`lesson_id` integer NOT NULL,
|
||||
`question` text NOT NULL,
|
||||
`answer` text NOT NULL,
|
||||
`hint` text,
|
||||
`position` integer DEFAULT 0 NOT NULL,
|
||||
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||
`updated_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||
FOREIGN KEY (`lesson_id`) REFERENCES `lessons`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `lessons` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`parent_id` integer,
|
||||
`name` text NOT NULL,
|
||||
`description` text,
|
||||
`position` integer DEFAULT 0 NOT NULL,
|
||||
`bidirectional` integer DEFAULT false NOT NULL,
|
||||
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||
`updated_at` integer DEFAULT (unixepoch()) NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `sessions` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`lesson_id` integer NOT NULL,
|
||||
`started_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||
`ended_at` integer,
|
||||
`duration_seconds` integer,
|
||||
`cards_shown` integer DEFAULT 0 NOT NULL,
|
||||
`cards_correct` integer DEFAULT 0 NOT NULL,
|
||||
`cards_incorrect` integer DEFAULT 0 NOT NULL,
|
||||
`status` text DEFAULT 'active' NOT NULL,
|
||||
`queue_snapshot` text,
|
||||
FOREIGN KEY (`lesson_id`) REFERENCES `lessons`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `attempts_session_idx` ON `attempts` (`session_id`);--> statement-breakpoint
|
||||
CREATE INDEX `attempts_card_idx` ON `attempts` (`card_id`);--> statement-breakpoint
|
||||
CREATE INDEX `card_progress_pk` ON `card_progress` (`card_id`,`direction`);--> statement-breakpoint
|
||||
CREATE INDEX `card_progress_due_idx` ON `card_progress` (`next_due_at`);--> statement-breakpoint
|
||||
CREATE INDEX `cards_lesson_idx` ON `cards` (`lesson_id`);--> statement-breakpoint
|
||||
CREATE INDEX `sessions_status_idx` ON `sessions` (`status`);
|
||||
475
packages/backend/drizzle/meta/0000_snapshot.json
Normal file
475
packages/backend/drizzle/meta/0000_snapshot.json
Normal file
@@ -0,0 +1,475 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "00509e45-a6d9-417d-b3c0-a7e936e7001f",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"attempts": {
|
||||
"name": "attempts",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"session_id": {
|
||||
"name": "session_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"card_id": {
|
||||
"name": "card_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"direction": {
|
||||
"name": "direction",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"shown_at": {
|
||||
"name": "shown_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch())"
|
||||
},
|
||||
"result": {
|
||||
"name": "result",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_to_answer_ms": {
|
||||
"name": "time_to_answer_ms",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"attempts_session_idx": {
|
||||
"name": "attempts_session_idx",
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"attempts_card_idx": {
|
||||
"name": "attempts_card_idx",
|
||||
"columns": [
|
||||
"card_id"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"attempts_session_id_sessions_id_fk": {
|
||||
"name": "attempts_session_id_sessions_id_fk",
|
||||
"tableFrom": "attempts",
|
||||
"tableTo": "sessions",
|
||||
"columnsFrom": [
|
||||
"session_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"attempts_card_id_cards_id_fk": {
|
||||
"name": "attempts_card_id_cards_id_fk",
|
||||
"tableFrom": "attempts",
|
||||
"tableTo": "cards",
|
||||
"columnsFrom": [
|
||||
"card_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"card_progress": {
|
||||
"name": "card_progress",
|
||||
"columns": {
|
||||
"card_id": {
|
||||
"name": "card_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"direction": {
|
||||
"name": "direction",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"box": {
|
||||
"name": "box",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 1
|
||||
},
|
||||
"correct_count": {
|
||||
"name": "correct_count",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"incorrect_count": {
|
||||
"name": "incorrect_count",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"last_shown_at": {
|
||||
"name": "last_shown_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"next_due_at": {
|
||||
"name": "next_due_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"card_progress_pk": {
|
||||
"name": "card_progress_pk",
|
||||
"columns": [
|
||||
"card_id",
|
||||
"direction"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"card_progress_due_idx": {
|
||||
"name": "card_progress_due_idx",
|
||||
"columns": [
|
||||
"next_due_at"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"card_progress_card_id_cards_id_fk": {
|
||||
"name": "card_progress_card_id_cards_id_fk",
|
||||
"tableFrom": "card_progress",
|
||||
"tableTo": "cards",
|
||||
"columnsFrom": [
|
||||
"card_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"cards": {
|
||||
"name": "cards",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"lesson_id": {
|
||||
"name": "lesson_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"question": {
|
||||
"name": "question",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"answer": {
|
||||
"name": "answer",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"hint": {
|
||||
"name": "hint",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"position": {
|
||||
"name": "position",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch())"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch())"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"cards_lesson_idx": {
|
||||
"name": "cards_lesson_idx",
|
||||
"columns": [
|
||||
"lesson_id"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"cards_lesson_id_lessons_id_fk": {
|
||||
"name": "cards_lesson_id_lessons_id_fk",
|
||||
"tableFrom": "cards",
|
||||
"tableTo": "lessons",
|
||||
"columnsFrom": [
|
||||
"lesson_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"lessons": {
|
||||
"name": "lessons",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"parent_id": {
|
||||
"name": "parent_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"position": {
|
||||
"name": "position",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"bidirectional": {
|
||||
"name": "bidirectional",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch())"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"sessions": {
|
||||
"name": "sessions",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"lesson_id": {
|
||||
"name": "lesson_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"started_at": {
|
||||
"name": "started_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch())"
|
||||
},
|
||||
"ended_at": {
|
||||
"name": "ended_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"duration_seconds": {
|
||||
"name": "duration_seconds",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"cards_shown": {
|
||||
"name": "cards_shown",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"cards_correct": {
|
||||
"name": "cards_correct",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"cards_incorrect": {
|
||||
"name": "cards_incorrect",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'active'"
|
||||
},
|
||||
"queue_snapshot": {
|
||||
"name": "queue_snapshot",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"sessions_status_idx": {
|
||||
"name": "sessions_status_idx",
|
||||
"columns": [
|
||||
"status"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"sessions_lesson_id_lessons_id_fk": {
|
||||
"name": "sessions_lesson_id_lessons_id_fk",
|
||||
"tableFrom": "sessions",
|
||||
"tableTo": "lessons",
|
||||
"columnsFrom": [
|
||||
"lesson_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
13
packages/backend/drizzle/meta/_journal.json
Normal file
13
packages/backend/drizzle/meta/_journal.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1779302371078,
|
||||
"tag": "0000_exotic_wrecker",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
19
packages/backend/src/db/client.ts
Normal file
19
packages/backend/src/db/client.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import { drizzle, type BetterSQLite3Database } from 'drizzle-orm/better-sqlite3';
|
||||
import { mkdirSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import * as schema from './schema.js';
|
||||
|
||||
export type Db = BetterSQLite3Database<typeof schema>;
|
||||
|
||||
export function createDb(dbPath?: string): { db: Db; sqlite: Database.Database } {
|
||||
const path = dbPath ?? process.env.DB_PATH ?? resolve(process.cwd(), '../../data/flashcard.db');
|
||||
if (path !== ':memory:') {
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
}
|
||||
const sqlite = new Database(path);
|
||||
sqlite.pragma('journal_mode = WAL');
|
||||
sqlite.pragma('foreign_keys = ON');
|
||||
const db = drizzle(sqlite, { schema });
|
||||
return { db, sqlite };
|
||||
}
|
||||
8
packages/backend/src/db/migrate.ts
Normal file
8
packages/backend/src/db/migrate.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { migrate } from 'drizzle-orm/better-sqlite3/migrator';
|
||||
import { resolve } from 'node:path';
|
||||
import { createDb } from './client.js';
|
||||
|
||||
const { db, sqlite } = createDb();
|
||||
migrate(db, { migrationsFolder: resolve(import.meta.dirname, '../../drizzle') });
|
||||
sqlite.close();
|
||||
console.log('Migrations applied.');
|
||||
85
packages/backend/src/db/schema.ts
Normal file
85
packages/backend/src/db/schema.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { integer, sqliteTable, text, index } from 'drizzle-orm/sqlite-core';
|
||||
|
||||
export const lessons = sqliteTable('lessons', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
parentId: integer('parent_id'),
|
||||
name: text('name').notNull(),
|
||||
description: text('description'),
|
||||
position: integer('position').notNull().default(0),
|
||||
bidirectional: integer('bidirectional', { mode: 'boolean' }).notNull().default(false),
|
||||
createdAt: integer('created_at').notNull().default(sql`(unixepoch())`),
|
||||
updatedAt: integer('updated_at').notNull().default(sql`(unixepoch())`),
|
||||
});
|
||||
|
||||
export const cards = sqliteTable(
|
||||
'cards',
|
||||
{
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
lessonId: integer('lesson_id').notNull().references(() => lessons.id, { onDelete: 'cascade' }),
|
||||
question: text('question').notNull(),
|
||||
answer: text('answer').notNull(),
|
||||
hint: text('hint'),
|
||||
position: integer('position').notNull().default(0),
|
||||
createdAt: integer('created_at').notNull().default(sql`(unixepoch())`),
|
||||
updatedAt: integer('updated_at').notNull().default(sql`(unixepoch())`),
|
||||
},
|
||||
(t) => ({ lessonIdx: index('cards_lesson_idx').on(t.lessonId) })
|
||||
);
|
||||
|
||||
export const cardProgress = sqliteTable(
|
||||
'card_progress',
|
||||
{
|
||||
cardId: integer('card_id').notNull().references(() => cards.id, { onDelete: 'cascade' }),
|
||||
direction: text('direction', { enum: ['forward', 'backward'] }).notNull(),
|
||||
box: integer('box').notNull().default(1),
|
||||
correctCount: integer('correct_count').notNull().default(0),
|
||||
incorrectCount: integer('incorrect_count').notNull().default(0),
|
||||
lastShownAt: integer('last_shown_at'),
|
||||
nextDueAt: integer('next_due_at').notNull().default(0),
|
||||
},
|
||||
(t) => ({
|
||||
pk: index('card_progress_pk').on(t.cardId, t.direction),
|
||||
dueIdx: index('card_progress_due_idx').on(t.nextDueAt),
|
||||
})
|
||||
);
|
||||
|
||||
export const sessions = sqliteTable(
|
||||
'sessions',
|
||||
{
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
lessonId: integer('lesson_id').notNull().references(() => lessons.id, { onDelete: 'cascade' }),
|
||||
startedAt: integer('started_at').notNull().default(sql`(unixepoch())`),
|
||||
endedAt: integer('ended_at'),
|
||||
durationSeconds: integer('duration_seconds'),
|
||||
cardsShown: integer('cards_shown').notNull().default(0),
|
||||
cardsCorrect: integer('cards_correct').notNull().default(0),
|
||||
cardsIncorrect: integer('cards_incorrect').notNull().default(0),
|
||||
status: text('status', { enum: ['active', 'completed', 'abandoned'] }).notNull().default('active'),
|
||||
queueSnapshot: text('queue_snapshot'),
|
||||
},
|
||||
(t) => ({ statusIdx: index('sessions_status_idx').on(t.status) })
|
||||
);
|
||||
|
||||
export const attempts = sqliteTable(
|
||||
'attempts',
|
||||
{
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
sessionId: integer('session_id').notNull().references(() => sessions.id, { onDelete: 'cascade' }),
|
||||
cardId: integer('card_id').notNull().references(() => cards.id, { onDelete: 'cascade' }),
|
||||
direction: text('direction', { enum: ['forward', 'backward'] }).notNull(),
|
||||
shownAt: integer('shown_at').notNull().default(sql`(unixepoch())`),
|
||||
result: text('result', { enum: ['correct', 'incorrect'] }).notNull(),
|
||||
timeToAnswerMs: integer('time_to_answer_ms'),
|
||||
},
|
||||
(t) => ({
|
||||
sessionIdx: index('attempts_session_idx').on(t.sessionId),
|
||||
cardIdx: index('attempts_card_idx').on(t.cardId),
|
||||
})
|
||||
);
|
||||
|
||||
export type LessonRow = typeof lessons.$inferSelect;
|
||||
export type CardRow = typeof cards.$inferSelect;
|
||||
export type CardProgressRow = typeof cardProgress.$inferSelect;
|
||||
export type SessionRow = typeof sessions.$inferSelect;
|
||||
export type AttemptRow = typeof attempts.$inferSelect;
|
||||
13
packages/backend/src/db/seed.ts
Normal file
13
packages/backend/src/db/seed.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createDb } from './client.js';
|
||||
import { cards, lessons } from './schema.js';
|
||||
|
||||
const { db, sqlite } = createDb();
|
||||
const [root] = db.insert(lessons).values({ name: 'Demo: Spaans', position: 0 }).returning().all();
|
||||
const [sub] = db.insert(lessons).values({ name: 'Begroetingen', parentId: root.id, position: 0 }).returning().all();
|
||||
db.insert(cards).values([
|
||||
{ lessonId: sub.id, question: 'Hallo', answer: 'Hola', position: 0 },
|
||||
{ lessonId: sub.id, question: 'Goedemorgen', answer: 'Buenos días', position: 1 },
|
||||
{ lessonId: sub.id, question: 'Tot ziens', answer: 'Adiós', position: 2 },
|
||||
]).run();
|
||||
sqlite.close();
|
||||
console.log('Seed inserted.');
|
||||
Reference in New Issue
Block a user