import { bigint, boolean, date, doublePrecision, foreignKey, index, integer, json, jsonb, pgEnum, pgTable, pgView, serial, text, timestamp, unique, uniqueIndex, uuid, varchar } from 'drizzle-orm/pg-core'; import type { AnswerData } from '@cio/question-types'; import { sql } from 'drizzle-orm'; export const courseType = pgEnum('COURSE_TYPE', ['SELF_PACED', 'LIVE_CLASS', 'COMPLIANCE', 'PUBLIC']); export const locale = pgEnum('LOCALE', ['en', 'hi', 'fr', 'pt', 'de', 'vi', 'ru', 'es', 'pl', 'da']); export const plan = pgEnum('PLAN', ['EARLY_ADOPTER', 'ENTERPRISE', 'BASIC']); export const courseImportSourceType = pgEnum('COURSE_IMPORT_SOURCE_TYPE', ['prompt', 'pdf', 'course']); export const courseImportDraftStatus = pgEnum('COURSE_IMPORT_DRAFT_STATUS', ['DRAFT', 'PUBLISHED', 'ARCHIVED']); export const organizationApiKeyType = pgEnum('ORGANIZATION_API_KEY_TYPE', ['mcp', 'api', 'zapier']); export const automationUsageCategory = pgEnum('AUTOMATION_USAGE_CATEGORY', ['read', 'write', 'publish']); export const widgetStatus = pgEnum('WIDGET_STATUS', ['DRAFT', 'PUBLISHED', 'ARCHIVED']); export const widgetLayoutType = pgEnum('WIDGET_LAYOUT_TYPE', [ 'card_grid', 'tag_filter', 'carousel', 'primary_course', 'compact_list', 'editorial_spotlight', 'category_shelf' ]); export const widgetSelectionMode = pgEnum('WIDGET_SELECTION_MODE', ['manual', 'published']); export const courseInviteEventType = pgEnum('COURSE_INVITE_EVENT_TYPE', [ 'CREATED', 'REVOKED', 'PREVIEWED', 'ACCEPTED', 'EMAIL_SENT', 'EMAIL_FAILED', 'ABUSE_BLOCKED' ]); export const organizationInviteEventType = pgEnum('ORGANIZATION_INVITE_EVENT_TYPE', [ 'CREATED', 'REVOKED', 'PREVIEWED', 'ACCEPTED', 'EMAIL_SENT', 'EMAIL_FAILED', 'ABUSE_BLOCKED' ]); export const user = pgTable('user', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), name: text('name').notNull(), email: text('email').notNull().unique(), emailVerified: boolean('email_verified').default(false).notNull(), image: text('image'), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at') .defaultNow() .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull(), role: text('role'), banned: boolean('banned').default(false), banReason: text('ban_reason'), banExpires: timestamp('ban_expires'), isAnonymous: boolean('is_anonymous') }); export const ssoProvider = pgTable('sso_provider', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), issuer: text('issuer').notNull(), oidcConfig: text('oidc_config'), samlConfig: text('saml_config'), userId: uuid('user_id') .notNull() .references(() => user.id, { onDelete: 'cascade' }), providerId: text('provider_id').notNull().unique(), organizationId: text('organization_id'), domain: text('domain').notNull() }); export const session = pgTable('session', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), expiresAt: timestamp('expires_at').notNull(), token: text('token').notNull().unique(), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at') .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull(), ipAddress: text('ip_address'), userAgent: text('user_agent'), userId: uuid('user_id') .notNull() .references(() => user.id, { onDelete: 'cascade' }), impersonatedBy: text('impersonated_by') }); export const account = pgTable('account', { id: uuid('id') .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), accountId: text('account_id').notNull(), providerId: text('provider_id').notNull(), userId: uuid('user_id') .notNull() .references(() => user.id, { onDelete: 'cascade' }), accessToken: text('access_token'), refreshToken: text('refresh_token'), idToken: text('id_token'), accessTokenExpiresAt: timestamp('access_token_expires_at'), refreshTokenExpiresAt: timestamp('refresh_token_expires_at'), scope: text('scope'), password: text('password'), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at') .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull() }); export const verification = pgTable('verification', { id: uuid('id') .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), identifier: text('identifier').notNull(), value: text('value').notNull(), expiresAt: timestamp('expires_at').notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at') .defaultNow() .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull() }); export const analyticsLoginEvents = pgTable( 'analytics_login_events', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), userId: uuid('user_id').notNull(), loggedInAt: timestamp('logged_in_at', { withTimezone: true, mode: 'string' }).defaultNow(), loggedInDate: date('logged_in_date') .default(sql`CURRENT_DATE`) .notNull() }, (table) => [ index('idx_analytics_login_events_logged_in_at').using( 'btree', table.loggedInAt.asc().nullsLast().op('timestamptz_ops') ), index('idx_analytics_login_events_user_id').using('btree', table.userId.asc().nullsLast().op('uuid_ops')), index('idx_analytics_login_events_logged_in_date').using( 'btree', table.loggedInDate.asc().nullsLast().op('date_ops') ), foreignKey({ columns: [table.userId], foreignColumns: [user.id], name: 'analytics_login_events_user_id_fkey' }), unique('analytics_login_events_user_id_date_unique').on(table.userId, table.loggedInDate) ] ); export const analyticsPageEvent = pgTable( 'analytics_page_event', { id: uuid().defaultRandom().primaryKey().notNull(), occurredAt: timestamp('occurred_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), orgId: uuid('org_id'), sessionId: text('session_id').notNull(), userId: uuid('user_id'), eventType: text('event_type').notNull(), courseId: uuid('course_id'), path: text('path'), referrerHost: text('referrer_host'), utmSource: text('utm_source'), utmMedium: text('utm_medium'), utmCampaign: text('utm_campaign'), country: varchar('country', { length: 2 }), deviceType: varchar('device_type', { length: 16 }), locale: varchar('locale', { length: 8 }), props: jsonb().default({}).notNull() }, (table) => [ index('idx_ape_org_occurred').on(table.orgId, table.occurredAt), index('idx_ape_course_occurred').on(table.courseId, table.occurredAt), index('idx_ape_session').on(table.sessionId), index('idx_ape_event_type').on(table.eventType), index('idx_ape_user').on(table.userId) ] ); export const analyticsOrgDaily = pgTable( 'analytics_org_daily', { id: uuid().defaultRandom().primaryKey().notNull(), orgId: uuid('org_id').notNull(), date: date('date').notNull(), landingViews: integer('landing_views').default(0).notNull(), uniqueVisitors: integer('unique_visitors').default(0).notNull(), coursePageViews: integer('course_page_views').default(0).notNull(), enrollments: integer('enrollments').default(0).notNull(), completions: integer('completions').default(0).notNull(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull() }, (table) => [ unique('analytics_org_daily_org_date_unique').on(table.orgId, table.date), index('idx_aod_org_date').on(table.orgId, table.date) ] ); export const analyticsCourseDaily = pgTable( 'analytics_course_daily', { id: uuid().defaultRandom().primaryKey().notNull(), courseId: uuid('course_id').notNull(), orgId: uuid('org_id').notNull(), date: date('date').notNull(), views: integer('views').default(0).notNull(), uniqueVisitors: integer('unique_visitors').default(0).notNull(), enrollments: integer('enrollments').default(0).notNull(), completions: integer('completions').default(0).notNull(), avgTimeToEnrollSeconds: integer('avg_time_to_enroll_seconds'), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull() }, (table) => [ unique('analytics_course_daily_course_date_unique').on(table.courseId, table.date), index('idx_acd_org_date').on(table.orgId, table.date), index('idx_acd_course_date').on(table.courseId, table.date) ] ); export const analyticsCountryDaily = pgTable( 'analytics_country_daily', { id: uuid().defaultRandom().primaryKey().notNull(), orgId: uuid('org_id').notNull(), date: date('date').notNull(), country: varchar('country', { length: 2 }).notNull(), views: integer('views').default(0).notNull(), enrollments: integer('enrollments').default(0).notNull(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull() }, (table) => [ unique('analytics_country_daily_org_date_country_unique').on(table.orgId, table.date, table.country), index('idx_acntd_org_date').on(table.orgId, table.date) ] ); export const courseSection = pgTable('course_section', { id: uuid().defaultRandom().primaryKey().notNull(), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(), title: varchar(), // You can use { mode: "bigint" } if numbers are exceeding js number limitations order: bigint({ mode: 'number' }).default(sql`'0'`), courseId: uuid('course_id').references(() => course.id, { onDelete: 'cascade', onUpdate: 'cascade' }) }); export const group = pgTable( 'group', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), name: varchar().notNull(), description: text(), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(), organizationId: uuid('organization_id') }, (table) => [ foreignKey({ columns: [table.organizationId], foreignColumns: [organization.id], name: 'group_organization_id_fkey' }), index('idx_group_organization_id').on(table.organizationId) ] ); export const groupAttendance = pgTable( 'group_attendance', { // You can use { mode: "bigint" } if numbers are exceeding js number limitations id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({ name: 'group_attendance_id_seq', startWith: 1, increment: 1, minValue: 1, cache: 1 }), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(), courseId: uuid('course_id'), studentId: uuid('student_id'), isPresent: boolean('is_present').default(false), lessonId: uuid('lesson_id').notNull() }, (table) => [ foreignKey({ columns: [table.courseId], foreignColumns: [course.id], name: 'group_attendance_course_id_fkey' }), foreignKey({ columns: [table.studentId], foreignColumns: [groupmember.id], name: 'group_attendance_student_id_fkey' }) ] ); export const currency = pgTable('currency', { // You can use { mode: "bigint" } if numbers are exceeding js number limitations id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({ name: 'currency_id_seq', startWith: 1, increment: 1, minValue: 1, cache: 1 }), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(), name: varchar() }); export const appsPollOption = pgTable( 'apps_poll_option', { // You can use { mode: "bigint" } if numbers are exceeding js number limitations id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({ name: 'apps_poll_option_id_seq', startWith: 1, increment: 1, minValue: 1, cache: 1 }), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }), pollId: uuid('poll_id'), label: varchar() }, (table) => [ foreignKey({ columns: [table.pollId], foreignColumns: [appsPoll.id], name: 'apps_poll_option_poll_id_fkey' }) ] ); export const profile = pgTable( 'profile', { id: uuid().primaryKey().notNull(), fullname: text().notNull(), username: text().notNull(), avatarUrl: text('avatar_url'), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(), email: varchar(), canAddCourse: boolean('can_add_course').default(true), role: varchar(), goal: varchar(), source: varchar(), metadata: json(), // You can use { mode: "bigint" } if numbers are exceeding js number limitations telegramChatId: bigint('telegram_chat_id', { mode: 'number' }), isEmailVerified: boolean('is_email_verified').default(false), verifiedAt: timestamp('verified_at', { withTimezone: true, mode: 'string' }), locale: locale().default('en'), isRestricted: boolean('is_restricted').default(false).notNull() }, (table) => [ foreignKey({ columns: [table.id], foreignColumns: [user.id], name: 'profile_id_fkey' }), unique('profile_username_key').on(table.username), unique('profile_email_key').on(table.email), index('idx_profile_id').on(table.id), index('idx_profile_email').on(table.email) ] ); export const option = pgTable( 'option', { // You can use { mode: "bigint" } if numbers are exceeding js number limitations id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({ name: 'option_id_seq', startWith: 1, increment: 1, minValue: 1, cache: 1 }), label: varchar().notNull(), isCorrect: boolean('is_correct').default(false).notNull(), settings: jsonb().default({}), // You can use { mode: "bigint" } if numbers are exceeding js number limitations questionId: bigint('question_id', { mode: 'number' }).notNull(), value: uuid().default(sql`gen_random_uuid()`), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow() }, (table) => [ foreignKey({ columns: [table.questionId], foreignColumns: [question.id], name: 'option_question_id_fkey' }).onDelete('cascade') ] ); export const quizPlay = pgTable( 'quiz_play', { // You can use { mode: "bigint" } if numbers are exceeding js number limitations id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({ name: 'play_quiz_id_seq', startWith: 1, increment: 1, minValue: 1, cache: 1 }), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(), quizId: uuid('quiz_id'), players: json().default([]), started: boolean().default(false), // You can use { mode: "bigint" } if numbers are exceeding js number limitations currentQuestionId: bigint({ mode: 'number' }).default(sql`'0'`), showCurrentQuestionAnswer: boolean().default(false), isLastQuestion: boolean(), step: text().default('CONNECT_TO_PLAY'), studentStep: text().default('PIN_SETUP'), pin: text() }, (table) => [ foreignKey({ columns: [table.quizId], foreignColumns: [quiz.id], name: 'quiz_play_quiz_id_fkey' }), unique('quiz_play_pin_key').on(table.pin) ] ); export const organizationContacts = pgTable( 'organization_contacts', { // You can use { mode: "bigint" } if numbers are exceeding js number limitations id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({ name: 'organization_contacts_id_seq', startWith: 1, increment: 1, minValue: 1, cache: 1 }), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), email: text(), phone: text(), name: text(), message: text(), organizationId: uuid('organization_id') }, (table) => [ foreignKey({ columns: [table.organizationId], foreignColumns: [organization.id], name: 'organization_contacts_organization_id_fkey' }) ] ); export const lessonComment = pgTable( 'lesson_comment', { // You can use { mode: "bigint" } if numbers are exceeding js number limitations id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({ name: 'lesson_comment_id_seq', startWith: 1, increment: 1, minValue: 1, cache: 1 }), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(), lessonId: uuid('lesson_id'), groupmemberId: uuid('groupmember_id'), comment: text() }, (table) => [ foreignKey({ columns: [table.groupmemberId], foreignColumns: [groupmember.id], name: 'lesson_comment_groupmember_id_fkey' }), foreignKey({ columns: [table.lessonId], foreignColumns: [lesson.id], name: 'lesson_comment_lesson_id_fkey' }).onDelete('cascade') ] ); export const quiz = pgTable( 'quiz', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(), title: text(), questions: json(), timelimit: varchar().default('10s'), theme: varchar().default('standard'), organizationId: uuid('organization_id').notNull() }, (table) => [ foreignKey({ columns: [table.organizationId], foreignColumns: [organization.id], name: 'quiz_organization_id_fkey' }) ] ); export const submission = pgTable( 'submission', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), // You can use { mode: "bigint" } if numbers are exceeding js number limitations reviewerId: bigint('reviewer_id', { mode: 'number' }), // You can use { mode: "bigint" } if numbers are exceeding js number limitations statusId: bigint('status_id', { mode: 'number' }).default(sql`'1'`), gradingState: varchar('grading_state').default('queued').notNull(), overallStatus: varchar('overall_status').default('manual_required').notNull(), // You can use { mode: "bigint" } if numbers are exceeding js number limitations total: bigint({ mode: 'number' }).default(sql`'0'`), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(), exerciseId: uuid('exercise_id').notNull(), submittedBy: uuid('submitted_by'), courseId: uuid('course_id'), feedback: text() }, (table) => [ foreignKey({ columns: [table.courseId], foreignColumns: [course.id], name: 'submission_course_id_fkey' }).onDelete('cascade'), foreignKey({ columns: [table.exerciseId], foreignColumns: [exercise.id], name: 'submission_exercise_id_fkey' }).onDelete('cascade'), foreignKey({ columns: [table.statusId], foreignColumns: [submissionstatus.id], name: 'submission_status_id_fkey' }), foreignKey({ columns: [table.submittedBy], foreignColumns: [groupmember.id], name: 'submission_submitted_by_fkey' }) ] ); export const submissionstatus = pgTable( 'submissionstatus', { // You can use { mode: "bigint" } if numbers are exceeding js number limitations id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({ name: 'submission_status_id_seq', startWith: 1, increment: 1, minValue: 1, cache: 1 }), label: varchar().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow() }, () => [] ); export const course = pgTable( 'course', { title: varchar().notNull(), description: varchar().notNull(), overview: varchar().default('Welcome to this amazing course 🚀 '), id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(), groupId: uuid('group_id'), isTemplate: boolean('is_template').default(true), logo: text().default('').notNull(), slug: varchar(), metadata: jsonb().default({ goals: '', description: '', requirements: '' }).notNull().$type<{ requirements?: string; description?: string; goals?: string; videoUrl?: string; showDiscount?: boolean; discount?: number; paymentLink?: string; reward?: { show: boolean; description: string; }; instructor?: { name: string; role: string; coursesNo: number; description: string; imgUrl: string; }; certificate?: { templateUrl: string; }; reviews?: { id: number; hide: boolean; name: string; avatar_url: string; rating: number; created_at: number; description: string; }[]; lessonTabsOrder?: { id: 1 | 2 | 3 | 4; name: string; }[]; grading?: boolean; lessonDownload?: boolean; allowNewStudent: boolean; sectionDisplay?: Record; isContentGroupingEnabled?: boolean; /** Per-course AI tutor override; missing fields inherit from org defaults. */ aiTutor?: { inheritFromOrg?: boolean; enabled?: boolean; persona?: 'friendly' | 'formal' | 'encouraging' | 'socratic' | 'custom'; customPersona?: string; responseLength?: 'short' | 'medium' | 'long'; welcomeMessage?: string; disclaimerFooter?: string; thingsToSay?: string; thingsNotToSay?: string; forbiddenTopics?: string[]; groundingScope?: 'lesson' | 'course' | 'workspace'; requireCitations?: boolean; assessmentMode?: 'direct_answer' | 'hint_only' | 'block_during_exercise'; revealSolutionsAfterAttempts?: number; codePolicy?: 'allowed' | 'hints_only' | 'forbidden'; blockOffTopic?: boolean; profanityFilter?: boolean; escalation?: { enabled?: boolean; email?: string }; }; }>(), // You can use { mode: "bigint" } if numbers are exceeding js number limitations cost: bigint({ mode: 'number' }).default(sql`'0'`), currency: varchar().default('USD').notNull(), bannerImage: text('banner_image'), isPublished: boolean('is_published').default(false), certificate: jsonb().default({}).$type<{ isDownloadable?: boolean; /** @deprecated Use `design.templateId`. Legacy 6-theme id; mapped on read via LEGACY_THEME_MAP. */ theme?: string; /** Atelier-era certificate design. Source of truth for new courses. */ design?: { templateId: 'classique' | 'brutalist' | 'noir' | 'poster' | 'minimal'; accentColor: string; subtitle?: string; descriptionOverride?: string; signatories: [{ name: string; role: string }, { name: string; role: string }]; idFormat?: string; }; deadline?: string | null; threshold?: number; requiredExerciseId?: string | null; exerciseMinScorePercent?: number | null; emailMessage?: string | null; }>(), compliance: jsonb().$type<{ retakeIntervalMonths: number; gracePeriodDays?: number; reminderDaysBefore?: number[]; isMandatory?: boolean; framework?: 'HIPAA' | 'OSHA' | 'SOX' | 'GDPR' | 'PCI_DSS' | 'FERPA' | 'ISO' | 'CUSTOM' | null; maxRetakeAttempts?: number | null; passingScore?: number; }>(), status: text().default('ACTIVE').notNull(), type: courseType().default('SELF_PACED'), callout: jsonb().$type<{ title: string; description: string; buttonLabel: string; buttonUrl: string; animation?: 'waves' | 'dotted' | 'none'; } | null>() }, (table) => [ foreignKey({ columns: [table.groupId], foreignColumns: [group.id], name: 'course_group_id_fkey' }), unique('course_slug_key').on(table.slug), index('idx_course_group_id').on(table.groupId) ] ); export const courseCompletionRecord = pgTable( 'course_completion_record', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), courseId: uuid('course_id').notNull(), groupMemberId: uuid('group_member_id').notNull(), profileId: uuid('profile_id').notNull(), cycleNumber: integer('cycle_number').default(1).notNull(), status: varchar().default('not_started').notNull(), dueDate: timestamp('due_date', { withTimezone: true, mode: 'string' }).notNull(), startedAt: timestamp('started_at', { withTimezone: true, mode: 'string' }), completedAt: timestamp('completed_at', { withTimezone: true, mode: 'string' }), validUntil: timestamp('valid_until', { withTimezone: true, mode: 'string' }), expiredAt: timestamp('expired_at', { withTimezone: true, mode: 'string' }), score: integer(), attempts: integer().default(0), timeSpentMinutes: integer('time_spent_minutes').default(0), waivedBy: uuid('waived_by'), waiverReason: text('waiver_reason'), waiverExpiresAt: timestamp('waiver_expires_at', { withTimezone: true, mode: 'string' }), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull() }, (table) => [ foreignKey({ columns: [table.courseId], foreignColumns: [course.id], name: 'course_completion_record_course_id_fkey' }).onDelete('cascade'), foreignKey({ columns: [table.groupMemberId], foreignColumns: [groupmember.id], name: 'course_completion_record_group_member_id_fkey' }).onDelete('cascade'), foreignKey({ columns: [table.profileId], foreignColumns: [profile.id], name: 'course_completion_record_profile_id_fkey' }).onDelete('cascade'), foreignKey({ columns: [table.waivedBy], foreignColumns: [profile.id], name: 'course_completion_record_waived_by_fkey' }).onDelete('set null'), unique('course_completion_record_course_profile_cycle_key').on(table.courseId, table.profileId, table.cycleNumber), index('idx_course_completion_record_course_id').on(table.courseId), index('idx_course_completion_record_profile_id').on(table.profileId), index('idx_course_completion_record_status').on(table.status), index('idx_course_completion_record_due_date').on(table.dueDate), index('idx_course_completion_record_valid_until').on(table.validUntil) ] ); export const courseCompletionNotificationEvent = pgTable( 'course_completion_notification_event', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), courseCompletionRecordId: uuid('course_completion_record_id').notNull(), channel: varchar().notNull(), eventType: varchar('event_type').notNull(), sentAt: timestamp('sent_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull() }, (table) => [ foreignKey({ columns: [table.courseCompletionRecordId], foreignColumns: [courseCompletionRecord.id], name: 'course_completion_notification_event_record_id_fkey' }).onDelete('cascade'), unique('course_completion_notification_event_record_channel_type_key').on( table.courseCompletionRecordId, table.channel, table.eventType ) ] ); export const courseCertificateIssue = pgTable( 'course_certificate_issue', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), courseId: uuid('course_id').notNull(), profileId: uuid('profile_id').notNull(), courseCompletionRecordId: uuid('course_completion_record_id').notNull(), cycleNumber: integer('cycle_number').notNull(), issuedAt: timestamp('issued_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), expiresAt: timestamp('expires_at', { withTimezone: true, mode: 'string' }).notNull(), status: varchar().default('valid').notNull(), fileUrl: text('file_url') }, (table) => [ foreignKey({ columns: [table.courseId], foreignColumns: [course.id], name: 'course_certificate_issue_course_id_fkey' }).onDelete('cascade'), foreignKey({ columns: [table.profileId], foreignColumns: [profile.id], name: 'course_certificate_issue_profile_id_fkey' }).onDelete('cascade'), foreignKey({ columns: [table.courseCompletionRecordId], foreignColumns: [courseCompletionRecord.id], name: 'course_certificate_issue_record_id_fkey' }).onDelete('cascade'), unique('course_certificate_issue_record_id_key').on(table.courseCompletionRecordId), index('idx_course_certificate_issue_course_id').on(table.courseId), index('idx_course_certificate_issue_profile_id').on(table.profileId), index('idx_course_certificate_issue_status').on(table.status) ] ); export const appsPoll = pgTable( 'apps_poll', { id: uuid().defaultRandom().primaryKey().notNull(), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(), question: text(), authorId: uuid(), isPublic: boolean(), status: varchar().default('draft'), expiration: timestamp({ withTimezone: true, mode: 'string' }), courseId: uuid() }, (table) => [ foreignKey({ columns: [table.authorId], foreignColumns: [groupmember.id], name: 'apps_poll_authorId_fkey' }), foreignKey({ columns: [table.courseId], foreignColumns: [course.id], name: 'apps_poll_courseId_fkey' }) ] ); export const videoTranscripts = pgTable('video_transcripts', { // You can use { mode: "bigint" } if numbers are exceeding js number limitations id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({ name: 'video_transcripts_id_seq', startWith: 1, increment: 1, minValue: 1, cache: 1 }), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), museSvid: text('muse_svid'), transcript: text(), downloaded: boolean().default(false), link: text() }); export const organizationEmaillist = pgTable( 'organization_emaillist', { // You can use { mode: "bigint" } if numbers are exceeding js number limitations id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({ name: 'organization_emaillist_id_seq', startWith: 1, increment: 1, minValue: 1, cache: 1 }), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), email: text(), organizationId: uuid('organization_id') }, (table) => [ foreignKey({ columns: [table.organizationId], foreignColumns: [organization.id], name: 'organization_emaillist_organization_id_fkey' }) ] ); export const lesson = pgTable( 'lesson', { note: varchar(), videoUrl: varchar('video_url'), slideUrl: varchar('slide_url'), courseId: uuid('course_id').notNull(), id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(), title: varchar().notNull(), public: boolean().default(false), lessonAt: timestamp('lesson_at', { withTimezone: true, mode: 'string' }).defaultNow(), teacherId: uuid('teacher_id'), isComplete: boolean('is_complete').default(false), callUrl: text('call_url'), // You can use { mode: "bigint" } if numbers are exceeding js number limitations order: bigint({ mode: 'number' }), isUnlocked: boolean('is_unlocked').default(false), videos: jsonb().default([]).$type< { type: 'youtube' | 'generic' | 'upload' | 'google_drive'; link: string; key?: string; assetId?: string; /** Display name; stored on add/upload. Fallback: derive from key or type. */ fileName?: string; metadata?: { svid?: string; title?: string; description?: string; thumbnailUrl?: string; duration?: number; aspectRatio?: string; createdAt?: string; }; }[] >(), documents: jsonb().default([]).$type< { type: string; name: string; link: string; size?: number; key: string; assetId?: string; }[] >(), sectionId: uuid('section_id').references(() => courseSection.id, { onDelete: 'cascade', onUpdate: 'cascade' }), slug: varchar() }, (table) => [ foreignKey({ columns: [table.courseId], foreignColumns: [course.id], name: 'lesson_course_id_fkey' }), foreignKey({ columns: [table.teacherId], foreignColumns: [profile.id], name: 'lesson_teacher_id_fkey' }), index('idx_lesson_course_slug').on(table.courseId, table.slug) ] ); export const asset = pgTable( 'assets', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), organizationId: uuid('organization_id').notNull(), kind: varchar().default('video').notNull(), provider: varchar().default('upload').notNull(), storageProvider: varchar('storage_provider').default('s3').notNull(), storageKey: text('storage_key'), sourceUrl: text('source_url'), mimeType: text('mime_type'), byteSize: bigint('byte_size', { mode: 'number' }), checksum: text(), title: text(), description: text(), thumbnailUrl: text('thumbnail_url'), durationSeconds: integer('duration_seconds'), aspectRatio: text('aspect_ratio'), isExternal: boolean('is_external').default(false).notNull(), status: varchar().default('active').notNull(), metadata: jsonb().default({}), createdByProfileId: uuid('created_by_profile_id'), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow() }, (table) => [ foreignKey({ columns: [table.organizationId], foreignColumns: [organization.id], name: 'assets_organization_id_fkey' }) .onUpdate('cascade') .onDelete('cascade'), foreignKey({ columns: [table.createdByProfileId], foreignColumns: [profile.id], name: 'assets_created_by_profile_id_fkey' }) .onUpdate('cascade') .onDelete('set null'), index('idx_assets_organization_id').on(table.organizationId), index('idx_assets_organization_status').on(table.organizationId, table.status), index('idx_assets_organization_kind_status').on(table.organizationId, table.kind, table.status), index('idx_assets_organization_created_at').on(table.organizationId, table.createdAt), index('idx_assets_organization_byte_size').on(table.organizationId, table.byteSize), unique('assets_org_provider_storage_key_unique').on(table.organizationId, table.provider, table.storageKey) ] ); export const assetUsage = pgTable( 'asset_usages', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), organizationId: uuid('organization_id').notNull(), assetId: uuid('asset_id').notNull(), targetType: varchar('target_type').notNull(), targetId: text('target_id').notNull(), slotType: varchar('slot_type').default('lesson_video').notNull(), slotKey: text('slot_key'), position: integer(), createdByProfileId: uuid('created_by_profile_id'), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull() }, (table) => [ foreignKey({ columns: [table.organizationId], foreignColumns: [organization.id], name: 'asset_usages_organization_id_fkey' }) .onUpdate('cascade') .onDelete('cascade'), foreignKey({ columns: [table.assetId], foreignColumns: [asset.id], name: 'asset_usages_asset_id_fkey' }) .onUpdate('cascade') .onDelete('cascade'), foreignKey({ columns: [table.createdByProfileId], foreignColumns: [profile.id], name: 'asset_usages_created_by_profile_id_fkey' }) .onUpdate('cascade') .onDelete('set null'), index('idx_asset_usages_org_id').on(table.organizationId), index('idx_asset_usages_asset_id').on(table.assetId), index('idx_asset_usages_target').on(table.targetType, table.targetId), unique('asset_usages_asset_target_slot_unique').on( table.assetId, table.targetType, table.targetId, table.slotType, table.slotKey, table.position ) ] ); export const appsPollSubmission = pgTable( 'apps_poll_submission', { // You can use { mode: "bigint" } if numbers are exceeding js number limitations id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({ name: 'apps_poll_submision_id_seq', startWith: 1, increment: 1, minValue: 1, cache: 1 }), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), // You can use { mode: "bigint" } if numbers are exceeding js number limitations pollOptionId: bigint('poll_option_id', { mode: 'number' }), selectedById: uuid('selected_by_id'), pollId: uuid('poll_id') }, (table) => [ foreignKey({ columns: [table.pollId], foreignColumns: [appsPoll.id], name: 'apps_poll_submission_poll_id_fkey' }), foreignKey({ columns: [table.pollOptionId], foreignColumns: [appsPollOption.id], name: 'apps_poll_submission_poll_option_id_fkey' }), foreignKey({ columns: [table.selectedById], foreignColumns: [groupmember.id], name: 'apps_poll_submission_selected_by_id_fkey' }) ] ); export const exercise = pgTable( 'exercise', { title: varchar().notNull(), description: varchar(), lessonId: uuid('lesson_id'), courseId: uuid('course_id'), sectionId: uuid('section_id'), // You can use { mode: "bigint" } if numbers are exceeding js number limitations order: bigint({ mode: 'number' }), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(), id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), isUnlocked: boolean('is_unlocked').default(true), dueBy: timestamp('due_by', { mode: 'string' }), allowMultipleAttempts: boolean('allow_multiple_attempts').default(false).notNull(), sectionDisplayMode: varchar('section_display_mode').default('one_question'), slug: varchar() }, (table) => [ foreignKey({ columns: [table.lessonId], foreignColumns: [lesson.id], name: 'exercise_lesson_id_fkey' }).onDelete('cascade'), foreignKey({ columns: [table.courseId], foreignColumns: [course.id], name: 'exercise_course_id_fkey' }), foreignKey({ columns: [table.sectionId], foreignColumns: [courseSection.id], name: 'exercise_section_id_fkey' }), index('idx_exercise_course_slug').on(table.courseId, table.slug) ] ); export const exerciseSection = pgTable( 'exercise_section', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), exerciseId: uuid('exercise_id').notNull(), title: varchar().notNull().default('Untitled Section'), description: text(), order: bigint({ mode: 'number' }).notNull().default(0), colorTheme: varchar('color_theme').notNull().default('blue'), afterBehavior: jsonb('after_behavior').notNull().default({ action: 'continue' }), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow() }, (table) => [ foreignKey({ columns: [table.exerciseId], foreignColumns: [exercise.id], name: 'exercise_section_exercise_id_fkey' }).onDelete('cascade'), index('idx_exercise_section_exercise_id').on(table.exerciseId) ] ); export const groupmember = pgTable( 'groupmember', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), groupId: uuid('group_id').notNull(), // You can use { mode: "bigint" } if numbers are exceeding js number limitations roleId: bigint('role_id', { mode: 'number' }).notNull(), profileId: uuid('profile_id'), email: varchar(), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(), assignedStudentId: varchar('assigned_student_id'), certificateEarnedAt: timestamp('certificate_earned_at', { withTimezone: true, mode: 'string' }), certificationEmailSentAt: timestamp('certification_email_sent_at', { withTimezone: true, mode: 'string' }) }, (table) => [ foreignKey({ columns: [table.groupId], foreignColumns: [group.id], name: 'groupmember_group_id_fkey' }), foreignKey({ columns: [table.profileId], foreignColumns: [profile.id], name: 'groupmember_profile_id_fkey' }), foreignKey({ columns: [table.roleId], foreignColumns: [role.id], name: 'groupmember_role_id_fkey' }), unique('unique_entries').on(table.groupId, table.profileId, table.email), unique('unique_group_email').on(table.groupId, table.email), unique('unique_group_profile').on(table.groupId, table.profileId) ] ); export const courseInvite = pgTable( 'course_invite', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), courseId: uuid('course_id').notNull(), // Reserved for future invite-role expansion (defaults to student invites) roleId: bigint('role_id', { mode: 'number' }) .default(sql`'3'`) .notNull(), tokenHash: text('token_hash').notNull(), createdByProfileId: uuid('created_by_profile_id').notNull(), revokedByProfileId: uuid('revoked_by_profile_id'), revokedAt: timestamp('revoked_at', { withTimezone: true, mode: 'string' }), expiresAt: timestamp('expires_at', { withTimezone: true, mode: 'string' }).notNull(), maxUses: integer('max_uses').default(1).notNull(), usedCount: integer('used_count').default(0).notNull(), isRevoked: boolean('is_revoked').default(false).notNull(), allowedEmails: text('allowed_emails').array(), allowedDomains: text('allowed_domains').array(), metadata: jsonb().default({}).notNull(), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) .default(sql`timezone('utc'::text, now())`) .notNull(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) .default(sql`timezone('utc'::text, now())`) .notNull(), lastUsedAt: timestamp('last_used_at', { withTimezone: true, mode: 'string' }) }, (table) => [ foreignKey({ columns: [table.courseId], foreignColumns: [course.id], name: 'course_invite_course_id_fkey' }).onDelete('cascade'), foreignKey({ columns: [table.createdByProfileId], foreignColumns: [profile.id], name: 'course_invite_created_by_profile_id_fkey' }), foreignKey({ columns: [table.revokedByProfileId], foreignColumns: [profile.id], name: 'course_invite_revoked_by_profile_id_fkey' }), foreignKey({ columns: [table.roleId], foreignColumns: [role.id], name: 'course_invite_role_id_fkey' }), unique('course_invite_token_hash_key').on(table.tokenHash), index('idx_course_invite_course_id').on(table.courseId), index('idx_course_invite_expires_at').on(table.expiresAt), index('idx_course_invite_created_by').on(table.createdByProfileId), index('idx_course_invite_revoked_by').on(table.revokedByProfileId) ] ); export const courseInviteAudit = pgTable( 'course_invite_audit', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), inviteId: uuid('invite_id').notNull(), courseId: uuid('course_id').notNull(), eventType: courseInviteEventType('event_type').notNull(), actorProfileId: uuid('actor_profile_id'), targetEmail: varchar('target_email'), ipAddress: text('ip_address'), userAgent: text('user_agent'), metadata: jsonb().default({}).notNull(), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) .default(sql`timezone('utc'::text, now())`) .notNull() }, (table) => [ foreignKey({ columns: [table.inviteId], foreignColumns: [courseInvite.id], name: 'course_invite_audit_invite_id_fkey' }).onDelete('cascade'), foreignKey({ columns: [table.courseId], foreignColumns: [course.id], name: 'course_invite_audit_course_id_fkey' }).onDelete('cascade'), foreignKey({ columns: [table.actorProfileId], foreignColumns: [profile.id], name: 'course_invite_audit_actor_profile_id_fkey' }), index('idx_course_invite_audit_invite_id').on(table.inviteId), index('idx_course_invite_audit_course_id').on(table.courseId), index('idx_course_invite_audit_event_type').on(table.eventType), index('idx_course_invite_audit_created_at').on(table.createdAt) ] ); export const organizationInvite = pgTable( 'organization_invite', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), organizationId: uuid('organization_id').notNull(), // Role assigned when invite is accepted (e.g ADMIN, TUTOR) roleId: bigint('role_id', { mode: 'number' }).notNull(), email: text().notNull(), tokenHash: text('token_hash').notNull(), createdByProfileId: uuid('created_by_profile_id').notNull(), acceptedByProfileId: uuid('accepted_by_profile_id'), acceptedAt: timestamp('accepted_at', { withTimezone: true, mode: 'string' }), revokedByProfileId: uuid('revoked_by_profile_id'), revokedAt: timestamp('revoked_at', { withTimezone: true, mode: 'string' }), expiresAt: timestamp('expires_at', { withTimezone: true, mode: 'string' }).notNull(), isRevoked: boolean('is_revoked').default(false).notNull(), metadata: jsonb().default({}).notNull(), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) .default(sql`timezone('utc'::text, now())`) .notNull(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) .default(sql`timezone('utc'::text, now())`) .notNull() }, (table) => [ foreignKey({ columns: [table.organizationId], foreignColumns: [organization.id], name: 'organization_invite_organization_id_fkey' }).onDelete('cascade'), foreignKey({ columns: [table.roleId], foreignColumns: [role.id], name: 'organization_invite_role_id_fkey' }), foreignKey({ columns: [table.createdByProfileId], foreignColumns: [profile.id], name: 'organization_invite_created_by_profile_id_fkey' }), foreignKey({ columns: [table.acceptedByProfileId], foreignColumns: [profile.id], name: 'organization_invite_accepted_by_profile_id_fkey' }), foreignKey({ columns: [table.revokedByProfileId], foreignColumns: [profile.id], name: 'organization_invite_revoked_by_profile_id_fkey' }), unique('organization_invite_token_hash_key').on(table.tokenHash), index('idx_organization_invite_organization_id').on(table.organizationId), index('idx_organization_invite_email_org').on(table.email, table.organizationId), index('idx_organization_invite_expires_at').on(table.expiresAt), index('idx_organization_invite_created_by').on(table.createdByProfileId), index('idx_organization_invite_accepted_by').on(table.acceptedByProfileId), index('idx_organization_invite_revoked_by').on(table.revokedByProfileId) ] ); export const organizationInviteAudit = pgTable( 'organization_invite_audit', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), inviteId: uuid('invite_id').notNull(), organizationId: uuid('organization_id').notNull(), eventType: organizationInviteEventType('event_type').notNull(), actorProfileId: uuid('actor_profile_id'), targetEmail: varchar('target_email'), ipAddress: text('ip_address'), userAgent: text('user_agent'), metadata: jsonb().default({}).notNull(), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) .default(sql`timezone('utc'::text, now())`) .notNull() }, (table) => [ foreignKey({ columns: [table.inviteId], foreignColumns: [organizationInvite.id], name: 'organization_invite_audit_invite_id_fkey' }).onDelete('cascade'), foreignKey({ columns: [table.organizationId], foreignColumns: [organization.id], name: 'organization_invite_audit_organization_id_fkey' }).onDelete('cascade'), foreignKey({ columns: [table.actorProfileId], foreignColumns: [profile.id], name: 'organization_invite_audit_actor_profile_id_fkey' }), index('idx_organization_invite_audit_invite_id').on(table.inviteId), index('idx_organization_invite_audit_org_id').on(table.organizationId), index('idx_organization_invite_audit_event_type').on(table.eventType), index('idx_organization_invite_audit_created_at').on(table.createdAt) ] ); export const lessonCompletion = pgTable( 'lesson_completion', { // You can use { mode: "bigint" } if numbers are exceeding js number limitations id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({ name: 'lesson_completion_id_seq', startWith: 1, increment: 1, minValue: 1, cache: 1 }), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), lessonId: uuid('lesson_id'), profileId: uuid('profile_id'), isComplete: boolean('is_complete').default(false), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow() }, (table) => [ foreignKey({ columns: [table.lessonId], foreignColumns: [lesson.id], name: 'lesson_completion_lesson_id_fkey' }).onDelete('cascade'), foreignKey({ columns: [table.profileId], foreignColumns: [profile.id], name: 'lesson_completion_profile_id_fkey' }), unique('unique_lesson_profile').on(table.lessonId, table.profileId) ] ); export const communityAnswer = pgTable( 'community_answer', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(), // You can use { mode: "bigint" } if numbers are exceeding js number limitations questionId: bigint('question_id', { mode: 'number' }), body: varchar(), // You can use { mode: "bigint" } if numbers are exceeding js number limitations authorId: bigint('author_id', { mode: 'number' }), // You can use { mode: "bigint" } if numbers are exceeding js number limitations votes: bigint({ mode: 'number' }), authorProfileId: uuid('author_profile_id') }, (table) => [ foreignKey({ columns: [table.authorId], foreignColumns: [organizationmember.id], name: 'community_answer_author_id_fkey' }), foreignKey({ columns: [table.authorProfileId], foreignColumns: [profile.id], name: 'community_answer_author_profile_id_fkey' }), foreignKey({ columns: [table.questionId], foreignColumns: [communityQuestion.id], name: 'community_answer_question_id_fkey' }) ] ); export const communityQuestion = pgTable( 'community_question', { // You can use { mode: "bigint" } if numbers are exceeding js number limitations id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({ name: 'community_question_id_seq', startWith: 1, increment: 1, minValue: 1, cache: 1 }), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(), title: varchar(), body: text(), // You can use { mode: "bigint" } if numbers are exceeding js number limitations authorId: bigint('author_id', { mode: 'number' }), // You can use { mode: "bigint" } if numbers are exceeding js number limitations votes: bigint({ mode: 'number' }).default(sql`'0'`), organizationId: uuid('organization_id'), slug: text(), authorProfileId: uuid('author_profile_id'), courseId: uuid('course_id').notNull() }, (table) => [ foreignKey({ columns: [table.authorId], foreignColumns: [organizationmember.id], name: 'community_question_author_id_fkey' }), foreignKey({ columns: [table.authorProfileId], foreignColumns: [profile.id], name: 'community_question_author_profile_id_fkey' }), foreignKey({ columns: [table.courseId], foreignColumns: [course.id], name: 'community_question_course_id_fkey' }), foreignKey({ columns: [table.organizationId], foreignColumns: [organization.id], name: 'community_question_organization_id_fkey' }) ] ); export const courseNewsfeed = pgTable( 'course_newsfeed', { createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), authorId: uuid('author_id'), content: text(), id: uuid().defaultRandom().primaryKey().notNull(), courseId: uuid('course_id'), reaction: jsonb() .default({ clap: [], smile: [], thumbsup: [], thumbsdown: [] }) .$type<{ clap: string[]; smile: string[]; thumbsup: string[]; thumbsdown: string[] }>(), isPinned: boolean('is_pinned').default(false).notNull() }, (table) => [ foreignKey({ columns: [table.authorId], foreignColumns: [groupmember.id], name: 'course_newsfeed_author_id_fkey' }), foreignKey({ columns: [table.courseId], foreignColumns: [course.id], name: 'course_newsfeed_course_id_fkey' }) ] ); export const courseNewsfeedComment = pgTable( 'course_newsfeed_comment', { createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), authorId: uuid('author_id'), content: text(), // You can use { mode: "bigint" } if numbers are exceeding js number limitations id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({ name: 'course_newsfeed_comment_id_seq', startWith: 1, increment: 1, minValue: 1, cache: 1 }), courseNewsfeedId: uuid('course_newsfeed_id') }, (table) => [ foreignKey({ columns: [table.authorId], foreignColumns: [groupmember.id], name: 'course_newsfeed_comment_author_id_fkey' }), foreignKey({ columns: [table.courseNewsfeedId], foreignColumns: [courseNewsfeed.id], name: 'course_newsfeed_comment_course_newsfeed_id_fkey' }) ] ); export const testTenant = pgTable('test_tenant', { id: serial().primaryKey().notNull(), details: text() }); export const organizationPlan = pgTable( 'organization_plan', { // You can use { mode: "bigint" } if numbers are exceeding js number limitations id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({ name: 'organization_plan_id_seq', startWith: 1, increment: 1, minValue: 1, cache: 1 }), activatedAt: timestamp('activated_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), orgId: uuid('org_id'), planName: plan('plan_name'), isActive: boolean('is_active'), deactivatedAt: timestamp('deactivated_at', { withTimezone: true, mode: 'string' }), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(), payload: jsonb(), // You can use { mode: "bigint" } if numbers are exceeding js number limitations triggeredBy: bigint('triggered_by', { mode: 'number' }), provider: text().default('lmz'), subscriptionId: text('subscription_id') }, (table) => [ foreignKey({ columns: [table.orgId], foreignColumns: [organization.id], name: 'organization_plan_org_id_fkey' }), foreignKey({ columns: [table.triggeredBy], foreignColumns: [organizationmember.id], name: 'organization_plan_triggered_by_fkey' }), unique('organization_plan_subscription_id_key').on(table.subscriptionId), index('idx_organization_plan_org_id').on(table.orgId) ] ); export const lessonLanguage = pgTable( 'lesson_language', { // You can use { mode: "bigint" } if numbers are exceeding js number limitations id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({ name: 'lesson_language_id_seq', startWith: 1, increment: 1, minValue: 1, cache: 1 }), content: text(), lessonId: uuid('lesson_id').defaultRandom(), locale: locale().default('en') }, (table) => [ foreignKey({ columns: [table.lessonId], foreignColumns: [lesson.id], name: 'public_lesson_language_lesson_id_fkey' }) .onUpdate('cascade') .onDelete('cascade') ] ); export const lessonLanguageHistory = pgTable( 'lesson_language_history', { id: serial().primaryKey().notNull(), lessonLanguageId: integer('lesson_language_id'), oldContent: text('old_content'), newContent: text('new_content'), timestamp: timestamp({ mode: 'string' }) .default(sql`CURRENT_TIMESTAMP`) .notNull() }, (table) => [ foreignKey({ columns: [table.lessonLanguageId], foreignColumns: [lessonLanguage.id], name: 'public_lesson_language_history_lesson_language_id_fkey' }) .onUpdate('cascade') .onDelete('cascade') ] ); export const organizationmember = pgTable( 'organizationmember', { // You can use { mode: "bigint" } if numbers are exceeding js number limitations id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({ name: 'organizationmember_id_seq', startWith: 1, increment: 1, minValue: 1, cache: 1 }), organizationId: uuid('organization_id').notNull(), // You can use { mode: "bigint" } if numbers are exceeding js number limitations roleId: bigint('role_id', { mode: 'number' }).notNull(), profileId: uuid('profile_id'), email: text(), verified: boolean().default(false), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) .default(sql`timezone('utc'::text, now())`) .notNull() }, (table) => [ foreignKey({ columns: [table.organizationId], foreignColumns: [organization.id], name: 'organizationmember_organization_id_fkey' }), foreignKey({ columns: [table.profileId], foreignColumns: [profile.id], name: 'organizationmember_profile_id_fkey' }), foreignKey({ columns: [table.roleId], foreignColumns: [role.id], name: 'organizationmember_role_id_fkey' }), index('idx_organizationmember_profile_id').on(table.profileId), index('idx_organizationmember_organization_id').on(table.organizationId), index('idx_organizationmember_profile_org').on(table.profileId, table.organizationId), uniqueIndex('organizationmember_org_profile_unique') .on(table.organizationId, table.profileId) .where(sql`${table.profileId} IS NOT NULL`), uniqueIndex('organizationmember_org_email_unique') .on(table.organizationId, table.email) .where(sql`${table.email} IS NOT NULL`) ] ); export const question = pgTable( 'question', { // You can use { mode: "bigint" } if numbers are exceeding js number limitations id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({ name: 'question_id_seq', startWith: 1, increment: 1, minValue: 1, cache: 1 }), // You can use { mode: "bigint" } if numbers are exceeding js number limitations questionTypeId: bigint('question_type_id', { mode: 'number' }).notNull(), title: varchar().notNull(), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(), exerciseId: uuid('exercise_id').notNull(), exerciseSectionId: uuid('exercise_section_id'), name: uuid().default(sql`gen_random_uuid()`), points: doublePrecision(), settings: jsonb().default({}), // You can use { mode: "bigint" } if numbers are exceeding js number limitations order: bigint({ mode: 'number' }) }, (table) => [ foreignKey({ columns: [table.exerciseId], foreignColumns: [exercise.id], name: 'question_exercise_id_fkey' }).onDelete('cascade'), foreignKey({ columns: [table.exerciseSectionId], foreignColumns: [exerciseSection.id], name: 'question_exercise_section_id_fkey' }).onDelete('set null'), foreignKey({ columns: [table.questionTypeId], foreignColumns: [questionType.id], name: 'question_question_type_id_fkey' }) ] ); export const questionAnswer = pgTable( 'question_answer', { // You can use { mode: "bigint" } if numbers are exceeding js number limitations id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({ name: 'question_answer_id_seq', startWith: 1, increment: 1, minValue: 1, cache: 1 }), answerData: jsonb('answer_data').$type(), /** @deprecated Use answer_data instead. Kept for backwards compatibility. */ answers: varchar().array(), /** @deprecated Use answer_data instead. Kept for backwards compatibility. */ openAnswer: text('open_answer'), // You can use { mode: "bigint" } if numbers are exceeding js number limitations questionId: bigint('question_id', { mode: 'number' }).notNull(), groupMemberId: uuid('group_member_id').notNull(), submissionId: uuid('submission_id'), // You can use { mode: "bigint" } if numbers are exceeding js number limitations point: bigint({ mode: 'number' }).default(sql`'0'`) }, (table) => [ foreignKey({ columns: [table.groupMemberId], foreignColumns: [groupmember.id], name: 'question_answer_group_member_id_fkey' }), foreignKey({ columns: [table.questionId], foreignColumns: [question.id], name: 'question_answer_question_id_fkey' }).onDelete('cascade'), foreignKey({ columns: [table.submissionId], foreignColumns: [submission.id], name: 'question_answer_submission_id_fkey' }).onDelete('cascade') ] ); /** * Exercise question kinds (`typename` matches `@cio/question-types` keys). Expected ids: * 1 RADIO, 2 CHECKBOX, 3 TEXTAREA, 4 TRUE_FALSE, 5 SHORT_ANSWER, 6 NUMERIC, 7 FILL_BLANK, * 8 FILE_UPLOAD, 9 MATCHING, 10 ORDERING, 11 HOTSPOT, 12 LINK, 13 WORD_BANK, 14 STAR, * 15 VIDEO_RECORDING. */ export const questionType = pgTable( 'question_type', { // You can use { mode: "bigint" } if numbers are exceeding js number limitations id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({ name: 'question_type_id_seq', startWith: 1, increment: 1, minValue: 1, cache: 1 }), label: varchar().notNull(), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(), typename: varchar() }, () => [] ); export const role = pgTable( 'role', { type: varchar().notNull(), description: varchar(), // You can use { mode: "bigint" } if numbers are exceeding js number limitations id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({ name: 'role_id_seq', startWith: 1, increment: 1, minValue: 1, cache: 1 }), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow() }, () => [] ); export const waitinglist = pgTable( 'waitinglist', { // You can use { mode: "bigint" } if numbers are exceeding js number limitations id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({ name: 'waitinglist_id_seq', startWith: 1, increment: 1, minValue: 1, cache: 1 }), email: varchar().notNull(), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow() }, (table) => [unique('constraint_name').on(table.email)] ); export const organization = pgTable( 'organization', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), name: varchar().notNull(), siteName: text(), avatarUrl: text('avatar_url'), settings: jsonb().default({}).$type<{ signup?: { inviteOnly?: boolean; }; }>(), landingpage: jsonb().default({}).$type<{ header?: { title: string; titleHighlight: string; subtitle: string; action: { label: string; link: string; redirect: boolean; }; banner: { video: string; image: string; type: string; show: boolean; }; background: { image: string; show: boolean; }; show: boolean; }; aboutUs?: { title: string; content: string; imageUrl: string; show: boolean; }; courses?: { title: string; titleHighlight: string; subtitle: string; show: boolean; }; faq?: { title: string; questions: Array<{ id: number; title: string; content: string; }>; show: boolean; }; contact?: { title: string; titleHighlight: string; subtitle: string; address: string; phone: string; email: string; show: boolean; }; mailinglist?: { title: string; subtitle: string; buttonLabel: string; show: boolean; }; customLinks?: { show: boolean; links: Array<{ id: number; label: string; url: string; openInNewTab: boolean; }>; }; /** Legacy social URLs-only footer block */ footer?: | { facebook?: string; instagram?: string; twitter?: string; linkedin?: string; show?: boolean; } | { brand: { tagline?: string; copyright?: string; socials: Array<{ platform: string; href: string }>; }; columns: Array<{ id: string; heading: string; links: Array<{ label: string; href: string }>; cta?: { label: string; href: string }; }>; bottom?: { text?: string; links: Array<{ label: string; href: string }>; }; }; /** Current landing JSON shape (stored alongside legacy keys above) */ theme?: string; hero?: Record; navItems?: Array<{ label: string; href: string }>; footerLinks?: Array<{ label: string; href: string }>; footerText?: string; embed?: Record; callout?: Record; links?: Record; }>(), theme: text().default('blue'), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) .default(sql`timezone('utc'::text, now())`) .notNull(), customization: json() .default({ apps: { poll: true, comments: true }, course: { grading: true, newsfeed: true }, dashboard: { exercise: true, community: true, bannerText: '', bannerImage: '' } }) .$type<{ dashboard: { community: boolean; exercise: boolean; bannerImage: string; bannerText: string; }; course: { newsfeed: boolean; grading: boolean; }; apps: { poll: boolean; comments: boolean; }; }>() .notNull(), isRestricted: boolean('is_restricted').default(false).notNull(), customCode: text(), customDomain: text(), favicon: text(), isCustomDomainVerified: boolean().default(false), disableSignup: boolean('disable_signup').default(false), disableSignupMessage: text('disable_signup_message'), disableEmailPassword: boolean('disable_email_password').default(false), disableGoogleAuth: boolean('disable_google_auth').default(false), parentOrganizationId: uuid('parent_organization_id'), readOnlyUntil: timestamp('read_only_until', { withTimezone: true, mode: 'string' }), aiTutorSettings: jsonb('ai_tutor_settings') .default({ enabled: true, persona: 'encouraging', customPersona: '', responseLength: 'medium', welcomeMessage: '', disclaimerFooter: 'I am an AI tutor, not your instructor.', thingsToSay: '', thingsNotToSay: '', forbiddenTopics: [], groundingScope: 'course', requireCitations: true, assessmentMode: 'hint_only', revealSolutionsAfterAttempts: 3, codePolicy: 'hints_only', blockOffTopic: true, profanityFilter: true, escalation: { enabled: false, email: '' } }) .$type<{ enabled: boolean; persona: 'friendly' | 'formal' | 'encouraging' | 'socratic' | 'custom'; customPersona: string; responseLength: 'short' | 'medium' | 'long'; welcomeMessage: string; disclaimerFooter: string; thingsToSay: string; thingsNotToSay: string; forbiddenTopics: string[]; groundingScope: 'lesson' | 'course' | 'workspace'; requireCitations: boolean; assessmentMode: 'direct_answer' | 'hint_only' | 'block_during_exercise'; revealSolutionsAfterAttempts: number; codePolicy: 'allowed' | 'hints_only' | 'forbidden'; blockOffTopic: boolean; profanityFilter: boolean; escalation: { enabled: boolean; email: string }; }>() .notNull() }, (table) => [ unique('organization_siteName_key').on(table.siteName), unique('organization_customDomain_key').on(table.customDomain), foreignKey({ columns: [table.parentOrganizationId], foreignColumns: [table.id], name: 'organization_parent_organization_id_fkey' }).onDelete('set null'), index('idx_organization_parent_id').on(table.parentOrganizationId) ] ); export const tagGroup = pgTable( 'tag_group', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), organizationId: uuid('organization_id').notNull(), name: varchar().notNull(), slug: text().notNull(), description: text(), order: integer().default(0).notNull(), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) .default(sql`timezone('utc'::text, now())`) .notNull(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) .default(sql`timezone('utc'::text, now())`) .notNull() }, (table) => [ foreignKey({ columns: [table.organizationId], foreignColumns: [organization.id], name: 'tag_group_organization_id_fkey' }).onDelete('cascade'), index('idx_tag_group_organization_id').on(table.organizationId), unique('tag_group_org_slug_key').on(table.organizationId, table.slug) ] ); export const tag = pgTable( 'tag', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), organizationId: uuid('organization_id').notNull(), groupId: uuid('group_id').notNull(), name: varchar().notNull(), slug: text().notNull(), description: text(), color: varchar().notNull(), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) .default(sql`timezone('utc'::text, now())`) .notNull(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) .default(sql`timezone('utc'::text, now())`) .notNull() }, (table) => [ foreignKey({ columns: [table.organizationId], foreignColumns: [organization.id], name: 'tag_organization_id_fkey' }).onDelete('cascade'), foreignKey({ columns: [table.groupId], foreignColumns: [tagGroup.id], name: 'tag_group_id_fkey' }).onDelete('cascade'), index('idx_tag_organization_id').on(table.organizationId), index('idx_tag_group_id').on(table.groupId), unique('tag_org_slug_key').on(table.organizationId, table.slug) ] ); export const tagAssignment = pgTable( 'tag_assignment', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), tagId: uuid('tag_id').notNull(), courseId: uuid('course_id').notNull(), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) .default(sql`timezone('utc'::text, now())`) .notNull() }, (table) => [ foreignKey({ columns: [table.tagId], foreignColumns: [tag.id], name: 'tag_assignment_tag_id_fkey' }).onDelete('cascade'), foreignKey({ columns: [table.courseId], foreignColumns: [course.id], name: 'tag_assignment_course_id_fkey' }).onDelete('cascade'), index('idx_tag_assignment_tag_id').on(table.tagId), index('idx_tag_assignment_course_id').on(table.courseId), unique('tag_assignment_tag_course_key').on(table.tagId, table.courseId) ] ); export const widget = pgTable( 'widget', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), organizationId: uuid('organization_id').notNull(), name: varchar({ length: 120 }).notNull(), status: widgetStatus('status').default('DRAFT').notNull(), layoutType: widgetLayoutType('layout_type').default('card_grid').notNull(), selectionMode: widgetSelectionMode('selection_mode').default('manual').notNull(), publicKey: varchar('public_key', { length: 64 }).notNull(), config: jsonb('config') .default(sql`'{}'::jsonb`) .notNull(), hasUnpublishedChanges: boolean('has_unpublished_changes').default(false).notNull(), latestPublishedVersionId: uuid('latest_published_version_id'), createdByUserId: uuid('created_by_user_id').notNull(), updatedByUserId: uuid('updated_by_user_id'), deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) .default(sql`timezone('utc'::text, now())`) .notNull(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) .default(sql`timezone('utc'::text, now())`) .notNull() }, (table) => [ foreignKey({ columns: [table.organizationId], foreignColumns: [organization.id], name: 'widget_organization_id_fkey' }).onDelete('cascade'), foreignKey({ columns: [table.createdByUserId], foreignColumns: [user.id], name: 'widget_created_by_user_id_fkey' }).onDelete('restrict'), foreignKey({ columns: [table.updatedByUserId], foreignColumns: [user.id], name: 'widget_updated_by_user_id_fkey' }).onDelete('set null'), index('idx_widget_organization_id').on(table.organizationId), index('idx_widget_status').on(table.status), unique('widget_public_key_key').on(table.publicKey) ] ); export const widgetCourse = pgTable( 'widget_course', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), widgetId: uuid('widget_id').notNull(), courseId: uuid('course_id').notNull(), order: integer().default(0).notNull(), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) .default(sql`timezone('utc'::text, now())`) .notNull() }, (table) => [ foreignKey({ columns: [table.widgetId], foreignColumns: [widget.id], name: 'widget_course_widget_id_fkey' }).onDelete('cascade'), foreignKey({ columns: [table.courseId], foreignColumns: [course.id], name: 'widget_course_course_id_fkey' }).onDelete('cascade'), index('idx_widget_course_widget_id').on(table.widgetId), index('idx_widget_course_course_id').on(table.courseId), unique('widget_course_widget_course_key').on(table.widgetId, table.courseId) ] ); export const widgetVersion = pgTable( 'widget_version', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), widgetId: uuid('widget_id').notNull(), version: integer().notNull(), configSnapshot: jsonb('config_snapshot') .default(sql`'{}'::jsonb`) .notNull(), payloadSnapshot: jsonb('payload_snapshot') .default(sql`'{}'::jsonb`) .notNull(), runtimeManifest: jsonb('runtime_manifest') .default(sql`'{}'::jsonb`) .notNull(), rolledBackFromVersionId: uuid('rolled_back_from_version_id'), publishedByUserId: uuid('published_by_user_id').notNull(), publishedAt: timestamp('published_at', { withTimezone: true, mode: 'string' }) .default(sql`timezone('utc'::text, now())`) .notNull() }, (table) => [ foreignKey({ columns: [table.widgetId], foreignColumns: [widget.id], name: 'widget_version_widget_id_fkey' }).onDelete('cascade'), foreignKey({ columns: [table.publishedByUserId], foreignColumns: [user.id], name: 'widget_version_published_by_user_id_fkey' }).onDelete('restrict'), foreignKey({ columns: [table.rolledBackFromVersionId], foreignColumns: [table.id], name: 'widget_version_rolled_back_from_version_id_fkey' }).onDelete('set null'), index('idx_widget_version_widget_id').on(table.widgetId), unique('widget_version_widget_version_key').on(table.widgetId, table.version) ] ); export const dashOrgStats = pgView('dash_org_stats', { orgId: uuid('org_id'), // You can use { mode: "bigint" } if numbers are exceeding js number limitations noOfCourses: bigint('no_of_courses', { mode: 'number' }), // You can use { mode: "bigint" } if numbers are exceeding js number limitations enrolledStudents: bigint('enrolled_students', { mode: 'number' }) }).as( sql`SELECT gp.organization_id AS org_id, count(DISTINCT course.id) AS no_of_courses, count(DISTINCT gm.profile_id) AS enrolled_students FROM course JOIN "group" gp ON gp.id = course.group_id LEFT JOIN groupmember gm ON gm.group_id = gp.id AND gm.role_id = 3 WHERE course.status = 'ACTIVE'::text GROUP BY gp.organization_id` ); export const lessonVersions = pgView('lesson_versions', { oldContent: text('old_content'), newContent: text('new_content'), timestamp: timestamp({ mode: 'string' }), locale: locale(), lessonId: uuid('lesson_id') }).as( sql`SELECT llh.old_content, llh.new_content, llh."timestamp", ll.locale, ll.lesson_id FROM lesson_language_history llh JOIN lesson_language ll ON ll.id = llh.lesson_language_id` ); export const exerciseTemplate = pgTable('exercise_template', { id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({ name: 'exercise_template_id_seq', startWith: 1, increment: 1, minValue: 1, cache: 1 }), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), title: text(), description: text(), tag: text(), questionnaire: jsonb().default({}).$type<{ questions: [ { title: string; name: string; points: number; order: number; question_type: { id: number; label: string; }; options: [ { label: string; is_correct: boolean; }, { label: string; is_correct: boolean; }, { label: string; is_correct: boolean; } ]; } ]; }>() }); // Organization SSO provider type (our app enum for display/categorization) export const organizationSsoProviderType = pgEnum('SSO_PROVIDER', ['OKTA', 'GOOGLE_WORKSPACE', 'AUTH0']); // Organization Auth Policy - stores SSO policies per org export const organizationAuthPolicy = pgTable( 'organization_auth_policy', { organizationId: uuid('organization_id') .notNull() .primaryKey() .references(() => organization.id, { onDelete: 'cascade' }), forceSso: boolean('force_sso').default(false).notNull(), autoJoinSsoDomains: boolean('auto_join_sso_domains').default(false).notNull(), breakGlassEnabled: boolean('break_glass_enabled').default(true).notNull(), defaultRoleId: bigint('default_role_id', { mode: 'number' }) .default(sql`'3'`) .notNull(), roleMapping: jsonb('role_mapping').default({}).notNull(), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) .default(sql`timezone('utc'::text, now())`) .notNull(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) .default(sql`timezone('utc'::text, now())`) .notNull() }, (table) => [ foreignKey({ columns: [table.defaultRoleId], foreignColumns: [role.id], name: 'organization_auth_policy_default_role_id_fkey' }), index('idx_organization_auth_policy_org_id').on(table.organizationId) ] ); // Organization SSO Config - metadata for SSO connections (Better Auth stores the actual config) export const organizationSsoConfig = pgTable( 'organization_sso_config', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), organizationId: uuid('organization_id') .notNull() .unique() .references(() => organization.id, { onDelete: 'cascade' }), betterAuthProviderId: text('better_auth_provider_id').notNull(), provider: organizationSsoProviderType().notNull(), displayName: text('display_name').notNull(), domain: text().notNull(), isActive: boolean('is_active').default(false).notNull(), createdByProfileId: uuid('created_by_profile_id') .notNull() .references(() => profile.id), updatedByProfileId: uuid('updated_by_profile_id').references(() => profile.id), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) .default(sql`timezone('utc'::text, now())`) .notNull(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) .default(sql`timezone('utc'::text, now())`) .notNull() }, (table) => [ index('idx_organization_sso_config_org_id').on(table.organizationId), index('idx_organization_sso_config_domain').on(table.domain), index('idx_organization_sso_config_provider_id').on(table.betterAuthProviderId) ] ); export const organizationTokenAuth = pgTable( 'organization_token_auth', { id: uuid('id') .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), organizationId: uuid('organization_id') .notNull() .unique() .references(() => organization.id, { onDelete: 'cascade' }), signingSecret: text('signing_secret').notNull(), isActive: boolean('is_active').default(false).notNull(), createdByProfileId: uuid('created_by_profile_id') .notNull() .references(() => profile.id), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) .default(sql`timezone('utc'::text, now())`) .notNull(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) .default(sql`timezone('utc'::text, now())`) .notNull() }, (table) => [index('idx_organization_token_auth_org_id').on(table.organizationId)] ); export const organizationApiKey = pgTable( 'organization_api_key', { id: uuid('id') .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), organizationId: uuid('organization_id') .notNull() .references(() => organization.id, { onDelete: 'cascade' }), createdByProfileId: uuid('created_by_profile_id') .notNull() .references(() => profile.id), type: organizationApiKeyType('type').notNull(), label: varchar('label', { length: 120 }).notNull(), secretPrefix: varchar('secret_prefix', { length: 32 }).notNull(), secretHash: text('secret_hash').notNull(), scopes: jsonb('scopes').default([]).notNull().$type(), lastUsedAt: timestamp('last_used_at', { withTimezone: true, mode: 'string' }), expiresAt: timestamp('expires_at', { withTimezone: true, mode: 'string' }), revokedAt: timestamp('revoked_at', { withTimezone: true, mode: 'string' }), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) .default(sql`timezone('utc'::text, now())`) .notNull(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) .default(sql`timezone('utc'::text, now())`) .notNull() }, (table) => [ index('idx_organization_api_key_org_id').on(table.organizationId), index('idx_organization_api_key_type').on(table.type), uniqueIndex('organization_api_key_secret_hash_unique').on(table.secretHash) ] ); export const organizationAutomationUsage = pgTable( 'organization_automation_usage', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), organizationId: uuid('organization_id') .notNull() .references(() => organization.id), organizationApiKeyId: uuid('organization_api_key_id').references(() => organizationApiKey.id, { onDelete: 'set null' }), type: organizationApiKeyType('type').notNull(), action: varchar('action', { length: 120 }).notNull(), category: automationUsageCategory('category').notNull(), creditsConsumed: integer('credits_consumed').default(0).notNull(), metadata: jsonb('metadata').default({}).$type>().notNull(), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) .default(sql`timezone('utc'::text, now())`) .notNull() }, (table) => [ index('idx_org_automation_usage_org_created_at').on(table.organizationId, table.createdAt), index('idx_org_automation_usage_key_created_at').on(table.organizationApiKeyId, table.createdAt), index('idx_org_automation_usage_type_created_at').on(table.type, table.createdAt) ] ); export const courseImportDraft = pgTable( 'course_import_draft', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), organizationId: uuid('organization_id') .notNull() .references(() => organization.id, { onDelete: 'cascade' }), createdByProfileId: uuid('created_by_profile_id') .notNull() .references(() => profile.id), sourceType: courseImportSourceType('source_type').notNull(), status: courseImportDraftStatus('status').default('DRAFT').notNull(), title: varchar().notNull(), locale: locale().default('en').notNull(), idempotencyKey: varchar('idempotency_key'), summary: jsonb().default({}).notNull().$type>(), draft: jsonb().notNull().$type>(), warnings: jsonb().default([]).notNull().$type>>(), sourceArtifacts: jsonb().default([]).notNull().$type>>(), publishedCourseId: uuid('published_course_id').references(() => course.id, { onDelete: 'set null' }), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) .default(sql`timezone('utc'::text, now())`) .notNull(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) .default(sql`timezone('utc'::text, now())`) .notNull() }, (table) => [ index('idx_course_import_draft_org_id').on(table.organizationId), index('idx_course_import_draft_status').on(table.status), uniqueIndex('course_import_draft_org_idempotency_key') .on(table.organizationId, table.idempotencyKey) .where(sql`${table.idempotencyKey} IS NOT NULL`) ] ); // ─── Programs ──────────────────────────────────────────────────────────────── export const programStatus = pgEnum('PROGRAM_STATUS', ['ACTIVE', 'INACTIVE', 'ARCHIVED']); export const program = pgTable( 'program', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(), organizationId: uuid('organization_id').notNull(), name: varchar().notNull(), description: text(), coverImage: text('cover_image'), status: programStatus().default('ACTIVE').notNull(), createdByProfileId: uuid('created_by_profile_id') }, (table) => [ foreignKey({ columns: [table.organizationId], foreignColumns: [organization.id], name: 'program_organization_id_fkey' }).onDelete('cascade'), foreignKey({ columns: [table.createdByProfileId], foreignColumns: [profile.id], name: 'program_created_by_profile_id_fkey' }), index('idx_program_organization_id').on(table.organizationId) ] ); export const programCourse = pgTable( 'program_course', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), programId: uuid('program_id').notNull(), courseId: uuid('course_id').notNull(), addedAt: timestamp('added_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull() }, (table) => [ foreignKey({ columns: [table.programId], foreignColumns: [program.id], name: 'program_course_program_id_fkey' }).onDelete('cascade'), foreignKey({ columns: [table.courseId], foreignColumns: [course.id], name: 'program_course_course_id_fkey' }).onDelete('cascade'), unique('program_course_program_id_course_id_unique').on(table.programId, table.courseId), index('idx_program_course_program_id').on(table.programId) ] ); export const programMember = pgTable( 'program_member', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), programId: uuid('program_id').notNull(), profileId: uuid('profile_id'), roleId: integer('role_id').notNull(), email: text(), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull() }, (table) => [ foreignKey({ columns: [table.programId], foreignColumns: [program.id], name: 'program_member_program_id_fkey' }).onDelete('cascade'), foreignKey({ columns: [table.profileId], foreignColumns: [profile.id], name: 'program_member_profile_id_fkey' }), foreignKey({ columns: [table.roleId], foreignColumns: [role.id], name: 'program_member_role_id_fkey' }), unique('program_member_program_id_profile_id_unique').on(table.programId, table.profileId), index('idx_program_member_program_id').on(table.programId), index('idx_program_member_profile_id').on(table.profileId) ] ); export const programNewsfeed = pgTable( 'program_newsfeed', { id: uuid().defaultRandom().primaryKey().notNull(), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), authorId: uuid('author_id'), programId: uuid('program_id'), content: text(), reaction: jsonb() .default({ clap: [], smile: [], thumbsup: [], thumbsdown: [] }) .$type<{ clap: string[]; smile: string[]; thumbsup: string[]; thumbsdown: string[] }>(), isPinned: boolean('is_pinned').default(false).notNull() }, (table) => [ foreignKey({ columns: [table.authorId], foreignColumns: [programMember.id], name: 'program_newsfeed_author_id_fkey' }), foreignKey({ columns: [table.programId], foreignColumns: [program.id], name: 'program_newsfeed_program_id_fkey' }).onDelete('cascade'), index('idx_program_newsfeed_program_id').on(table.programId) ] ); export const programNewsfeedComment = pgTable( 'program_newsfeed_comment', { id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({ name: 'program_newsfeed_comment_id_seq', startWith: 1, increment: 1, minValue: 1, cache: 1 }), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), authorId: uuid('author_id'), content: text(), programNewsfeedId: uuid('program_newsfeed_id') }, (table) => [ foreignKey({ columns: [table.authorId], foreignColumns: [programMember.id], name: 'program_newsfeed_comment_author_id_fkey' }), foreignKey({ columns: [table.programNewsfeedId], foreignColumns: [programNewsfeed.id], name: 'program_newsfeed_comment_program_newsfeed_id_fkey' }).onDelete('cascade') ] ); // ─── Program Goals ──────────────────────────────────────────────────────────── // // A team-level "is the team done" target. A small generalisation of the // per-course `course.compliance` field: applies across multiple courses, scoped // to a program's members, and aggregated for owner roll-ups. Per-learner status // is materialised in `programGoalAssignment` so dashboards stay cheap. export const programGoalType = pgEnum('PROGRAM_GOAL_TYPE', [ 'complete_all', 'n_of_m', 'score', 'pass_rate', 'readiness' ]); export const programGoalDeadlineKind = pgEnum('PROGRAM_GOAL_DEADLINE_KIND', [ 'absolute', 'relative_to_join', 'recurring', 'none' ]); export const programGoalStatus = pgEnum('PROGRAM_GOAL_STATUS', ['active', 'archived']); export const programGoalAssignmentStatus = pgEnum('PROGRAM_GOAL_ASSIGNMENT_STATUS', [ 'not_started', 'in_progress', 'completed', 'at_risk', 'overdue', 'waived' ]); export const programGoal = pgTable( 'program_goal', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), programId: uuid('program_id').notNull(), title: varchar({ length: 255 }).notNull(), description: text(), type: programGoalType().notNull(), /** Course IDs the goal targets. JSONB so we can attach per-course config later if needed. */ courseIds: jsonb('course_ids').$type().default([]).notNull(), /** Required only for n_of_m. */ requiredCount: integer('required_count'), /** Per-learner score threshold (0–100). Required for `score` and `pass_rate`. */ scoreThreshold: integer('score_threshold'), /** Team-wide pass-rate threshold (0–100). Required for `pass_rate`. */ teamPassRateThreshold: integer('team_pass_rate_threshold'), deadlineKind: programGoalDeadlineKind('deadline_kind').default('none').notNull(), /** Used when deadlineKind = 'absolute'. */ deadlineDate: timestamp('deadline_date', { withTimezone: true, mode: 'string' }), /** Used when deadlineKind = 'relative_to_join'. */ relativeDays: integer('relative_days'), /** Used when deadlineKind = 'recurring'. */ recurringMonths: integer('recurring_months'), /** Reminder cadence (days before due). Mirrors `course.compliance.reminderDaysBefore`. */ reminderDaysBefore: jsonb('reminder_days_before').$type().default([7, 1]).notNull(), status: programGoalStatus().default('active').notNull(), createdByProfileId: uuid('created_by_profile_id'), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull() }, (table) => [ foreignKey({ columns: [table.programId], foreignColumns: [program.id], name: 'program_goal_program_id_fkey' }).onDelete('cascade'), foreignKey({ columns: [table.createdByProfileId], foreignColumns: [profile.id], name: 'program_goal_created_by_profile_id_fkey' }).onDelete('set null'), index('idx_program_goal_program_id').on(table.programId), index('idx_program_goal_status').on(table.status) ] ); export const programGoalAssignment = pgTable( 'program_goal_assignment', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), goalId: uuid('goal_id').notNull(), programMemberId: uuid('program_member_id').notNull(), /** Resolved due date for this learner (null when goal has no deadline). */ dueDate: timestamp('due_date', { withTimezone: true, mode: 'string' }), status: programGoalAssignmentStatus().default('not_started').notNull(), /** Cached count of completed required courses (for n_of_m / complete_all dashboards). */ completedCount: integer('completed_count').default(0).notNull(), /** Cached numerator for the requirement (e.g. courses required to complete). */ requiredCount: integer('required_count').default(0).notNull(), completedAt: timestamp('completed_at', { withTimezone: true, mode: 'string' }), lastEvaluatedAt: timestamp('last_evaluated_at', { withTimezone: true, mode: 'string' }), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull() }, (table) => [ foreignKey({ columns: [table.goalId], foreignColumns: [programGoal.id], name: 'program_goal_assignment_goal_id_fkey' }).onDelete('cascade'), foreignKey({ columns: [table.programMemberId], foreignColumns: [programMember.id], name: 'program_goal_assignment_program_member_id_fkey' }).onDelete('cascade'), unique('program_goal_assignment_goal_id_program_member_id_unique').on(table.goalId, table.programMemberId), index('idx_program_goal_assignment_goal_id').on(table.goalId), index('idx_program_goal_assignment_program_member_id').on(table.programMemberId), index('idx_program_goal_assignment_due_date').on(table.dueDate) ] ); // ─── AI Token Usage ────────────────────────────────────────────────────────── export const aiTokenUsage = pgTable( 'ai_token_usage', { id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({ name: 'ai_token_usage_id_seq', startWith: 1, increment: 1, minValue: 1, cache: 1 }), orgId: uuid('org_id').notNull(), userId: uuid('user_id').notNull(), courseId: uuid('course_id').notNull(), promptTokens: integer('prompt_tokens').notNull(), completionTokens: integer('completion_tokens').notNull(), costUnits: integer('cost_units'), model: text().notNull(), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull() }, (table) => [ foreignKey({ columns: [table.orgId], foreignColumns: [organization.id], name: 'ai_token_usage_org_id_fkey' }), index('idx_ai_token_usage_org_month').on(table.orgId, table.createdAt) ] ); // ─── AI Credit Balance ─────────────────────────────────────────────────────── export const aiCreditBalance = pgTable( 'ai_credit_balance', { id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({ name: 'ai_credit_balance_id_seq', startWith: 1, increment: 1, minValue: 1, cache: 1 }), orgId: uuid('org_id').notNull(), balance: integer().notNull().default(0), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull() }, (table) => [ foreignKey({ columns: [table.orgId], foreignColumns: [organization.id], name: 'ai_credit_balance_org_id_fkey' }), unique('ai_credit_balance_org_id_key').on(table.orgId) ] ); export const aiCreditPurchase = pgTable( 'ai_credit_purchase', { id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({ name: 'ai_credit_purchase_id_seq', startWith: 1, increment: 1, minValue: 1, cache: 1 }), orgId: uuid('org_id').notNull(), triggeredBy: uuid('triggered_by'), provider: text().notNull().default('polar'), providerOrderId: text('provider_order_id').notNull(), tokens: integer().notNull(), quantity: integer().notNull().default(1), unitPriceCents: integer('unit_price_cents').notNull().default(0), currency: text().notNull().default('USD'), payload: jsonb(), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull() }, (table) => [ foreignKey({ columns: [table.orgId], foreignColumns: [organization.id], name: 'ai_credit_purchase_org_id_fkey' }), foreignKey({ columns: [table.triggeredBy], foreignColumns: [profile.id], name: 'ai_credit_purchase_triggered_by_fkey' }), unique('ai_credit_purchase_provider_order_id_key').on(table.providerOrderId), index('idx_ai_credit_purchase_org_created').on(table.orgId, table.createdAt) ] ); // ─── AI Tutor Fair-Use ─────────────────────────────────────────────────────── export const aiTutorMessageCount = pgTable( 'ai_tutor_message_count', { id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({ name: 'ai_tutor_message_count_id_seq', startWith: 1, increment: 1, minValue: 1, cache: 1 }), orgId: uuid('org_id').notNull(), userId: uuid('user_id').notNull(), /** First day of the calendar month this counter covers. */ periodStart: date('period_start').notNull(), messageCount: integer('message_count').notNull().default(0), lastMessageAt: timestamp('last_message_at', { withTimezone: true, mode: 'string' }), capHitAt: timestamp('cap_hit_at', { withTimezone: true, mode: 'string' }), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull() }, (table) => [ foreignKey({ columns: [table.orgId], foreignColumns: [organization.id], name: 'ai_tutor_message_count_org_id_fkey' }).onDelete('cascade'), foreignKey({ columns: [table.userId], foreignColumns: [profile.id], name: 'ai_tutor_message_count_user_id_fkey' }).onDelete('cascade'), unique('ai_tutor_message_count_org_user_period_key').on(table.orgId, table.userId, table.periodStart), index('idx_ai_tutor_message_count_org_period').on(table.orgId, table.periodStart), index('idx_ai_tutor_message_count_user').on(table.userId, table.periodStart) ] ); export const aiTutorCapEvent = pgTable( 'ai_tutor_cap_event', { id: bigint({ mode: 'number' }).primaryKey().generatedByDefaultAsIdentity({ name: 'ai_tutor_cap_event_id_seq', startWith: 1, increment: 1, minValue: 1, cache: 1 }), orgId: uuid('org_id').notNull(), userId: uuid('user_id').notNull(), courseId: uuid('course_id'), /** 'cap_reached' | 'pool_exhausted' | 'tutor_disabled' */ eventType: text('event_type').notNull(), occurredAt: timestamp('occurred_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull() }, (table) => [ foreignKey({ columns: [table.orgId], foreignColumns: [organization.id], name: 'ai_tutor_cap_event_org_id_fkey' }).onDelete('cascade'), foreignKey({ columns: [table.userId], foreignColumns: [profile.id], name: 'ai_tutor_cap_event_user_id_fkey' }).onDelete('cascade'), index('idx_ai_tutor_cap_event_org_occurred').on(table.orgId, table.occurredAt), index('idx_ai_tutor_cap_event_user').on(table.userId, table.occurredAt) ] ); export const aiChatConversation = pgTable( 'ai_chat_conversation', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey(), courseId: uuid('course_id').notNull(), userId: uuid('user_id').notNull(), title: text().default('New conversation'), messages: jsonb().notNull().default([]), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull() }, (table) => [ foreignKey({ columns: [table.courseId], foreignColumns: [course.id], name: 'ai_chat_conversation_course_id_fkey' }), index('idx_ai_chat_conversation_course_user').on(table.courseId, table.userId) ] ); export const aiChatDocument = pgTable( 'ai_chat_document', { id: text().primaryKey(), conversationId: uuid('conversation_id').notNull(), courseId: uuid('course_id').notNull(), userId: uuid('user_id').notNull(), assetId: uuid('asset_id'), fileName: text('file_name').notNull(), mimeType: text('mime_type').notNull(), text: text().notNull(), wordCount: integer('word_count').notNull().default(0), pageCount: integer('page_count'), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull() }, (table) => [ foreignKey({ columns: [table.conversationId], foreignColumns: [aiChatConversation.id], name: 'ai_chat_document_conversation_id_fkey' }).onDelete('cascade'), index('idx_ai_chat_document_conversation').on(table.conversationId) ] ); // ─── BullMQ Job State (Postgres source of truth) ───────────────────────────── // User-visible status for queued/long-running domain runs. BullMQ holds the // operational job; these tables hold the entity the dashboard polls and // resumes against. See prd/notification-system and the bullmq plan. export const jobStatus = pgEnum('JOB_STATUS', ['queued', 'running', 'completed', 'failed', 'canceled']); /** * `media_job` — one user-facing media post-processing run (per uploaded asset). * The BullMQ FlowProducer orchestrates one or more attempts; this row stores * the latest status the dashboard polls. */ export const mediaJob = pgTable( 'media_job', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), organizationId: uuid('organization_id').notNull(), assetId: uuid('asset_id').notNull(), /** Snapshot of who started this run; never re-derived inside the worker. */ triggeredByProfileId: uuid('triggered_by_profile_id'), status: jobStatus().default('queued').notNull(), stage: varchar({ length: 64 }), progressPercent: integer('progress_percent').default(0), /** Latest BullMQ root job id from the FlowProducer tree. Operational only. */ rootJobId: text('root_job_id'), /** All BullMQ job ids for this run, keyed by job name (operational). */ jobIds: jsonb('job_ids').default({}).$type>(), attempt: integer().default(0).notNull(), maxAttempts: integer('max_attempts').default(3).notNull(), cancelRequestedAt: timestamp('cancel_requested_at', { withTimezone: true, mode: 'string' }), /** Provider/cost data once we wire transcription billing. */ costCents: integer('cost_cents').default(0), result: jsonb().$type | null>(), error: jsonb().$type<{ code: string; message: string; details?: unknown } | null>(), warnings: jsonb().default([]).$type(), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull() }, (table) => [ foreignKey({ columns: [table.organizationId], foreignColumns: [organization.id], name: 'media_job_organization_id_fkey' }) .onUpdate('cascade') .onDelete('cascade'), foreignKey({ columns: [table.assetId], foreignColumns: [asset.id], name: 'media_job_asset_id_fkey' }) .onUpdate('cascade') .onDelete('cascade'), foreignKey({ columns: [table.triggeredByProfileId], foreignColumns: [profile.id], name: 'media_job_triggered_by_profile_id_fkey' }).onDelete('set null'), index('idx_media_job_asset_id').on(table.assetId), index('idx_media_job_org_status').on(table.organizationId, table.status), index('idx_media_job_updated_at').on(table.updatedAt) ] ); /** * `job_step` — generic checkpoint ledger for any queued domain run. Workers * look up `(runId, stepKey)` first and short-circuit if the step has a result, * which makes BullMQ retries safe against partial completion. */ export const jobStep = pgTable( 'job_step', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), /** The owning domain (e.g. `media`, `course-import`, `onboarding-bootstrap`). */ domain: varchar({ length: 64 }).notNull(), /** Domain run id (e.g. `media_job.id`). */ runId: uuid('run_id').notNull(), /** Stable key for this step (e.g. `probe-metadata`). */ stepKey: varchar('step_key', { length: 96 }).notNull(), status: jobStatus().default('queued').notNull(), /** Hash of inputs to detect changed inputs across retries. */ inputHash: text('input_hash'), /** Provider id (whisper job id, ffmpeg run id, etc.) for de-dup. */ providerId: text('provider_id'), result: jsonb().$type | null>(), error: jsonb().$type<{ code: string; message: string; details?: unknown } | null>(), attempt: integer().default(0).notNull(), startedAt: timestamp('started_at', { withTimezone: true, mode: 'string' }), finishedAt: timestamp('finished_at', { withTimezone: true, mode: 'string' }), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull() }, (table) => [ unique('job_step_run_step_unique').on(table.domain, table.runId, table.stepKey), index('idx_job_step_run').on(table.domain, table.runId) ] ); /** * Whisper-derived transcript + captions artifact for an uploaded video asset. * One row per asset (re-transcribe replaces via upsert on asset_id). */ export const mediaTranscript = pgTable( 'media_transcript', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), organizationId: uuid('organization_id').notNull(), assetId: uuid('asset_id').notNull(), mediaJobId: uuid('media_job_id'), language: varchar({ length: 8 }).notNull(), provider: varchar({ length: 32 }).default('openai').notNull(), model: varchar({ length: 64 }).default('whisper-1').notNull(), text: text().notNull(), segments: jsonb().$type<{ start: number; end: number; text: string }[]>().notNull(), vttStorageKey: text('vtt_storage_key').notNull(), vttBucket: text('vtt_bucket').notNull(), durationSeconds: integer('duration_seconds'), costCents: integer('cost_cents').default(0).notNull(), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull() }, (table) => [ foreignKey({ columns: [table.organizationId], foreignColumns: [organization.id], name: 'media_transcript_organization_id_fkey' }) .onUpdate('cascade') .onDelete('cascade'), foreignKey({ columns: [table.assetId], foreignColumns: [asset.id], name: 'media_transcript_asset_id_fkey' }) .onUpdate('cascade') .onDelete('cascade'), foreignKey({ columns: [table.mediaJobId], foreignColumns: [mediaJob.id], name: 'media_transcript_media_job_id_fkey' }) .onUpdate('cascade') .onDelete('set null'), unique('media_transcript_asset_id_unique').on(table.assetId), index('idx_media_transcript_asset').on(table.assetId) ] ); /** * `dead_letter_job` — terminal failure record after BullMQ exhausts attempts * or a domain explicitly dead-letters a run. Drives the admin retry/replay UI * and Tinybird/Slack alerting. */ export const deadLetterJob = pgTable( 'dead_letter_job', { id: uuid() .default(sql`gen_random_uuid()`) .primaryKey() .notNull(), organizationId: uuid('organization_id'), domain: varchar({ length: 64 }).notNull(), runId: uuid('run_id'), queueName: varchar('queue_name', { length: 64 }).notNull(), jobName: varchar('job_name', { length: 96 }).notNull(), /** Original BullMQ job id. */ bullmqJobId: text('bullmq_job_id'), payload: jsonb().$type>(), error: jsonb().$type<{ code: string; message: string; stack?: string }>(), attempts: integer().default(0).notNull(), /** Optional pointer to a replay job once an operator retries. */ replayedAt: timestamp('replayed_at', { withTimezone: true, mode: 'string' }), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull() }, (table) => [ index('idx_dead_letter_job_domain_created').on(table.domain, table.createdAt), index('idx_dead_letter_job_org_created').on(table.organizationId, table.createdAt) ] );