@xcelsior/auth-adapter-knex 1.1.2 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Knex } from 'knex';
2
- import { IStorageProvider, CreateUserInput, User, UserId, UserFilter, FindUsersOptions, FindUsersResult, CreateSessionInput, Session, SessionId } from '@xcelsior/auth';
2
+ import { UserMeta, IStorageProvider, CreateUserInput, User, UserId, UserFilter, FindUsersOptions, FindUsersResult, CreateSessionInput, Session, SessionId } from '@xcelsior/auth';
3
3
 
4
4
  interface KnexConfig {
5
5
  /** Pre-configured Knex instance */
@@ -11,7 +11,7 @@ interface KnexConfig {
11
11
  /** Optional schema (for PostgreSQL) */
12
12
  schema?: string;
13
13
  }
14
- declare class KnexStorageProvider implements IStorageProvider {
14
+ declare class KnexStorageProvider<Meta extends UserMeta = Record<string, never>> implements IStorageProvider<Meta> {
15
15
  private knex;
16
16
  private tableName;
17
17
  private sessionsTableName;
@@ -19,14 +19,14 @@ declare class KnexStorageProvider implements IStorageProvider {
19
19
  constructor(config: KnexConfig);
20
20
  private get table();
21
21
  private get sessionsTable();
22
- createUser(user: CreateUserInput): Promise<User>;
23
- getUserById(id: UserId): Promise<User | null>;
24
- getUserByEmail(email: string): Promise<User | null>;
25
- getUserByResetPasswordToken(resetPasswordToken: string): Promise<User | null>;
26
- getUserByVerifyEmailToken(verifyEmailToken: string): Promise<User | null>;
27
- updateUser(id: UserId, updates: Partial<User>): Promise<void>;
22
+ createUser(user: CreateUserInput<Meta>): Promise<User<Meta>>;
23
+ getUserById(id: UserId): Promise<User<Meta> | null>;
24
+ getUserByEmail(email: string): Promise<User<Meta> | null>;
25
+ getUserByResetPasswordToken(resetPasswordToken: string): Promise<User<Meta> | null>;
26
+ getUserByVerifyEmailToken(verifyEmailToken: string): Promise<User<Meta> | null>;
27
+ updateUser(id: UserId, updates: Partial<User<Meta>>): Promise<void>;
28
28
  deleteUser(id: UserId): Promise<void>;
29
- findUsers(filter: UserFilter, options?: FindUsersOptions): Promise<FindUsersResult>;
29
+ findUsers(filter: UserFilter, options?: FindUsersOptions): Promise<FindUsersResult<Meta>>;
30
30
  createSession(session: CreateSessionInput): Promise<Session>;
31
31
  getSessionById(id: SessionId): Promise<Session | null>;
32
32
  getSessionsByUserId(userId: UserId): Promise<Session[]>;
@@ -39,15 +39,18 @@ declare class KnexStorageProvider implements IStorageProvider {
39
39
  */
40
40
  private toDateString;
41
41
  /**
42
- * Convert User object to database row format (snake_case)
42
+ * Convert User object to database row format (snake_case).
43
+ * Meta fields are flattened as individual snake_case columns.
43
44
  */
44
45
  private toDbUser;
45
46
  /**
46
- * Convert database row to User object (camelCase)
47
+ * Convert database row to User object (camelCase).
48
+ * Non-core columns are gathered into the typed `meta` object.
47
49
  */
48
50
  private fromDbUser;
49
51
  /**
50
- * Convert partial User updates to database format
52
+ * Convert partial User updates to database format.
53
+ * Meta fields in updates are flattened as individual snake_case columns.
51
54
  */
52
55
  private toDbUpdates;
53
56
  /**
@@ -68,6 +71,11 @@ declare class KnexStorageProvider implements IStorageProvider {
68
71
  * SQL migration helper to create the users table
69
72
  * Can be used with Knex migrations
70
73
  *
74
+ * @param knex - Knex instance
75
+ * @param tableName - Table name for users
76
+ * @param schemaOrCustomColumns - Optional schema name (for PostgreSQL) or callback to add custom columns
77
+ * @param customColumns - Optional callback to add custom columns to the table (when schema is provided)
78
+ *
71
79
  * @example
72
80
  * ```ts
73
81
  * // In your migration file
@@ -78,13 +86,30 @@ declare class KnexStorageProvider implements IStorageProvider {
78
86
  * await createSessionsTableMigration(knex, 'sessions');
79
87
  * }
80
88
  *
89
+ * // With custom meta columns
90
+ * export async function up(knex: Knex): Promise<void> {
91
+ * await createUsersTableMigration(knex, 'users', (table) => {
92
+ * table.string('phone').nullable();
93
+ * table.string('company').nullable();
94
+ * });
95
+ * await createSessionsTableMigration(knex, 'sessions');
96
+ * }
97
+ *
98
+ * // With schema and custom meta columns
99
+ * export async function up(knex: Knex): Promise<void> {
100
+ * await createUsersTableMigration(knex, 'users', 'my_schema', (table) => {
101
+ * table.string('phone').nullable();
102
+ * table.string('company').nullable();
103
+ * });
104
+ * }
105
+ *
81
106
  * export async function down(knex: Knex): Promise<void> {
82
107
  * await knex.schema.dropTableIfExists('sessions');
83
108
  * await knex.schema.dropTableIfExists('users');
84
109
  * }
85
110
  * ```
86
111
  */
87
- declare function createUsersTableMigration(knex: Knex, tableName: string, schema?: string): Promise<void>;
112
+ declare function createUsersTableMigration(knex: Knex, tableName: string, schemaOrCustomColumns?: string | ((table: Knex.CreateTableBuilder) => void), customColumns?: (table: Knex.CreateTableBuilder) => void): Promise<void>;
88
113
  /**
89
114
  * SQL migration helper to create the sessions table
90
115
  * Can be used with Knex migrations
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Knex } from 'knex';
2
- import { IStorageProvider, CreateUserInput, User, UserId, UserFilter, FindUsersOptions, FindUsersResult, CreateSessionInput, Session, SessionId } from '@xcelsior/auth';
2
+ import { UserMeta, IStorageProvider, CreateUserInput, User, UserId, UserFilter, FindUsersOptions, FindUsersResult, CreateSessionInput, Session, SessionId } from '@xcelsior/auth';
3
3
 
4
4
  interface KnexConfig {
5
5
  /** Pre-configured Knex instance */
@@ -11,7 +11,7 @@ interface KnexConfig {
11
11
  /** Optional schema (for PostgreSQL) */
12
12
  schema?: string;
13
13
  }
14
- declare class KnexStorageProvider implements IStorageProvider {
14
+ declare class KnexStorageProvider<Meta extends UserMeta = Record<string, never>> implements IStorageProvider<Meta> {
15
15
  private knex;
16
16
  private tableName;
17
17
  private sessionsTableName;
@@ -19,14 +19,14 @@ declare class KnexStorageProvider implements IStorageProvider {
19
19
  constructor(config: KnexConfig);
20
20
  private get table();
21
21
  private get sessionsTable();
22
- createUser(user: CreateUserInput): Promise<User>;
23
- getUserById(id: UserId): Promise<User | null>;
24
- getUserByEmail(email: string): Promise<User | null>;
25
- getUserByResetPasswordToken(resetPasswordToken: string): Promise<User | null>;
26
- getUserByVerifyEmailToken(verifyEmailToken: string): Promise<User | null>;
27
- updateUser(id: UserId, updates: Partial<User>): Promise<void>;
22
+ createUser(user: CreateUserInput<Meta>): Promise<User<Meta>>;
23
+ getUserById(id: UserId): Promise<User<Meta> | null>;
24
+ getUserByEmail(email: string): Promise<User<Meta> | null>;
25
+ getUserByResetPasswordToken(resetPasswordToken: string): Promise<User<Meta> | null>;
26
+ getUserByVerifyEmailToken(verifyEmailToken: string): Promise<User<Meta> | null>;
27
+ updateUser(id: UserId, updates: Partial<User<Meta>>): Promise<void>;
28
28
  deleteUser(id: UserId): Promise<void>;
29
- findUsers(filter: UserFilter, options?: FindUsersOptions): Promise<FindUsersResult>;
29
+ findUsers(filter: UserFilter, options?: FindUsersOptions): Promise<FindUsersResult<Meta>>;
30
30
  createSession(session: CreateSessionInput): Promise<Session>;
31
31
  getSessionById(id: SessionId): Promise<Session | null>;
32
32
  getSessionsByUserId(userId: UserId): Promise<Session[]>;
@@ -39,15 +39,18 @@ declare class KnexStorageProvider implements IStorageProvider {
39
39
  */
40
40
  private toDateString;
41
41
  /**
42
- * Convert User object to database row format (snake_case)
42
+ * Convert User object to database row format (snake_case).
43
+ * Meta fields are flattened as individual snake_case columns.
43
44
  */
44
45
  private toDbUser;
45
46
  /**
46
- * Convert database row to User object (camelCase)
47
+ * Convert database row to User object (camelCase).
48
+ * Non-core columns are gathered into the typed `meta` object.
47
49
  */
48
50
  private fromDbUser;
49
51
  /**
50
- * Convert partial User updates to database format
52
+ * Convert partial User updates to database format.
53
+ * Meta fields in updates are flattened as individual snake_case columns.
51
54
  */
52
55
  private toDbUpdates;
53
56
  /**
@@ -68,6 +71,11 @@ declare class KnexStorageProvider implements IStorageProvider {
68
71
  * SQL migration helper to create the users table
69
72
  * Can be used with Knex migrations
70
73
  *
74
+ * @param knex - Knex instance
75
+ * @param tableName - Table name for users
76
+ * @param schemaOrCustomColumns - Optional schema name (for PostgreSQL) or callback to add custom columns
77
+ * @param customColumns - Optional callback to add custom columns to the table (when schema is provided)
78
+ *
71
79
  * @example
72
80
  * ```ts
73
81
  * // In your migration file
@@ -78,13 +86,30 @@ declare class KnexStorageProvider implements IStorageProvider {
78
86
  * await createSessionsTableMigration(knex, 'sessions');
79
87
  * }
80
88
  *
89
+ * // With custom meta columns
90
+ * export async function up(knex: Knex): Promise<void> {
91
+ * await createUsersTableMigration(knex, 'users', (table) => {
92
+ * table.string('phone').nullable();
93
+ * table.string('company').nullable();
94
+ * });
95
+ * await createSessionsTableMigration(knex, 'sessions');
96
+ * }
97
+ *
98
+ * // With schema and custom meta columns
99
+ * export async function up(knex: Knex): Promise<void> {
100
+ * await createUsersTableMigration(knex, 'users', 'my_schema', (table) => {
101
+ * table.string('phone').nullable();
102
+ * table.string('company').nullable();
103
+ * });
104
+ * }
105
+ *
81
106
  * export async function down(knex: Knex): Promise<void> {
82
107
  * await knex.schema.dropTableIfExists('sessions');
83
108
  * await knex.schema.dropTableIfExists('users');
84
109
  * }
85
110
  * ```
86
111
  */
87
- declare function createUsersTableMigration(knex: Knex, tableName: string, schema?: string): Promise<void>;
112
+ declare function createUsersTableMigration(knex: Knex, tableName: string, schemaOrCustomColumns?: string | ((table: Knex.CreateTableBuilder) => void), customColumns?: (table: Knex.CreateTableBuilder) => void): Promise<void>;
88
113
  /**
89
114
  * SQL migration helper to create the sessions table
90
115
  * Can be used with Knex migrations
package/dist/index.js CHANGED
@@ -27,6 +27,26 @@ __export(index_exports, {
27
27
  module.exports = __toCommonJS(index_exports);
28
28
 
29
29
  // src/knex.ts
30
+ function camelToSnake(str) {
31
+ return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
32
+ }
33
+ function snakeToCamel(str) {
34
+ return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
35
+ }
36
+ var CORE_USER_DB_FIELDS = /* @__PURE__ */ new Set([
37
+ "id",
38
+ "email",
39
+ "first_name",
40
+ "last_name",
41
+ "password_hash",
42
+ "roles",
43
+ "is_email_verified",
44
+ "verification_token",
45
+ "reset_password_token",
46
+ "reset_password_expires",
47
+ "created_at",
48
+ "updated_at"
49
+ ]);
30
50
  var KnexStorageProvider = class {
31
51
  constructor(config) {
32
52
  this.knex = config.knex;
@@ -51,7 +71,8 @@ var KnexStorageProvider = class {
51
71
  // ==================== User Methods ====================
52
72
  async createUser(user) {
53
73
  const dbUser = this.toDbUser(user);
54
- const [insertedId] = await this.table.insert(dbUser);
74
+ const result = await this.table.insert(dbUser).returning("id");
75
+ const insertedId = result?.[0]?.id ?? result?.[0] ?? user.id;
55
76
  const finalId = user.id ?? insertedId;
56
77
  return { ...user, id: finalId };
57
78
  }
@@ -131,7 +152,8 @@ var KnexStorageProvider = class {
131
152
  // ==================== Session Methods ====================
132
153
  async createSession(session) {
133
154
  const dbSession = this.toDbSession(session);
134
- const [insertedId] = await this.sessionsTable.insert(dbSession);
155
+ const result = await this.sessionsTable.insert(dbSession).returning("id");
156
+ const insertedId = result?.[0]?.id ?? result?.[0] ?? session.id;
135
157
  const finalId = session.id ?? insertedId;
136
158
  return { ...session, id: finalId };
137
159
  }
@@ -169,7 +191,8 @@ var KnexStorageProvider = class {
169
191
  return String(value);
170
192
  }
171
193
  /**
172
- * Convert User object to database row format (snake_case)
194
+ * Convert User object to database row format (snake_case).
195
+ * Meta fields are flattened as individual snake_case columns.
173
196
  */
174
197
  toDbUser(user) {
175
198
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -189,12 +212,24 @@ var KnexStorageProvider = class {
189
212
  if (user.id !== void 0) {
190
213
  row.id = user.id;
191
214
  }
215
+ if (user.meta) {
216
+ for (const [key, value] of Object.entries(user.meta)) {
217
+ row[camelToSnake(key)] = value;
218
+ }
219
+ }
192
220
  return row;
193
221
  }
194
222
  /**
195
- * Convert database row to User object (camelCase)
223
+ * Convert database row to User object (camelCase).
224
+ * Non-core columns are gathered into the typed `meta` object.
196
225
  */
197
226
  fromDbUser(row) {
227
+ const meta = {};
228
+ for (const [key, value] of Object.entries(row)) {
229
+ if (!CORE_USER_DB_FIELDS.has(key)) {
230
+ meta[snakeToCamel(key)] = value;
231
+ }
232
+ }
198
233
  return {
199
234
  id: row.id,
200
235
  email: row.email,
@@ -207,11 +242,13 @@ var KnexStorageProvider = class {
207
242
  resetPasswordToken: row.reset_password_token,
208
243
  resetPasswordExpires: row.reset_password_expires ? this.toDateString(row.reset_password_expires) : void 0,
209
244
  createdAt: this.toDateString(row.created_at),
210
- updatedAt: this.toDateString(row.updated_at)
245
+ updatedAt: this.toDateString(row.updated_at),
246
+ ...Object.keys(meta).length > 0 ? { meta } : {}
211
247
  };
212
248
  }
213
249
  /**
214
- * Convert partial User updates to database format
250
+ * Convert partial User updates to database format.
251
+ * Meta fields in updates are flattened as individual snake_case columns.
215
252
  */
216
253
  toDbUpdates(updates) {
217
254
  const dbUpdates = {};
@@ -245,6 +282,11 @@ var KnexStorageProvider = class {
245
282
  if (updates.updatedAt !== void 0) {
246
283
  dbUpdates.updated_at = updates.updatedAt;
247
284
  }
285
+ if (updates.meta) {
286
+ for (const [key, value] of Object.entries(updates.meta)) {
287
+ dbUpdates[camelToSnake(key)] = value;
288
+ }
289
+ }
248
290
  return dbUpdates;
249
291
  }
250
292
  // ==================== Session Mapping Helpers ====================
@@ -311,7 +353,9 @@ var KnexStorageProvider = class {
311
353
  };
312
354
 
313
355
  // src/migration.ts
314
- async function createUsersTableMigration(knex, tableName, schema) {
356
+ async function createUsersTableMigration(knex, tableName, schemaOrCustomColumns, customColumns) {
357
+ const schema = typeof schemaOrCustomColumns === "string" ? schemaOrCustomColumns : void 0;
358
+ const addCustomColumns = typeof schemaOrCustomColumns === "function" ? schemaOrCustomColumns : customColumns;
315
359
  const schemaBuilder = schema ? knex.schema.withSchema(schema) : knex.schema;
316
360
  await schemaBuilder.createTable(tableName, (table) => {
317
361
  table.string("id").primary();
@@ -329,6 +373,9 @@ async function createUsersTableMigration(knex, tableName, schema) {
329
373
  table.index("email");
330
374
  table.index("verification_token");
331
375
  table.index("reset_password_token");
376
+ if (addCustomColumns) {
377
+ addCustomColumns(table);
378
+ }
332
379
  });
333
380
  }
334
381
  async function createSessionsTableMigration(knex, tableName, schema) {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/knex.ts","../src/migration.ts"],"sourcesContent":["export { KnexStorageProvider, type KnexConfig } from './knex';\nexport { createUsersTableMigration, createSessionsTableMigration } from './migration';\n","import type { Knex } from 'knex';\nimport type {\n CreateSessionInput,\n CreateUserInput,\n FindUsersOptions,\n FindUsersResult,\n IStorageProvider,\n Session,\n SessionId,\n User,\n UserFilter,\n UserId,\n} from '@xcelsior/auth';\n\nexport interface KnexConfig {\n /** Pre-configured Knex instance */\n knex: Knex;\n /** Table name for users */\n tableName: string;\n /** Table name for sessions */\n sessionsTableName: string;\n /** Optional schema (for PostgreSQL) */\n schema?: string;\n}\n\nexport class KnexStorageProvider implements IStorageProvider {\n private knex: Knex;\n private tableName: string;\n private sessionsTableName: string;\n private schema?: string;\n\n constructor(config: KnexConfig) {\n this.knex = config.knex;\n this.tableName = config.tableName;\n this.sessionsTableName = config.sessionsTableName;\n this.schema = config.schema;\n }\n\n private get table() {\n const query = this.knex(this.tableName);\n if (this.schema) {\n return query.withSchema(this.schema);\n }\n return query;\n }\n\n private get sessionsTable() {\n const query = this.knex(this.sessionsTableName);\n if (this.schema) {\n return query.withSchema(this.schema);\n }\n return query;\n }\n\n // ==================== User Methods ====================\n\n async createUser(user: CreateUserInput): Promise<User> {\n const dbUser = this.toDbUser(user);\n const [insertedId] = await this.table.insert(dbUser);\n // For auto-increment DBs, insertedId is the new ID\n // For string PKs, it returns 0, so use the original id\n const finalId = user.id ?? insertedId;\n return { ...user, id: finalId } as User;\n }\n\n async getUserById(id: UserId): Promise<User | null> {\n const row = await this.table.where({ id }).first();\n return row ? this.fromDbUser(row) : null;\n }\n\n async getUserByEmail(email: string): Promise<User | null> {\n const row = await this.table.where({ email }).first();\n return row ? this.fromDbUser(row) : null;\n }\n\n async getUserByResetPasswordToken(resetPasswordToken: string): Promise<User | null> {\n const row = await this.table.where({ reset_password_token: resetPasswordToken }).first();\n return row ? this.fromDbUser(row) : null;\n }\n\n async getUserByVerifyEmailToken(verifyEmailToken: string): Promise<User | null> {\n const row = await this.table.where({ verification_token: verifyEmailToken }).first();\n return row ? this.fromDbUser(row) : null;\n }\n\n async updateUser(id: UserId, updates: Partial<User>): Promise<void> {\n const dbUpdates = this.toDbUpdates(updates);\n\n if (Object.keys(dbUpdates).length === 0) {\n return;\n }\n\n await this.table.where({ id }).update(dbUpdates);\n }\n\n async deleteUser(id: UserId): Promise<void> {\n // Delete all user sessions first\n await this.deleteAllUserSessions(id);\n // Then delete the user\n await this.table.where({ id }).delete();\n }\n\n async findUsers(filter: UserFilter, options: FindUsersOptions = {}): Promise<FindUsersResult> {\n const limit = options.limit ?? 50;\n\n let query = this.table.clone();\n\n // Email exact match\n if (filter.email) {\n query = query.where('email', filter.email);\n }\n\n // Email contains (partial match)\n if (filter.emailContains) {\n query = query.where('email', 'like', `%${filter.emailContains}%`);\n }\n\n // Email verification status\n if (filter.isEmailVerified !== undefined) {\n query = query.where('is_email_verified', filter.isEmailVerified);\n }\n\n // Roles filtering - user must have ALL specified roles\n if (filter.roles && filter.roles.length > 0) {\n for (const role of filter.roles) {\n // Use JSON contains - works for PostgreSQL (jsonb) and MySQL (json)\n // For SQLite, we fall back to LIKE on the JSON string\n query = query.whereRaw(\n this.knex.client.config.client === 'sqlite3' ? `roles LIKE ?` : `roles @> ?`,\n this.knex.client.config.client === 'sqlite3'\n ? [`%\"${role}\"%`]\n : [JSON.stringify([role])]\n );\n }\n }\n\n // HasAnyRole - user must have at least ONE of specified roles\n if (filter.hasAnyRole && filter.hasAnyRole.length > 0) {\n query = query.where((builder: Knex.QueryBuilder) => {\n for (const role of filter.hasAnyRole!) {\n builder.orWhereRaw(\n this.knex.client.config.client === 'sqlite3'\n ? `roles LIKE ?`\n : `roles @> ?`,\n this.knex.client.config.client === 'sqlite3'\n ? [`%\"${role}\"%`]\n : [JSON.stringify([role])]\n );\n }\n });\n }\n\n // Apply pagination\n if (options.cursor) {\n const cursorData = JSON.parse(Buffer.from(options.cursor, 'base64').toString());\n query = query.where('id', '>', cursorData.lastId);\n }\n\n // Order by id for consistent pagination\n query = query.orderBy('id', 'asc').limit(limit + 1);\n\n const rows = await query;\n const hasMore = rows.length > limit;\n const resultRows = hasMore ? rows.slice(0, limit) : rows;\n const users = resultRows.map((row: Record<string, unknown>) => this.fromDbUser(row));\n\n let nextCursor: string | undefined;\n if (hasMore && resultRows.length > 0) {\n const lastUser = resultRows[resultRows.length - 1];\n nextCursor = Buffer.from(JSON.stringify({ lastId: lastUser.id })).toString('base64');\n }\n\n return { users, nextCursor };\n }\n\n // ==================== Session Methods ====================\n\n async createSession(session: CreateSessionInput): Promise<Session> {\n const dbSession = this.toDbSession(session);\n const [insertedId] = await this.sessionsTable.insert(dbSession);\n const finalId = session.id ?? insertedId;\n return { ...session, id: finalId } as Session;\n }\n\n async getSessionById(id: SessionId): Promise<Session | null> {\n const row = await this.sessionsTable.where({ id }).first();\n return row ? this.fromDbSession(row) : null;\n }\n\n async getSessionsByUserId(userId: UserId): Promise<Session[]> {\n const rows = await this.sessionsTable.where({ user_id: userId });\n return rows.map((row: Record<string, unknown>) => this.fromDbSession(row));\n }\n\n async updateSession(id: SessionId, updates: Partial<Session>): Promise<void> {\n const dbUpdates = this.toDbSessionUpdates(updates);\n\n if (Object.keys(dbUpdates).length === 0) {\n return;\n }\n\n await this.sessionsTable.where({ id }).update(dbUpdates);\n }\n\n async deleteSession(id: SessionId): Promise<void> {\n await this.sessionsTable.where({ id }).delete();\n }\n\n async deleteAllUserSessions(userId: UserId): Promise<void> {\n await this.sessionsTable.where({ user_id: userId }).delete();\n }\n\n async deleteExpiredSessions(): Promise<void> {\n const now = new Date().toISOString();\n await this.sessionsTable.where('expires_at', '<', now).delete();\n }\n\n // ==================== User Mapping Helpers ====================\n\n /**\n * Safely convert a DB date value (Date object or string) to ISO 8601 string\n */\n private toDateString(value: unknown): string {\n if (value instanceof Date) return value.toISOString();\n return String(value);\n }\n\n /**\n * Convert User object to database row format (snake_case)\n */\n private toDbUser(user: CreateUserInput): Record<string, unknown> {\n const now = new Date().toISOString();\n const row: Record<string, unknown> = {\n email: user.email,\n first_name: user.firstName ?? null,\n last_name: user.lastName ?? null,\n password_hash: user.passwordHash,\n roles: JSON.stringify(user.roles),\n is_email_verified: user.isEmailVerified ? 1 : 0,\n verification_token: user.verificationToken ?? null,\n reset_password_token: user.resetPasswordToken ?? null,\n reset_password_expires: user.resetPasswordExpires ?? null,\n created_at: user.createdAt ?? now,\n updated_at: user.updatedAt ?? now,\n };\n // Only include id if provided (for string UUIDs)\n // Omit for auto-increment DBs\n if (user.id !== undefined) {\n row.id = user.id;\n }\n return row;\n }\n\n /**\n * Convert database row to User object (camelCase)\n */\n private fromDbUser(row: Record<string, unknown>): User {\n return {\n id: row.id as string | number,\n email: row.email as string,\n firstName: row.first_name as string | undefined,\n lastName: row.last_name as string | undefined,\n passwordHash: row.password_hash as string,\n roles: typeof row.roles === 'string' ? JSON.parse(row.roles) : row.roles,\n isEmailVerified: Boolean(row.is_email_verified),\n verificationToken: row.verification_token as string | undefined,\n resetPasswordToken: row.reset_password_token as string | undefined,\n resetPasswordExpires: row.reset_password_expires\n ? this.toDateString(row.reset_password_expires)\n : undefined,\n createdAt: this.toDateString(row.created_at),\n updatedAt: this.toDateString(row.updated_at),\n };\n }\n\n /**\n * Convert partial User updates to database format\n */\n private toDbUpdates(updates: Partial<User>): Record<string, unknown> {\n const dbUpdates: Record<string, unknown> = {};\n\n if (updates.email !== undefined) {\n dbUpdates.email = updates.email;\n }\n if ('firstName' in updates) {\n dbUpdates.first_name = updates.firstName ?? null;\n }\n if ('lastName' in updates) {\n dbUpdates.last_name = updates.lastName ?? null;\n }\n if (updates.passwordHash !== undefined) {\n dbUpdates.password_hash = updates.passwordHash;\n }\n if (updates.roles !== undefined) {\n dbUpdates.roles = JSON.stringify(updates.roles);\n }\n if (updates.isEmailVerified !== undefined) {\n dbUpdates.is_email_verified = updates.isEmailVerified ? 1 : 0;\n }\n if ('verificationToken' in updates) {\n dbUpdates.verification_token = updates.verificationToken ?? null;\n }\n if ('resetPasswordToken' in updates) {\n dbUpdates.reset_password_token = updates.resetPasswordToken ?? null;\n }\n if ('resetPasswordExpires' in updates) {\n dbUpdates.reset_password_expires = updates.resetPasswordExpires ?? null;\n }\n if (updates.updatedAt !== undefined) {\n dbUpdates.updated_at = updates.updatedAt;\n }\n\n return dbUpdates;\n }\n\n // ==================== Session Mapping Helpers ====================\n\n /**\n * Convert Session object to database row format (snake_case)\n */\n private toDbSession(session: CreateSessionInput): Record<string, unknown> {\n const row: Record<string, unknown> = {\n user_id: session.userId,\n refresh_token_hash: session.refreshTokenHash,\n user_agent: session.userAgent ?? null,\n ip_address: session.ipAddress ?? null,\n device_name: session.deviceName ?? null,\n created_at: session.createdAt,\n last_used_at: session.lastUsedAt,\n expires_at: session.expiresAt,\n };\n // Only include id if provided (for string UUIDs)\n // Omit for auto-increment DBs\n if (session.id !== undefined) {\n row.id = session.id;\n }\n return row;\n }\n\n /**\n * Convert database row to Session object (camelCase)\n */\n private fromDbSession(row: Record<string, unknown>): Session {\n return {\n id: row.id as string | number,\n userId: row.user_id as string | number,\n refreshTokenHash: row.refresh_token_hash as string,\n userAgent: row.user_agent as string | undefined,\n ipAddress: row.ip_address as string | undefined,\n deviceName: row.device_name as string | undefined,\n createdAt: this.toDateString(row.created_at),\n lastUsedAt: this.toDateString(row.last_used_at),\n expiresAt: this.toDateString(row.expires_at),\n };\n }\n\n /**\n * Convert partial Session updates to database format\n */\n private toDbSessionUpdates(updates: Partial<Session>): Record<string, unknown> {\n const dbUpdates: Record<string, unknown> = {};\n\n if (updates.refreshTokenHash !== undefined) {\n dbUpdates.refresh_token_hash = updates.refreshTokenHash;\n }\n if ('userAgent' in updates) {\n dbUpdates.user_agent = updates.userAgent ?? null;\n }\n if ('ipAddress' in updates) {\n dbUpdates.ip_address = updates.ipAddress ?? null;\n }\n if ('deviceName' in updates) {\n dbUpdates.device_name = updates.deviceName ?? null;\n }\n if (updates.lastUsedAt !== undefined) {\n dbUpdates.last_used_at = updates.lastUsedAt;\n }\n if (updates.expiresAt !== undefined) {\n dbUpdates.expires_at = updates.expiresAt;\n }\n\n return dbUpdates;\n }\n}\n","import type { Knex } from 'knex';\n\n/**\n * SQL migration helper to create the users table\n * Can be used with Knex migrations\n *\n * @example\n * ```ts\n * // In your migration file\n * import { createUsersTableMigration, createSessionsTableMigration } from '@xcelsior/auth-adapter-knex';\n *\n * export async function up(knex: Knex): Promise<void> {\n * await createUsersTableMigration(knex, 'users');\n * await createSessionsTableMigration(knex, 'sessions');\n * }\n *\n * export async function down(knex: Knex): Promise<void> {\n * await knex.schema.dropTableIfExists('sessions');\n * await knex.schema.dropTableIfExists('users');\n * }\n * ```\n */\nexport async function createUsersTableMigration(\n knex: Knex,\n tableName: string,\n schema?: string\n): Promise<void> {\n const schemaBuilder = schema ? knex.schema.withSchema(schema) : knex.schema;\n\n await schemaBuilder.createTable(tableName, (table) => {\n table.string('id').primary();\n table.string('email').notNullable().unique();\n table.string('first_name').nullable();\n table.string('last_name').nullable();\n table.string('password_hash').notNullable();\n table.jsonb('roles').notNullable();\n table.boolean('is_email_verified').notNullable().defaultTo(false);\n table.string('verification_token').nullable();\n table.string('reset_password_token').nullable();\n table.timestamp('reset_password_expires', { useTz: true }).nullable();\n table.timestamp('created_at', { useTz: true }).notNullable();\n table.timestamp('updated_at', { useTz: true }).notNullable();\n\n // Indexes for common lookups\n table.index('email');\n table.index('verification_token');\n table.index('reset_password_token');\n });\n}\n\n/**\n * SQL migration helper to create the sessions table\n * Can be used with Knex migrations\n */\nexport async function createSessionsTableMigration(\n knex: Knex,\n tableName: string,\n schema?: string\n): Promise<void> {\n const schemaBuilder = schema ? knex.schema.withSchema(schema) : knex.schema;\n\n await schemaBuilder.createTable(tableName, (table) => {\n table.string('id').primary();\n table.string('user_id').notNullable();\n table.string('refresh_token_hash').notNullable();\n table.string('user_agent').nullable();\n table.string('ip_address').nullable();\n table.string('device_name').nullable();\n table.timestamp('created_at', { useTz: true }).notNullable();\n table.timestamp('last_used_at', { useTz: true }).notNullable();\n table.timestamp('expires_at', { useTz: true }).notNullable();\n\n // Index for querying sessions by user\n table.index('user_id');\n // Index for cleaning up expired sessions\n table.index('expires_at');\n });\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/knex.ts","../src/migration.ts"],"sourcesContent":["export { KnexStorageProvider, type KnexConfig } from './knex';\nexport { createUsersTableMigration, createSessionsTableMigration } from './migration';\n","import type { Knex } from 'knex';\nimport type {\n CreateSessionInput,\n CreateUserInput,\n FindUsersOptions,\n FindUsersResult,\n IStorageProvider,\n Session,\n SessionId,\n User,\n UserFilter,\n UserId,\n UserMeta,\n} from '@xcelsior/auth';\n\n/** Convert camelCase string to snake_case */\nfunction camelToSnake(str: string): string {\n return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);\n}\n\n/** Convert snake_case string to camelCase */\nfunction snakeToCamel(str: string): string {\n return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());\n}\n\n/** Core user DB columns that are not part of meta */\nconst CORE_USER_DB_FIELDS = new Set([\n 'id',\n 'email',\n 'first_name',\n 'last_name',\n 'password_hash',\n 'roles',\n 'is_email_verified',\n 'verification_token',\n 'reset_password_token',\n 'reset_password_expires',\n 'created_at',\n 'updated_at',\n]);\n\nexport interface KnexConfig {\n /** Pre-configured Knex instance */\n knex: Knex;\n /** Table name for users */\n tableName: string;\n /** Table name for sessions */\n sessionsTableName: string;\n /** Optional schema (for PostgreSQL) */\n schema?: string;\n}\n\nexport class KnexStorageProvider<Meta extends UserMeta = Record<string, never>>\n implements IStorageProvider<Meta>\n{\n private knex: Knex;\n private tableName: string;\n private sessionsTableName: string;\n private schema?: string;\n\n constructor(config: KnexConfig) {\n this.knex = config.knex;\n this.tableName = config.tableName;\n this.sessionsTableName = config.sessionsTableName;\n this.schema = config.schema;\n }\n\n private get table() {\n const query = this.knex(this.tableName);\n if (this.schema) {\n return query.withSchema(this.schema);\n }\n return query;\n }\n\n private get sessionsTable() {\n const query = this.knex(this.sessionsTableName);\n if (this.schema) {\n return query.withSchema(this.schema);\n }\n return query;\n }\n\n // ==================== User Methods ====================\n\n async createUser(user: CreateUserInput<Meta>): Promise<User<Meta>> {\n const dbUser = this.toDbUser(user);\n const result = await this.table.insert(dbUser).returning('id');\n // PostgreSQL returns [{ id }] from .returning(), SQLite returns [insertedId]\n const insertedId = result?.[0]?.id ?? result?.[0] ?? user.id;\n const finalId = user.id ?? insertedId;\n return { ...user, id: finalId } as User<Meta>;\n }\n\n async getUserById(id: UserId): Promise<User<Meta> | null> {\n const row = await this.table.where({ id }).first();\n return row ? this.fromDbUser(row) : null;\n }\n\n async getUserByEmail(email: string): Promise<User<Meta> | null> {\n const row = await this.table.where({ email }).first();\n return row ? this.fromDbUser(row) : null;\n }\n\n async getUserByResetPasswordToken(resetPasswordToken: string): Promise<User<Meta> | null> {\n const row = await this.table.where({ reset_password_token: resetPasswordToken }).first();\n return row ? this.fromDbUser(row) : null;\n }\n\n async getUserByVerifyEmailToken(verifyEmailToken: string): Promise<User<Meta> | null> {\n const row = await this.table.where({ verification_token: verifyEmailToken }).first();\n return row ? this.fromDbUser(row) : null;\n }\n\n async updateUser(id: UserId, updates: Partial<User<Meta>>): Promise<void> {\n const dbUpdates = this.toDbUpdates(updates);\n\n if (Object.keys(dbUpdates).length === 0) {\n return;\n }\n\n await this.table.where({ id }).update(dbUpdates);\n }\n\n async deleteUser(id: UserId): Promise<void> {\n // Delete all user sessions first\n await this.deleteAllUserSessions(id);\n // Then delete the user\n await this.table.where({ id }).delete();\n }\n\n async findUsers(filter: UserFilter, options: FindUsersOptions = {}): Promise<FindUsersResult<Meta>> {\n const limit = options.limit ?? 50;\n\n let query = this.table.clone();\n\n // Email exact match\n if (filter.email) {\n query = query.where('email', filter.email);\n }\n\n // Email contains (partial match)\n if (filter.emailContains) {\n query = query.where('email', 'like', `%${filter.emailContains}%`);\n }\n\n // Email verification status\n if (filter.isEmailVerified !== undefined) {\n query = query.where('is_email_verified', filter.isEmailVerified);\n }\n\n // Roles filtering - user must have ALL specified roles\n if (filter.roles && filter.roles.length > 0) {\n for (const role of filter.roles) {\n // Use JSON contains - works for PostgreSQL (jsonb) and MySQL (json)\n // For SQLite, we fall back to LIKE on the JSON string\n query = query.whereRaw(\n this.knex.client.config.client === 'sqlite3' ? `roles LIKE ?` : `roles @> ?`,\n this.knex.client.config.client === 'sqlite3'\n ? [`%\"${role}\"%`]\n : [JSON.stringify([role])]\n );\n }\n }\n\n // HasAnyRole - user must have at least ONE of specified roles\n if (filter.hasAnyRole && filter.hasAnyRole.length > 0) {\n query = query.where((builder: Knex.QueryBuilder) => {\n for (const role of filter.hasAnyRole!) {\n builder.orWhereRaw(\n this.knex.client.config.client === 'sqlite3'\n ? `roles LIKE ?`\n : `roles @> ?`,\n this.knex.client.config.client === 'sqlite3'\n ? [`%\"${role}\"%`]\n : [JSON.stringify([role])]\n );\n }\n });\n }\n\n // Apply pagination\n if (options.cursor) {\n const cursorData = JSON.parse(Buffer.from(options.cursor, 'base64').toString());\n query = query.where('id', '>', cursorData.lastId);\n }\n\n // Order by id for consistent pagination\n query = query.orderBy('id', 'asc').limit(limit + 1);\n\n const rows = await query;\n const hasMore = rows.length > limit;\n const resultRows = hasMore ? rows.slice(0, limit) : rows;\n const users = resultRows.map((row: Record<string, unknown>) => this.fromDbUser(row));\n\n let nextCursor: string | undefined;\n if (hasMore && resultRows.length > 0) {\n const lastUser = resultRows[resultRows.length - 1];\n nextCursor = Buffer.from(JSON.stringify({ lastId: lastUser.id })).toString('base64');\n }\n\n return { users, nextCursor } as FindUsersResult<Meta>;\n }\n\n // ==================== Session Methods ====================\n\n async createSession(session: CreateSessionInput): Promise<Session> {\n const dbSession = this.toDbSession(session);\n const result = await this.sessionsTable.insert(dbSession).returning('id');\n // PostgreSQL returns [{ id }] from .returning(), SQLite returns [insertedId]\n const insertedId = result?.[0]?.id ?? result?.[0] ?? session.id;\n const finalId = session.id ?? insertedId;\n return { ...session, id: finalId } as Session;\n }\n\n async getSessionById(id: SessionId): Promise<Session | null> {\n const row = await this.sessionsTable.where({ id }).first();\n return row ? this.fromDbSession(row) : null;\n }\n\n async getSessionsByUserId(userId: UserId): Promise<Session[]> {\n const rows = await this.sessionsTable.where({ user_id: userId });\n return rows.map((row: Record<string, unknown>) => this.fromDbSession(row));\n }\n\n async updateSession(id: SessionId, updates: Partial<Session>): Promise<void> {\n const dbUpdates = this.toDbSessionUpdates(updates);\n\n if (Object.keys(dbUpdates).length === 0) {\n return;\n }\n\n await this.sessionsTable.where({ id }).update(dbUpdates);\n }\n\n async deleteSession(id: SessionId): Promise<void> {\n await this.sessionsTable.where({ id }).delete();\n }\n\n async deleteAllUserSessions(userId: UserId): Promise<void> {\n await this.sessionsTable.where({ user_id: userId }).delete();\n }\n\n async deleteExpiredSessions(): Promise<void> {\n const now = new Date().toISOString();\n await this.sessionsTable.where('expires_at', '<', now).delete();\n }\n\n // ==================== User Mapping Helpers ====================\n\n /**\n * Safely convert a DB date value (Date object or string) to ISO 8601 string\n */\n private toDateString(value: unknown): string {\n if (value instanceof Date) return value.toISOString();\n return String(value);\n }\n\n /**\n * Convert User object to database row format (snake_case).\n * Meta fields are flattened as individual snake_case columns.\n */\n private toDbUser(user: CreateUserInput<Meta>): Record<string, unknown> {\n const now = new Date().toISOString();\n const row: Record<string, unknown> = {\n\n email: user.email,\n first_name: user.firstName ?? null,\n last_name: user.lastName ?? null,\n password_hash: user.passwordHash,\n roles: JSON.stringify(user.roles),\n is_email_verified: user.isEmailVerified ? 1 : 0,\n verification_token: user.verificationToken ?? null,\n reset_password_token: user.resetPasswordToken ?? null,\n reset_password_expires: user.resetPasswordExpires ?? null,\n created_at: user.createdAt ?? now,\n updated_at: user.updatedAt ?? now,\n };\n // Only include id if provided (for string UUIDs)\n // Omit for auto-increment DBs\n if (user.id !== undefined) {\n row.id = user.id;\n }\n // Flatten meta fields as individual snake_case columns\n if (user.meta) {\n for (const [key, value] of Object.entries(user.meta)) {\n row[camelToSnake(key)] = value;\n }\n }\n return row;\n }\n\n /**\n * Convert database row to User object (camelCase).\n * Non-core columns are gathered into the typed `meta` object.\n */\n private fromDbUser(row: Record<string, unknown>): User<Meta> {\n const meta: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(row)) {\n if (!CORE_USER_DB_FIELDS.has(key)) {\n meta[snakeToCamel(key)] = value;\n }\n }\n\n return {\n id: row.id as string | number,\n email: row.email as string,\n firstName: row.first_name as string | undefined,\n lastName: row.last_name as string | undefined,\n passwordHash: row.password_hash as string,\n roles: typeof row.roles === 'string' ? JSON.parse(row.roles) : row.roles,\n isEmailVerified: Boolean(row.is_email_verified),\n verificationToken: row.verification_token as string | undefined,\n resetPasswordToken: row.reset_password_token as string | undefined,\n resetPasswordExpires: row.reset_password_expires\n ? this.toDateString(row.reset_password_expires)\n : undefined,\n createdAt: this.toDateString(row.created_at),\n updatedAt: this.toDateString(row.updated_at),\n ...(Object.keys(meta).length > 0 ? { meta: meta as Meta } : {}),\n } as User<Meta>;\n }\n\n /**\n * Convert partial User updates to database format.\n * Meta fields in updates are flattened as individual snake_case columns.\n */\n private toDbUpdates(updates: Partial<User<Meta>>): Record<string, unknown> {\n const dbUpdates: Record<string, unknown> = {};\n\n if (updates.email !== undefined) {\n dbUpdates.email = updates.email;\n }\n if ('firstName' in updates) {\n dbUpdates.first_name = updates.firstName ?? null;\n }\n if ('lastName' in updates) {\n dbUpdates.last_name = updates.lastName ?? null;\n }\n if (updates.passwordHash !== undefined) {\n dbUpdates.password_hash = updates.passwordHash;\n }\n if (updates.roles !== undefined) {\n dbUpdates.roles = JSON.stringify(updates.roles);\n }\n if (updates.isEmailVerified !== undefined) {\n dbUpdates.is_email_verified = updates.isEmailVerified ? 1 : 0;\n }\n if ('verificationToken' in updates) {\n dbUpdates.verification_token = updates.verificationToken ?? null;\n }\n if ('resetPasswordToken' in updates) {\n dbUpdates.reset_password_token = updates.resetPasswordToken ?? null;\n }\n if ('resetPasswordExpires' in updates) {\n dbUpdates.reset_password_expires = updates.resetPasswordExpires ?? null;\n }\n if (updates.updatedAt !== undefined) {\n dbUpdates.updated_at = updates.updatedAt;\n }\n // Flatten meta fields as individual snake_case columns\n if (updates.meta) {\n for (const [key, value] of Object.entries(updates.meta)) {\n dbUpdates[camelToSnake(key)] = value;\n }\n }\n\n return dbUpdates;\n }\n\n // ==================== Session Mapping Helpers ====================\n\n /**\n * Convert Session object to database row format (snake_case)\n */\n private toDbSession(session: CreateSessionInput): Record<string, unknown> {\n const row: Record<string, unknown> = {\n user_id: session.userId,\n refresh_token_hash: session.refreshTokenHash,\n user_agent: session.userAgent ?? null,\n ip_address: session.ipAddress ?? null,\n device_name: session.deviceName ?? null,\n created_at: session.createdAt,\n last_used_at: session.lastUsedAt,\n expires_at: session.expiresAt,\n };\n // Only include id if provided (for string UUIDs)\n // Omit for auto-increment DBs\n if (session.id !== undefined) {\n row.id = session.id;\n }\n return row;\n }\n\n /**\n * Convert database row to Session object (camelCase)\n */\n private fromDbSession(row: Record<string, unknown>): Session {\n return {\n id: row.id as string | number,\n userId: row.user_id as string | number,\n refreshTokenHash: row.refresh_token_hash as string,\n userAgent: row.user_agent as string | undefined,\n ipAddress: row.ip_address as string | undefined,\n deviceName: row.device_name as string | undefined,\n createdAt: this.toDateString(row.created_at),\n lastUsedAt: this.toDateString(row.last_used_at),\n expiresAt: this.toDateString(row.expires_at),\n };\n }\n\n /**\n * Convert partial Session updates to database format\n */\n private toDbSessionUpdates(updates: Partial<Session>): Record<string, unknown> {\n const dbUpdates: Record<string, unknown> = {};\n\n if (updates.refreshTokenHash !== undefined) {\n dbUpdates.refresh_token_hash = updates.refreshTokenHash;\n }\n if ('userAgent' in updates) {\n dbUpdates.user_agent = updates.userAgent ?? null;\n }\n if ('ipAddress' in updates) {\n dbUpdates.ip_address = updates.ipAddress ?? null;\n }\n if ('deviceName' in updates) {\n dbUpdates.device_name = updates.deviceName ?? null;\n }\n if (updates.lastUsedAt !== undefined) {\n dbUpdates.last_used_at = updates.lastUsedAt;\n }\n if (updates.expiresAt !== undefined) {\n dbUpdates.expires_at = updates.expiresAt;\n }\n\n return dbUpdates;\n }\n}\n","import type { Knex } from 'knex';\n\n/**\n * SQL migration helper to create the users table\n * Can be used with Knex migrations\n *\n * @param knex - Knex instance\n * @param tableName - Table name for users\n * @param schemaOrCustomColumns - Optional schema name (for PostgreSQL) or callback to add custom columns\n * @param customColumns - Optional callback to add custom columns to the table (when schema is provided)\n *\n * @example\n * ```ts\n * // In your migration file\n * import { createUsersTableMigration, createSessionsTableMigration } from '@xcelsior/auth-adapter-knex';\n *\n * export async function up(knex: Knex): Promise<void> {\n * await createUsersTableMigration(knex, 'users');\n * await createSessionsTableMigration(knex, 'sessions');\n * }\n *\n * // With custom meta columns\n * export async function up(knex: Knex): Promise<void> {\n * await createUsersTableMigration(knex, 'users', (table) => {\n * table.string('phone').nullable();\n * table.string('company').nullable();\n * });\n * await createSessionsTableMigration(knex, 'sessions');\n * }\n *\n * // With schema and custom meta columns\n * export async function up(knex: Knex): Promise<void> {\n * await createUsersTableMigration(knex, 'users', 'my_schema', (table) => {\n * table.string('phone').nullable();\n * table.string('company').nullable();\n * });\n * }\n *\n * export async function down(knex: Knex): Promise<void> {\n * await knex.schema.dropTableIfExists('sessions');\n * await knex.schema.dropTableIfExists('users');\n * }\n * ```\n */\nexport async function createUsersTableMigration(\n knex: Knex,\n tableName: string,\n schemaOrCustomColumns?: string | ((table: Knex.CreateTableBuilder) => void),\n customColumns?: (table: Knex.CreateTableBuilder) => void\n): Promise<void> {\n const schema = typeof schemaOrCustomColumns === 'string' ? schemaOrCustomColumns : undefined;\n const addCustomColumns =\n typeof schemaOrCustomColumns === 'function' ? schemaOrCustomColumns : customColumns;\n const schemaBuilder = schema ? knex.schema.withSchema(schema) : knex.schema;\n\n await schemaBuilder.createTable(tableName, (table) => {\n table.string('id').primary();\n table.string('email').notNullable().unique();\n table.string('first_name').nullable();\n table.string('last_name').nullable();\n table.string('password_hash').notNullable();\n table.jsonb('roles').notNullable();\n table.boolean('is_email_verified').notNullable().defaultTo(false);\n table.string('verification_token').nullable();\n table.string('reset_password_token').nullable();\n table.timestamp('reset_password_expires', { useTz: true }).nullable();\n table.timestamp('created_at', { useTz: true }).notNullable();\n table.timestamp('updated_at', { useTz: true }).notNullable();\n\n // Indexes for common lookups\n table.index('email');\n table.index('verification_token');\n table.index('reset_password_token');\n\n // Add custom meta columns if provided\n if (addCustomColumns) {\n addCustomColumns(table);\n }\n });\n}\n\n/**\n * SQL migration helper to create the sessions table\n * Can be used with Knex migrations\n */\nexport async function createSessionsTableMigration(\n knex: Knex,\n tableName: string,\n schema?: string\n): Promise<void> {\n const schemaBuilder = schema ? knex.schema.withSchema(schema) : knex.schema;\n\n await schemaBuilder.createTable(tableName, (table) => {\n table.string('id').primary();\n table.string('user_id').notNullable();\n table.string('refresh_token_hash').notNullable();\n table.string('user_agent').nullable();\n table.string('ip_address').nullable();\n table.string('device_name').nullable();\n table.timestamp('created_at', { useTz: true }).notNullable();\n table.timestamp('last_used_at', { useTz: true }).notNullable();\n table.timestamp('expires_at', { useTz: true }).notNullable();\n\n // Index for querying sessions by user\n table.index('user_id');\n // Index for cleaning up expired sessions\n table.index('expires_at');\n });\n}\n"],"mappings":"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","names":[]}
package/dist/index.mjs CHANGED
@@ -1,4 +1,24 @@
1
1
  // src/knex.ts
2
+ function camelToSnake(str) {
3
+ return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
4
+ }
5
+ function snakeToCamel(str) {
6
+ return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
7
+ }
8
+ var CORE_USER_DB_FIELDS = /* @__PURE__ */ new Set([
9
+ "id",
10
+ "email",
11
+ "first_name",
12
+ "last_name",
13
+ "password_hash",
14
+ "roles",
15
+ "is_email_verified",
16
+ "verification_token",
17
+ "reset_password_token",
18
+ "reset_password_expires",
19
+ "created_at",
20
+ "updated_at"
21
+ ]);
2
22
  var KnexStorageProvider = class {
3
23
  constructor(config) {
4
24
  this.knex = config.knex;
@@ -23,7 +43,8 @@ var KnexStorageProvider = class {
23
43
  // ==================== User Methods ====================
24
44
  async createUser(user) {
25
45
  const dbUser = this.toDbUser(user);
26
- const [insertedId] = await this.table.insert(dbUser);
46
+ const result = await this.table.insert(dbUser).returning("id");
47
+ const insertedId = result?.[0]?.id ?? result?.[0] ?? user.id;
27
48
  const finalId = user.id ?? insertedId;
28
49
  return { ...user, id: finalId };
29
50
  }
@@ -103,7 +124,8 @@ var KnexStorageProvider = class {
103
124
  // ==================== Session Methods ====================
104
125
  async createSession(session) {
105
126
  const dbSession = this.toDbSession(session);
106
- const [insertedId] = await this.sessionsTable.insert(dbSession);
127
+ const result = await this.sessionsTable.insert(dbSession).returning("id");
128
+ const insertedId = result?.[0]?.id ?? result?.[0] ?? session.id;
107
129
  const finalId = session.id ?? insertedId;
108
130
  return { ...session, id: finalId };
109
131
  }
@@ -141,7 +163,8 @@ var KnexStorageProvider = class {
141
163
  return String(value);
142
164
  }
143
165
  /**
144
- * Convert User object to database row format (snake_case)
166
+ * Convert User object to database row format (snake_case).
167
+ * Meta fields are flattened as individual snake_case columns.
145
168
  */
146
169
  toDbUser(user) {
147
170
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -161,12 +184,24 @@ var KnexStorageProvider = class {
161
184
  if (user.id !== void 0) {
162
185
  row.id = user.id;
163
186
  }
187
+ if (user.meta) {
188
+ for (const [key, value] of Object.entries(user.meta)) {
189
+ row[camelToSnake(key)] = value;
190
+ }
191
+ }
164
192
  return row;
165
193
  }
166
194
  /**
167
- * Convert database row to User object (camelCase)
195
+ * Convert database row to User object (camelCase).
196
+ * Non-core columns are gathered into the typed `meta` object.
168
197
  */
169
198
  fromDbUser(row) {
199
+ const meta = {};
200
+ for (const [key, value] of Object.entries(row)) {
201
+ if (!CORE_USER_DB_FIELDS.has(key)) {
202
+ meta[snakeToCamel(key)] = value;
203
+ }
204
+ }
170
205
  return {
171
206
  id: row.id,
172
207
  email: row.email,
@@ -179,11 +214,13 @@ var KnexStorageProvider = class {
179
214
  resetPasswordToken: row.reset_password_token,
180
215
  resetPasswordExpires: row.reset_password_expires ? this.toDateString(row.reset_password_expires) : void 0,
181
216
  createdAt: this.toDateString(row.created_at),
182
- updatedAt: this.toDateString(row.updated_at)
217
+ updatedAt: this.toDateString(row.updated_at),
218
+ ...Object.keys(meta).length > 0 ? { meta } : {}
183
219
  };
184
220
  }
185
221
  /**
186
- * Convert partial User updates to database format
222
+ * Convert partial User updates to database format.
223
+ * Meta fields in updates are flattened as individual snake_case columns.
187
224
  */
188
225
  toDbUpdates(updates) {
189
226
  const dbUpdates = {};
@@ -217,6 +254,11 @@ var KnexStorageProvider = class {
217
254
  if (updates.updatedAt !== void 0) {
218
255
  dbUpdates.updated_at = updates.updatedAt;
219
256
  }
257
+ if (updates.meta) {
258
+ for (const [key, value] of Object.entries(updates.meta)) {
259
+ dbUpdates[camelToSnake(key)] = value;
260
+ }
261
+ }
220
262
  return dbUpdates;
221
263
  }
222
264
  // ==================== Session Mapping Helpers ====================
@@ -283,7 +325,9 @@ var KnexStorageProvider = class {
283
325
  };
284
326
 
285
327
  // src/migration.ts
286
- async function createUsersTableMigration(knex, tableName, schema) {
328
+ async function createUsersTableMigration(knex, tableName, schemaOrCustomColumns, customColumns) {
329
+ const schema = typeof schemaOrCustomColumns === "string" ? schemaOrCustomColumns : void 0;
330
+ const addCustomColumns = typeof schemaOrCustomColumns === "function" ? schemaOrCustomColumns : customColumns;
287
331
  const schemaBuilder = schema ? knex.schema.withSchema(schema) : knex.schema;
288
332
  await schemaBuilder.createTable(tableName, (table) => {
289
333
  table.string("id").primary();
@@ -301,6 +345,9 @@ async function createUsersTableMigration(knex, tableName, schema) {
301
345
  table.index("email");
302
346
  table.index("verification_token");
303
347
  table.index("reset_password_token");
348
+ if (addCustomColumns) {
349
+ addCustomColumns(table);
350
+ }
304
351
  });
305
352
  }
306
353
  async function createSessionsTableMigration(knex, tableName, schema) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/knex.ts","../src/migration.ts"],"sourcesContent":["import type { Knex } from 'knex';\nimport type {\n CreateSessionInput,\n CreateUserInput,\n FindUsersOptions,\n FindUsersResult,\n IStorageProvider,\n Session,\n SessionId,\n User,\n UserFilter,\n UserId,\n} from '@xcelsior/auth';\n\nexport interface KnexConfig {\n /** Pre-configured Knex instance */\n knex: Knex;\n /** Table name for users */\n tableName: string;\n /** Table name for sessions */\n sessionsTableName: string;\n /** Optional schema (for PostgreSQL) */\n schema?: string;\n}\n\nexport class KnexStorageProvider implements IStorageProvider {\n private knex: Knex;\n private tableName: string;\n private sessionsTableName: string;\n private schema?: string;\n\n constructor(config: KnexConfig) {\n this.knex = config.knex;\n this.tableName = config.tableName;\n this.sessionsTableName = config.sessionsTableName;\n this.schema = config.schema;\n }\n\n private get table() {\n const query = this.knex(this.tableName);\n if (this.schema) {\n return query.withSchema(this.schema);\n }\n return query;\n }\n\n private get sessionsTable() {\n const query = this.knex(this.sessionsTableName);\n if (this.schema) {\n return query.withSchema(this.schema);\n }\n return query;\n }\n\n // ==================== User Methods ====================\n\n async createUser(user: CreateUserInput): Promise<User> {\n const dbUser = this.toDbUser(user);\n const [insertedId] = await this.table.insert(dbUser);\n // For auto-increment DBs, insertedId is the new ID\n // For string PKs, it returns 0, so use the original id\n const finalId = user.id ?? insertedId;\n return { ...user, id: finalId } as User;\n }\n\n async getUserById(id: UserId): Promise<User | null> {\n const row = await this.table.where({ id }).first();\n return row ? this.fromDbUser(row) : null;\n }\n\n async getUserByEmail(email: string): Promise<User | null> {\n const row = await this.table.where({ email }).first();\n return row ? this.fromDbUser(row) : null;\n }\n\n async getUserByResetPasswordToken(resetPasswordToken: string): Promise<User | null> {\n const row = await this.table.where({ reset_password_token: resetPasswordToken }).first();\n return row ? this.fromDbUser(row) : null;\n }\n\n async getUserByVerifyEmailToken(verifyEmailToken: string): Promise<User | null> {\n const row = await this.table.where({ verification_token: verifyEmailToken }).first();\n return row ? this.fromDbUser(row) : null;\n }\n\n async updateUser(id: UserId, updates: Partial<User>): Promise<void> {\n const dbUpdates = this.toDbUpdates(updates);\n\n if (Object.keys(dbUpdates).length === 0) {\n return;\n }\n\n await this.table.where({ id }).update(dbUpdates);\n }\n\n async deleteUser(id: UserId): Promise<void> {\n // Delete all user sessions first\n await this.deleteAllUserSessions(id);\n // Then delete the user\n await this.table.where({ id }).delete();\n }\n\n async findUsers(filter: UserFilter, options: FindUsersOptions = {}): Promise<FindUsersResult> {\n const limit = options.limit ?? 50;\n\n let query = this.table.clone();\n\n // Email exact match\n if (filter.email) {\n query = query.where('email', filter.email);\n }\n\n // Email contains (partial match)\n if (filter.emailContains) {\n query = query.where('email', 'like', `%${filter.emailContains}%`);\n }\n\n // Email verification status\n if (filter.isEmailVerified !== undefined) {\n query = query.where('is_email_verified', filter.isEmailVerified);\n }\n\n // Roles filtering - user must have ALL specified roles\n if (filter.roles && filter.roles.length > 0) {\n for (const role of filter.roles) {\n // Use JSON contains - works for PostgreSQL (jsonb) and MySQL (json)\n // For SQLite, we fall back to LIKE on the JSON string\n query = query.whereRaw(\n this.knex.client.config.client === 'sqlite3' ? `roles LIKE ?` : `roles @> ?`,\n this.knex.client.config.client === 'sqlite3'\n ? [`%\"${role}\"%`]\n : [JSON.stringify([role])]\n );\n }\n }\n\n // HasAnyRole - user must have at least ONE of specified roles\n if (filter.hasAnyRole && filter.hasAnyRole.length > 0) {\n query = query.where((builder: Knex.QueryBuilder) => {\n for (const role of filter.hasAnyRole!) {\n builder.orWhereRaw(\n this.knex.client.config.client === 'sqlite3'\n ? `roles LIKE ?`\n : `roles @> ?`,\n this.knex.client.config.client === 'sqlite3'\n ? [`%\"${role}\"%`]\n : [JSON.stringify([role])]\n );\n }\n });\n }\n\n // Apply pagination\n if (options.cursor) {\n const cursorData = JSON.parse(Buffer.from(options.cursor, 'base64').toString());\n query = query.where('id', '>', cursorData.lastId);\n }\n\n // Order by id for consistent pagination\n query = query.orderBy('id', 'asc').limit(limit + 1);\n\n const rows = await query;\n const hasMore = rows.length > limit;\n const resultRows = hasMore ? rows.slice(0, limit) : rows;\n const users = resultRows.map((row: Record<string, unknown>) => this.fromDbUser(row));\n\n let nextCursor: string | undefined;\n if (hasMore && resultRows.length > 0) {\n const lastUser = resultRows[resultRows.length - 1];\n nextCursor = Buffer.from(JSON.stringify({ lastId: lastUser.id })).toString('base64');\n }\n\n return { users, nextCursor };\n }\n\n // ==================== Session Methods ====================\n\n async createSession(session: CreateSessionInput): Promise<Session> {\n const dbSession = this.toDbSession(session);\n const [insertedId] = await this.sessionsTable.insert(dbSession);\n const finalId = session.id ?? insertedId;\n return { ...session, id: finalId } as Session;\n }\n\n async getSessionById(id: SessionId): Promise<Session | null> {\n const row = await this.sessionsTable.where({ id }).first();\n return row ? this.fromDbSession(row) : null;\n }\n\n async getSessionsByUserId(userId: UserId): Promise<Session[]> {\n const rows = await this.sessionsTable.where({ user_id: userId });\n return rows.map((row: Record<string, unknown>) => this.fromDbSession(row));\n }\n\n async updateSession(id: SessionId, updates: Partial<Session>): Promise<void> {\n const dbUpdates = this.toDbSessionUpdates(updates);\n\n if (Object.keys(dbUpdates).length === 0) {\n return;\n }\n\n await this.sessionsTable.where({ id }).update(dbUpdates);\n }\n\n async deleteSession(id: SessionId): Promise<void> {\n await this.sessionsTable.where({ id }).delete();\n }\n\n async deleteAllUserSessions(userId: UserId): Promise<void> {\n await this.sessionsTable.where({ user_id: userId }).delete();\n }\n\n async deleteExpiredSessions(): Promise<void> {\n const now = new Date().toISOString();\n await this.sessionsTable.where('expires_at', '<', now).delete();\n }\n\n // ==================== User Mapping Helpers ====================\n\n /**\n * Safely convert a DB date value (Date object or string) to ISO 8601 string\n */\n private toDateString(value: unknown): string {\n if (value instanceof Date) return value.toISOString();\n return String(value);\n }\n\n /**\n * Convert User object to database row format (snake_case)\n */\n private toDbUser(user: CreateUserInput): Record<string, unknown> {\n const now = new Date().toISOString();\n const row: Record<string, unknown> = {\n email: user.email,\n first_name: user.firstName ?? null,\n last_name: user.lastName ?? null,\n password_hash: user.passwordHash,\n roles: JSON.stringify(user.roles),\n is_email_verified: user.isEmailVerified ? 1 : 0,\n verification_token: user.verificationToken ?? null,\n reset_password_token: user.resetPasswordToken ?? null,\n reset_password_expires: user.resetPasswordExpires ?? null,\n created_at: user.createdAt ?? now,\n updated_at: user.updatedAt ?? now,\n };\n // Only include id if provided (for string UUIDs)\n // Omit for auto-increment DBs\n if (user.id !== undefined) {\n row.id = user.id;\n }\n return row;\n }\n\n /**\n * Convert database row to User object (camelCase)\n */\n private fromDbUser(row: Record<string, unknown>): User {\n return {\n id: row.id as string | number,\n email: row.email as string,\n firstName: row.first_name as string | undefined,\n lastName: row.last_name as string | undefined,\n passwordHash: row.password_hash as string,\n roles: typeof row.roles === 'string' ? JSON.parse(row.roles) : row.roles,\n isEmailVerified: Boolean(row.is_email_verified),\n verificationToken: row.verification_token as string | undefined,\n resetPasswordToken: row.reset_password_token as string | undefined,\n resetPasswordExpires: row.reset_password_expires\n ? this.toDateString(row.reset_password_expires)\n : undefined,\n createdAt: this.toDateString(row.created_at),\n updatedAt: this.toDateString(row.updated_at),\n };\n }\n\n /**\n * Convert partial User updates to database format\n */\n private toDbUpdates(updates: Partial<User>): Record<string, unknown> {\n const dbUpdates: Record<string, unknown> = {};\n\n if (updates.email !== undefined) {\n dbUpdates.email = updates.email;\n }\n if ('firstName' in updates) {\n dbUpdates.first_name = updates.firstName ?? null;\n }\n if ('lastName' in updates) {\n dbUpdates.last_name = updates.lastName ?? null;\n }\n if (updates.passwordHash !== undefined) {\n dbUpdates.password_hash = updates.passwordHash;\n }\n if (updates.roles !== undefined) {\n dbUpdates.roles = JSON.stringify(updates.roles);\n }\n if (updates.isEmailVerified !== undefined) {\n dbUpdates.is_email_verified = updates.isEmailVerified ? 1 : 0;\n }\n if ('verificationToken' in updates) {\n dbUpdates.verification_token = updates.verificationToken ?? null;\n }\n if ('resetPasswordToken' in updates) {\n dbUpdates.reset_password_token = updates.resetPasswordToken ?? null;\n }\n if ('resetPasswordExpires' in updates) {\n dbUpdates.reset_password_expires = updates.resetPasswordExpires ?? null;\n }\n if (updates.updatedAt !== undefined) {\n dbUpdates.updated_at = updates.updatedAt;\n }\n\n return dbUpdates;\n }\n\n // ==================== Session Mapping Helpers ====================\n\n /**\n * Convert Session object to database row format (snake_case)\n */\n private toDbSession(session: CreateSessionInput): Record<string, unknown> {\n const row: Record<string, unknown> = {\n user_id: session.userId,\n refresh_token_hash: session.refreshTokenHash,\n user_agent: session.userAgent ?? null,\n ip_address: session.ipAddress ?? null,\n device_name: session.deviceName ?? null,\n created_at: session.createdAt,\n last_used_at: session.lastUsedAt,\n expires_at: session.expiresAt,\n };\n // Only include id if provided (for string UUIDs)\n // Omit for auto-increment DBs\n if (session.id !== undefined) {\n row.id = session.id;\n }\n return row;\n }\n\n /**\n * Convert database row to Session object (camelCase)\n */\n private fromDbSession(row: Record<string, unknown>): Session {\n return {\n id: row.id as string | number,\n userId: row.user_id as string | number,\n refreshTokenHash: row.refresh_token_hash as string,\n userAgent: row.user_agent as string | undefined,\n ipAddress: row.ip_address as string | undefined,\n deviceName: row.device_name as string | undefined,\n createdAt: this.toDateString(row.created_at),\n lastUsedAt: this.toDateString(row.last_used_at),\n expiresAt: this.toDateString(row.expires_at),\n };\n }\n\n /**\n * Convert partial Session updates to database format\n */\n private toDbSessionUpdates(updates: Partial<Session>): Record<string, unknown> {\n const dbUpdates: Record<string, unknown> = {};\n\n if (updates.refreshTokenHash !== undefined) {\n dbUpdates.refresh_token_hash = updates.refreshTokenHash;\n }\n if ('userAgent' in updates) {\n dbUpdates.user_agent = updates.userAgent ?? null;\n }\n if ('ipAddress' in updates) {\n dbUpdates.ip_address = updates.ipAddress ?? null;\n }\n if ('deviceName' in updates) {\n dbUpdates.device_name = updates.deviceName ?? null;\n }\n if (updates.lastUsedAt !== undefined) {\n dbUpdates.last_used_at = updates.lastUsedAt;\n }\n if (updates.expiresAt !== undefined) {\n dbUpdates.expires_at = updates.expiresAt;\n }\n\n return dbUpdates;\n }\n}\n","import type { Knex } from 'knex';\n\n/**\n * SQL migration helper to create the users table\n * Can be used with Knex migrations\n *\n * @example\n * ```ts\n * // In your migration file\n * import { createUsersTableMigration, createSessionsTableMigration } from '@xcelsior/auth-adapter-knex';\n *\n * export async function up(knex: Knex): Promise<void> {\n * await createUsersTableMigration(knex, 'users');\n * await createSessionsTableMigration(knex, 'sessions');\n * }\n *\n * export async function down(knex: Knex): Promise<void> {\n * await knex.schema.dropTableIfExists('sessions');\n * await knex.schema.dropTableIfExists('users');\n * }\n * ```\n */\nexport async function createUsersTableMigration(\n knex: Knex,\n tableName: string,\n schema?: string\n): Promise<void> {\n const schemaBuilder = schema ? knex.schema.withSchema(schema) : knex.schema;\n\n await schemaBuilder.createTable(tableName, (table) => {\n table.string('id').primary();\n table.string('email').notNullable().unique();\n table.string('first_name').nullable();\n table.string('last_name').nullable();\n table.string('password_hash').notNullable();\n table.jsonb('roles').notNullable();\n table.boolean('is_email_verified').notNullable().defaultTo(false);\n table.string('verification_token').nullable();\n table.string('reset_password_token').nullable();\n table.timestamp('reset_password_expires', { useTz: true }).nullable();\n table.timestamp('created_at', { useTz: true }).notNullable();\n table.timestamp('updated_at', { useTz: true }).notNullable();\n\n // Indexes for common lookups\n table.index('email');\n table.index('verification_token');\n table.index('reset_password_token');\n });\n}\n\n/**\n * SQL migration helper to create the sessions table\n * Can be used with Knex migrations\n */\nexport async function createSessionsTableMigration(\n knex: Knex,\n tableName: string,\n schema?: string\n): Promise<void> {\n const schemaBuilder = schema ? knex.schema.withSchema(schema) : knex.schema;\n\n await schemaBuilder.createTable(tableName, (table) => {\n table.string('id').primary();\n table.string('user_id').notNullable();\n table.string('refresh_token_hash').notNullable();\n table.string('user_agent').nullable();\n table.string('ip_address').nullable();\n table.string('device_name').nullable();\n table.timestamp('created_at', { useTz: true }).notNullable();\n table.timestamp('last_used_at', { useTz: true }).notNullable();\n table.timestamp('expires_at', { useTz: true }).notNullable();\n\n // Index for querying sessions by user\n table.index('user_id');\n // Index for cleaning up expired sessions\n table.index('expires_at');\n });\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/knex.ts","../src/migration.ts"],"sourcesContent":["import type { Knex } from 'knex';\nimport type {\n CreateSessionInput,\n CreateUserInput,\n FindUsersOptions,\n FindUsersResult,\n IStorageProvider,\n Session,\n SessionId,\n User,\n UserFilter,\n UserId,\n UserMeta,\n} from '@xcelsior/auth';\n\n/** Convert camelCase string to snake_case */\nfunction camelToSnake(str: string): string {\n return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);\n}\n\n/** Convert snake_case string to camelCase */\nfunction snakeToCamel(str: string): string {\n return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());\n}\n\n/** Core user DB columns that are not part of meta */\nconst CORE_USER_DB_FIELDS = new Set([\n 'id',\n 'email',\n 'first_name',\n 'last_name',\n 'password_hash',\n 'roles',\n 'is_email_verified',\n 'verification_token',\n 'reset_password_token',\n 'reset_password_expires',\n 'created_at',\n 'updated_at',\n]);\n\nexport interface KnexConfig {\n /** Pre-configured Knex instance */\n knex: Knex;\n /** Table name for users */\n tableName: string;\n /** Table name for sessions */\n sessionsTableName: string;\n /** Optional schema (for PostgreSQL) */\n schema?: string;\n}\n\nexport class KnexStorageProvider<Meta extends UserMeta = Record<string, never>>\n implements IStorageProvider<Meta>\n{\n private knex: Knex;\n private tableName: string;\n private sessionsTableName: string;\n private schema?: string;\n\n constructor(config: KnexConfig) {\n this.knex = config.knex;\n this.tableName = config.tableName;\n this.sessionsTableName = config.sessionsTableName;\n this.schema = config.schema;\n }\n\n private get table() {\n const query = this.knex(this.tableName);\n if (this.schema) {\n return query.withSchema(this.schema);\n }\n return query;\n }\n\n private get sessionsTable() {\n const query = this.knex(this.sessionsTableName);\n if (this.schema) {\n return query.withSchema(this.schema);\n }\n return query;\n }\n\n // ==================== User Methods ====================\n\n async createUser(user: CreateUserInput<Meta>): Promise<User<Meta>> {\n const dbUser = this.toDbUser(user);\n const result = await this.table.insert(dbUser).returning('id');\n // PostgreSQL returns [{ id }] from .returning(), SQLite returns [insertedId]\n const insertedId = result?.[0]?.id ?? result?.[0] ?? user.id;\n const finalId = user.id ?? insertedId;\n return { ...user, id: finalId } as User<Meta>;\n }\n\n async getUserById(id: UserId): Promise<User<Meta> | null> {\n const row = await this.table.where({ id }).first();\n return row ? this.fromDbUser(row) : null;\n }\n\n async getUserByEmail(email: string): Promise<User<Meta> | null> {\n const row = await this.table.where({ email }).first();\n return row ? this.fromDbUser(row) : null;\n }\n\n async getUserByResetPasswordToken(resetPasswordToken: string): Promise<User<Meta> | null> {\n const row = await this.table.where({ reset_password_token: resetPasswordToken }).first();\n return row ? this.fromDbUser(row) : null;\n }\n\n async getUserByVerifyEmailToken(verifyEmailToken: string): Promise<User<Meta> | null> {\n const row = await this.table.where({ verification_token: verifyEmailToken }).first();\n return row ? this.fromDbUser(row) : null;\n }\n\n async updateUser(id: UserId, updates: Partial<User<Meta>>): Promise<void> {\n const dbUpdates = this.toDbUpdates(updates);\n\n if (Object.keys(dbUpdates).length === 0) {\n return;\n }\n\n await this.table.where({ id }).update(dbUpdates);\n }\n\n async deleteUser(id: UserId): Promise<void> {\n // Delete all user sessions first\n await this.deleteAllUserSessions(id);\n // Then delete the user\n await this.table.where({ id }).delete();\n }\n\n async findUsers(filter: UserFilter, options: FindUsersOptions = {}): Promise<FindUsersResult<Meta>> {\n const limit = options.limit ?? 50;\n\n let query = this.table.clone();\n\n // Email exact match\n if (filter.email) {\n query = query.where('email', filter.email);\n }\n\n // Email contains (partial match)\n if (filter.emailContains) {\n query = query.where('email', 'like', `%${filter.emailContains}%`);\n }\n\n // Email verification status\n if (filter.isEmailVerified !== undefined) {\n query = query.where('is_email_verified', filter.isEmailVerified);\n }\n\n // Roles filtering - user must have ALL specified roles\n if (filter.roles && filter.roles.length > 0) {\n for (const role of filter.roles) {\n // Use JSON contains - works for PostgreSQL (jsonb) and MySQL (json)\n // For SQLite, we fall back to LIKE on the JSON string\n query = query.whereRaw(\n this.knex.client.config.client === 'sqlite3' ? `roles LIKE ?` : `roles @> ?`,\n this.knex.client.config.client === 'sqlite3'\n ? [`%\"${role}\"%`]\n : [JSON.stringify([role])]\n );\n }\n }\n\n // HasAnyRole - user must have at least ONE of specified roles\n if (filter.hasAnyRole && filter.hasAnyRole.length > 0) {\n query = query.where((builder: Knex.QueryBuilder) => {\n for (const role of filter.hasAnyRole!) {\n builder.orWhereRaw(\n this.knex.client.config.client === 'sqlite3'\n ? `roles LIKE ?`\n : `roles @> ?`,\n this.knex.client.config.client === 'sqlite3'\n ? [`%\"${role}\"%`]\n : [JSON.stringify([role])]\n );\n }\n });\n }\n\n // Apply pagination\n if (options.cursor) {\n const cursorData = JSON.parse(Buffer.from(options.cursor, 'base64').toString());\n query = query.where('id', '>', cursorData.lastId);\n }\n\n // Order by id for consistent pagination\n query = query.orderBy('id', 'asc').limit(limit + 1);\n\n const rows = await query;\n const hasMore = rows.length > limit;\n const resultRows = hasMore ? rows.slice(0, limit) : rows;\n const users = resultRows.map((row: Record<string, unknown>) => this.fromDbUser(row));\n\n let nextCursor: string | undefined;\n if (hasMore && resultRows.length > 0) {\n const lastUser = resultRows[resultRows.length - 1];\n nextCursor = Buffer.from(JSON.stringify({ lastId: lastUser.id })).toString('base64');\n }\n\n return { users, nextCursor } as FindUsersResult<Meta>;\n }\n\n // ==================== Session Methods ====================\n\n async createSession(session: CreateSessionInput): Promise<Session> {\n const dbSession = this.toDbSession(session);\n const result = await this.sessionsTable.insert(dbSession).returning('id');\n // PostgreSQL returns [{ id }] from .returning(), SQLite returns [insertedId]\n const insertedId = result?.[0]?.id ?? result?.[0] ?? session.id;\n const finalId = session.id ?? insertedId;\n return { ...session, id: finalId } as Session;\n }\n\n async getSessionById(id: SessionId): Promise<Session | null> {\n const row = await this.sessionsTable.where({ id }).first();\n return row ? this.fromDbSession(row) : null;\n }\n\n async getSessionsByUserId(userId: UserId): Promise<Session[]> {\n const rows = await this.sessionsTable.where({ user_id: userId });\n return rows.map((row: Record<string, unknown>) => this.fromDbSession(row));\n }\n\n async updateSession(id: SessionId, updates: Partial<Session>): Promise<void> {\n const dbUpdates = this.toDbSessionUpdates(updates);\n\n if (Object.keys(dbUpdates).length === 0) {\n return;\n }\n\n await this.sessionsTable.where({ id }).update(dbUpdates);\n }\n\n async deleteSession(id: SessionId): Promise<void> {\n await this.sessionsTable.where({ id }).delete();\n }\n\n async deleteAllUserSessions(userId: UserId): Promise<void> {\n await this.sessionsTable.where({ user_id: userId }).delete();\n }\n\n async deleteExpiredSessions(): Promise<void> {\n const now = new Date().toISOString();\n await this.sessionsTable.where('expires_at', '<', now).delete();\n }\n\n // ==================== User Mapping Helpers ====================\n\n /**\n * Safely convert a DB date value (Date object or string) to ISO 8601 string\n */\n private toDateString(value: unknown): string {\n if (value instanceof Date) return value.toISOString();\n return String(value);\n }\n\n /**\n * Convert User object to database row format (snake_case).\n * Meta fields are flattened as individual snake_case columns.\n */\n private toDbUser(user: CreateUserInput<Meta>): Record<string, unknown> {\n const now = new Date().toISOString();\n const row: Record<string, unknown> = {\n\n email: user.email,\n first_name: user.firstName ?? null,\n last_name: user.lastName ?? null,\n password_hash: user.passwordHash,\n roles: JSON.stringify(user.roles),\n is_email_verified: user.isEmailVerified ? 1 : 0,\n verification_token: user.verificationToken ?? null,\n reset_password_token: user.resetPasswordToken ?? null,\n reset_password_expires: user.resetPasswordExpires ?? null,\n created_at: user.createdAt ?? now,\n updated_at: user.updatedAt ?? now,\n };\n // Only include id if provided (for string UUIDs)\n // Omit for auto-increment DBs\n if (user.id !== undefined) {\n row.id = user.id;\n }\n // Flatten meta fields as individual snake_case columns\n if (user.meta) {\n for (const [key, value] of Object.entries(user.meta)) {\n row[camelToSnake(key)] = value;\n }\n }\n return row;\n }\n\n /**\n * Convert database row to User object (camelCase).\n * Non-core columns are gathered into the typed `meta` object.\n */\n private fromDbUser(row: Record<string, unknown>): User<Meta> {\n const meta: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(row)) {\n if (!CORE_USER_DB_FIELDS.has(key)) {\n meta[snakeToCamel(key)] = value;\n }\n }\n\n return {\n id: row.id as string | number,\n email: row.email as string,\n firstName: row.first_name as string | undefined,\n lastName: row.last_name as string | undefined,\n passwordHash: row.password_hash as string,\n roles: typeof row.roles === 'string' ? JSON.parse(row.roles) : row.roles,\n isEmailVerified: Boolean(row.is_email_verified),\n verificationToken: row.verification_token as string | undefined,\n resetPasswordToken: row.reset_password_token as string | undefined,\n resetPasswordExpires: row.reset_password_expires\n ? this.toDateString(row.reset_password_expires)\n : undefined,\n createdAt: this.toDateString(row.created_at),\n updatedAt: this.toDateString(row.updated_at),\n ...(Object.keys(meta).length > 0 ? { meta: meta as Meta } : {}),\n } as User<Meta>;\n }\n\n /**\n * Convert partial User updates to database format.\n * Meta fields in updates are flattened as individual snake_case columns.\n */\n private toDbUpdates(updates: Partial<User<Meta>>): Record<string, unknown> {\n const dbUpdates: Record<string, unknown> = {};\n\n if (updates.email !== undefined) {\n dbUpdates.email = updates.email;\n }\n if ('firstName' in updates) {\n dbUpdates.first_name = updates.firstName ?? null;\n }\n if ('lastName' in updates) {\n dbUpdates.last_name = updates.lastName ?? null;\n }\n if (updates.passwordHash !== undefined) {\n dbUpdates.password_hash = updates.passwordHash;\n }\n if (updates.roles !== undefined) {\n dbUpdates.roles = JSON.stringify(updates.roles);\n }\n if (updates.isEmailVerified !== undefined) {\n dbUpdates.is_email_verified = updates.isEmailVerified ? 1 : 0;\n }\n if ('verificationToken' in updates) {\n dbUpdates.verification_token = updates.verificationToken ?? null;\n }\n if ('resetPasswordToken' in updates) {\n dbUpdates.reset_password_token = updates.resetPasswordToken ?? null;\n }\n if ('resetPasswordExpires' in updates) {\n dbUpdates.reset_password_expires = updates.resetPasswordExpires ?? null;\n }\n if (updates.updatedAt !== undefined) {\n dbUpdates.updated_at = updates.updatedAt;\n }\n // Flatten meta fields as individual snake_case columns\n if (updates.meta) {\n for (const [key, value] of Object.entries(updates.meta)) {\n dbUpdates[camelToSnake(key)] = value;\n }\n }\n\n return dbUpdates;\n }\n\n // ==================== Session Mapping Helpers ====================\n\n /**\n * Convert Session object to database row format (snake_case)\n */\n private toDbSession(session: CreateSessionInput): Record<string, unknown> {\n const row: Record<string, unknown> = {\n user_id: session.userId,\n refresh_token_hash: session.refreshTokenHash,\n user_agent: session.userAgent ?? null,\n ip_address: session.ipAddress ?? null,\n device_name: session.deviceName ?? null,\n created_at: session.createdAt,\n last_used_at: session.lastUsedAt,\n expires_at: session.expiresAt,\n };\n // Only include id if provided (for string UUIDs)\n // Omit for auto-increment DBs\n if (session.id !== undefined) {\n row.id = session.id;\n }\n return row;\n }\n\n /**\n * Convert database row to Session object (camelCase)\n */\n private fromDbSession(row: Record<string, unknown>): Session {\n return {\n id: row.id as string | number,\n userId: row.user_id as string | number,\n refreshTokenHash: row.refresh_token_hash as string,\n userAgent: row.user_agent as string | undefined,\n ipAddress: row.ip_address as string | undefined,\n deviceName: row.device_name as string | undefined,\n createdAt: this.toDateString(row.created_at),\n lastUsedAt: this.toDateString(row.last_used_at),\n expiresAt: this.toDateString(row.expires_at),\n };\n }\n\n /**\n * Convert partial Session updates to database format\n */\n private toDbSessionUpdates(updates: Partial<Session>): Record<string, unknown> {\n const dbUpdates: Record<string, unknown> = {};\n\n if (updates.refreshTokenHash !== undefined) {\n dbUpdates.refresh_token_hash = updates.refreshTokenHash;\n }\n if ('userAgent' in updates) {\n dbUpdates.user_agent = updates.userAgent ?? null;\n }\n if ('ipAddress' in updates) {\n dbUpdates.ip_address = updates.ipAddress ?? null;\n }\n if ('deviceName' in updates) {\n dbUpdates.device_name = updates.deviceName ?? null;\n }\n if (updates.lastUsedAt !== undefined) {\n dbUpdates.last_used_at = updates.lastUsedAt;\n }\n if (updates.expiresAt !== undefined) {\n dbUpdates.expires_at = updates.expiresAt;\n }\n\n return dbUpdates;\n }\n}\n","import type { Knex } from 'knex';\n\n/**\n * SQL migration helper to create the users table\n * Can be used with Knex migrations\n *\n * @param knex - Knex instance\n * @param tableName - Table name for users\n * @param schemaOrCustomColumns - Optional schema name (for PostgreSQL) or callback to add custom columns\n * @param customColumns - Optional callback to add custom columns to the table (when schema is provided)\n *\n * @example\n * ```ts\n * // In your migration file\n * import { createUsersTableMigration, createSessionsTableMigration } from '@xcelsior/auth-adapter-knex';\n *\n * export async function up(knex: Knex): Promise<void> {\n * await createUsersTableMigration(knex, 'users');\n * await createSessionsTableMigration(knex, 'sessions');\n * }\n *\n * // With custom meta columns\n * export async function up(knex: Knex): Promise<void> {\n * await createUsersTableMigration(knex, 'users', (table) => {\n * table.string('phone').nullable();\n * table.string('company').nullable();\n * });\n * await createSessionsTableMigration(knex, 'sessions');\n * }\n *\n * // With schema and custom meta columns\n * export async function up(knex: Knex): Promise<void> {\n * await createUsersTableMigration(knex, 'users', 'my_schema', (table) => {\n * table.string('phone').nullable();\n * table.string('company').nullable();\n * });\n * }\n *\n * export async function down(knex: Knex): Promise<void> {\n * await knex.schema.dropTableIfExists('sessions');\n * await knex.schema.dropTableIfExists('users');\n * }\n * ```\n */\nexport async function createUsersTableMigration(\n knex: Knex,\n tableName: string,\n schemaOrCustomColumns?: string | ((table: Knex.CreateTableBuilder) => void),\n customColumns?: (table: Knex.CreateTableBuilder) => void\n): Promise<void> {\n const schema = typeof schemaOrCustomColumns === 'string' ? schemaOrCustomColumns : undefined;\n const addCustomColumns =\n typeof schemaOrCustomColumns === 'function' ? schemaOrCustomColumns : customColumns;\n const schemaBuilder = schema ? knex.schema.withSchema(schema) : knex.schema;\n\n await schemaBuilder.createTable(tableName, (table) => {\n table.string('id').primary();\n table.string('email').notNullable().unique();\n table.string('first_name').nullable();\n table.string('last_name').nullable();\n table.string('password_hash').notNullable();\n table.jsonb('roles').notNullable();\n table.boolean('is_email_verified').notNullable().defaultTo(false);\n table.string('verification_token').nullable();\n table.string('reset_password_token').nullable();\n table.timestamp('reset_password_expires', { useTz: true }).nullable();\n table.timestamp('created_at', { useTz: true }).notNullable();\n table.timestamp('updated_at', { useTz: true }).notNullable();\n\n // Indexes for common lookups\n table.index('email');\n table.index('verification_token');\n table.index('reset_password_token');\n\n // Add custom meta columns if provided\n if (addCustomColumns) {\n addCustomColumns(table);\n }\n });\n}\n\n/**\n * SQL migration helper to create the sessions table\n * Can be used with Knex migrations\n */\nexport async function createSessionsTableMigration(\n knex: Knex,\n tableName: string,\n schema?: string\n): Promise<void> {\n const schemaBuilder = schema ? knex.schema.withSchema(schema) : knex.schema;\n\n await schemaBuilder.createTable(tableName, (table) => {\n table.string('id').primary();\n table.string('user_id').notNullable();\n table.string('refresh_token_hash').notNullable();\n table.string('user_agent').nullable();\n table.string('ip_address').nullable();\n table.string('device_name').nullable();\n table.timestamp('created_at', { useTz: true }).notNullable();\n table.timestamp('last_used_at', { useTz: true }).notNullable();\n table.timestamp('expires_at', { useTz: true }).notNullable();\n\n // Index for querying sessions by user\n table.index('user_id');\n // Index for cleaning up expired sessions\n table.index('expires_at');\n });\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcelsior/auth-adapter-knex",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "Knex storage adapter for @xcelsior/auth (PostgreSQL, MySQL, SQLite, etc.)",
5
5
  "exports": {
6
6
  ".": {
@@ -11,11 +11,12 @@
11
11
  "main": "src/index.ts",
12
12
  "devDependencies": {
13
13
  "@types/node": "^20.0.0",
14
- "knex": "^3.1.0"
14
+ "knex": "^3.1.0",
15
+ "@xcelsior/utils": "1.0.1"
15
16
  },
16
17
  "peerDependencies": {
17
18
  "knex": "^3.1.0",
18
- "@xcelsior/auth": "1.1.2"
19
+ "@xcelsior/auth": "1.1.5"
19
20
  },
20
21
  "scripts": {
21
22
  "build": "tsup && tsc --noEmit",
package/src/knex.ts CHANGED
@@ -10,8 +10,35 @@ import type {
10
10
  User,
11
11
  UserFilter,
12
12
  UserId,
13
+ UserMeta,
13
14
  } from '@xcelsior/auth';
14
15
 
16
+ /** Convert camelCase string to snake_case */
17
+ function camelToSnake(str: string): string {
18
+ return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
19
+ }
20
+
21
+ /** Convert snake_case string to camelCase */
22
+ function snakeToCamel(str: string): string {
23
+ return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
24
+ }
25
+
26
+ /** Core user DB columns that are not part of meta */
27
+ const CORE_USER_DB_FIELDS = new Set([
28
+ 'id',
29
+ 'email',
30
+ 'first_name',
31
+ 'last_name',
32
+ 'password_hash',
33
+ 'roles',
34
+ 'is_email_verified',
35
+ 'verification_token',
36
+ 'reset_password_token',
37
+ 'reset_password_expires',
38
+ 'created_at',
39
+ 'updated_at',
40
+ ]);
41
+
15
42
  export interface KnexConfig {
16
43
  /** Pre-configured Knex instance */
17
44
  knex: Knex;
@@ -23,7 +50,9 @@ export interface KnexConfig {
23
50
  schema?: string;
24
51
  }
25
52
 
26
- export class KnexStorageProvider implements IStorageProvider {
53
+ export class KnexStorageProvider<Meta extends UserMeta = Record<string, never>>
54
+ implements IStorageProvider<Meta>
55
+ {
27
56
  private knex: Knex;
28
57
  private tableName: string;
29
58
  private sessionsTableName: string;
@@ -54,36 +83,36 @@ export class KnexStorageProvider implements IStorageProvider {
54
83
 
55
84
  // ==================== User Methods ====================
56
85
 
57
- async createUser(user: CreateUserInput): Promise<User> {
86
+ async createUser(user: CreateUserInput<Meta>): Promise<User<Meta>> {
58
87
  const dbUser = this.toDbUser(user);
59
- const [insertedId] = await this.table.insert(dbUser);
60
- // For auto-increment DBs, insertedId is the new ID
61
- // For string PKs, it returns 0, so use the original id
88
+ const result = await this.table.insert(dbUser).returning('id');
89
+ // PostgreSQL returns [{ id }] from .returning(), SQLite returns [insertedId]
90
+ const insertedId = result?.[0]?.id ?? result?.[0] ?? user.id;
62
91
  const finalId = user.id ?? insertedId;
63
- return { ...user, id: finalId } as User;
92
+ return { ...user, id: finalId } as User<Meta>;
64
93
  }
65
94
 
66
- async getUserById(id: UserId): Promise<User | null> {
95
+ async getUserById(id: UserId): Promise<User<Meta> | null> {
67
96
  const row = await this.table.where({ id }).first();
68
97
  return row ? this.fromDbUser(row) : null;
69
98
  }
70
99
 
71
- async getUserByEmail(email: string): Promise<User | null> {
100
+ async getUserByEmail(email: string): Promise<User<Meta> | null> {
72
101
  const row = await this.table.where({ email }).first();
73
102
  return row ? this.fromDbUser(row) : null;
74
103
  }
75
104
 
76
- async getUserByResetPasswordToken(resetPasswordToken: string): Promise<User | null> {
105
+ async getUserByResetPasswordToken(resetPasswordToken: string): Promise<User<Meta> | null> {
77
106
  const row = await this.table.where({ reset_password_token: resetPasswordToken }).first();
78
107
  return row ? this.fromDbUser(row) : null;
79
108
  }
80
109
 
81
- async getUserByVerifyEmailToken(verifyEmailToken: string): Promise<User | null> {
110
+ async getUserByVerifyEmailToken(verifyEmailToken: string): Promise<User<Meta> | null> {
82
111
  const row = await this.table.where({ verification_token: verifyEmailToken }).first();
83
112
  return row ? this.fromDbUser(row) : null;
84
113
  }
85
114
 
86
- async updateUser(id: UserId, updates: Partial<User>): Promise<void> {
115
+ async updateUser(id: UserId, updates: Partial<User<Meta>>): Promise<void> {
87
116
  const dbUpdates = this.toDbUpdates(updates);
88
117
 
89
118
  if (Object.keys(dbUpdates).length === 0) {
@@ -100,7 +129,7 @@ export class KnexStorageProvider implements IStorageProvider {
100
129
  await this.table.where({ id }).delete();
101
130
  }
102
131
 
103
- async findUsers(filter: UserFilter, options: FindUsersOptions = {}): Promise<FindUsersResult> {
132
+ async findUsers(filter: UserFilter, options: FindUsersOptions = {}): Promise<FindUsersResult<Meta>> {
104
133
  const limit = options.limit ?? 50;
105
134
 
106
135
  let query = this.table.clone();
@@ -170,14 +199,16 @@ export class KnexStorageProvider implements IStorageProvider {
170
199
  nextCursor = Buffer.from(JSON.stringify({ lastId: lastUser.id })).toString('base64');
171
200
  }
172
201
 
173
- return { users, nextCursor };
202
+ return { users, nextCursor } as FindUsersResult<Meta>;
174
203
  }
175
204
 
176
205
  // ==================== Session Methods ====================
177
206
 
178
207
  async createSession(session: CreateSessionInput): Promise<Session> {
179
208
  const dbSession = this.toDbSession(session);
180
- const [insertedId] = await this.sessionsTable.insert(dbSession);
209
+ const result = await this.sessionsTable.insert(dbSession).returning('id');
210
+ // PostgreSQL returns [{ id }] from .returning(), SQLite returns [insertedId]
211
+ const insertedId = result?.[0]?.id ?? result?.[0] ?? session.id;
181
212
  const finalId = session.id ?? insertedId;
182
213
  return { ...session, id: finalId } as Session;
183
214
  }
@@ -226,11 +257,13 @@ export class KnexStorageProvider implements IStorageProvider {
226
257
  }
227
258
 
228
259
  /**
229
- * Convert User object to database row format (snake_case)
260
+ * Convert User object to database row format (snake_case).
261
+ * Meta fields are flattened as individual snake_case columns.
230
262
  */
231
- private toDbUser(user: CreateUserInput): Record<string, unknown> {
263
+ private toDbUser(user: CreateUserInput<Meta>): Record<string, unknown> {
232
264
  const now = new Date().toISOString();
233
265
  const row: Record<string, unknown> = {
266
+
234
267
  email: user.email,
235
268
  first_name: user.firstName ?? null,
236
269
  last_name: user.lastName ?? null,
@@ -248,13 +281,27 @@ export class KnexStorageProvider implements IStorageProvider {
248
281
  if (user.id !== undefined) {
249
282
  row.id = user.id;
250
283
  }
284
+ // Flatten meta fields as individual snake_case columns
285
+ if (user.meta) {
286
+ for (const [key, value] of Object.entries(user.meta)) {
287
+ row[camelToSnake(key)] = value;
288
+ }
289
+ }
251
290
  return row;
252
291
  }
253
292
 
254
293
  /**
255
- * Convert database row to User object (camelCase)
294
+ * Convert database row to User object (camelCase).
295
+ * Non-core columns are gathered into the typed `meta` object.
256
296
  */
257
- private fromDbUser(row: Record<string, unknown>): User {
297
+ private fromDbUser(row: Record<string, unknown>): User<Meta> {
298
+ const meta: Record<string, unknown> = {};
299
+ for (const [key, value] of Object.entries(row)) {
300
+ if (!CORE_USER_DB_FIELDS.has(key)) {
301
+ meta[snakeToCamel(key)] = value;
302
+ }
303
+ }
304
+
258
305
  return {
259
306
  id: row.id as string | number,
260
307
  email: row.email as string,
@@ -270,13 +317,15 @@ export class KnexStorageProvider implements IStorageProvider {
270
317
  : undefined,
271
318
  createdAt: this.toDateString(row.created_at),
272
319
  updatedAt: this.toDateString(row.updated_at),
273
- };
320
+ ...(Object.keys(meta).length > 0 ? { meta: meta as Meta } : {}),
321
+ } as User<Meta>;
274
322
  }
275
323
 
276
324
  /**
277
- * Convert partial User updates to database format
325
+ * Convert partial User updates to database format.
326
+ * Meta fields in updates are flattened as individual snake_case columns.
278
327
  */
279
- private toDbUpdates(updates: Partial<User>): Record<string, unknown> {
328
+ private toDbUpdates(updates: Partial<User<Meta>>): Record<string, unknown> {
280
329
  const dbUpdates: Record<string, unknown> = {};
281
330
 
282
331
  if (updates.email !== undefined) {
@@ -309,6 +358,12 @@ export class KnexStorageProvider implements IStorageProvider {
309
358
  if (updates.updatedAt !== undefined) {
310
359
  dbUpdates.updated_at = updates.updatedAt;
311
360
  }
361
+ // Flatten meta fields as individual snake_case columns
362
+ if (updates.meta) {
363
+ for (const [key, value] of Object.entries(updates.meta)) {
364
+ dbUpdates[camelToSnake(key)] = value;
365
+ }
366
+ }
312
367
 
313
368
  return dbUpdates;
314
369
  }
package/src/migration.ts CHANGED
@@ -4,6 +4,11 @@ import type { Knex } from 'knex';
4
4
  * SQL migration helper to create the users table
5
5
  * Can be used with Knex migrations
6
6
  *
7
+ * @param knex - Knex instance
8
+ * @param tableName - Table name for users
9
+ * @param schemaOrCustomColumns - Optional schema name (for PostgreSQL) or callback to add custom columns
10
+ * @param customColumns - Optional callback to add custom columns to the table (when schema is provided)
11
+ *
7
12
  * @example
8
13
  * ```ts
9
14
  * // In your migration file
@@ -14,6 +19,23 @@ import type { Knex } from 'knex';
14
19
  * await createSessionsTableMigration(knex, 'sessions');
15
20
  * }
16
21
  *
22
+ * // With custom meta columns
23
+ * export async function up(knex: Knex): Promise<void> {
24
+ * await createUsersTableMigration(knex, 'users', (table) => {
25
+ * table.string('phone').nullable();
26
+ * table.string('company').nullable();
27
+ * });
28
+ * await createSessionsTableMigration(knex, 'sessions');
29
+ * }
30
+ *
31
+ * // With schema and custom meta columns
32
+ * export async function up(knex: Knex): Promise<void> {
33
+ * await createUsersTableMigration(knex, 'users', 'my_schema', (table) => {
34
+ * table.string('phone').nullable();
35
+ * table.string('company').nullable();
36
+ * });
37
+ * }
38
+ *
17
39
  * export async function down(knex: Knex): Promise<void> {
18
40
  * await knex.schema.dropTableIfExists('sessions');
19
41
  * await knex.schema.dropTableIfExists('users');
@@ -23,8 +45,12 @@ import type { Knex } from 'knex';
23
45
  export async function createUsersTableMigration(
24
46
  knex: Knex,
25
47
  tableName: string,
26
- schema?: string
48
+ schemaOrCustomColumns?: string | ((table: Knex.CreateTableBuilder) => void),
49
+ customColumns?: (table: Knex.CreateTableBuilder) => void
27
50
  ): Promise<void> {
51
+ const schema = typeof schemaOrCustomColumns === 'string' ? schemaOrCustomColumns : undefined;
52
+ const addCustomColumns =
53
+ typeof schemaOrCustomColumns === 'function' ? schemaOrCustomColumns : customColumns;
28
54
  const schemaBuilder = schema ? knex.schema.withSchema(schema) : knex.schema;
29
55
 
30
56
  await schemaBuilder.createTable(tableName, (table) => {
@@ -45,6 +71,11 @@ export async function createUsersTableMigration(
45
71
  table.index('email');
46
72
  table.index('verification_token');
47
73
  table.index('reset_password_token');
74
+
75
+ // Add custom meta columns if provided
76
+ if (addCustomColumns) {
77
+ addCustomColumns(table);
78
+ }
48
79
  });
49
80
  }
50
81