@xcelsior/auth-adapter-knex 1.1.4 → 1.1.5

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 { UserMeta, IStorageProvider, CreateUserInput, User, UserId, UserFilter, FindUsersOptions, FindUsersResult, CreateSessionInput, Session, SessionId } from '@xcelsior/auth';
2
+ import { UserMeta, SessionMeta, 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<Meta extends UserMeta = Record<string, never>> implements IStorageProvider<Meta> {
14
+ declare class KnexStorageProvider<Meta extends UserMeta = Record<string, any>, SMeta extends SessionMeta = Record<string, any>> implements IStorageProvider<Meta, SMeta> {
15
15
  private knex;
16
16
  private tableName;
17
17
  private sessionsTableName;
@@ -27,10 +27,10 @@ declare class KnexStorageProvider<Meta extends UserMeta = Record<string, never>>
27
27
  updateUser(id: UserId, updates: Partial<User<Meta>>): Promise<void>;
28
28
  deleteUser(id: UserId): Promise<void>;
29
29
  findUsers(filter: UserFilter, options?: FindUsersOptions): Promise<FindUsersResult<Meta>>;
30
- createSession(session: CreateSessionInput): Promise<Session>;
31
- getSessionById(id: SessionId): Promise<Session | null>;
32
- getSessionsByUserId(userId: UserId): Promise<Session[]>;
33
- updateSession(id: SessionId, updates: Partial<Session>): Promise<void>;
30
+ createSession(session: CreateSessionInput<SMeta>): Promise<Session<SMeta>>;
31
+ getSessionById(id: SessionId): Promise<Session<SMeta> | null>;
32
+ getSessionsByUserId(userId: UserId): Promise<Session<SMeta>[]>;
33
+ updateSession(id: SessionId, updates: Partial<Session<SMeta>> & Record<string, unknown>): Promise<void>;
34
34
  deleteSession(id: SessionId): Promise<void>;
35
35
  deleteAllUserSessions(userId: UserId): Promise<void>;
36
36
  deleteExpiredSessions(): Promise<void>;
@@ -54,15 +54,18 @@ declare class KnexStorageProvider<Meta extends UserMeta = Record<string, never>>
54
54
  */
55
55
  private toDbUpdates;
56
56
  /**
57
- * Convert Session object to database row format (snake_case)
57
+ * Convert Session object to database row format (snake_case).
58
+ * Meta fields are flattened as individual snake_case columns.
58
59
  */
59
60
  private toDbSession;
60
61
  /**
61
- * Convert database row to Session object (camelCase)
62
+ * Convert database row to Session object (camelCase).
63
+ * Non-core columns are gathered into the typed `meta` object.
62
64
  */
63
65
  private fromDbSession;
64
66
  /**
65
- * Convert partial Session updates to database format
67
+ * Convert partial Session updates to database format.
68
+ * Meta fields in updates are flattened as individual snake_case columns.
66
69
  */
67
70
  private toDbSessionUpdates;
68
71
  }
@@ -113,7 +116,25 @@ declare function createUsersTableMigration(knex: Knex, tableName: string, schema
113
116
  /**
114
117
  * SQL migration helper to create the sessions table
115
118
  * Can be used with Knex migrations
119
+ *
120
+ * @param knex - Knex instance
121
+ * @param tableName - Table name for sessions
122
+ * @param schemaOrCustomColumns - Optional schema name (for PostgreSQL) or callback to add custom columns
123
+ * @param customColumns - Optional callback to add custom columns to the table (when schema is provided)
124
+ *
125
+ * @example
126
+ * ```ts
127
+ * // With custom meta columns
128
+ * await createSessionsTableMigration(knex, 'sessions', (table) => {
129
+ * table.string('organization_id').nullable();
130
+ * });
131
+ *
132
+ * // With schema and custom meta columns
133
+ * await createSessionsTableMigration(knex, 'sessions', 'my_schema', (table) => {
134
+ * table.string('organization_id').nullable();
135
+ * });
136
+ * ```
116
137
  */
117
- declare function createSessionsTableMigration(knex: Knex, tableName: string, schema?: string): Promise<void>;
138
+ declare function createSessionsTableMigration(knex: Knex, tableName: string, schemaOrCustomColumns?: string | ((table: Knex.CreateTableBuilder) => void), customColumns?: (table: Knex.CreateTableBuilder) => void): Promise<void>;
118
139
 
119
140
  export { type KnexConfig, KnexStorageProvider, createSessionsTableMigration, createUsersTableMigration };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Knex } from 'knex';
2
- import { UserMeta, IStorageProvider, CreateUserInput, User, UserId, UserFilter, FindUsersOptions, FindUsersResult, CreateSessionInput, Session, SessionId } from '@xcelsior/auth';
2
+ import { UserMeta, SessionMeta, 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<Meta extends UserMeta = Record<string, never>> implements IStorageProvider<Meta> {
14
+ declare class KnexStorageProvider<Meta extends UserMeta = Record<string, any>, SMeta extends SessionMeta = Record<string, any>> implements IStorageProvider<Meta, SMeta> {
15
15
  private knex;
16
16
  private tableName;
17
17
  private sessionsTableName;
@@ -27,10 +27,10 @@ declare class KnexStorageProvider<Meta extends UserMeta = Record<string, never>>
27
27
  updateUser(id: UserId, updates: Partial<User<Meta>>): Promise<void>;
28
28
  deleteUser(id: UserId): Promise<void>;
29
29
  findUsers(filter: UserFilter, options?: FindUsersOptions): Promise<FindUsersResult<Meta>>;
30
- createSession(session: CreateSessionInput): Promise<Session>;
31
- getSessionById(id: SessionId): Promise<Session | null>;
32
- getSessionsByUserId(userId: UserId): Promise<Session[]>;
33
- updateSession(id: SessionId, updates: Partial<Session>): Promise<void>;
30
+ createSession(session: CreateSessionInput<SMeta>): Promise<Session<SMeta>>;
31
+ getSessionById(id: SessionId): Promise<Session<SMeta> | null>;
32
+ getSessionsByUserId(userId: UserId): Promise<Session<SMeta>[]>;
33
+ updateSession(id: SessionId, updates: Partial<Session<SMeta>> & Record<string, unknown>): Promise<void>;
34
34
  deleteSession(id: SessionId): Promise<void>;
35
35
  deleteAllUserSessions(userId: UserId): Promise<void>;
36
36
  deleteExpiredSessions(): Promise<void>;
@@ -54,15 +54,18 @@ declare class KnexStorageProvider<Meta extends UserMeta = Record<string, never>>
54
54
  */
55
55
  private toDbUpdates;
56
56
  /**
57
- * Convert Session object to database row format (snake_case)
57
+ * Convert Session object to database row format (snake_case).
58
+ * Meta fields are flattened as individual snake_case columns.
58
59
  */
59
60
  private toDbSession;
60
61
  /**
61
- * Convert database row to Session object (camelCase)
62
+ * Convert database row to Session object (camelCase).
63
+ * Non-core columns are gathered into the typed `meta` object.
62
64
  */
63
65
  private fromDbSession;
64
66
  /**
65
- * Convert partial Session updates to database format
67
+ * Convert partial Session updates to database format.
68
+ * Meta fields in updates are flattened as individual snake_case columns.
66
69
  */
67
70
  private toDbSessionUpdates;
68
71
  }
@@ -113,7 +116,25 @@ declare function createUsersTableMigration(knex: Knex, tableName: string, schema
113
116
  /**
114
117
  * SQL migration helper to create the sessions table
115
118
  * Can be used with Knex migrations
119
+ *
120
+ * @param knex - Knex instance
121
+ * @param tableName - Table name for sessions
122
+ * @param schemaOrCustomColumns - Optional schema name (for PostgreSQL) or callback to add custom columns
123
+ * @param customColumns - Optional callback to add custom columns to the table (when schema is provided)
124
+ *
125
+ * @example
126
+ * ```ts
127
+ * // With custom meta columns
128
+ * await createSessionsTableMigration(knex, 'sessions', (table) => {
129
+ * table.string('organization_id').nullable();
130
+ * });
131
+ *
132
+ * // With schema and custom meta columns
133
+ * await createSessionsTableMigration(knex, 'sessions', 'my_schema', (table) => {
134
+ * table.string('organization_id').nullable();
135
+ * });
136
+ * ```
116
137
  */
117
- declare function createSessionsTableMigration(knex: Knex, tableName: string, schema?: string): Promise<void>;
138
+ declare function createSessionsTableMigration(knex: Knex, tableName: string, schemaOrCustomColumns?: string | ((table: Knex.CreateTableBuilder) => void), customColumns?: (table: Knex.CreateTableBuilder) => void): Promise<void>;
118
139
 
119
140
  export { type KnexConfig, KnexStorageProvider, createSessionsTableMigration, createUsersTableMigration };
package/dist/index.js CHANGED
@@ -47,6 +47,17 @@ var CORE_USER_DB_FIELDS = /* @__PURE__ */ new Set([
47
47
  "created_at",
48
48
  "updated_at"
49
49
  ]);
50
+ var CORE_SESSION_DB_FIELDS = /* @__PURE__ */ new Set([
51
+ "id",
52
+ "user_id",
53
+ "refresh_token_hash",
54
+ "user_agent",
55
+ "ip_address",
56
+ "device_name",
57
+ "created_at",
58
+ "last_used_at",
59
+ "expires_at"
60
+ ]);
50
61
  var KnexStorageProvider = class {
51
62
  constructor(config) {
52
63
  this.knex = config.knex;
@@ -291,7 +302,8 @@ var KnexStorageProvider = class {
291
302
  }
292
303
  // ==================== Session Mapping Helpers ====================
293
304
  /**
294
- * Convert Session object to database row format (snake_case)
305
+ * Convert Session object to database row format (snake_case).
306
+ * Meta fields are flattened as individual snake_case columns.
295
307
  */
296
308
  toDbSession(session) {
297
309
  const row = {
@@ -307,12 +319,24 @@ var KnexStorageProvider = class {
307
319
  if (session.id !== void 0) {
308
320
  row.id = session.id;
309
321
  }
322
+ if (session.meta) {
323
+ for (const [key, value] of Object.entries(session.meta)) {
324
+ row[camelToSnake(key)] = value;
325
+ }
326
+ }
310
327
  return row;
311
328
  }
312
329
  /**
313
- * Convert database row to Session object (camelCase)
330
+ * Convert database row to Session object (camelCase).
331
+ * Non-core columns are gathered into the typed `meta` object.
314
332
  */
315
333
  fromDbSession(row) {
334
+ const meta = {};
335
+ for (const [key, value] of Object.entries(row)) {
336
+ if (!CORE_SESSION_DB_FIELDS.has(key)) {
337
+ meta[snakeToCamel(key)] = value;
338
+ }
339
+ }
316
340
  return {
317
341
  id: row.id,
318
342
  userId: row.user_id,
@@ -322,11 +346,13 @@ var KnexStorageProvider = class {
322
346
  deviceName: row.device_name,
323
347
  createdAt: this.toDateString(row.created_at),
324
348
  lastUsedAt: this.toDateString(row.last_used_at),
325
- expiresAt: this.toDateString(row.expires_at)
349
+ expiresAt: this.toDateString(row.expires_at),
350
+ ...Object.keys(meta).length > 0 ? { meta } : {}
326
351
  };
327
352
  }
328
353
  /**
329
- * Convert partial Session updates to database format
354
+ * Convert partial Session updates to database format.
355
+ * Meta fields in updates are flattened as individual snake_case columns.
330
356
  */
331
357
  toDbSessionUpdates(updates) {
332
358
  const dbUpdates = {};
@@ -348,6 +374,11 @@ var KnexStorageProvider = class {
348
374
  if (updates.expiresAt !== void 0) {
349
375
  dbUpdates.expires_at = updates.expiresAt;
350
376
  }
377
+ if (updates.meta) {
378
+ for (const [key, value] of Object.entries(updates.meta)) {
379
+ dbUpdates[camelToSnake(key)] = value;
380
+ }
381
+ }
351
382
  return dbUpdates;
352
383
  }
353
384
  };
@@ -378,7 +409,9 @@ async function createUsersTableMigration(knex, tableName, schemaOrCustomColumns,
378
409
  }
379
410
  });
380
411
  }
381
- async function createSessionsTableMigration(knex, tableName, schema) {
412
+ async function createSessionsTableMigration(knex, tableName, schemaOrCustomColumns, customColumns) {
413
+ const schema = typeof schemaOrCustomColumns === "string" ? schemaOrCustomColumns : void 0;
414
+ const addCustomColumns = typeof schemaOrCustomColumns === "function" ? schemaOrCustomColumns : customColumns;
382
415
  const schemaBuilder = schema ? knex.schema.withSchema(schema) : knex.schema;
383
416
  await schemaBuilder.createTable(tableName, (table) => {
384
417
  table.string("id").primary();
@@ -392,6 +425,9 @@ async function createSessionsTableMigration(knex, tableName, schema) {
392
425
  table.timestamp("expires_at", { useTz: true }).notNullable();
393
426
  table.index("user_id");
394
427
  table.index("expires_at");
428
+ if (addCustomColumns) {
429
+ addCustomColumns(table);
430
+ }
395
431
  });
396
432
  }
397
433
  // Annotate the CommonJS export names for ESM import in node:
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 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":[]}
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 SessionMeta,\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\n/** Core session DB columns that are not part of meta */\nconst CORE_SESSION_DB_FIELDS = new Set([\n 'id',\n 'user_id',\n 'refresh_token_hash',\n 'user_agent',\n 'ip_address',\n 'device_name',\n 'created_at',\n 'last_used_at',\n 'expires_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<\n Meta extends UserMeta = Record<string, any>,\n SMeta extends SessionMeta = Record<string, any>,\n> implements IStorageProvider<Meta, SMeta>\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<SMeta>): Promise<Session<SMeta>> {\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<SMeta>;\n }\n\n async getSessionById(id: SessionId): Promise<Session<SMeta> | 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<SMeta>[]> {\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<SMeta>> & Record<string, unknown>): 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 * Meta fields are flattened as individual snake_case columns.\n */\n private toDbSession(session: CreateSessionInput<SMeta>): 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 // Flatten meta fields as individual snake_case columns\n if (session.meta) {\n for (const [key, value] of Object.entries(session.meta)) {\n row[camelToSnake(key)] = value;\n }\n }\n return row;\n }\n\n /**\n * Convert database row to Session object (camelCase).\n * Non-core columns are gathered into the typed `meta` object.\n */\n private fromDbSession(row: Record<string, unknown>): Session<SMeta> {\n const meta: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(row)) {\n if (!CORE_SESSION_DB_FIELDS.has(key)) {\n meta[snakeToCamel(key)] = value;\n }\n }\n\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 ...(Object.keys(meta).length > 0 ? { meta: meta as SMeta } : {}),\n } as Session<SMeta>;\n }\n\n /**\n * Convert partial Session updates to database format.\n * Meta fields in updates are flattened as individual snake_case columns.\n */\n private toDbSessionUpdates(updates: Partial<Session<SMeta>> & Record<string, unknown>): 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 // 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","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 *\n * @param knex - Knex instance\n * @param tableName - Table name for sessions\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 * // With custom meta columns\n * await createSessionsTableMigration(knex, 'sessions', (table) => {\n * table.string('organization_id').nullable();\n * });\n *\n * // With schema and custom meta columns\n * await createSessionsTableMigration(knex, 'sessions', 'my_schema', (table) => {\n * table.string('organization_id').nullable();\n * });\n * ```\n */\nexport async function createSessionsTableMigration(\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('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 // Add custom meta columns if provided\n if (addCustomColumns) {\n addCustomColumns(table);\n }\n });\n}\n"],"mappings":"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","names":[]}
package/dist/index.mjs CHANGED
@@ -19,6 +19,17 @@ var CORE_USER_DB_FIELDS = /* @__PURE__ */ new Set([
19
19
  "created_at",
20
20
  "updated_at"
21
21
  ]);
22
+ var CORE_SESSION_DB_FIELDS = /* @__PURE__ */ new Set([
23
+ "id",
24
+ "user_id",
25
+ "refresh_token_hash",
26
+ "user_agent",
27
+ "ip_address",
28
+ "device_name",
29
+ "created_at",
30
+ "last_used_at",
31
+ "expires_at"
32
+ ]);
22
33
  var KnexStorageProvider = class {
23
34
  constructor(config) {
24
35
  this.knex = config.knex;
@@ -263,7 +274,8 @@ var KnexStorageProvider = class {
263
274
  }
264
275
  // ==================== Session Mapping Helpers ====================
265
276
  /**
266
- * Convert Session object to database row format (snake_case)
277
+ * Convert Session object to database row format (snake_case).
278
+ * Meta fields are flattened as individual snake_case columns.
267
279
  */
268
280
  toDbSession(session) {
269
281
  const row = {
@@ -279,12 +291,24 @@ var KnexStorageProvider = class {
279
291
  if (session.id !== void 0) {
280
292
  row.id = session.id;
281
293
  }
294
+ if (session.meta) {
295
+ for (const [key, value] of Object.entries(session.meta)) {
296
+ row[camelToSnake(key)] = value;
297
+ }
298
+ }
282
299
  return row;
283
300
  }
284
301
  /**
285
- * Convert database row to Session object (camelCase)
302
+ * Convert database row to Session object (camelCase).
303
+ * Non-core columns are gathered into the typed `meta` object.
286
304
  */
287
305
  fromDbSession(row) {
306
+ const meta = {};
307
+ for (const [key, value] of Object.entries(row)) {
308
+ if (!CORE_SESSION_DB_FIELDS.has(key)) {
309
+ meta[snakeToCamel(key)] = value;
310
+ }
311
+ }
288
312
  return {
289
313
  id: row.id,
290
314
  userId: row.user_id,
@@ -294,11 +318,13 @@ var KnexStorageProvider = class {
294
318
  deviceName: row.device_name,
295
319
  createdAt: this.toDateString(row.created_at),
296
320
  lastUsedAt: this.toDateString(row.last_used_at),
297
- expiresAt: this.toDateString(row.expires_at)
321
+ expiresAt: this.toDateString(row.expires_at),
322
+ ...Object.keys(meta).length > 0 ? { meta } : {}
298
323
  };
299
324
  }
300
325
  /**
301
- * Convert partial Session updates to database format
326
+ * Convert partial Session updates to database format.
327
+ * Meta fields in updates are flattened as individual snake_case columns.
302
328
  */
303
329
  toDbSessionUpdates(updates) {
304
330
  const dbUpdates = {};
@@ -320,6 +346,11 @@ var KnexStorageProvider = class {
320
346
  if (updates.expiresAt !== void 0) {
321
347
  dbUpdates.expires_at = updates.expiresAt;
322
348
  }
349
+ if (updates.meta) {
350
+ for (const [key, value] of Object.entries(updates.meta)) {
351
+ dbUpdates[camelToSnake(key)] = value;
352
+ }
353
+ }
323
354
  return dbUpdates;
324
355
  }
325
356
  };
@@ -350,7 +381,9 @@ async function createUsersTableMigration(knex, tableName, schemaOrCustomColumns,
350
381
  }
351
382
  });
352
383
  }
353
- async function createSessionsTableMigration(knex, tableName, schema) {
384
+ async function createSessionsTableMigration(knex, tableName, schemaOrCustomColumns, customColumns) {
385
+ const schema = typeof schemaOrCustomColumns === "string" ? schemaOrCustomColumns : void 0;
386
+ const addCustomColumns = typeof schemaOrCustomColumns === "function" ? schemaOrCustomColumns : customColumns;
354
387
  const schemaBuilder = schema ? knex.schema.withSchema(schema) : knex.schema;
355
388
  await schemaBuilder.createTable(tableName, (table) => {
356
389
  table.string("id").primary();
@@ -364,6 +397,9 @@ async function createSessionsTableMigration(knex, tableName, schema) {
364
397
  table.timestamp("expires_at", { useTz: true }).notNullable();
365
398
  table.index("user_id");
366
399
  table.index("expires_at");
400
+ if (addCustomColumns) {
401
+ addCustomColumns(table);
402
+ }
367
403
  });
368
404
  }
369
405
  export {
@@ -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 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":[]}
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 SessionMeta,\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\n/** Core session DB columns that are not part of meta */\nconst CORE_SESSION_DB_FIELDS = new Set([\n 'id',\n 'user_id',\n 'refresh_token_hash',\n 'user_agent',\n 'ip_address',\n 'device_name',\n 'created_at',\n 'last_used_at',\n 'expires_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<\n Meta extends UserMeta = Record<string, any>,\n SMeta extends SessionMeta = Record<string, any>,\n> implements IStorageProvider<Meta, SMeta>\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<SMeta>): Promise<Session<SMeta>> {\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<SMeta>;\n }\n\n async getSessionById(id: SessionId): Promise<Session<SMeta> | 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<SMeta>[]> {\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<SMeta>> & Record<string, unknown>): 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 * Meta fields are flattened as individual snake_case columns.\n */\n private toDbSession(session: CreateSessionInput<SMeta>): 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 // Flatten meta fields as individual snake_case columns\n if (session.meta) {\n for (const [key, value] of Object.entries(session.meta)) {\n row[camelToSnake(key)] = value;\n }\n }\n return row;\n }\n\n /**\n * Convert database row to Session object (camelCase).\n * Non-core columns are gathered into the typed `meta` object.\n */\n private fromDbSession(row: Record<string, unknown>): Session<SMeta> {\n const meta: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(row)) {\n if (!CORE_SESSION_DB_FIELDS.has(key)) {\n meta[snakeToCamel(key)] = value;\n }\n }\n\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 ...(Object.keys(meta).length > 0 ? { meta: meta as SMeta } : {}),\n } as Session<SMeta>;\n }\n\n /**\n * Convert partial Session updates to database format.\n * Meta fields in updates are flattened as individual snake_case columns.\n */\n private toDbSessionUpdates(updates: Partial<Session<SMeta>> & Record<string, unknown>): 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 // 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","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 *\n * @param knex - Knex instance\n * @param tableName - Table name for sessions\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 * // With custom meta columns\n * await createSessionsTableMigration(knex, 'sessions', (table) => {\n * table.string('organization_id').nullable();\n * });\n *\n * // With schema and custom meta columns\n * await createSessionsTableMigration(knex, 'sessions', 'my_schema', (table) => {\n * table.string('organization_id').nullable();\n * });\n * ```\n */\nexport async function createSessionsTableMigration(\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('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 // Add custom meta columns if provided\n if (addCustomColumns) {\n addCustomColumns(table);\n }\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.4",
3
+ "version": "1.1.5",
4
4
  "description": "Knex storage adapter for @xcelsior/auth (PostgreSQL, MySQL, SQLite, etc.)",
5
5
  "exports": {
6
6
  ".": {
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "peerDependencies": {
18
18
  "knex": "^3.1.0",
19
- "@xcelsior/auth": "1.1.5"
19
+ "@xcelsior/auth": "1.1.7"
20
20
  },
21
21
  "scripts": {
22
22
  "build": "tsup && tsc --noEmit",
package/src/knex.ts CHANGED
@@ -11,6 +11,7 @@ import type {
11
11
  UserFilter,
12
12
  UserId,
13
13
  UserMeta,
14
+ SessionMeta,
14
15
  } from '@xcelsior/auth';
15
16
 
16
17
  /** Convert camelCase string to snake_case */
@@ -39,6 +40,19 @@ const CORE_USER_DB_FIELDS = new Set([
39
40
  'updated_at',
40
41
  ]);
41
42
 
43
+ /** Core session DB columns that are not part of meta */
44
+ const CORE_SESSION_DB_FIELDS = new Set([
45
+ 'id',
46
+ 'user_id',
47
+ 'refresh_token_hash',
48
+ 'user_agent',
49
+ 'ip_address',
50
+ 'device_name',
51
+ 'created_at',
52
+ 'last_used_at',
53
+ 'expires_at',
54
+ ]);
55
+
42
56
  export interface KnexConfig {
43
57
  /** Pre-configured Knex instance */
44
58
  knex: Knex;
@@ -50,8 +64,10 @@ export interface KnexConfig {
50
64
  schema?: string;
51
65
  }
52
66
 
53
- export class KnexStorageProvider<Meta extends UserMeta = Record<string, never>>
54
- implements IStorageProvider<Meta>
67
+ export class KnexStorageProvider<
68
+ Meta extends UserMeta = Record<string, any>,
69
+ SMeta extends SessionMeta = Record<string, any>,
70
+ > implements IStorageProvider<Meta, SMeta>
55
71
  {
56
72
  private knex: Knex;
57
73
  private tableName: string;
@@ -204,26 +220,26 @@ export class KnexStorageProvider<Meta extends UserMeta = Record<string, never>>
204
220
 
205
221
  // ==================== Session Methods ====================
206
222
 
207
- async createSession(session: CreateSessionInput): Promise<Session> {
223
+ async createSession(session: CreateSessionInput<SMeta>): Promise<Session<SMeta>> {
208
224
  const dbSession = this.toDbSession(session);
209
225
  const result = await this.sessionsTable.insert(dbSession).returning('id');
210
226
  // PostgreSQL returns [{ id }] from .returning(), SQLite returns [insertedId]
211
227
  const insertedId = result?.[0]?.id ?? result?.[0] ?? session.id;
212
228
  const finalId = session.id ?? insertedId;
213
- return { ...session, id: finalId } as Session;
229
+ return { ...session, id: finalId } as Session<SMeta>;
214
230
  }
215
231
 
216
- async getSessionById(id: SessionId): Promise<Session | null> {
232
+ async getSessionById(id: SessionId): Promise<Session<SMeta> | null> {
217
233
  const row = await this.sessionsTable.where({ id }).first();
218
234
  return row ? this.fromDbSession(row) : null;
219
235
  }
220
236
 
221
- async getSessionsByUserId(userId: UserId): Promise<Session[]> {
237
+ async getSessionsByUserId(userId: UserId): Promise<Session<SMeta>[]> {
222
238
  const rows = await this.sessionsTable.where({ user_id: userId });
223
239
  return rows.map((row: Record<string, unknown>) => this.fromDbSession(row));
224
240
  }
225
241
 
226
- async updateSession(id: SessionId, updates: Partial<Session>): Promise<void> {
242
+ async updateSession(id: SessionId, updates: Partial<Session<SMeta>> & Record<string, unknown>): Promise<void> {
227
243
  const dbUpdates = this.toDbSessionUpdates(updates);
228
244
 
229
245
  if (Object.keys(dbUpdates).length === 0) {
@@ -371,9 +387,10 @@ export class KnexStorageProvider<Meta extends UserMeta = Record<string, never>>
371
387
  // ==================== Session Mapping Helpers ====================
372
388
 
373
389
  /**
374
- * Convert Session object to database row format (snake_case)
390
+ * Convert Session object to database row format (snake_case).
391
+ * Meta fields are flattened as individual snake_case columns.
375
392
  */
376
- private toDbSession(session: CreateSessionInput): Record<string, unknown> {
393
+ private toDbSession(session: CreateSessionInput<SMeta>): Record<string, unknown> {
377
394
  const row: Record<string, unknown> = {
378
395
  user_id: session.userId,
379
396
  refresh_token_hash: session.refreshTokenHash,
@@ -389,13 +406,27 @@ export class KnexStorageProvider<Meta extends UserMeta = Record<string, never>>
389
406
  if (session.id !== undefined) {
390
407
  row.id = session.id;
391
408
  }
409
+ // Flatten meta fields as individual snake_case columns
410
+ if (session.meta) {
411
+ for (const [key, value] of Object.entries(session.meta)) {
412
+ row[camelToSnake(key)] = value;
413
+ }
414
+ }
392
415
  return row;
393
416
  }
394
417
 
395
418
  /**
396
- * Convert database row to Session object (camelCase)
419
+ * Convert database row to Session object (camelCase).
420
+ * Non-core columns are gathered into the typed `meta` object.
397
421
  */
398
- private fromDbSession(row: Record<string, unknown>): Session {
422
+ private fromDbSession(row: Record<string, unknown>): Session<SMeta> {
423
+ const meta: Record<string, unknown> = {};
424
+ for (const [key, value] of Object.entries(row)) {
425
+ if (!CORE_SESSION_DB_FIELDS.has(key)) {
426
+ meta[snakeToCamel(key)] = value;
427
+ }
428
+ }
429
+
399
430
  return {
400
431
  id: row.id as string | number,
401
432
  userId: row.user_id as string | number,
@@ -406,13 +437,15 @@ export class KnexStorageProvider<Meta extends UserMeta = Record<string, never>>
406
437
  createdAt: this.toDateString(row.created_at),
407
438
  lastUsedAt: this.toDateString(row.last_used_at),
408
439
  expiresAt: this.toDateString(row.expires_at),
409
- };
440
+ ...(Object.keys(meta).length > 0 ? { meta: meta as SMeta } : {}),
441
+ } as Session<SMeta>;
410
442
  }
411
443
 
412
444
  /**
413
- * Convert partial Session updates to database format
445
+ * Convert partial Session updates to database format.
446
+ * Meta fields in updates are flattened as individual snake_case columns.
414
447
  */
415
- private toDbSessionUpdates(updates: Partial<Session>): Record<string, unknown> {
448
+ private toDbSessionUpdates(updates: Partial<Session<SMeta>> & Record<string, unknown>): Record<string, unknown> {
416
449
  const dbUpdates: Record<string, unknown> = {};
417
450
 
418
451
  if (updates.refreshTokenHash !== undefined) {
@@ -433,6 +466,12 @@ export class KnexStorageProvider<Meta extends UserMeta = Record<string, never>>
433
466
  if (updates.expiresAt !== undefined) {
434
467
  dbUpdates.expires_at = updates.expiresAt;
435
468
  }
469
+ // Flatten meta fields as individual snake_case columns
470
+ if (updates.meta) {
471
+ for (const [key, value] of Object.entries(updates.meta)) {
472
+ dbUpdates[camelToSnake(key)] = value;
473
+ }
474
+ }
436
475
 
437
476
  return dbUpdates;
438
477
  }
package/src/migration.ts CHANGED
@@ -82,12 +82,34 @@ export async function createUsersTableMigration(
82
82
  /**
83
83
  * SQL migration helper to create the sessions table
84
84
  * Can be used with Knex migrations
85
+ *
86
+ * @param knex - Knex instance
87
+ * @param tableName - Table name for sessions
88
+ * @param schemaOrCustomColumns - Optional schema name (for PostgreSQL) or callback to add custom columns
89
+ * @param customColumns - Optional callback to add custom columns to the table (when schema is provided)
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * // With custom meta columns
94
+ * await createSessionsTableMigration(knex, 'sessions', (table) => {
95
+ * table.string('organization_id').nullable();
96
+ * });
97
+ *
98
+ * // With schema and custom meta columns
99
+ * await createSessionsTableMigration(knex, 'sessions', 'my_schema', (table) => {
100
+ * table.string('organization_id').nullable();
101
+ * });
102
+ * ```
85
103
  */
86
104
  export async function createSessionsTableMigration(
87
105
  knex: Knex,
88
106
  tableName: string,
89
- schema?: string
107
+ schemaOrCustomColumns?: string | ((table: Knex.CreateTableBuilder) => void),
108
+ customColumns?: (table: Knex.CreateTableBuilder) => void
90
109
  ): Promise<void> {
110
+ const schema = typeof schemaOrCustomColumns === 'string' ? schemaOrCustomColumns : undefined;
111
+ const addCustomColumns =
112
+ typeof schemaOrCustomColumns === 'function' ? schemaOrCustomColumns : customColumns;
91
113
  const schemaBuilder = schema ? knex.schema.withSchema(schema) : knex.schema;
92
114
 
93
115
  await schemaBuilder.createTable(tableName, (table) => {
@@ -105,5 +127,10 @@ export async function createSessionsTableMigration(
105
127
  table.index('user_id');
106
128
  // Index for cleaning up expired sessions
107
129
  table.index('expires_at');
130
+
131
+ // Add custom meta columns if provided
132
+ if (addCustomColumns) {
133
+ addCustomColumns(table);
134
+ }
108
135
  });
109
136
  }