@xcelsior/auth-adapter-dynamodb 1.0.0

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.
@@ -0,0 +1,22 @@
1
+
2
+ > @xcelsior/auth-adapter-dynamodb@1.0.0 build /home/circleci/repo/packages/services/auth-adapter-dynamodb
3
+ > tsup && tsc --noEmit
4
+
5
+ CLI Building entry: src/index.ts
6
+ CLI Using tsconfig: tsconfig.json
7
+ CLI tsup v8.5.1
8
+ CLI Using tsup config: /home/circleci/repo/packages/services/auth-adapter-dynamodb/tsup.config.ts
9
+ CLI Target: es2020
10
+ CLI Cleaning output folder
11
+ CJS Build start
12
+ ESM Build start
13
+ ESM dist/index.mjs 9.94 KB
14
+ ESM dist/index.mjs.map 21.64 KB
15
+ ESM ⚡️ Build success in 198ms
16
+ CJS dist/index.js 11.23 KB
17
+ CJS dist/index.js.map 21.71 KB
18
+ CJS ⚡️ Build success in 199ms
19
+ DTS Build start
20
+ DTS ⚡️ Build success in 20379ms
21
+ DTS dist/index.d.ts 1.33 KB
22
+ DTS dist/index.d.mts 1.33 KB
package/biome.json ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "//"
3
+ }
@@ -0,0 +1,30 @@
1
+ import { IStorageProvider, User, UserFilter, FindUsersOptions, FindUsersResult, Session } from '@xcelsior/auth';
2
+
3
+ interface DynamoDBConfig {
4
+ tableName: string;
5
+ sessionsTableName: string;
6
+ region: string;
7
+ }
8
+ declare class DynamoDBStorageProvider implements IStorageProvider {
9
+ private client;
10
+ private tableName;
11
+ private sessionsTableName;
12
+ constructor(config: DynamoDBConfig);
13
+ createUser(user: User): Promise<void>;
14
+ getUserByResetPasswordToken(resetPasswordToken: string): Promise<User | null>;
15
+ getUserByVerifyEmailToken(verifyEmailToken: string): Promise<User | null>;
16
+ getUserById(id: string): Promise<User | null>;
17
+ getUserByEmail(email: string): Promise<User | null>;
18
+ updateUser(id: string, updates: Partial<User>): Promise<void>;
19
+ deleteUser(id: string): Promise<void>;
20
+ findUsers(filter: UserFilter, options?: FindUsersOptions): Promise<FindUsersResult>;
21
+ createSession(session: Session): Promise<void>;
22
+ getSessionById(id: string): Promise<Session | null>;
23
+ getSessionsByUserId(userId: string): Promise<Session[]>;
24
+ updateSession(id: string, updates: Partial<Session>): Promise<void>;
25
+ deleteSession(id: string): Promise<void>;
26
+ deleteAllUserSessions(userId: string): Promise<void>;
27
+ deleteExpiredSessions(): Promise<void>;
28
+ }
29
+
30
+ export { type DynamoDBConfig, DynamoDBStorageProvider };
@@ -0,0 +1,30 @@
1
+ import { IStorageProvider, User, UserFilter, FindUsersOptions, FindUsersResult, Session } from '@xcelsior/auth';
2
+
3
+ interface DynamoDBConfig {
4
+ tableName: string;
5
+ sessionsTableName: string;
6
+ region: string;
7
+ }
8
+ declare class DynamoDBStorageProvider implements IStorageProvider {
9
+ private client;
10
+ private tableName;
11
+ private sessionsTableName;
12
+ constructor(config: DynamoDBConfig);
13
+ createUser(user: User): Promise<void>;
14
+ getUserByResetPasswordToken(resetPasswordToken: string): Promise<User | null>;
15
+ getUserByVerifyEmailToken(verifyEmailToken: string): Promise<User | null>;
16
+ getUserById(id: string): Promise<User | null>;
17
+ getUserByEmail(email: string): Promise<User | null>;
18
+ updateUser(id: string, updates: Partial<User>): Promise<void>;
19
+ deleteUser(id: string): Promise<void>;
20
+ findUsers(filter: UserFilter, options?: FindUsersOptions): Promise<FindUsersResult>;
21
+ createSession(session: Session): Promise<void>;
22
+ getSessionById(id: string): Promise<Session | null>;
23
+ getSessionsByUserId(userId: string): Promise<Session[]>;
24
+ updateSession(id: string, updates: Partial<Session>): Promise<void>;
25
+ deleteSession(id: string): Promise<void>;
26
+ deleteAllUserSessions(userId: string): Promise<void>;
27
+ deleteExpiredSessions(): Promise<void>;
28
+ }
29
+
30
+ export { type DynamoDBConfig, DynamoDBStorageProvider };
package/dist/index.js ADDED
@@ -0,0 +1,336 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ DynamoDBStorageProvider: () => DynamoDBStorageProvider
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/dynamodb.ts
28
+ var import_client_dynamodb = require("@aws-sdk/client-dynamodb");
29
+ var import_lib_dynamodb = require("@aws-sdk/lib-dynamodb");
30
+ var DynamoDBStorageProvider = class {
31
+ constructor(config) {
32
+ const dbClient = new import_client_dynamodb.DynamoDBClient({ region: config.region });
33
+ this.client = import_lib_dynamodb.DynamoDBDocumentClient.from(dbClient, {});
34
+ this.tableName = config.tableName;
35
+ this.sessionsTableName = config.sessionsTableName;
36
+ }
37
+ // ==================== User Methods ====================
38
+ async createUser(user) {
39
+ await this.client.send(
40
+ new import_lib_dynamodb.PutCommand({
41
+ TableName: this.tableName,
42
+ Item: user,
43
+ ConditionExpression: "attribute_not_exists(email)"
44
+ })
45
+ );
46
+ }
47
+ async getUserByResetPasswordToken(resetPasswordToken) {
48
+ const response = await this.client.send(
49
+ new import_lib_dynamodb.QueryCommand({
50
+ TableName: this.tableName,
51
+ IndexName: "ResetPasswordTokenIndex",
52
+ KeyConditionExpression: "resetPasswordToken = :token",
53
+ ExpressionAttributeValues: {
54
+ ":token": resetPasswordToken
55
+ }
56
+ })
57
+ );
58
+ return response.Items?.[0];
59
+ }
60
+ async getUserByVerifyEmailToken(verifyEmailToken) {
61
+ const response = await this.client.send(
62
+ new import_lib_dynamodb.QueryCommand({
63
+ TableName: this.tableName,
64
+ IndexName: "VerifyEmailTokenIndex",
65
+ KeyConditionExpression: "verificationToken = :token",
66
+ ExpressionAttributeValues: {
67
+ ":token": verifyEmailToken
68
+ }
69
+ })
70
+ );
71
+ return response.Items?.[0];
72
+ }
73
+ async getUserById(id) {
74
+ const response = await this.client.send(
75
+ new import_lib_dynamodb.GetCommand({
76
+ TableName: this.tableName,
77
+ Key: { id }
78
+ })
79
+ );
80
+ return response.Item;
81
+ }
82
+ async getUserByEmail(email) {
83
+ const response = await this.client.send(
84
+ new import_lib_dynamodb.QueryCommand({
85
+ TableName: this.tableName,
86
+ IndexName: "EmailIndex",
87
+ KeyConditionExpression: "email = :email",
88
+ ExpressionAttributeValues: {
89
+ ":email": email
90
+ }
91
+ })
92
+ );
93
+ return response.Items?.[0];
94
+ }
95
+ async updateUser(id, updates) {
96
+ const toSet = {};
97
+ const toRemove = [];
98
+ Object.entries(updates).forEach(([key, value]) => {
99
+ if (value === void 0) {
100
+ toRemove.push(key);
101
+ } else {
102
+ toSet[key] = value;
103
+ }
104
+ });
105
+ if (Object.keys(toSet).length === 0 && toRemove.length === 0) {
106
+ return;
107
+ }
108
+ const setPart = Object.keys(toSet).length > 0 ? `SET ${Object.keys(toSet).map((key) => `#${key} = :${key}`).join(", ")}` : "";
109
+ const removePart = toRemove.length > 0 ? `REMOVE ${toRemove.map((key) => `#${key}`).join(", ")}` : "";
110
+ const updateExpression = [setPart, removePart].filter(Boolean).join(" ");
111
+ const expressionAttributeNames = [...Object.keys(toSet), ...toRemove].reduce(
112
+ (acc, key) => ({ ...acc, [`#${key}`]: key }),
113
+ {}
114
+ );
115
+ const expressionAttributeValues = Object.entries(toSet).reduce(
116
+ (acc, [key, value]) => ({ ...acc, [`:${key}`]: value }),
117
+ {}
118
+ );
119
+ await this.client.send(
120
+ new import_lib_dynamodb.UpdateCommand({
121
+ TableName: this.tableName,
122
+ Key: { id },
123
+ UpdateExpression: updateExpression,
124
+ ExpressionAttributeNames: expressionAttributeNames,
125
+ ...Object.keys(expressionAttributeValues).length > 0 && {
126
+ ExpressionAttributeValues: expressionAttributeValues
127
+ }
128
+ })
129
+ );
130
+ }
131
+ async deleteUser(id) {
132
+ await this.deleteAllUserSessions(id);
133
+ await this.client.send(
134
+ new import_lib_dynamodb.DeleteCommand({
135
+ TableName: this.tableName,
136
+ Key: { id }
137
+ })
138
+ );
139
+ }
140
+ async findUsers(filter, options = {}) {
141
+ const limit = options.limit ?? 50;
142
+ const exclusiveStartKey = options.cursor ? JSON.parse(Buffer.from(options.cursor, "base64").toString()) : void 0;
143
+ if (filter.email && !filter.emailContains && !filter.roles && !filter.hasAnyRole && filter.isEmailVerified === void 0) {
144
+ const response2 = await this.client.send(
145
+ new import_lib_dynamodb.QueryCommand({
146
+ TableName: this.tableName,
147
+ IndexName: "EmailIndex",
148
+ KeyConditionExpression: "email = :email",
149
+ ExpressionAttributeValues: { ":email": filter.email },
150
+ Limit: limit,
151
+ ExclusiveStartKey: exclusiveStartKey
152
+ })
153
+ );
154
+ return {
155
+ users: response2.Items || [],
156
+ nextCursor: response2.LastEvaluatedKey ? Buffer.from(JSON.stringify(response2.LastEvaluatedKey)).toString("base64") : void 0
157
+ };
158
+ }
159
+ const filterExpressions = [];
160
+ const expressionAttributeNames = {};
161
+ const expressionAttributeValues = {};
162
+ if (filter.email) {
163
+ filterExpressions.push("email = :email");
164
+ expressionAttributeValues[":email"] = filter.email;
165
+ }
166
+ if (filter.emailContains) {
167
+ filterExpressions.push("contains(email, :emailContains)");
168
+ expressionAttributeValues[":emailContains"] = filter.emailContains;
169
+ }
170
+ if (filter.isEmailVerified !== void 0) {
171
+ filterExpressions.push("isEmailVerified = :isEmailVerified");
172
+ expressionAttributeValues[":isEmailVerified"] = filter.isEmailVerified;
173
+ }
174
+ if (filter.roles && filter.roles.length > 0) {
175
+ filter.roles.forEach((role, index) => {
176
+ filterExpressions.push(`contains(#roles, :role${index})`);
177
+ expressionAttributeValues[`:role${index}`] = role;
178
+ });
179
+ expressionAttributeNames["#roles"] = "roles";
180
+ }
181
+ if (filter.hasAnyRole && filter.hasAnyRole.length > 0) {
182
+ const anyRoleConditions = filter.hasAnyRole.map((role, index) => {
183
+ expressionAttributeValues[`:anyRole${index}`] = role;
184
+ return `contains(#roles, :anyRole${index})`;
185
+ });
186
+ filterExpressions.push(`(${anyRoleConditions.join(" OR ")})`);
187
+ if (!expressionAttributeNames["#roles"]) {
188
+ expressionAttributeNames["#roles"] = "roles";
189
+ }
190
+ }
191
+ const response = await this.client.send(
192
+ new import_lib_dynamodb.ScanCommand({
193
+ TableName: this.tableName,
194
+ FilterExpression: filterExpressions.length > 0 ? filterExpressions.join(" AND ") : void 0,
195
+ ExpressionAttributeNames: Object.keys(expressionAttributeNames).length > 0 ? expressionAttributeNames : void 0,
196
+ ExpressionAttributeValues: Object.keys(expressionAttributeValues).length > 0 ? expressionAttributeValues : void 0,
197
+ Limit: limit,
198
+ ExclusiveStartKey: exclusiveStartKey
199
+ })
200
+ );
201
+ return {
202
+ users: response.Items || [],
203
+ nextCursor: response.LastEvaluatedKey ? Buffer.from(JSON.stringify(response.LastEvaluatedKey)).toString("base64") : void 0
204
+ };
205
+ }
206
+ // ==================== Session Methods ====================
207
+ async createSession(session) {
208
+ await this.client.send(
209
+ new import_lib_dynamodb.PutCommand({
210
+ TableName: this.sessionsTableName,
211
+ Item: session
212
+ })
213
+ );
214
+ }
215
+ async getSessionById(id) {
216
+ const response = await this.client.send(
217
+ new import_lib_dynamodb.GetCommand({
218
+ TableName: this.sessionsTableName,
219
+ Key: { id }
220
+ })
221
+ );
222
+ return response.Item;
223
+ }
224
+ async getSessionsByUserId(userId) {
225
+ const response = await this.client.send(
226
+ new import_lib_dynamodb.QueryCommand({
227
+ TableName: this.sessionsTableName,
228
+ IndexName: "UserIdIndex",
229
+ KeyConditionExpression: "userId = :userId",
230
+ ExpressionAttributeValues: {
231
+ ":userId": userId
232
+ }
233
+ })
234
+ );
235
+ return response.Items || [];
236
+ }
237
+ async updateSession(id, updates) {
238
+ const toSet = {};
239
+ Object.entries(updates).forEach(([key, value]) => {
240
+ if (value !== void 0) {
241
+ toSet[key] = value;
242
+ }
243
+ });
244
+ if (Object.keys(toSet).length === 0) {
245
+ return;
246
+ }
247
+ const updateExpression = `SET ${Object.keys(toSet).map((key) => `#${key} = :${key}`).join(", ")}`;
248
+ const expressionAttributeNames = Object.keys(toSet).reduce(
249
+ (acc, key) => ({ ...acc, [`#${key}`]: key }),
250
+ {}
251
+ );
252
+ const expressionAttributeValues = Object.entries(toSet).reduce(
253
+ (acc, [key, value]) => ({ ...acc, [`:${key}`]: value }),
254
+ {}
255
+ );
256
+ await this.client.send(
257
+ new import_lib_dynamodb.UpdateCommand({
258
+ TableName: this.sessionsTableName,
259
+ Key: { id },
260
+ UpdateExpression: updateExpression,
261
+ ExpressionAttributeNames: expressionAttributeNames,
262
+ ExpressionAttributeValues: expressionAttributeValues
263
+ })
264
+ );
265
+ }
266
+ async deleteSession(id) {
267
+ await this.client.send(
268
+ new import_lib_dynamodb.DeleteCommand({
269
+ TableName: this.sessionsTableName,
270
+ Key: { id }
271
+ })
272
+ );
273
+ }
274
+ async deleteAllUserSessions(userId) {
275
+ const sessions = await this.getSessionsByUserId(userId);
276
+ if (sessions.length === 0) {
277
+ return;
278
+ }
279
+ const chunks = [];
280
+ for (let i = 0; i < sessions.length; i += 25) {
281
+ chunks.push(sessions.slice(i, i + 25));
282
+ }
283
+ for (const chunk of chunks) {
284
+ await this.client.send(
285
+ new import_lib_dynamodb.BatchWriteCommand({
286
+ RequestItems: {
287
+ [this.sessionsTableName]: chunk.map((session) => ({
288
+ DeleteRequest: {
289
+ Key: { id: session.id }
290
+ }
291
+ }))
292
+ }
293
+ })
294
+ );
295
+ }
296
+ }
297
+ async deleteExpiredSessions() {
298
+ const now = Date.now();
299
+ const response = await this.client.send(
300
+ new import_lib_dynamodb.QueryCommand({
301
+ TableName: this.sessionsTableName,
302
+ IndexName: "ExpiresAtIndex",
303
+ KeyConditionExpression: "expiresAt < :now",
304
+ ExpressionAttributeValues: {
305
+ ":now": now
306
+ }
307
+ })
308
+ );
309
+ const expiredSessions = response.Items || [];
310
+ if (expiredSessions.length === 0) {
311
+ return;
312
+ }
313
+ const chunks = [];
314
+ for (let i = 0; i < expiredSessions.length; i += 25) {
315
+ chunks.push(expiredSessions.slice(i, i + 25));
316
+ }
317
+ for (const chunk of chunks) {
318
+ await this.client.send(
319
+ new import_lib_dynamodb.BatchWriteCommand({
320
+ RequestItems: {
321
+ [this.sessionsTableName]: chunk.map((session) => ({
322
+ DeleteRequest: {
323
+ Key: { id: session.id }
324
+ }
325
+ }))
326
+ }
327
+ })
328
+ );
329
+ }
330
+ }
331
+ };
332
+ // Annotate the CommonJS export names for ESM import in node:
333
+ 0 && (module.exports = {
334
+ DynamoDBStorageProvider
335
+ });
336
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/dynamodb.ts"],"sourcesContent":["export { DynamoDBStorageProvider, type DynamoDBConfig } from './dynamodb';\n","import { DynamoDBClient } from '@aws-sdk/client-dynamodb';\nimport {\n DynamoDBDocumentClient,\n PutCommand,\n GetCommand,\n UpdateCommand,\n DeleteCommand,\n QueryCommand,\n ScanCommand,\n BatchWriteCommand,\n} from '@aws-sdk/lib-dynamodb';\nimport type {\n IStorageProvider,\n User,\n Session,\n UserFilter,\n FindUsersOptions,\n FindUsersResult,\n UserRole,\n} from '@xcelsior/auth';\n\nexport interface DynamoDBConfig {\n tableName: string;\n sessionsTableName: string;\n region: string;\n}\n\nexport class DynamoDBStorageProvider implements IStorageProvider {\n private client: DynamoDBDocumentClient;\n private tableName: string;\n private sessionsTableName: string;\n\n constructor(config: DynamoDBConfig) {\n const dbClient = new DynamoDBClient({ region: config.region });\n this.client = DynamoDBDocumentClient.from(dbClient, {});\n this.tableName = config.tableName;\n this.sessionsTableName = config.sessionsTableName;\n }\n\n // ==================== User Methods ====================\n\n async createUser(user: User): Promise<void> {\n await this.client.send(\n new PutCommand({\n TableName: this.tableName,\n Item: user,\n ConditionExpression: 'attribute_not_exists(email)',\n })\n );\n }\n\n async getUserByResetPasswordToken(resetPasswordToken: string): Promise<User | null> {\n const response = await this.client.send(\n new QueryCommand({\n TableName: this.tableName,\n IndexName: 'ResetPasswordTokenIndex',\n KeyConditionExpression: 'resetPasswordToken = :token',\n ExpressionAttributeValues: {\n ':token': resetPasswordToken,\n },\n })\n );\n\n return response.Items?.[0] as User | null;\n }\n\n async getUserByVerifyEmailToken(verifyEmailToken: string): Promise<User | null> {\n const response = await this.client.send(\n new QueryCommand({\n TableName: this.tableName,\n IndexName: 'VerifyEmailTokenIndex',\n KeyConditionExpression: 'verificationToken = :token',\n ExpressionAttributeValues: {\n ':token': verifyEmailToken,\n },\n })\n );\n\n return response.Items?.[0] as User | null;\n }\n\n async getUserById(id: string): Promise<User | null> {\n const response = await this.client.send(\n new GetCommand({\n TableName: this.tableName,\n Key: { id },\n })\n );\n\n return response.Item as User | null;\n }\n\n async getUserByEmail(email: string): Promise<User | null> {\n const response = await this.client.send(\n new QueryCommand({\n TableName: this.tableName,\n IndexName: 'EmailIndex',\n KeyConditionExpression: 'email = :email',\n ExpressionAttributeValues: {\n ':email': email,\n },\n })\n );\n\n return response.Items?.[0] as User | null;\n }\n\n async updateUser(id: string, updates: Partial<User>): Promise<void> {\n const toSet: Record<string, unknown> = {};\n const toRemove: string[] = [];\n\n // Separate attributes to set and remove\n Object.entries(updates).forEach(([key, value]) => {\n if (value === undefined) {\n toRemove.push(key);\n } else {\n toSet[key] = value;\n }\n });\n\n // If no updates at all, return early\n if (Object.keys(toSet).length === 0 && toRemove.length === 0) {\n return;\n }\n\n // Build the update expression\n const setPart =\n Object.keys(toSet).length > 0\n ? `SET ${Object.keys(toSet)\n .map((key) => `#${key} = :${key}`)\n .join(', ')}`\n : '';\n\n const removePart =\n toRemove.length > 0 ? `REMOVE ${toRemove.map((key) => `#${key}`).join(', ')}` : '';\n\n const updateExpression = [setPart, removePart].filter(Boolean).join(' ');\n\n // Build expression attribute names (needed for both SET and REMOVE)\n const expressionAttributeNames = [...Object.keys(toSet), ...toRemove].reduce(\n (acc, key) => ({ ...acc, [`#${key}`]: key }),\n {}\n );\n\n // Build expression attribute values (only needed for SET)\n const expressionAttributeValues = Object.entries(toSet).reduce(\n (acc, [key, value]) => ({ ...acc, [`:${key}`]: value }),\n {}\n );\n\n await this.client.send(\n new UpdateCommand({\n TableName: this.tableName,\n Key: { id },\n UpdateExpression: updateExpression,\n ExpressionAttributeNames: expressionAttributeNames,\n ...(Object.keys(expressionAttributeValues).length > 0 && {\n ExpressionAttributeValues: expressionAttributeValues,\n }),\n })\n );\n }\n\n async deleteUser(id: string): Promise<void> {\n // Delete all user sessions first\n await this.deleteAllUserSessions(id);\n\n await this.client.send(\n new DeleteCommand({\n TableName: this.tableName,\n Key: { id },\n })\n );\n }\n\n async findUsers(filter: UserFilter, options: FindUsersOptions = {}): Promise<FindUsersResult> {\n const limit = options.limit ?? 50;\n const exclusiveStartKey = options.cursor\n ? JSON.parse(Buffer.from(options.cursor, 'base64').toString())\n : undefined;\n\n // If filtering by exact email only, use the EmailIndex (most performant)\n if (\n filter.email &&\n !filter.emailContains &&\n !filter.roles &&\n !filter.hasAnyRole &&\n filter.isEmailVerified === undefined\n ) {\n const response = await this.client.send(\n new QueryCommand({\n TableName: this.tableName,\n IndexName: 'EmailIndex',\n KeyConditionExpression: 'email = :email',\n ExpressionAttributeValues: { ':email': filter.email },\n Limit: limit,\n ExclusiveStartKey: exclusiveStartKey,\n })\n );\n\n return {\n users: (response.Items || []) as User[],\n nextCursor: response.LastEvaluatedKey\n ? Buffer.from(JSON.stringify(response.LastEvaluatedKey)).toString('base64')\n : undefined,\n };\n }\n\n // For other filters, use Scan with FilterExpression\n const filterExpressions: string[] = [];\n const expressionAttributeNames: Record<string, string> = {};\n const expressionAttributeValues: Record<string, unknown> = {};\n\n if (filter.email) {\n filterExpressions.push('email = :email');\n expressionAttributeValues[':email'] = filter.email;\n }\n\n if (filter.emailContains) {\n filterExpressions.push('contains(email, :emailContains)');\n expressionAttributeValues[':emailContains'] = filter.emailContains;\n }\n\n if (filter.isEmailVerified !== undefined) {\n filterExpressions.push('isEmailVerified = :isEmailVerified');\n expressionAttributeValues[':isEmailVerified'] = filter.isEmailVerified;\n }\n\n // For roles (user must have ALL specified roles)\n if (filter.roles && filter.roles.length > 0) {\n filter.roles.forEach((role: UserRole, index: number) => {\n filterExpressions.push(`contains(#roles, :role${index})`);\n expressionAttributeValues[`:role${index}`] = role;\n });\n expressionAttributeNames['#roles'] = 'roles';\n }\n\n // For hasAnyRole (user must have at least ONE)\n if (filter.hasAnyRole && filter.hasAnyRole.length > 0) {\n const anyRoleConditions = filter.hasAnyRole.map((role: UserRole, index: number) => {\n expressionAttributeValues[`:anyRole${index}`] = role;\n return `contains(#roles, :anyRole${index})`;\n });\n filterExpressions.push(`(${anyRoleConditions.join(' OR ')})`);\n if (!expressionAttributeNames['#roles']) {\n expressionAttributeNames['#roles'] = 'roles';\n }\n }\n\n const response = await this.client.send(\n new ScanCommand({\n TableName: this.tableName,\n FilterExpression:\n filterExpressions.length > 0 ? filterExpressions.join(' AND ') : undefined,\n ExpressionAttributeNames:\n Object.keys(expressionAttributeNames).length > 0\n ? expressionAttributeNames\n : undefined,\n ExpressionAttributeValues:\n Object.keys(expressionAttributeValues).length > 0\n ? expressionAttributeValues\n : undefined,\n Limit: limit,\n ExclusiveStartKey: exclusiveStartKey,\n })\n );\n\n return {\n users: (response.Items || []) as User[],\n nextCursor: response.LastEvaluatedKey\n ? Buffer.from(JSON.stringify(response.LastEvaluatedKey)).toString('base64')\n : undefined,\n };\n }\n\n // ==================== Session Methods ====================\n\n async createSession(session: Session): Promise<void> {\n await this.client.send(\n new PutCommand({\n TableName: this.sessionsTableName,\n Item: session,\n })\n );\n }\n\n async getSessionById(id: string): Promise<Session | null> {\n const response = await this.client.send(\n new GetCommand({\n TableName: this.sessionsTableName,\n Key: { id },\n })\n );\n\n return response.Item as Session | null;\n }\n\n async getSessionsByUserId(userId: string): Promise<Session[]> {\n const response = await this.client.send(\n new QueryCommand({\n TableName: this.sessionsTableName,\n IndexName: 'UserIdIndex',\n KeyConditionExpression: 'userId = :userId',\n ExpressionAttributeValues: {\n ':userId': userId,\n },\n })\n );\n\n return (response.Items || []) as Session[];\n }\n\n async updateSession(id: string, updates: Partial<Session>): Promise<void> {\n const toSet: Record<string, unknown> = {};\n\n Object.entries(updates).forEach(([key, value]) => {\n if (value !== undefined) {\n toSet[key] = value;\n }\n });\n\n if (Object.keys(toSet).length === 0) {\n return;\n }\n\n const updateExpression = `SET ${Object.keys(toSet)\n .map((key) => `#${key} = :${key}`)\n .join(', ')}`;\n\n const expressionAttributeNames = Object.keys(toSet).reduce(\n (acc, key) => ({ ...acc, [`#${key}`]: key }),\n {}\n );\n\n const expressionAttributeValues = Object.entries(toSet).reduce(\n (acc, [key, value]) => ({ ...acc, [`:${key}`]: value }),\n {}\n );\n\n await this.client.send(\n new UpdateCommand({\n TableName: this.sessionsTableName,\n Key: { id },\n UpdateExpression: updateExpression,\n ExpressionAttributeNames: expressionAttributeNames,\n ExpressionAttributeValues: expressionAttributeValues,\n })\n );\n }\n\n async deleteSession(id: string): Promise<void> {\n await this.client.send(\n new DeleteCommand({\n TableName: this.sessionsTableName,\n Key: { id },\n })\n );\n }\n\n async deleteAllUserSessions(userId: string): Promise<void> {\n const sessions = await this.getSessionsByUserId(userId);\n\n if (sessions.length === 0) {\n return;\n }\n\n // Batch delete in chunks of 25 (DynamoDB limit)\n const chunks = [];\n for (let i = 0; i < sessions.length; i += 25) {\n chunks.push(sessions.slice(i, i + 25));\n }\n\n for (const chunk of chunks) {\n await this.client.send(\n new BatchWriteCommand({\n RequestItems: {\n [this.sessionsTableName]: chunk.map((session) => ({\n DeleteRequest: {\n Key: { id: session.id },\n },\n })),\n },\n })\n );\n }\n }\n\n async deleteExpiredSessions(): Promise<void> {\n // Note: This requires a full table scan which is expensive.\n // Consider using DynamoDB TTL instead for automatic expiration.\n // This method is provided for manual cleanup if needed.\n const now = Date.now();\n\n const response = await this.client.send(\n new QueryCommand({\n TableName: this.sessionsTableName,\n IndexName: 'ExpiresAtIndex',\n KeyConditionExpression: 'expiresAt < :now',\n ExpressionAttributeValues: {\n ':now': now,\n },\n })\n );\n\n const expiredSessions = (response.Items || []) as Session[];\n\n if (expiredSessions.length === 0) {\n return;\n }\n\n const chunks = [];\n for (let i = 0; i < expiredSessions.length; i += 25) {\n chunks.push(expiredSessions.slice(i, i + 25));\n }\n\n for (const chunk of chunks) {\n await this.client.send(\n new BatchWriteCommand({\n RequestItems: {\n [this.sessionsTableName]: chunk.map((session) => ({\n DeleteRequest: {\n Key: { id: session.id },\n },\n })),\n },\n })\n );\n }\n }\n}\n"],"mappings":"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","names":["response"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,318 @@
1
+ // src/dynamodb.ts
2
+ import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
3
+ import {
4
+ DynamoDBDocumentClient,
5
+ PutCommand,
6
+ GetCommand,
7
+ UpdateCommand,
8
+ DeleteCommand,
9
+ QueryCommand,
10
+ ScanCommand,
11
+ BatchWriteCommand
12
+ } from "@aws-sdk/lib-dynamodb";
13
+ var DynamoDBStorageProvider = class {
14
+ constructor(config) {
15
+ const dbClient = new DynamoDBClient({ region: config.region });
16
+ this.client = DynamoDBDocumentClient.from(dbClient, {});
17
+ this.tableName = config.tableName;
18
+ this.sessionsTableName = config.sessionsTableName;
19
+ }
20
+ // ==================== User Methods ====================
21
+ async createUser(user) {
22
+ await this.client.send(
23
+ new PutCommand({
24
+ TableName: this.tableName,
25
+ Item: user,
26
+ ConditionExpression: "attribute_not_exists(email)"
27
+ })
28
+ );
29
+ }
30
+ async getUserByResetPasswordToken(resetPasswordToken) {
31
+ const response = await this.client.send(
32
+ new QueryCommand({
33
+ TableName: this.tableName,
34
+ IndexName: "ResetPasswordTokenIndex",
35
+ KeyConditionExpression: "resetPasswordToken = :token",
36
+ ExpressionAttributeValues: {
37
+ ":token": resetPasswordToken
38
+ }
39
+ })
40
+ );
41
+ return response.Items?.[0];
42
+ }
43
+ async getUserByVerifyEmailToken(verifyEmailToken) {
44
+ const response = await this.client.send(
45
+ new QueryCommand({
46
+ TableName: this.tableName,
47
+ IndexName: "VerifyEmailTokenIndex",
48
+ KeyConditionExpression: "verificationToken = :token",
49
+ ExpressionAttributeValues: {
50
+ ":token": verifyEmailToken
51
+ }
52
+ })
53
+ );
54
+ return response.Items?.[0];
55
+ }
56
+ async getUserById(id) {
57
+ const response = await this.client.send(
58
+ new GetCommand({
59
+ TableName: this.tableName,
60
+ Key: { id }
61
+ })
62
+ );
63
+ return response.Item;
64
+ }
65
+ async getUserByEmail(email) {
66
+ const response = await this.client.send(
67
+ new QueryCommand({
68
+ TableName: this.tableName,
69
+ IndexName: "EmailIndex",
70
+ KeyConditionExpression: "email = :email",
71
+ ExpressionAttributeValues: {
72
+ ":email": email
73
+ }
74
+ })
75
+ );
76
+ return response.Items?.[0];
77
+ }
78
+ async updateUser(id, updates) {
79
+ const toSet = {};
80
+ const toRemove = [];
81
+ Object.entries(updates).forEach(([key, value]) => {
82
+ if (value === void 0) {
83
+ toRemove.push(key);
84
+ } else {
85
+ toSet[key] = value;
86
+ }
87
+ });
88
+ if (Object.keys(toSet).length === 0 && toRemove.length === 0) {
89
+ return;
90
+ }
91
+ const setPart = Object.keys(toSet).length > 0 ? `SET ${Object.keys(toSet).map((key) => `#${key} = :${key}`).join(", ")}` : "";
92
+ const removePart = toRemove.length > 0 ? `REMOVE ${toRemove.map((key) => `#${key}`).join(", ")}` : "";
93
+ const updateExpression = [setPart, removePart].filter(Boolean).join(" ");
94
+ const expressionAttributeNames = [...Object.keys(toSet), ...toRemove].reduce(
95
+ (acc, key) => ({ ...acc, [`#${key}`]: key }),
96
+ {}
97
+ );
98
+ const expressionAttributeValues = Object.entries(toSet).reduce(
99
+ (acc, [key, value]) => ({ ...acc, [`:${key}`]: value }),
100
+ {}
101
+ );
102
+ await this.client.send(
103
+ new UpdateCommand({
104
+ TableName: this.tableName,
105
+ Key: { id },
106
+ UpdateExpression: updateExpression,
107
+ ExpressionAttributeNames: expressionAttributeNames,
108
+ ...Object.keys(expressionAttributeValues).length > 0 && {
109
+ ExpressionAttributeValues: expressionAttributeValues
110
+ }
111
+ })
112
+ );
113
+ }
114
+ async deleteUser(id) {
115
+ await this.deleteAllUserSessions(id);
116
+ await this.client.send(
117
+ new DeleteCommand({
118
+ TableName: this.tableName,
119
+ Key: { id }
120
+ })
121
+ );
122
+ }
123
+ async findUsers(filter, options = {}) {
124
+ const limit = options.limit ?? 50;
125
+ const exclusiveStartKey = options.cursor ? JSON.parse(Buffer.from(options.cursor, "base64").toString()) : void 0;
126
+ if (filter.email && !filter.emailContains && !filter.roles && !filter.hasAnyRole && filter.isEmailVerified === void 0) {
127
+ const response2 = await this.client.send(
128
+ new QueryCommand({
129
+ TableName: this.tableName,
130
+ IndexName: "EmailIndex",
131
+ KeyConditionExpression: "email = :email",
132
+ ExpressionAttributeValues: { ":email": filter.email },
133
+ Limit: limit,
134
+ ExclusiveStartKey: exclusiveStartKey
135
+ })
136
+ );
137
+ return {
138
+ users: response2.Items || [],
139
+ nextCursor: response2.LastEvaluatedKey ? Buffer.from(JSON.stringify(response2.LastEvaluatedKey)).toString("base64") : void 0
140
+ };
141
+ }
142
+ const filterExpressions = [];
143
+ const expressionAttributeNames = {};
144
+ const expressionAttributeValues = {};
145
+ if (filter.email) {
146
+ filterExpressions.push("email = :email");
147
+ expressionAttributeValues[":email"] = filter.email;
148
+ }
149
+ if (filter.emailContains) {
150
+ filterExpressions.push("contains(email, :emailContains)");
151
+ expressionAttributeValues[":emailContains"] = filter.emailContains;
152
+ }
153
+ if (filter.isEmailVerified !== void 0) {
154
+ filterExpressions.push("isEmailVerified = :isEmailVerified");
155
+ expressionAttributeValues[":isEmailVerified"] = filter.isEmailVerified;
156
+ }
157
+ if (filter.roles && filter.roles.length > 0) {
158
+ filter.roles.forEach((role, index) => {
159
+ filterExpressions.push(`contains(#roles, :role${index})`);
160
+ expressionAttributeValues[`:role${index}`] = role;
161
+ });
162
+ expressionAttributeNames["#roles"] = "roles";
163
+ }
164
+ if (filter.hasAnyRole && filter.hasAnyRole.length > 0) {
165
+ const anyRoleConditions = filter.hasAnyRole.map((role, index) => {
166
+ expressionAttributeValues[`:anyRole${index}`] = role;
167
+ return `contains(#roles, :anyRole${index})`;
168
+ });
169
+ filterExpressions.push(`(${anyRoleConditions.join(" OR ")})`);
170
+ if (!expressionAttributeNames["#roles"]) {
171
+ expressionAttributeNames["#roles"] = "roles";
172
+ }
173
+ }
174
+ const response = await this.client.send(
175
+ new ScanCommand({
176
+ TableName: this.tableName,
177
+ FilterExpression: filterExpressions.length > 0 ? filterExpressions.join(" AND ") : void 0,
178
+ ExpressionAttributeNames: Object.keys(expressionAttributeNames).length > 0 ? expressionAttributeNames : void 0,
179
+ ExpressionAttributeValues: Object.keys(expressionAttributeValues).length > 0 ? expressionAttributeValues : void 0,
180
+ Limit: limit,
181
+ ExclusiveStartKey: exclusiveStartKey
182
+ })
183
+ );
184
+ return {
185
+ users: response.Items || [],
186
+ nextCursor: response.LastEvaluatedKey ? Buffer.from(JSON.stringify(response.LastEvaluatedKey)).toString("base64") : void 0
187
+ };
188
+ }
189
+ // ==================== Session Methods ====================
190
+ async createSession(session) {
191
+ await this.client.send(
192
+ new PutCommand({
193
+ TableName: this.sessionsTableName,
194
+ Item: session
195
+ })
196
+ );
197
+ }
198
+ async getSessionById(id) {
199
+ const response = await this.client.send(
200
+ new GetCommand({
201
+ TableName: this.sessionsTableName,
202
+ Key: { id }
203
+ })
204
+ );
205
+ return response.Item;
206
+ }
207
+ async getSessionsByUserId(userId) {
208
+ const response = await this.client.send(
209
+ new QueryCommand({
210
+ TableName: this.sessionsTableName,
211
+ IndexName: "UserIdIndex",
212
+ KeyConditionExpression: "userId = :userId",
213
+ ExpressionAttributeValues: {
214
+ ":userId": userId
215
+ }
216
+ })
217
+ );
218
+ return response.Items || [];
219
+ }
220
+ async updateSession(id, updates) {
221
+ const toSet = {};
222
+ Object.entries(updates).forEach(([key, value]) => {
223
+ if (value !== void 0) {
224
+ toSet[key] = value;
225
+ }
226
+ });
227
+ if (Object.keys(toSet).length === 0) {
228
+ return;
229
+ }
230
+ const updateExpression = `SET ${Object.keys(toSet).map((key) => `#${key} = :${key}`).join(", ")}`;
231
+ const expressionAttributeNames = Object.keys(toSet).reduce(
232
+ (acc, key) => ({ ...acc, [`#${key}`]: key }),
233
+ {}
234
+ );
235
+ const expressionAttributeValues = Object.entries(toSet).reduce(
236
+ (acc, [key, value]) => ({ ...acc, [`:${key}`]: value }),
237
+ {}
238
+ );
239
+ await this.client.send(
240
+ new UpdateCommand({
241
+ TableName: this.sessionsTableName,
242
+ Key: { id },
243
+ UpdateExpression: updateExpression,
244
+ ExpressionAttributeNames: expressionAttributeNames,
245
+ ExpressionAttributeValues: expressionAttributeValues
246
+ })
247
+ );
248
+ }
249
+ async deleteSession(id) {
250
+ await this.client.send(
251
+ new DeleteCommand({
252
+ TableName: this.sessionsTableName,
253
+ Key: { id }
254
+ })
255
+ );
256
+ }
257
+ async deleteAllUserSessions(userId) {
258
+ const sessions = await this.getSessionsByUserId(userId);
259
+ if (sessions.length === 0) {
260
+ return;
261
+ }
262
+ const chunks = [];
263
+ for (let i = 0; i < sessions.length; i += 25) {
264
+ chunks.push(sessions.slice(i, i + 25));
265
+ }
266
+ for (const chunk of chunks) {
267
+ await this.client.send(
268
+ new BatchWriteCommand({
269
+ RequestItems: {
270
+ [this.sessionsTableName]: chunk.map((session) => ({
271
+ DeleteRequest: {
272
+ Key: { id: session.id }
273
+ }
274
+ }))
275
+ }
276
+ })
277
+ );
278
+ }
279
+ }
280
+ async deleteExpiredSessions() {
281
+ const now = Date.now();
282
+ const response = await this.client.send(
283
+ new QueryCommand({
284
+ TableName: this.sessionsTableName,
285
+ IndexName: "ExpiresAtIndex",
286
+ KeyConditionExpression: "expiresAt < :now",
287
+ ExpressionAttributeValues: {
288
+ ":now": now
289
+ }
290
+ })
291
+ );
292
+ const expiredSessions = response.Items || [];
293
+ if (expiredSessions.length === 0) {
294
+ return;
295
+ }
296
+ const chunks = [];
297
+ for (let i = 0; i < expiredSessions.length; i += 25) {
298
+ chunks.push(expiredSessions.slice(i, i + 25));
299
+ }
300
+ for (const chunk of chunks) {
301
+ await this.client.send(
302
+ new BatchWriteCommand({
303
+ RequestItems: {
304
+ [this.sessionsTableName]: chunk.map((session) => ({
305
+ DeleteRequest: {
306
+ Key: { id: session.id }
307
+ }
308
+ }))
309
+ }
310
+ })
311
+ );
312
+ }
313
+ }
314
+ };
315
+ export {
316
+ DynamoDBStorageProvider
317
+ };
318
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/dynamodb.ts"],"sourcesContent":["import { DynamoDBClient } from '@aws-sdk/client-dynamodb';\nimport {\n DynamoDBDocumentClient,\n PutCommand,\n GetCommand,\n UpdateCommand,\n DeleteCommand,\n QueryCommand,\n ScanCommand,\n BatchWriteCommand,\n} from '@aws-sdk/lib-dynamodb';\nimport type {\n IStorageProvider,\n User,\n Session,\n UserFilter,\n FindUsersOptions,\n FindUsersResult,\n UserRole,\n} from '@xcelsior/auth';\n\nexport interface DynamoDBConfig {\n tableName: string;\n sessionsTableName: string;\n region: string;\n}\n\nexport class DynamoDBStorageProvider implements IStorageProvider {\n private client: DynamoDBDocumentClient;\n private tableName: string;\n private sessionsTableName: string;\n\n constructor(config: DynamoDBConfig) {\n const dbClient = new DynamoDBClient({ region: config.region });\n this.client = DynamoDBDocumentClient.from(dbClient, {});\n this.tableName = config.tableName;\n this.sessionsTableName = config.sessionsTableName;\n }\n\n // ==================== User Methods ====================\n\n async createUser(user: User): Promise<void> {\n await this.client.send(\n new PutCommand({\n TableName: this.tableName,\n Item: user,\n ConditionExpression: 'attribute_not_exists(email)',\n })\n );\n }\n\n async getUserByResetPasswordToken(resetPasswordToken: string): Promise<User | null> {\n const response = await this.client.send(\n new QueryCommand({\n TableName: this.tableName,\n IndexName: 'ResetPasswordTokenIndex',\n KeyConditionExpression: 'resetPasswordToken = :token',\n ExpressionAttributeValues: {\n ':token': resetPasswordToken,\n },\n })\n );\n\n return response.Items?.[0] as User | null;\n }\n\n async getUserByVerifyEmailToken(verifyEmailToken: string): Promise<User | null> {\n const response = await this.client.send(\n new QueryCommand({\n TableName: this.tableName,\n IndexName: 'VerifyEmailTokenIndex',\n KeyConditionExpression: 'verificationToken = :token',\n ExpressionAttributeValues: {\n ':token': verifyEmailToken,\n },\n })\n );\n\n return response.Items?.[0] as User | null;\n }\n\n async getUserById(id: string): Promise<User | null> {\n const response = await this.client.send(\n new GetCommand({\n TableName: this.tableName,\n Key: { id },\n })\n );\n\n return response.Item as User | null;\n }\n\n async getUserByEmail(email: string): Promise<User | null> {\n const response = await this.client.send(\n new QueryCommand({\n TableName: this.tableName,\n IndexName: 'EmailIndex',\n KeyConditionExpression: 'email = :email',\n ExpressionAttributeValues: {\n ':email': email,\n },\n })\n );\n\n return response.Items?.[0] as User | null;\n }\n\n async updateUser(id: string, updates: Partial<User>): Promise<void> {\n const toSet: Record<string, unknown> = {};\n const toRemove: string[] = [];\n\n // Separate attributes to set and remove\n Object.entries(updates).forEach(([key, value]) => {\n if (value === undefined) {\n toRemove.push(key);\n } else {\n toSet[key] = value;\n }\n });\n\n // If no updates at all, return early\n if (Object.keys(toSet).length === 0 && toRemove.length === 0) {\n return;\n }\n\n // Build the update expression\n const setPart =\n Object.keys(toSet).length > 0\n ? `SET ${Object.keys(toSet)\n .map((key) => `#${key} = :${key}`)\n .join(', ')}`\n : '';\n\n const removePart =\n toRemove.length > 0 ? `REMOVE ${toRemove.map((key) => `#${key}`).join(', ')}` : '';\n\n const updateExpression = [setPart, removePart].filter(Boolean).join(' ');\n\n // Build expression attribute names (needed for both SET and REMOVE)\n const expressionAttributeNames = [...Object.keys(toSet), ...toRemove].reduce(\n (acc, key) => ({ ...acc, [`#${key}`]: key }),\n {}\n );\n\n // Build expression attribute values (only needed for SET)\n const expressionAttributeValues = Object.entries(toSet).reduce(\n (acc, [key, value]) => ({ ...acc, [`:${key}`]: value }),\n {}\n );\n\n await this.client.send(\n new UpdateCommand({\n TableName: this.tableName,\n Key: { id },\n UpdateExpression: updateExpression,\n ExpressionAttributeNames: expressionAttributeNames,\n ...(Object.keys(expressionAttributeValues).length > 0 && {\n ExpressionAttributeValues: expressionAttributeValues,\n }),\n })\n );\n }\n\n async deleteUser(id: string): Promise<void> {\n // Delete all user sessions first\n await this.deleteAllUserSessions(id);\n\n await this.client.send(\n new DeleteCommand({\n TableName: this.tableName,\n Key: { id },\n })\n );\n }\n\n async findUsers(filter: UserFilter, options: FindUsersOptions = {}): Promise<FindUsersResult> {\n const limit = options.limit ?? 50;\n const exclusiveStartKey = options.cursor\n ? JSON.parse(Buffer.from(options.cursor, 'base64').toString())\n : undefined;\n\n // If filtering by exact email only, use the EmailIndex (most performant)\n if (\n filter.email &&\n !filter.emailContains &&\n !filter.roles &&\n !filter.hasAnyRole &&\n filter.isEmailVerified === undefined\n ) {\n const response = await this.client.send(\n new QueryCommand({\n TableName: this.tableName,\n IndexName: 'EmailIndex',\n KeyConditionExpression: 'email = :email',\n ExpressionAttributeValues: { ':email': filter.email },\n Limit: limit,\n ExclusiveStartKey: exclusiveStartKey,\n })\n );\n\n return {\n users: (response.Items || []) as User[],\n nextCursor: response.LastEvaluatedKey\n ? Buffer.from(JSON.stringify(response.LastEvaluatedKey)).toString('base64')\n : undefined,\n };\n }\n\n // For other filters, use Scan with FilterExpression\n const filterExpressions: string[] = [];\n const expressionAttributeNames: Record<string, string> = {};\n const expressionAttributeValues: Record<string, unknown> = {};\n\n if (filter.email) {\n filterExpressions.push('email = :email');\n expressionAttributeValues[':email'] = filter.email;\n }\n\n if (filter.emailContains) {\n filterExpressions.push('contains(email, :emailContains)');\n expressionAttributeValues[':emailContains'] = filter.emailContains;\n }\n\n if (filter.isEmailVerified !== undefined) {\n filterExpressions.push('isEmailVerified = :isEmailVerified');\n expressionAttributeValues[':isEmailVerified'] = filter.isEmailVerified;\n }\n\n // For roles (user must have ALL specified roles)\n if (filter.roles && filter.roles.length > 0) {\n filter.roles.forEach((role: UserRole, index: number) => {\n filterExpressions.push(`contains(#roles, :role${index})`);\n expressionAttributeValues[`:role${index}`] = role;\n });\n expressionAttributeNames['#roles'] = 'roles';\n }\n\n // For hasAnyRole (user must have at least ONE)\n if (filter.hasAnyRole && filter.hasAnyRole.length > 0) {\n const anyRoleConditions = filter.hasAnyRole.map((role: UserRole, index: number) => {\n expressionAttributeValues[`:anyRole${index}`] = role;\n return `contains(#roles, :anyRole${index})`;\n });\n filterExpressions.push(`(${anyRoleConditions.join(' OR ')})`);\n if (!expressionAttributeNames['#roles']) {\n expressionAttributeNames['#roles'] = 'roles';\n }\n }\n\n const response = await this.client.send(\n new ScanCommand({\n TableName: this.tableName,\n FilterExpression:\n filterExpressions.length > 0 ? filterExpressions.join(' AND ') : undefined,\n ExpressionAttributeNames:\n Object.keys(expressionAttributeNames).length > 0\n ? expressionAttributeNames\n : undefined,\n ExpressionAttributeValues:\n Object.keys(expressionAttributeValues).length > 0\n ? expressionAttributeValues\n : undefined,\n Limit: limit,\n ExclusiveStartKey: exclusiveStartKey,\n })\n );\n\n return {\n users: (response.Items || []) as User[],\n nextCursor: response.LastEvaluatedKey\n ? Buffer.from(JSON.stringify(response.LastEvaluatedKey)).toString('base64')\n : undefined,\n };\n }\n\n // ==================== Session Methods ====================\n\n async createSession(session: Session): Promise<void> {\n await this.client.send(\n new PutCommand({\n TableName: this.sessionsTableName,\n Item: session,\n })\n );\n }\n\n async getSessionById(id: string): Promise<Session | null> {\n const response = await this.client.send(\n new GetCommand({\n TableName: this.sessionsTableName,\n Key: { id },\n })\n );\n\n return response.Item as Session | null;\n }\n\n async getSessionsByUserId(userId: string): Promise<Session[]> {\n const response = await this.client.send(\n new QueryCommand({\n TableName: this.sessionsTableName,\n IndexName: 'UserIdIndex',\n KeyConditionExpression: 'userId = :userId',\n ExpressionAttributeValues: {\n ':userId': userId,\n },\n })\n );\n\n return (response.Items || []) as Session[];\n }\n\n async updateSession(id: string, updates: Partial<Session>): Promise<void> {\n const toSet: Record<string, unknown> = {};\n\n Object.entries(updates).forEach(([key, value]) => {\n if (value !== undefined) {\n toSet[key] = value;\n }\n });\n\n if (Object.keys(toSet).length === 0) {\n return;\n }\n\n const updateExpression = `SET ${Object.keys(toSet)\n .map((key) => `#${key} = :${key}`)\n .join(', ')}`;\n\n const expressionAttributeNames = Object.keys(toSet).reduce(\n (acc, key) => ({ ...acc, [`#${key}`]: key }),\n {}\n );\n\n const expressionAttributeValues = Object.entries(toSet).reduce(\n (acc, [key, value]) => ({ ...acc, [`:${key}`]: value }),\n {}\n );\n\n await this.client.send(\n new UpdateCommand({\n TableName: this.sessionsTableName,\n Key: { id },\n UpdateExpression: updateExpression,\n ExpressionAttributeNames: expressionAttributeNames,\n ExpressionAttributeValues: expressionAttributeValues,\n })\n );\n }\n\n async deleteSession(id: string): Promise<void> {\n await this.client.send(\n new DeleteCommand({\n TableName: this.sessionsTableName,\n Key: { id },\n })\n );\n }\n\n async deleteAllUserSessions(userId: string): Promise<void> {\n const sessions = await this.getSessionsByUserId(userId);\n\n if (sessions.length === 0) {\n return;\n }\n\n // Batch delete in chunks of 25 (DynamoDB limit)\n const chunks = [];\n for (let i = 0; i < sessions.length; i += 25) {\n chunks.push(sessions.slice(i, i + 25));\n }\n\n for (const chunk of chunks) {\n await this.client.send(\n new BatchWriteCommand({\n RequestItems: {\n [this.sessionsTableName]: chunk.map((session) => ({\n DeleteRequest: {\n Key: { id: session.id },\n },\n })),\n },\n })\n );\n }\n }\n\n async deleteExpiredSessions(): Promise<void> {\n // Note: This requires a full table scan which is expensive.\n // Consider using DynamoDB TTL instead for automatic expiration.\n // This method is provided for manual cleanup if needed.\n const now = Date.now();\n\n const response = await this.client.send(\n new QueryCommand({\n TableName: this.sessionsTableName,\n IndexName: 'ExpiresAtIndex',\n KeyConditionExpression: 'expiresAt < :now',\n ExpressionAttributeValues: {\n ':now': now,\n },\n })\n );\n\n const expiredSessions = (response.Items || []) as Session[];\n\n if (expiredSessions.length === 0) {\n return;\n }\n\n const chunks = [];\n for (let i = 0; i < expiredSessions.length; i += 25) {\n chunks.push(expiredSessions.slice(i, i + 25));\n }\n\n for (const chunk of chunks) {\n await this.client.send(\n new BatchWriteCommand({\n RequestItems: {\n [this.sessionsTableName]: chunk.map((session) => ({\n DeleteRequest: {\n Key: { id: session.id },\n },\n })),\n },\n })\n );\n }\n }\n}\n"],"mappings":"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","names":["response"]}
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@xcelsior/auth-adapter-dynamodb",
3
+ "version": "1.0.0",
4
+ "description": "DynamoDB storage adapter for @xcelsior/auth",
5
+ "exports": {
6
+ ".": {
7
+ "import": "./src/index.ts",
8
+ "require": "./dist/index.js"
9
+ }
10
+ },
11
+ "main": "src/index.ts",
12
+ "dependencies": {
13
+ "@aws-sdk/client-dynamodb": "^3.0.0",
14
+ "@aws-sdk/lib-dynamodb": "^3.0.0"
15
+ },
16
+ "devDependencies": {
17
+ "@types/node": "^20.0.0"
18
+ },
19
+ "peerDependencies": {
20
+ "@xcelsior/auth": "1.0.0"
21
+ },
22
+ "scripts": {
23
+ "build": "tsup && tsc --noEmit",
24
+ "dev": "tsup --watch",
25
+ "prepublish": "npm run build",
26
+ "test": "jest --passWithNoTests",
27
+ "lint": "biome check ."
28
+ }
29
+ }
@@ -0,0 +1,430 @@
1
+ import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
2
+ import {
3
+ DynamoDBDocumentClient,
4
+ PutCommand,
5
+ GetCommand,
6
+ UpdateCommand,
7
+ DeleteCommand,
8
+ QueryCommand,
9
+ ScanCommand,
10
+ BatchWriteCommand,
11
+ } from '@aws-sdk/lib-dynamodb';
12
+ import type {
13
+ IStorageProvider,
14
+ User,
15
+ Session,
16
+ UserFilter,
17
+ FindUsersOptions,
18
+ FindUsersResult,
19
+ UserRole,
20
+ } from '@xcelsior/auth';
21
+
22
+ export interface DynamoDBConfig {
23
+ tableName: string;
24
+ sessionsTableName: string;
25
+ region: string;
26
+ }
27
+
28
+ export class DynamoDBStorageProvider implements IStorageProvider {
29
+ private client: DynamoDBDocumentClient;
30
+ private tableName: string;
31
+ private sessionsTableName: string;
32
+
33
+ constructor(config: DynamoDBConfig) {
34
+ const dbClient = new DynamoDBClient({ region: config.region });
35
+ this.client = DynamoDBDocumentClient.from(dbClient, {});
36
+ this.tableName = config.tableName;
37
+ this.sessionsTableName = config.sessionsTableName;
38
+ }
39
+
40
+ // ==================== User Methods ====================
41
+
42
+ async createUser(user: User): Promise<void> {
43
+ await this.client.send(
44
+ new PutCommand({
45
+ TableName: this.tableName,
46
+ Item: user,
47
+ ConditionExpression: 'attribute_not_exists(email)',
48
+ })
49
+ );
50
+ }
51
+
52
+ async getUserByResetPasswordToken(resetPasswordToken: string): Promise<User | null> {
53
+ const response = await this.client.send(
54
+ new QueryCommand({
55
+ TableName: this.tableName,
56
+ IndexName: 'ResetPasswordTokenIndex',
57
+ KeyConditionExpression: 'resetPasswordToken = :token',
58
+ ExpressionAttributeValues: {
59
+ ':token': resetPasswordToken,
60
+ },
61
+ })
62
+ );
63
+
64
+ return response.Items?.[0] as User | null;
65
+ }
66
+
67
+ async getUserByVerifyEmailToken(verifyEmailToken: string): Promise<User | null> {
68
+ const response = await this.client.send(
69
+ new QueryCommand({
70
+ TableName: this.tableName,
71
+ IndexName: 'VerifyEmailTokenIndex',
72
+ KeyConditionExpression: 'verificationToken = :token',
73
+ ExpressionAttributeValues: {
74
+ ':token': verifyEmailToken,
75
+ },
76
+ })
77
+ );
78
+
79
+ return response.Items?.[0] as User | null;
80
+ }
81
+
82
+ async getUserById(id: string): Promise<User | null> {
83
+ const response = await this.client.send(
84
+ new GetCommand({
85
+ TableName: this.tableName,
86
+ Key: { id },
87
+ })
88
+ );
89
+
90
+ return response.Item as User | null;
91
+ }
92
+
93
+ async getUserByEmail(email: string): Promise<User | null> {
94
+ const response = await this.client.send(
95
+ new QueryCommand({
96
+ TableName: this.tableName,
97
+ IndexName: 'EmailIndex',
98
+ KeyConditionExpression: 'email = :email',
99
+ ExpressionAttributeValues: {
100
+ ':email': email,
101
+ },
102
+ })
103
+ );
104
+
105
+ return response.Items?.[0] as User | null;
106
+ }
107
+
108
+ async updateUser(id: string, updates: Partial<User>): Promise<void> {
109
+ const toSet: Record<string, unknown> = {};
110
+ const toRemove: string[] = [];
111
+
112
+ // Separate attributes to set and remove
113
+ Object.entries(updates).forEach(([key, value]) => {
114
+ if (value === undefined) {
115
+ toRemove.push(key);
116
+ } else {
117
+ toSet[key] = value;
118
+ }
119
+ });
120
+
121
+ // If no updates at all, return early
122
+ if (Object.keys(toSet).length === 0 && toRemove.length === 0) {
123
+ return;
124
+ }
125
+
126
+ // Build the update expression
127
+ const setPart =
128
+ Object.keys(toSet).length > 0
129
+ ? `SET ${Object.keys(toSet)
130
+ .map((key) => `#${key} = :${key}`)
131
+ .join(', ')}`
132
+ : '';
133
+
134
+ const removePart =
135
+ toRemove.length > 0 ? `REMOVE ${toRemove.map((key) => `#${key}`).join(', ')}` : '';
136
+
137
+ const updateExpression = [setPart, removePart].filter(Boolean).join(' ');
138
+
139
+ // Build expression attribute names (needed for both SET and REMOVE)
140
+ const expressionAttributeNames = [...Object.keys(toSet), ...toRemove].reduce(
141
+ (acc, key) => ({ ...acc, [`#${key}`]: key }),
142
+ {}
143
+ );
144
+
145
+ // Build expression attribute values (only needed for SET)
146
+ const expressionAttributeValues = Object.entries(toSet).reduce(
147
+ (acc, [key, value]) => ({ ...acc, [`:${key}`]: value }),
148
+ {}
149
+ );
150
+
151
+ await this.client.send(
152
+ new UpdateCommand({
153
+ TableName: this.tableName,
154
+ Key: { id },
155
+ UpdateExpression: updateExpression,
156
+ ExpressionAttributeNames: expressionAttributeNames,
157
+ ...(Object.keys(expressionAttributeValues).length > 0 && {
158
+ ExpressionAttributeValues: expressionAttributeValues,
159
+ }),
160
+ })
161
+ );
162
+ }
163
+
164
+ async deleteUser(id: string): Promise<void> {
165
+ // Delete all user sessions first
166
+ await this.deleteAllUserSessions(id);
167
+
168
+ await this.client.send(
169
+ new DeleteCommand({
170
+ TableName: this.tableName,
171
+ Key: { id },
172
+ })
173
+ );
174
+ }
175
+
176
+ async findUsers(filter: UserFilter, options: FindUsersOptions = {}): Promise<FindUsersResult> {
177
+ const limit = options.limit ?? 50;
178
+ const exclusiveStartKey = options.cursor
179
+ ? JSON.parse(Buffer.from(options.cursor, 'base64').toString())
180
+ : undefined;
181
+
182
+ // If filtering by exact email only, use the EmailIndex (most performant)
183
+ if (
184
+ filter.email &&
185
+ !filter.emailContains &&
186
+ !filter.roles &&
187
+ !filter.hasAnyRole &&
188
+ filter.isEmailVerified === undefined
189
+ ) {
190
+ const response = await this.client.send(
191
+ new QueryCommand({
192
+ TableName: this.tableName,
193
+ IndexName: 'EmailIndex',
194
+ KeyConditionExpression: 'email = :email',
195
+ ExpressionAttributeValues: { ':email': filter.email },
196
+ Limit: limit,
197
+ ExclusiveStartKey: exclusiveStartKey,
198
+ })
199
+ );
200
+
201
+ return {
202
+ users: (response.Items || []) as User[],
203
+ nextCursor: response.LastEvaluatedKey
204
+ ? Buffer.from(JSON.stringify(response.LastEvaluatedKey)).toString('base64')
205
+ : undefined,
206
+ };
207
+ }
208
+
209
+ // For other filters, use Scan with FilterExpression
210
+ const filterExpressions: string[] = [];
211
+ const expressionAttributeNames: Record<string, string> = {};
212
+ const expressionAttributeValues: Record<string, unknown> = {};
213
+
214
+ if (filter.email) {
215
+ filterExpressions.push('email = :email');
216
+ expressionAttributeValues[':email'] = filter.email;
217
+ }
218
+
219
+ if (filter.emailContains) {
220
+ filterExpressions.push('contains(email, :emailContains)');
221
+ expressionAttributeValues[':emailContains'] = filter.emailContains;
222
+ }
223
+
224
+ if (filter.isEmailVerified !== undefined) {
225
+ filterExpressions.push('isEmailVerified = :isEmailVerified');
226
+ expressionAttributeValues[':isEmailVerified'] = filter.isEmailVerified;
227
+ }
228
+
229
+ // For roles (user must have ALL specified roles)
230
+ if (filter.roles && filter.roles.length > 0) {
231
+ filter.roles.forEach((role: UserRole, index: number) => {
232
+ filterExpressions.push(`contains(#roles, :role${index})`);
233
+ expressionAttributeValues[`:role${index}`] = role;
234
+ });
235
+ expressionAttributeNames['#roles'] = 'roles';
236
+ }
237
+
238
+ // For hasAnyRole (user must have at least ONE)
239
+ if (filter.hasAnyRole && filter.hasAnyRole.length > 0) {
240
+ const anyRoleConditions = filter.hasAnyRole.map((role: UserRole, index: number) => {
241
+ expressionAttributeValues[`:anyRole${index}`] = role;
242
+ return `contains(#roles, :anyRole${index})`;
243
+ });
244
+ filterExpressions.push(`(${anyRoleConditions.join(' OR ')})`);
245
+ if (!expressionAttributeNames['#roles']) {
246
+ expressionAttributeNames['#roles'] = 'roles';
247
+ }
248
+ }
249
+
250
+ const response = await this.client.send(
251
+ new ScanCommand({
252
+ TableName: this.tableName,
253
+ FilterExpression:
254
+ filterExpressions.length > 0 ? filterExpressions.join(' AND ') : undefined,
255
+ ExpressionAttributeNames:
256
+ Object.keys(expressionAttributeNames).length > 0
257
+ ? expressionAttributeNames
258
+ : undefined,
259
+ ExpressionAttributeValues:
260
+ Object.keys(expressionAttributeValues).length > 0
261
+ ? expressionAttributeValues
262
+ : undefined,
263
+ Limit: limit,
264
+ ExclusiveStartKey: exclusiveStartKey,
265
+ })
266
+ );
267
+
268
+ return {
269
+ users: (response.Items || []) as User[],
270
+ nextCursor: response.LastEvaluatedKey
271
+ ? Buffer.from(JSON.stringify(response.LastEvaluatedKey)).toString('base64')
272
+ : undefined,
273
+ };
274
+ }
275
+
276
+ // ==================== Session Methods ====================
277
+
278
+ async createSession(session: Session): Promise<void> {
279
+ await this.client.send(
280
+ new PutCommand({
281
+ TableName: this.sessionsTableName,
282
+ Item: session,
283
+ })
284
+ );
285
+ }
286
+
287
+ async getSessionById(id: string): Promise<Session | null> {
288
+ const response = await this.client.send(
289
+ new GetCommand({
290
+ TableName: this.sessionsTableName,
291
+ Key: { id },
292
+ })
293
+ );
294
+
295
+ return response.Item as Session | null;
296
+ }
297
+
298
+ async getSessionsByUserId(userId: string): Promise<Session[]> {
299
+ const response = await this.client.send(
300
+ new QueryCommand({
301
+ TableName: this.sessionsTableName,
302
+ IndexName: 'UserIdIndex',
303
+ KeyConditionExpression: 'userId = :userId',
304
+ ExpressionAttributeValues: {
305
+ ':userId': userId,
306
+ },
307
+ })
308
+ );
309
+
310
+ return (response.Items || []) as Session[];
311
+ }
312
+
313
+ async updateSession(id: string, updates: Partial<Session>): Promise<void> {
314
+ const toSet: Record<string, unknown> = {};
315
+
316
+ Object.entries(updates).forEach(([key, value]) => {
317
+ if (value !== undefined) {
318
+ toSet[key] = value;
319
+ }
320
+ });
321
+
322
+ if (Object.keys(toSet).length === 0) {
323
+ return;
324
+ }
325
+
326
+ const updateExpression = `SET ${Object.keys(toSet)
327
+ .map((key) => `#${key} = :${key}`)
328
+ .join(', ')}`;
329
+
330
+ const expressionAttributeNames = Object.keys(toSet).reduce(
331
+ (acc, key) => ({ ...acc, [`#${key}`]: key }),
332
+ {}
333
+ );
334
+
335
+ const expressionAttributeValues = Object.entries(toSet).reduce(
336
+ (acc, [key, value]) => ({ ...acc, [`:${key}`]: value }),
337
+ {}
338
+ );
339
+
340
+ await this.client.send(
341
+ new UpdateCommand({
342
+ TableName: this.sessionsTableName,
343
+ Key: { id },
344
+ UpdateExpression: updateExpression,
345
+ ExpressionAttributeNames: expressionAttributeNames,
346
+ ExpressionAttributeValues: expressionAttributeValues,
347
+ })
348
+ );
349
+ }
350
+
351
+ async deleteSession(id: string): Promise<void> {
352
+ await this.client.send(
353
+ new DeleteCommand({
354
+ TableName: this.sessionsTableName,
355
+ Key: { id },
356
+ })
357
+ );
358
+ }
359
+
360
+ async deleteAllUserSessions(userId: string): Promise<void> {
361
+ const sessions = await this.getSessionsByUserId(userId);
362
+
363
+ if (sessions.length === 0) {
364
+ return;
365
+ }
366
+
367
+ // Batch delete in chunks of 25 (DynamoDB limit)
368
+ const chunks = [];
369
+ for (let i = 0; i < sessions.length; i += 25) {
370
+ chunks.push(sessions.slice(i, i + 25));
371
+ }
372
+
373
+ for (const chunk of chunks) {
374
+ await this.client.send(
375
+ new BatchWriteCommand({
376
+ RequestItems: {
377
+ [this.sessionsTableName]: chunk.map((session) => ({
378
+ DeleteRequest: {
379
+ Key: { id: session.id },
380
+ },
381
+ })),
382
+ },
383
+ })
384
+ );
385
+ }
386
+ }
387
+
388
+ async deleteExpiredSessions(): Promise<void> {
389
+ // Note: This requires a full table scan which is expensive.
390
+ // Consider using DynamoDB TTL instead for automatic expiration.
391
+ // This method is provided for manual cleanup if needed.
392
+ const now = Date.now();
393
+
394
+ const response = await this.client.send(
395
+ new QueryCommand({
396
+ TableName: this.sessionsTableName,
397
+ IndexName: 'ExpiresAtIndex',
398
+ KeyConditionExpression: 'expiresAt < :now',
399
+ ExpressionAttributeValues: {
400
+ ':now': now,
401
+ },
402
+ })
403
+ );
404
+
405
+ const expiredSessions = (response.Items || []) as Session[];
406
+
407
+ if (expiredSessions.length === 0) {
408
+ return;
409
+ }
410
+
411
+ const chunks = [];
412
+ for (let i = 0; i < expiredSessions.length; i += 25) {
413
+ chunks.push(expiredSessions.slice(i, i + 25));
414
+ }
415
+
416
+ for (const chunk of chunks) {
417
+ await this.client.send(
418
+ new BatchWriteCommand({
419
+ RequestItems: {
420
+ [this.sessionsTableName]: chunk.map((session) => ({
421
+ DeleteRequest: {
422
+ Key: { id: session.id },
423
+ },
424
+ })),
425
+ },
426
+ })
427
+ );
428
+ }
429
+ }
430
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { DynamoDBStorageProvider, type DynamoDBConfig } from './dynamodb';
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"]
8
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ format: ['cjs', 'esm'],
6
+ dts: true,
7
+ splitting: false,
8
+ sourcemap: true,
9
+ clean: true,
10
+ });