diff --git a/packages/backend/drizzle/0002_youthful_storm.sql b/packages/backend/drizzle/0002_youthful_storm.sql new file mode 100644 index 0000000..562782a --- /dev/null +++ b/packages/backend/drizzle/0002_youthful_storm.sql @@ -0,0 +1,22 @@ +CREATE TABLE `lesson_subscriptions` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` integer NOT NULL, + `lesson_id` integer NOT NULL, + `created_at` integer DEFAULT (unixepoch()) NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`lesson_id`) REFERENCES `lessons`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +ALTER TABLE `card_progress` ADD `user_id` integer REFERENCES users(id);--> statement-breakpoint +ALTER TABLE `lessons` ADD `owner_id` integer REFERENCES users(id);--> statement-breakpoint +ALTER TABLE `lessons` ADD `visibility` text DEFAULT 'private' NOT NULL;--> statement-breakpoint +ALTER TABLE `lessons` ADD `is_curated` integer DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE `lessons` ADD `source_lesson_id` integer REFERENCES lessons(id);--> statement-breakpoint +ALTER TABLE `sessions` ADD `user_id` integer REFERENCES users(id);--> statement-breakpoint +CREATE INDEX `lesson_subscriptions_user_idx` ON `lesson_subscriptions` (`user_id`);--> statement-breakpoint +CREATE INDEX `lesson_subscriptions_lesson_idx` ON `lesson_subscriptions` (`lesson_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `lesson_subscriptions_user_lesson_unique` ON `lesson_subscriptions` (`user_id`,`lesson_id`);--> statement-breakpoint +CREATE INDEX `card_progress_user_idx` ON `card_progress` (`user_id`,`next_due_at`);--> statement-breakpoint +CREATE INDEX `lessons_owner_idx` ON `lessons` (`owner_id`);--> statement-breakpoint +CREATE INDEX `lessons_visibility_idx` ON `lessons` (`visibility`,`is_curated`);--> statement-breakpoint +CREATE INDEX `sessions_user_idx` ON `sessions` (`user_id`,`status`); \ No newline at end of file diff --git a/packages/backend/drizzle/meta/0002_snapshot.json b/packages/backend/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..a6e2834 --- /dev/null +++ b/packages/backend/drizzle/meta/0002_snapshot.json @@ -0,0 +1,975 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "763fe305-72a6-40e2-bc36-ed330b403044", + "prevId": "87d22455-00f9-454b-baac-bc7638274645", + "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": {} + }, + "auth_tokens": { + "name": "auth_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "purpose": { + "name": "purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "auth_tokens_hash_idx": { + "name": "auth_tokens_hash_idx", + "columns": [ + "token_hash" + ], + "isUnique": false + }, + "auth_tokens_user_purpose_idx": { + "name": "auth_tokens_user_purpose_idx", + "columns": [ + "user_id", + "purpose" + ], + "isUnique": false + } + }, + "foreignKeys": { + "auth_tokens_user_id_users_id_fk": { + "name": "auth_tokens_user_id_users_id_fk", + "tableFrom": "auth_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_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 + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "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 + }, + "card_progress_user_idx": { + "name": "card_progress_user_idx", + "columns": [ + "user_id", + "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" + }, + "card_progress_user_id_users_id_fk": { + "name": "card_progress_user_id_users_id_fk", + "tableFrom": "card_progress", + "tableTo": "users", + "columnsFrom": [ + "user_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": {} + }, + "lesson_subscriptions": { + "name": "lesson_subscriptions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lesson_id": { + "name": "lesson_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "lesson_subscriptions_user_idx": { + "name": "lesson_subscriptions_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "lesson_subscriptions_lesson_idx": { + "name": "lesson_subscriptions_lesson_idx", + "columns": [ + "lesson_id" + ], + "isUnique": false + }, + "lesson_subscriptions_user_lesson_unique": { + "name": "lesson_subscriptions_user_lesson_unique", + "columns": [ + "user_id", + "lesson_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "lesson_subscriptions_user_id_users_id_fk": { + "name": "lesson_subscriptions_user_id_users_id_fk", + "tableFrom": "lesson_subscriptions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "lesson_subscriptions_lesson_id_lessons_id_fk": { + "name": "lesson_subscriptions_lesson_id_lessons_id_fk", + "tableFrom": "lesson_subscriptions", + "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 + }, + "owner_id": { + "name": "owner_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'private'" + }, + "is_curated": { + "name": "is_curated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "source_lesson_id": { + "name": "source_lesson_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": 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": { + "lessons_owner_idx": { + "name": "lessons_owner_idx", + "columns": [ + "owner_id" + ], + "isUnique": false + }, + "lessons_visibility_idx": { + "name": "lessons_visibility_idx", + "columns": [ + "visibility", + "is_curated" + ], + "isUnique": false + } + }, + "foreignKeys": { + "lessons_owner_id_users_id_fk": { + "name": "lessons_owner_id_users_id_fk", + "tableFrom": "lessons", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "lessons_source_lesson_id_lessons_id_fk": { + "name": "lessons_source_lesson_id_lessons_id_fk", + "tableFrom": "lessons", + "tableTo": "lessons", + "columnsFrom": [ + "source_lesson_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "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 + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sessions_status_idx": { + "name": "sessions_status_idx", + "columns": [ + "status" + ], + "isUnique": false + }, + "sessions_user_idx": { + "name": "sessions_user_idx", + "columns": [ + "user_id", + "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" + }, + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "sessions_auth": { + "name": "sessions_auth", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sessions_auth_user_idx": { + "name": "sessions_auth_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "sessions_auth_expires_idx": { + "name": "sessions_auth_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_auth_user_id_users_id_fk": { + "name": "sessions_auth_user_id_users_id_fk", + "tableFrom": "sessions_auth", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "email_verified_at": { + "name": "email_verified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pending_email": { + "name": "pending_email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": 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": { + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ], + "isUnique": true + }, + "users_email_idx": { + "name": "users_email_idx", + "columns": [ + "email" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/backend/drizzle/meta/_journal.json b/packages/backend/drizzle/meta/_journal.json index 76e9881..6ef14e4 100644 --- a/packages/backend/drizzle/meta/_journal.json +++ b/packages/backend/drizzle/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1779309705807, "tag": "0001_fuzzy_silhouette", "breakpoints": true + }, + { + "idx": 2, + "version": "6", + "when": 1779314536550, + "tag": "0002_youthful_storm", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/backend/src/db/schema.ts b/packages/backend/src/db/schema.ts index 173e5a5..0215794 100644 --- a/packages/backend/src/db/schema.ts +++ b/packages/backend/src/db/schema.ts @@ -1,16 +1,28 @@ import { sql } from 'drizzle-orm'; -import { integer, sqliteTable, text, index } from 'drizzle-orm/sqlite-core'; +import type { AnySQLiteColumn } from 'drizzle-orm/sqlite-core'; +import { integer, sqliteTable, text, index, uniqueIndex } 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 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), + ownerId: integer('owner_id').references(() => users.id, { onDelete: 'cascade' }), + visibility: text('visibility', { enum: ['private', 'shared'] }).notNull().default('private'), + isCurated: integer('is_curated', { mode: 'boolean' }).notNull().default(false), + sourceLessonId: integer('source_lesson_id').references((): AnySQLiteColumn => lessons.id, { onDelete: 'set null' }), + createdAt: integer('created_at').notNull().default(sql`(unixepoch())`), + updatedAt: integer('updated_at').notNull().default(sql`(unixepoch())`), + }, + (t) => ({ + ownerIdx: index('lessons_owner_idx').on(t.ownerId), + visibilityIdx: index('lessons_visibility_idx').on(t.visibility, t.isCurated), + }) +); export const cards = sqliteTable( 'cards', @@ -37,10 +49,12 @@ export const cardProgress = sqliteTable( incorrectCount: integer('incorrect_count').notNull().default(0), lastShownAt: integer('last_shown_at'), nextDueAt: integer('next_due_at').notNull().default(0), + userId: integer('user_id').references(() => users.id, { onDelete: 'cascade' }), }, (t) => ({ pk: index('card_progress_pk').on(t.cardId, t.direction), dueIdx: index('card_progress_due_idx').on(t.nextDueAt), + userIdx: index('card_progress_user_idx').on(t.userId, t.nextDueAt), }) ); @@ -57,8 +71,12 @@ export const sessions = sqliteTable( cardsIncorrect: integer('cards_incorrect').notNull().default(0), status: text('status', { enum: ['active', 'completed', 'abandoned'] }).notNull().default('active'), queueSnapshot: text('queue_snapshot'), + userId: integer('user_id').references(() => users.id, { onDelete: 'cascade' }), }, - (t) => ({ statusIdx: index('sessions_status_idx').on(t.status) }) + (t) => ({ + statusIdx: index('sessions_status_idx').on(t.status), + userIdx: index('sessions_user_idx').on(t.userId, t.status), + }) ); export const attempts = sqliteTable( @@ -130,6 +148,23 @@ export const authTokens = sqliteTable( }) ); +export const lessonSubscriptions = sqliteTable( + 'lesson_subscriptions', + { + id: integer('id').primaryKey({ autoIncrement: true }), + userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), + lessonId: integer('lesson_id').notNull().references(() => lessons.id, { onDelete: 'cascade' }), + createdAt: integer('created_at').notNull().default(sql`(unixepoch())`), + }, + (t) => ({ + userIdx: index('lesson_subscriptions_user_idx').on(t.userId), + lessonIdx: index('lesson_subscriptions_lesson_idx').on(t.lessonId), + userLessonUnique: uniqueIndex('lesson_subscriptions_user_lesson_unique').on(t.userId, t.lessonId), + }) +); + +export type LessonSubscriptionRow = typeof lessonSubscriptions.$inferSelect; + export type LessonRow = typeof lessons.$inferSelect; export type CardRow = typeof cards.$inferSelect; export type CardProgressRow = typeof cardProgress.$inferSelect;