feat(db): ownership columns and lesson_subscriptions table

This commit is contained in:
2026-05-21 00:03:24 +02:00
parent 18fadc44f3
commit a872560477
4 changed files with 1051 additions and 12 deletions

View File

@@ -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;