@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 +31 -10
- package/dist/index.d.ts +31 -10
- package/dist/index.js +41 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +41 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/knex.ts +53 -14
- package/src/migration.ts +28 -1
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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 {
|
package/dist/index.mjs.map
CHANGED
|
@@ -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.
|
|
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.
|
|
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<
|
|
54
|
-
|
|
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
|
-
|
|
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
|
}
|