From 41b65f209cc058e6342a021187fac83551da98da Mon Sep 17 00:00:00 2001 From: Bas van Rossem Date: Wed, 17 Jun 2026 14:06:16 +0200 Subject: [PATCH] fix(api): reconcile better-auth schema with installed better-auth CLI output The Task 3 schema drifted from what better-auth@1.6.18's CLI generates: it used mode:'timestamp' (epoch seconds) instead of 'timestamp_ms', left verification timestamps nullable, and omitted the default expressions and helper indexes. Regenerated src/db/schema.ts from `@better-auth/cli generate` (authoritative per the plan's version-drift rule) and rebuilt migration 0000 from scratch (no data exists yet). Converted the index callbacks from the CLI's array form to the object form required by drizzle-orm@0.36.4's types. Adds session_userId_idx, account_userId_idx, verification_identifier_idx and the unixepoch defaults. Tests (health, db, auth sign-up/sign-in, /api/me round-trip) all pass; typecheck clean; db:generate reports no pending changes. --- ...sis.sql => 0000_stiff_captain_britain.sql} | 16 +- apps/api/drizzle/meta/0000_snapshot.json | 51 +++++-- apps/api/drizzle/meta/_journal.json | 4 +- apps/api/src/db/schema.ts | 142 +++++++++++++----- 4 files changed, 153 insertions(+), 60 deletions(-) rename apps/api/drizzle/{0000_youthful_genesis.sql => 0000_stiff_captain_britain.sql} (62%) diff --git a/apps/api/drizzle/0000_youthful_genesis.sql b/apps/api/drizzle/0000_stiff_captain_britain.sql similarity index 62% rename from apps/api/drizzle/0000_youthful_genesis.sql rename to apps/api/drizzle/0000_stiff_captain_britain.sql index c682e97..dc3063d 100644 --- a/apps/api/drizzle/0000_youthful_genesis.sql +++ b/apps/api/drizzle/0000_stiff_captain_britain.sql @@ -10,16 +10,17 @@ CREATE TABLE `account` ( `refresh_token_expires_at` integer, `scope` text, `password` text, - `created_at` integer NOT NULL, + `created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL, `updated_at` integer NOT NULL, FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade ); --> statement-breakpoint +CREATE INDEX `account_userId_idx` ON `account` (`user_id`);--> statement-breakpoint CREATE TABLE `session` ( `id` text PRIMARY KEY NOT NULL, `expires_at` integer NOT NULL, `token` text NOT NULL, - `created_at` integer NOT NULL, + `created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL, `updated_at` integer NOT NULL, `ip_address` text, `user_agent` text, @@ -28,14 +29,15 @@ CREATE TABLE `session` ( ); --> statement-breakpoint CREATE UNIQUE INDEX `session_token_unique` ON `session` (`token`);--> statement-breakpoint +CREATE INDEX `session_userId_idx` ON `session` (`user_id`);--> statement-breakpoint CREATE TABLE `user` ( `id` text PRIMARY KEY NOT NULL, `name` text NOT NULL, `email` text NOT NULL, `email_verified` integer DEFAULT false NOT NULL, `image` text, - `created_at` integer NOT NULL, - `updated_at` integer NOT NULL + `created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL, + `updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL ); --> statement-breakpoint CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`);--> statement-breakpoint @@ -44,6 +46,8 @@ CREATE TABLE `verification` ( `identifier` text NOT NULL, `value` text NOT NULL, `expires_at` integer NOT NULL, - `created_at` integer, - `updated_at` integer + `created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL, + `updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL ); +--> statement-breakpoint +CREATE INDEX `verification_identifier_idx` ON `verification` (`identifier`); \ No newline at end of file diff --git a/apps/api/drizzle/meta/0000_snapshot.json b/apps/api/drizzle/meta/0000_snapshot.json index a9da6fe..bbb9ca7 100644 --- a/apps/api/drizzle/meta/0000_snapshot.json +++ b/apps/api/drizzle/meta/0000_snapshot.json @@ -1,7 +1,7 @@ { "version": "6", "dialect": "sqlite", - "id": "4b21759d-56d7-435a-a345-205a6cdc017e", + "id": "e8545186-09fa-4515-bca1-891ed8364a07", "prevId": "00000000-0000-0000-0000-000000000000", "tables": { "account": { @@ -89,7 +89,8 @@ "type": "integer", "primaryKey": false, "notNull": true, - "autoincrement": false + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" }, "updated_at": { "name": "updated_at", @@ -99,7 +100,15 @@ "autoincrement": false } }, - "indexes": {}, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, "foreignKeys": { "account_user_id_user_id_fk": { "name": "account_user_id_user_id_fk", @@ -148,7 +157,8 @@ "type": "integer", "primaryKey": false, "notNull": true, - "autoincrement": false + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" }, "updated_at": { "name": "updated_at", @@ -186,6 +196,13 @@ "token" ], "isUnique": true + }, + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false } }, "foreignKeys": { @@ -251,14 +268,16 @@ "type": "integer", "primaryKey": false, "notNull": true, - "autoincrement": false + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" }, "updated_at": { "name": "updated_at", "type": "integer", "primaryKey": false, "notNull": true, - "autoincrement": false + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" } }, "indexes": { @@ -310,18 +329,28 @@ "name": "created_at", "type": "integer", "primaryKey": false, - "notNull": false, - "autoincrement": false + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" }, "updated_at": { "name": "updated_at", "type": "integer", "primaryKey": false, - "notNull": false, - "autoincrement": false + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + "identifier" + ], + "isUnique": false } }, - "indexes": {}, "foreignKeys": {}, "compositePrimaryKeys": {}, "uniqueConstraints": {}, diff --git a/apps/api/drizzle/meta/_journal.json b/apps/api/drizzle/meta/_journal.json index d8a09cd..ef57a15 100644 --- a/apps/api/drizzle/meta/_journal.json +++ b/apps/api/drizzle/meta/_journal.json @@ -5,8 +5,8 @@ { "idx": 0, "version": "6", - "when": 1781696310538, - "tag": "0000_youthful_genesis", + "when": 1781697895207, + "tag": "0000_stiff_captain_britain", "breakpoints": true } ] diff --git a/apps/api/src/db/schema.ts b/apps/api/src/db/schema.ts index 50ecad0..3225bb0 100644 --- a/apps/api/src/db/schema.ts +++ b/apps/api/src/db/schema.ts @@ -1,51 +1,111 @@ -import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'; +import { relations, sql } from 'drizzle-orm'; +import { sqliteTable, text, integer, index } from 'drizzle-orm/sqlite-core'; + +// better-auth core tables. This schema is generated by the better-auth CLI +// (`npx @better-auth/cli generate --config src/auth.ts`) and is the AUTHORITATIVE +// shape for the installed better-auth version — do not hand-edit; regenerate instead. export const user = sqliteTable('user', { id: text('id').primaryKey(), name: text('name').notNull(), email: text('email').notNull().unique(), - emailVerified: integer('email_verified', { mode: 'boolean' }).notNull().default(false), + emailVerified: integer('email_verified', { mode: 'boolean' }).default(false).notNull(), image: text('image'), - createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), - updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(), + createdAt: integer('created_at', { mode: 'timestamp_ms' }) + .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) + .notNull(), + updatedAt: integer('updated_at', { mode: 'timestamp_ms' }) + .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) + .$onUpdate(() => new Date()) + .notNull(), }); -export const session = sqliteTable('session', { - id: text('id').primaryKey(), - expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(), - token: text('token').notNull().unique(), - createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), - updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(), - ipAddress: text('ip_address'), - userAgent: text('user_agent'), - userId: text('user_id') - .notNull() - .references(() => user.id, { onDelete: 'cascade' }), -}); +export const session = sqliteTable( + 'session', + { + id: text('id').primaryKey(), + expiresAt: integer('expires_at', { mode: 'timestamp_ms' }).notNull(), + token: text('token').notNull().unique(), + createdAt: integer('created_at', { mode: 'timestamp_ms' }) + .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) + .notNull(), + updatedAt: integer('updated_at', { mode: 'timestamp_ms' }) + .$onUpdate(() => new Date()) + .notNull(), + ipAddress: text('ip_address'), + userAgent: text('user_agent'), + userId: text('user_id') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }), + }, + (table) => ({ + sessionUserIdIdx: index('session_userId_idx').on(table.userId), + }) +); -export const account = sqliteTable('account', { - id: text('id').primaryKey(), - accountId: text('account_id').notNull(), - providerId: text('provider_id').notNull(), - userId: text('user_id') - .notNull() - .references(() => user.id, { onDelete: 'cascade' }), - accessToken: text('access_token'), - refreshToken: text('refresh_token'), - idToken: text('id_token'), - accessTokenExpiresAt: integer('access_token_expires_at', { mode: 'timestamp' }), - refreshTokenExpiresAt: integer('refresh_token_expires_at', { mode: 'timestamp' }), - scope: text('scope'), - password: text('password'), - createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), - updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(), -}); +export const account = sqliteTable( + 'account', + { + id: text('id').primaryKey(), + accountId: text('account_id').notNull(), + providerId: text('provider_id').notNull(), + userId: text('user_id') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }), + accessToken: text('access_token'), + refreshToken: text('refresh_token'), + idToken: text('id_token'), + accessTokenExpiresAt: integer('access_token_expires_at', { mode: 'timestamp_ms' }), + refreshTokenExpiresAt: integer('refresh_token_expires_at', { mode: 'timestamp_ms' }), + scope: text('scope'), + password: text('password'), + createdAt: integer('created_at', { mode: 'timestamp_ms' }) + .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) + .notNull(), + updatedAt: integer('updated_at', { mode: 'timestamp_ms' }) + .$onUpdate(() => new Date()) + .notNull(), + }, + (table) => ({ + accountUserIdIdx: index('account_userId_idx').on(table.userId), + }) +); -export const verification = sqliteTable('verification', { - id: text('id').primaryKey(), - identifier: text('identifier').notNull(), - value: text('value').notNull(), - expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(), - createdAt: integer('created_at', { mode: 'timestamp' }), - updatedAt: integer('updated_at', { mode: 'timestamp' }), -}); +export const verification = sqliteTable( + 'verification', + { + id: text('id').primaryKey(), + identifier: text('identifier').notNull(), + value: text('value').notNull(), + expiresAt: integer('expires_at', { mode: 'timestamp_ms' }).notNull(), + createdAt: integer('created_at', { mode: 'timestamp_ms' }) + .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) + .notNull(), + updatedAt: integer('updated_at', { mode: 'timestamp_ms' }) + .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) + .$onUpdate(() => new Date()) + .notNull(), + }, + (table) => ({ + verificationIdentifierIdx: index('verification_identifier_idx').on(table.identifier), + }) +); + +export const userRelations = relations(user, ({ many }) => ({ + sessions: many(session), + accounts: many(account), +})); + +export const sessionRelations = relations(session, ({ one }) => ({ + user: one(user, { + fields: [session.userId], + references: [user.id], + }), +})); + +export const accountRelations = relations(account, ({ one }) => ({ + user: one(user, { + fields: [account.userId], + references: [user.id], + }), +}));