@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.
- package/.turbo/turbo-build.log +22 -0
- package/biome.json +3 -0
- package/dist/index.d.mts +30 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +336 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +318 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +29 -0
- package/src/dynamodb.ts +430 -0
- package/src/index.ts +1 -0
- package/tsconfig.json +8 -0
- package/tsup.config.ts +10 -0
|
@@ -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
|
+
[34mCLI[39m Building entry: src/index.ts
|
|
6
|
+
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
7
|
+
[34mCLI[39m tsup v8.5.1
|
|
8
|
+
[34mCLI[39m Using tsup config: /home/circleci/repo/packages/services/auth-adapter-dynamodb/tsup.config.ts
|
|
9
|
+
[34mCLI[39m Target: es2020
|
|
10
|
+
[34mCLI[39m Cleaning output folder
|
|
11
|
+
[34mCJS[39m Build start
|
|
12
|
+
[34mESM[39m Build start
|
|
13
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m9.94 KB[39m
|
|
14
|
+
[32mESM[39m [1mdist/index.mjs.map [22m[32m21.64 KB[39m
|
|
15
|
+
[32mESM[39m ⚡️ Build success in 198ms
|
|
16
|
+
[32mCJS[39m [1mdist/index.js [22m[32m11.23 KB[39m
|
|
17
|
+
[32mCJS[39m [1mdist/index.js.map [22m[32m21.71 KB[39m
|
|
18
|
+
[32mCJS[39m ⚡️ Build success in 199ms
|
|
19
|
+
[34mDTS[39m Build start
|
|
20
|
+
[32mDTS[39m ⚡️ Build success in 20379ms
|
|
21
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m1.33 KB[39m
|
|
22
|
+
[32mDTS[39m [1mdist/index.d.mts [22m[32m1.33 KB[39m
|
package/biome.json
ADDED
package/dist/index.d.mts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|
package/src/dynamodb.ts
ADDED
|
@@ -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