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