fluxor-cloud-db 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/.github/workflows/npm-publish.yml +34 -0
- package/README.md +296 -0
- package/dist/index.d.mts +347 -0
- package/dist/index.d.ts +347 -0
- package/dist/index.js +1844 -0
- package/dist/index.mjs +1811 -0
- package/jest.config.ts +12 -0
- package/package.json +33 -0
- package/src/contracts/database-adapter.ts +161 -0
- package/src/dynamo/dynamo.ts +859 -0
- package/src/dynamo/dynamo.types.ts +8 -0
- package/src/fluentapi.ts +71 -0
- package/src/index.ts +39 -0
- package/src/mongo/mongo.ts +690 -0
- package/src/types/error.ts +13 -0
- package/src/types/query.ts +53 -0
- package/tests/dynamodb.test.ts +547 -0
- package/tests/mongodb.test.ts +486 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,859 @@
|
|
|
1
|
+
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
|
|
2
|
+
import { Service } from "fluxor-cloud";
|
|
3
|
+
import { DynamoConfig } from "./dynamo.types";
|
|
4
|
+
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
5
|
+
import {
|
|
6
|
+
GetCommand,
|
|
7
|
+
QueryCommand,
|
|
8
|
+
ScanCommand,
|
|
9
|
+
PutCommand,
|
|
10
|
+
BatchWriteCommand,
|
|
11
|
+
UpdateCommand,
|
|
12
|
+
DeleteCommand
|
|
13
|
+
} from "@aws-sdk/lib-dynamodb";
|
|
14
|
+
import { DatabaseAdapter } from "../contracts/database-adapter";
|
|
15
|
+
import { DatabaseAdapterError } from "../types/error";
|
|
16
|
+
import { WhereClause, QueryOptions, PaginatedResult, BatchResult, UpdateClause, QueryOperator } from "../types/query";
|
|
17
|
+
|
|
18
|
+
// Source: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html
|
|
19
|
+
const RESERVED_KEYWORDS = new Set([
|
|
20
|
+
'ABORT', 'ABSOLUTE', 'ACTION', 'ADD', 'AFTER', 'AGENT', 'ALL', 'ALLOCATE', 'ALTER', 'ANALYZE',
|
|
21
|
+
'AND', 'ANY', 'ARCHIVE', 'ARE', 'ARRAY', 'AS', 'ASC', 'ASCII', 'ASENSITIVE', 'ASSERTION',
|
|
22
|
+
'ASYMMETRIC', 'AT', 'ATOMIC', 'ATTACH', 'ATTRIBUTE', 'AUTH', 'AUTHORIZATION', 'AUTHORIZE',
|
|
23
|
+
'AUTO', 'AVG', 'BACK', 'BACKUP', 'BASE', 'BATCH', 'BEFORE', 'BEGIN', 'BETWEEN', 'BIGINT',
|
|
24
|
+
'BINARY', 'BIT', 'BLOB', 'BLOCK', 'BOOLEAN', 'BOTH', 'BREADTH', 'BUCKET', 'BULK', 'BY',
|
|
25
|
+
'CALL', 'CALLED', 'CALLING', 'CAPACITY', 'CASCADE', 'CASCADED', 'CASE', 'CAST', 'CATALOG',
|
|
26
|
+
'CHAR', 'CHARACTER', 'CHECK', 'CLASS', 'CLOB', 'CLOSE', 'CLUSTER', 'CLUSTERED', 'CLUSTERING',
|
|
27
|
+
'CLUSTERS', 'COALESCE', 'COLLATE', 'COLLATION', 'COLLECTION', 'COLUMN', 'COLUMNS', 'COMBINE',
|
|
28
|
+
'COMMENT', 'COMMIT', 'COMPACT', 'COMPILE', 'COMPRESS', 'CONDITION', 'CONFLICT', 'CONNECT',
|
|
29
|
+
'CONNECTION', 'CONSISTENCY', 'CONSISTENT', 'CONSTRAINT', 'CONSTRAINTS', 'CONSTRUCTOR',
|
|
30
|
+
'CONSUMED', 'CONTINUE', 'CONVERT', 'COPY', 'CORRESPONDING', 'COUNT', 'COUNTER', 'CREATE',
|
|
31
|
+
'CROSS', 'CUBE', 'CURRENT', 'CURSOR', 'CYCLE', 'DATA', 'DATABASE', 'DATE', 'DATETIME',
|
|
32
|
+
'DAY', 'DEALLOCATE', 'DEC', 'DECIMAL', 'DECLARE', 'DEFAULT', 'DEFERRABLE', 'DEFERRED',
|
|
33
|
+
'DEFINE', 'DEFINED', 'DEFINITION', 'DELETE', 'DELIMITED', 'DEPTH', 'DEREF', 'DESC',
|
|
34
|
+
'DESCRIBE', 'DESCRIPTOR', 'DETACH', 'DETERMINISTIC', 'DIAGNOSTICS', 'DIRECTORIES', 'DISABLE',
|
|
35
|
+
'DISCONNECT', 'DISTINCT', 'DISTRIBUTE', 'DO', 'DOMAIN', 'DOUBLE', 'DROP', 'DUMP', 'DURATION',
|
|
36
|
+
'DYNAMIC', 'EACH', 'ELEMENT', 'ELSE', 'ELSEIF', 'EMPTY', 'ENABLE', 'END', 'EQUAL', 'EQUALS',
|
|
37
|
+
'ERROR', 'ESCAPE', 'EVALUATED', 'EXCEPT', 'EXCEPTION', 'EXCEPTIONS', 'EXCLUSIVE', 'EXEC',
|
|
38
|
+
'EXECUTE', 'EXISTS', 'EXIT', 'EXPLAIN', 'EXPLODE', 'EXPORT', 'EXPRESSION', 'EXTENDED',
|
|
39
|
+
'EXTERNAL', 'EXTRACT', 'FAIL', 'FALSE', 'FAMILY', 'FETCH', 'FIELDS', 'FILE', 'FILTER',
|
|
40
|
+
'FILTERING', 'FINAL', 'FINISH', 'FIRST', 'FIXED', 'FLATTERN', 'FLOAT', 'FOR', 'FORCE',
|
|
41
|
+
'FOREIGN', 'FORMAT', 'FORWARD', 'FOUND', 'FREE', 'FROM', 'FULL', 'FUNCTION', 'FUNCTIONS',
|
|
42
|
+
'GENERAL', 'GENERATE', 'GET', 'GLOB', 'GO', 'GOTO', 'GRANT', 'GREATER', 'GROUP', 'GROUPING',
|
|
43
|
+
'HANDLER', 'HASH', 'HAVE', 'HAVING', 'HEAP', 'HIDDEN', 'HOLD', 'HOUR', 'IDENTIFIED', 'IF',
|
|
44
|
+
'IGNORE', 'IMMEDIATE', 'IMPORT', 'IN', 'INCLUDING', 'INCLUSIVE', 'INCREMENT', 'INDETERMINATE',
|
|
45
|
+
'INDEX', 'INDEXED', 'INDEXES', 'INDICATOR', 'INFINITE', 'INITIALLY', 'INLINE', 'INNER',
|
|
46
|
+
'INNTER', 'INOUT', 'INPUT', 'INSENSITIVE', 'INSERT', 'INSTEAD', 'INT', 'INTEGER', 'INTERSECT',
|
|
47
|
+
'INTERVAL', 'INTO', 'INVALIDATE', 'IS', 'ISOLATION', 'ITEM', 'ITEMS', 'ITERATE', 'JOIN',
|
|
48
|
+
'KEY', 'KEYS', 'LAG', 'LANGUAGE', 'LARGE', 'LAST', 'LATERAL', 'LEAD', 'LEADING', 'LEAVE',
|
|
49
|
+
'LEFT', 'LENGTH', 'LESS', 'LEVEL', 'LIKE', 'LIMIT', 'LIST', 'LOAD', 'LOCAL', 'LOCALTIME',
|
|
50
|
+
'LOCALTIMESTAMP', 'LOCATION', 'LOCATOR', 'LOCK', 'LOCKS', 'LOG', 'LOGED', 'LONG', 'LOOP',
|
|
51
|
+
'LOWER', 'MAP', 'MATCH', 'MATERIALIZED', 'MAX', 'MAXLEN', 'MEMBER', 'MERGE', 'METHOD', 'MIN',
|
|
52
|
+
'MINUS', 'MINUTE', 'MISSING', 'MOD', 'MODE', 'MODIFIES', 'MODIFY', 'MODULE', 'MONTH',
|
|
53
|
+
'MULTI', 'MULTISET', 'NAME', 'NAMES', 'NATIONAL', 'NATURAL', 'NCHAR', 'NCLOB', 'NEW',
|
|
54
|
+
'NEXT', 'NO', 'NONE', 'NOT', 'NULL', 'NULLIF', 'NUMBER', 'NUMERIC', 'OBJECT', 'OF', 'OFFLINE',
|
|
55
|
+
'OFFSET', 'OLD', 'ON', 'ONLINE', 'ONLY', 'OPEN', 'OPTION', 'OR', 'ORDER', 'ORDINALITY',
|
|
56
|
+
'OUT', 'OUTER', 'OUTPUT', 'OVER', 'OVERLAPS', 'OVERRIDE', 'OWNER', 'PAD', 'PARALLEL',
|
|
57
|
+
'PARAMETER', 'PARAMETERS', 'PARTIAL', 'PARTITION', 'PARTITIONED', 'PARTITIONS', 'PATH',
|
|
58
|
+
'PERCENT', 'PERCENTILE', 'PERMISSION', 'PERMISSIONS', 'PIPE', 'PIPELINED', 'PLAN', 'POOL',
|
|
59
|
+
'POSITION', 'PRECISION', 'PREPARE', 'PRESERVE', 'PRIMARY', 'PRIOR', 'PRIVATE', 'PRIVILEGES',
|
|
60
|
+
'PROCEDURE', 'PROCESSED', 'PROJECT', 'PROJECTION', 'PROPERTY', 'PROVISIONING', 'PUBLIC',
|
|
61
|
+
'PUT', 'QUERY', 'QUIT', 'QUORUM', 'RAISE', 'RANDOM', 'RANGE', 'RANK', 'RAW', 'READ',
|
|
62
|
+
'READS', 'REAL', 'REBUILD', 'RECORD', 'RECURSIVE', 'REDUCE', 'REF', 'REFERENCE', 'REFERENCES',
|
|
63
|
+
'REFERENCING', 'REGEXP', 'REINDEX', 'RELATIVE', 'RELEASE', 'REMAINDER', 'RENAME', 'REPEAT',
|
|
64
|
+
'REPLACE', 'REQUEST', 'RESET', 'RESIGNAL', 'RESTRICT', 'RESULT', 'RETURN', 'RETURNING',
|
|
65
|
+
'RETURNS', 'REVERSE', 'REVOKE', 'RIGHT', 'ROLE', 'ROLES', 'ROLLBACK', 'ROLLUP', 'ROUTINE',
|
|
66
|
+
'ROW', 'ROWS', 'RULE', 'RULES', 'SAMPLE', 'SATISFIES', 'SAVE', 'SAVEPOINT', 'SCAN', 'SCHEMA',
|
|
67
|
+
'SCOPE', 'SCROLL', 'SEARCH', 'SECOND', 'SECTION', 'SEGMENT', 'SELECT', 'SELF', 'SEMI',
|
|
68
|
+
'SENSITIVE', 'SEPARATE', 'SEQUENCE', 'SERIALIZABLE', 'SESSION', 'SET', 'SETS', 'SHARD',
|
|
69
|
+
'SHARE', 'SHARED', 'SHORT', 'SHOW', 'SIGNAL', 'SIMILAR', 'SIZE', 'SKEWED', 'SOME', 'SOURCE',
|
|
70
|
+
'SPACE', 'SPACES', 'SPARSE', 'SPECIFIC', 'SPECIFICTYPE', 'SQL', 'SQLCODE', 'SQLERROR',
|
|
71
|
+
'SQLEXCEPTION', 'SQLSTATE', 'SQLWARNING', 'START', 'STATE', 'STATIC', 'STATUS', 'STORAGE',
|
|
72
|
+
'STORE', 'STORED', 'STREAM', 'STRING', 'STRUCT', 'STYLE', 'SUB', 'SUBMULTISET', 'SUBPARTITION',
|
|
73
|
+
'SUBSTRING', 'SUBTYPE', 'SUM', 'SUPER', 'SYMMETRIC', 'SYNONYM', 'SYSTEM', 'TABLE', 'TABLESAMPLE',
|
|
74
|
+
'TEMP', 'TEMPORARY', 'TERMINATED', 'TEXT', 'THAN', 'THEN', 'THROUGHPUT', 'TIME', 'TIMESTAMP',
|
|
75
|
+
'TIMEZONE', 'TINYINT', 'TO', 'TOKEN', 'TOTAL', 'TOUCH', 'TRAILING', 'TRANSACTION', 'TRANSFORM',
|
|
76
|
+
'TRANSLATE', 'TRANSLATION', 'TREAT', 'TRIGGER', 'TRIM', 'TRUE', 'TRUNCATE', 'TTL', 'TUPLE',
|
|
77
|
+
'TYPE', 'UNDER', 'UNDO', 'UNION', 'UNIQUE', 'UNIT', 'UNKNOWN', 'UNLOGGED', 'UNNEST', 'UNPROCESSED',
|
|
78
|
+
'UNSIGNED', 'UNTIL', 'UPDATE', 'UPPER', 'URL', 'USAGE', 'USE', 'USER', 'USERS', 'USING',
|
|
79
|
+
'UUID', 'VACUUM', 'VALUE', 'VALUED', 'VALUES', 'VARCHAR', 'VARIABLE', 'VARIANCE', 'VARINT',
|
|
80
|
+
'VARYING', 'VIEW', 'VIEWS', 'VIRTUAL', 'VOID', 'WAIT', 'WHEN', 'WHENEVER', 'WHERE', 'WHILE',
|
|
81
|
+
'WINDOW', 'WITH', 'WITHIN', 'WITHOUT', 'WORK', 'WRAPPED', 'WRITE', 'YEAR', 'ZONE',
|
|
82
|
+
// Common attribute names that often conflict
|
|
83
|
+
'NAME', 'STATUS', 'SCORE', 'PRICE', 'CATEGORY'
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
@Service()
|
|
87
|
+
export class DynamoService implements DatabaseAdapter {
|
|
88
|
+
private client: DynamoDBDocumentClient | undefined;
|
|
89
|
+
private dynamoDBClient: DynamoDBClient | undefined;
|
|
90
|
+
private isConnectedFlag = false;
|
|
91
|
+
private config: DynamoConfig | undefined;
|
|
92
|
+
|
|
93
|
+
public setConfig(config: DynamoConfig): void {
|
|
94
|
+
this.config = config;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
connect(): Promise<void> {
|
|
98
|
+
if (!this.config) {
|
|
99
|
+
return Promise.reject(new DatabaseAdapterError(
|
|
100
|
+
"No configuration provided. Call setConfig() first.",
|
|
101
|
+
"INVALID_CONFIG"
|
|
102
|
+
));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return Promise.resolve().then(() => {
|
|
106
|
+
try {
|
|
107
|
+
this.validateConfig(this.config!);
|
|
108
|
+
|
|
109
|
+
this.dynamoDBClient = new DynamoDBClient({
|
|
110
|
+
region: this.config!.region,
|
|
111
|
+
endpoint: this.config!.endpoint,
|
|
112
|
+
credentials: this.config!.accessKeyId
|
|
113
|
+
? {
|
|
114
|
+
accessKeyId: this.config!.accessKeyId,
|
|
115
|
+
secretAccessKey: this.config!.secretAccessKey!
|
|
116
|
+
}
|
|
117
|
+
: undefined
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
this.client = DynamoDBDocumentClient.from(this.dynamoDBClient);
|
|
121
|
+
this.isConnectedFlag = true;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
this.isConnectedFlag = false;
|
|
124
|
+
|
|
125
|
+
if (error instanceof DatabaseAdapterError) {
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (error instanceof Error) {
|
|
130
|
+
throw new DatabaseAdapterError(
|
|
131
|
+
`Failed to connect to DynamoDB: ${error.message}`,
|
|
132
|
+
"CONNECTION_FAILED"
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
throw new DatabaseAdapterError(
|
|
137
|
+
"Failed to connect to DynamoDB: Unknown error",
|
|
138
|
+
"CONNECTION_FAILED"
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async disconnect(): Promise<void> {
|
|
145
|
+
try {
|
|
146
|
+
if (this.dynamoDBClient) {
|
|
147
|
+
this.dynamoDBClient.destroy();
|
|
148
|
+
}
|
|
149
|
+
this.isConnectedFlag = false;
|
|
150
|
+
this.client = undefined;
|
|
151
|
+
this.dynamoDBClient = undefined;
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.warn("Error disconnecting from DynamoDB:", error);
|
|
154
|
+
this.isConnectedFlag = false;
|
|
155
|
+
this.client = undefined;
|
|
156
|
+
this.dynamoDBClient = undefined;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
isConnected(): boolean {
|
|
161
|
+
return this.isConnectedFlag && this.client !== undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async healthCheck(): Promise<boolean> {
|
|
165
|
+
try {
|
|
166
|
+
const client = this.getClient();
|
|
167
|
+
await client.send(new ScanCommand({ TableName: "fake_table", Limit: 1 }));
|
|
168
|
+
return true;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
if (error instanceof Error && error.message.includes("ResourceNotFoundException")) {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async selectOne<T = Record<string, any>>(
|
|
178
|
+
tableName: string,
|
|
179
|
+
where: WhereClause,
|
|
180
|
+
options?: QueryOptions
|
|
181
|
+
): Promise<T | null> {
|
|
182
|
+
try {
|
|
183
|
+
const client = this.getClient();
|
|
184
|
+
const key = this.buildKey(where);
|
|
185
|
+
|
|
186
|
+
const params: any = {
|
|
187
|
+
TableName: tableName,
|
|
188
|
+
Key: key
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
if (options?.projection) {
|
|
192
|
+
const { projection, names } = this.buildProjection(options.projection);
|
|
193
|
+
params.ProjectionExpression = projection;
|
|
194
|
+
if (Object.keys(names).length > 0) {
|
|
195
|
+
params.ExpressionAttributeNames = names;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const command = new GetCommand(params);
|
|
200
|
+
const result = await client.send(command);
|
|
201
|
+
return (result.Item as T) || null;
|
|
202
|
+
} catch (error) {
|
|
203
|
+
throw this.handleError(error, "selectOne");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async selectMany<T = Record<string, any>>(
|
|
208
|
+
tableName: string,
|
|
209
|
+
where?: WhereClause,
|
|
210
|
+
options?: QueryOptions
|
|
211
|
+
): Promise<PaginatedResult<T>> {
|
|
212
|
+
try {
|
|
213
|
+
const client = this.getClient();
|
|
214
|
+
let command;
|
|
215
|
+
let items: T[] = [];
|
|
216
|
+
let scannedCount = 0;
|
|
217
|
+
|
|
218
|
+
// Check if where clause contains only key attributes
|
|
219
|
+
const isKeyQuery = where && this.isKeyQuery(where);
|
|
220
|
+
|
|
221
|
+
if (isKeyQuery) {
|
|
222
|
+
const { keyConditionExpression, expressionAttributeValues, names } = this.buildKeyConditionExpression(where!);
|
|
223
|
+
|
|
224
|
+
const params: any = {
|
|
225
|
+
TableName: tableName,
|
|
226
|
+
KeyConditionExpression: keyConditionExpression,
|
|
227
|
+
ExpressionAttributeValues: expressionAttributeValues,
|
|
228
|
+
Limit: options?.limit,
|
|
229
|
+
ScanIndexForward: options?.orderBy?.[0]?.direction === "DESC" ? false : true,
|
|
230
|
+
ConsistentRead: options?.consistent
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
if (Object.keys(names).length > 0) {
|
|
234
|
+
params.ExpressionAttributeNames = names;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (options?.projection) {
|
|
238
|
+
const { projection, names: projNames } = this.buildProjection(options.projection);
|
|
239
|
+
params.ProjectionExpression = projection;
|
|
240
|
+
if (Object.keys(projNames).length > 0) {
|
|
241
|
+
params.ExpressionAttributeNames = { ...params.ExpressionAttributeNames, ...projNames };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
command = new QueryCommand(params);
|
|
246
|
+
} else {
|
|
247
|
+
// Use Scan for non-key queries or no where clause
|
|
248
|
+
const params: any = {
|
|
249
|
+
TableName: tableName,
|
|
250
|
+
Limit: options?.limit
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
if (where && Object.keys(where).length > 0) {
|
|
254
|
+
const { filterExpression, expressionAttributeValues, names } = this.buildFilterExpression(where);
|
|
255
|
+
params.FilterExpression = filterExpression;
|
|
256
|
+
params.ExpressionAttributeValues = expressionAttributeValues;
|
|
257
|
+
if (Object.keys(names).length > 0) {
|
|
258
|
+
params.ExpressionAttributeNames = names;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (options?.projection) {
|
|
263
|
+
const { projection, names: projNames } = this.buildProjection(options.projection);
|
|
264
|
+
params.ProjectionExpression = projection;
|
|
265
|
+
if (Object.keys(projNames).length > 0) {
|
|
266
|
+
params.ExpressionAttributeNames = { ...params.ExpressionAttributeNames, ...projNames };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
command = new ScanCommand(params);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const result = await client.send(command);
|
|
274
|
+
items = (result.Items as T[]) || [];
|
|
275
|
+
scannedCount = result.ScannedCount || 0;
|
|
276
|
+
|
|
277
|
+
// Apply client-side ordering (only meaningful for Scan; Query uses ScanIndexForward)
|
|
278
|
+
if (!isKeyQuery && options?.orderBy?.length) {
|
|
279
|
+
const { field, direction } = options.orderBy[0];
|
|
280
|
+
items = [...items].sort((a: any, b: any) => {
|
|
281
|
+
if (a[field] < b[field]) return direction === "DESC" ? 1 : -1;
|
|
282
|
+
if (a[field] > b[field]) return direction === "DESC" ? -1 : 1;
|
|
283
|
+
return 0;
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
items,
|
|
289
|
+
total: items.length,
|
|
290
|
+
hasMore: (result.LastEvaluatedKey !== undefined),
|
|
291
|
+
nextOffset: options?.offset ? options.offset + items.length : undefined
|
|
292
|
+
};
|
|
293
|
+
} catch (error) {
|
|
294
|
+
throw this.handleError(error, "selectMany");
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async createOne<T = Record<string, any>>(
|
|
299
|
+
tableName: string,
|
|
300
|
+
data: Record<string, any>
|
|
301
|
+
): Promise<T> {
|
|
302
|
+
try {
|
|
303
|
+
const client = this.getClient();
|
|
304
|
+
|
|
305
|
+
const command = new PutCommand({
|
|
306
|
+
TableName: tableName,
|
|
307
|
+
Item: data
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
await client.send(command);
|
|
311
|
+
return data as T;
|
|
312
|
+
} catch (error) {
|
|
313
|
+
throw this.handleError(error, "createOne");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async createMany<T = Record<string, any>>(
|
|
318
|
+
tableName: string,
|
|
319
|
+
data: Record<string, any>[],
|
|
320
|
+
options?: { stopOnError?: boolean }
|
|
321
|
+
): Promise<{ items: T[]; result: BatchResult }> {
|
|
322
|
+
try {
|
|
323
|
+
if (!Array.isArray(data)) {
|
|
324
|
+
throw new DatabaseAdapterError(
|
|
325
|
+
"createMany requires an array of records",
|
|
326
|
+
"INVALID_INPUT"
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const client = this.getClient();
|
|
331
|
+
const batchSize = 25;
|
|
332
|
+
let successful = 0;
|
|
333
|
+
let failed = 0;
|
|
334
|
+
const errors: { index: number; error: string }[] = [];
|
|
335
|
+
const items: T[] = [];
|
|
336
|
+
|
|
337
|
+
for (let i = 0; i < data.length; i += batchSize) {
|
|
338
|
+
const batch = data.slice(i, i + batchSize);
|
|
339
|
+
const requestItems = batch.map(item => ({
|
|
340
|
+
PutRequest: { Item: item }
|
|
341
|
+
}));
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
const command = new BatchWriteCommand({
|
|
345
|
+
RequestItems: {
|
|
346
|
+
[tableName]: requestItems
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
await client.send(command);
|
|
351
|
+
successful += batch.length;
|
|
352
|
+
items.push(...(batch as T[]));
|
|
353
|
+
} catch (error) {
|
|
354
|
+
if (options?.stopOnError) {
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
357
|
+
failed += batch.length;
|
|
358
|
+
batch.forEach((_, index) => {
|
|
359
|
+
errors.push({
|
|
360
|
+
index: i + index,
|
|
361
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
items,
|
|
369
|
+
result: { successful, failed, errors: errors.length > 0 ? errors : undefined }
|
|
370
|
+
};
|
|
371
|
+
} catch (error) {
|
|
372
|
+
throw this.handleError(error, "createMany");
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async updateOne<T = Record<string, any>>(
|
|
377
|
+
tableName: string,
|
|
378
|
+
update: UpdateClause,
|
|
379
|
+
where: WhereClause
|
|
380
|
+
): Promise<T> {
|
|
381
|
+
try {
|
|
382
|
+
const client = this.getClient();
|
|
383
|
+
const key = this.buildKey(where);
|
|
384
|
+
|
|
385
|
+
// Verify the item exists before updating
|
|
386
|
+
const existing = await client.send(new GetCommand({ TableName: tableName, Key: key }));
|
|
387
|
+
if (!existing.Item) {
|
|
388
|
+
throw new DatabaseAdapterError(
|
|
389
|
+
`Record not found`,
|
|
390
|
+
"NOT_FOUND"
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const { updateExpression, expressionAttributeValues, names } = this.buildUpdateExpression(update);
|
|
395
|
+
|
|
396
|
+
const params: any = {
|
|
397
|
+
TableName: tableName,
|
|
398
|
+
Key: key,
|
|
399
|
+
UpdateExpression: updateExpression,
|
|
400
|
+
ReturnValues: "ALL_NEW"
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
if (Object.keys(expressionAttributeValues).length > 0) {
|
|
404
|
+
params.ExpressionAttributeValues = expressionAttributeValues;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (Object.keys(names).length > 0) {
|
|
408
|
+
params.ExpressionAttributeNames = names;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const command = new UpdateCommand(params);
|
|
412
|
+
const result = await client.send(command);
|
|
413
|
+
return result.Attributes as T;
|
|
414
|
+
} catch (error) {
|
|
415
|
+
throw this.handleError(error, "updateOne");
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async updateMany<T = Record<string, any>>(
|
|
420
|
+
tableName: string,
|
|
421
|
+
update: UpdateClause,
|
|
422
|
+
where?: WhereClause,
|
|
423
|
+
options?: { limit?: number }
|
|
424
|
+
): Promise<{ items: T[]; result: BatchResult }> {
|
|
425
|
+
try {
|
|
426
|
+
const selectResult = await this.selectMany(
|
|
427
|
+
tableName,
|
|
428
|
+
where,
|
|
429
|
+
{ limit: options?.limit }
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
let successful = 0;
|
|
433
|
+
let failed = 0;
|
|
434
|
+
const errors: { index: number; error: string }[] = [];
|
|
435
|
+
const items: T[] = [];
|
|
436
|
+
|
|
437
|
+
for (let i = 0; i < selectResult.items.length; i++) {
|
|
438
|
+
try {
|
|
439
|
+
const item = selectResult.items[i];
|
|
440
|
+
const key = this.extractKey(item as Record<string, any>);
|
|
441
|
+
const updated = await this.updateOne(tableName, update, key);
|
|
442
|
+
successful++;
|
|
443
|
+
items.push(updated as T);
|
|
444
|
+
} catch (error) {
|
|
445
|
+
failed++;
|
|
446
|
+
errors.push({
|
|
447
|
+
index: i,
|
|
448
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return {
|
|
454
|
+
items,
|
|
455
|
+
result: { successful, failed, errors: errors.length > 0 ? errors : undefined }
|
|
456
|
+
};
|
|
457
|
+
} catch (error) {
|
|
458
|
+
throw this.handleError(error, "updateMany");
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async deleteOne(
|
|
463
|
+
tableName: string,
|
|
464
|
+
where: WhereClause
|
|
465
|
+
): Promise<{ success: boolean; deletedCount: number }> {
|
|
466
|
+
try {
|
|
467
|
+
const client = this.getClient();
|
|
468
|
+
const key = this.buildKey(where);
|
|
469
|
+
|
|
470
|
+
// Check if key exists first
|
|
471
|
+
const getResult = await client.send(new GetCommand({
|
|
472
|
+
TableName: tableName,
|
|
473
|
+
Key: key
|
|
474
|
+
}));
|
|
475
|
+
|
|
476
|
+
if (!getResult.Item) {
|
|
477
|
+
return { success: false, deletedCount: 0 };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const command = new DeleteCommand({
|
|
481
|
+
TableName: tableName,
|
|
482
|
+
Key: key
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
await client.send(command);
|
|
486
|
+
return { success: true, deletedCount: 1 };
|
|
487
|
+
} catch (error) {
|
|
488
|
+
throw this.handleError(error, "deleteOne");
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async deleteMany(
|
|
493
|
+
tableName: string,
|
|
494
|
+
where?: WhereClause,
|
|
495
|
+
options?: { limit?: number }
|
|
496
|
+
): Promise<{ success: boolean; deletedCount: number; result: BatchResult }> {
|
|
497
|
+
try {
|
|
498
|
+
const selectResult = await this.selectMany(
|
|
499
|
+
tableName,
|
|
500
|
+
where,
|
|
501
|
+
{ limit: options?.limit }
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
let successful = 0;
|
|
505
|
+
let failed = 0;
|
|
506
|
+
const errors: { index: number; error: string }[] = [];
|
|
507
|
+
|
|
508
|
+
for (let i = 0; i < selectResult.items.length; i++) {
|
|
509
|
+
try {
|
|
510
|
+
const item = selectResult.items[i];
|
|
511
|
+
const key = this.extractKey(item as Record<string, any>);
|
|
512
|
+
await this.deleteOne(tableName, key);
|
|
513
|
+
successful++;
|
|
514
|
+
} catch (error) {
|
|
515
|
+
failed++;
|
|
516
|
+
errors.push({
|
|
517
|
+
index: i,
|
|
518
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return {
|
|
524
|
+
success: failed === 0,
|
|
525
|
+
deletedCount: successful,
|
|
526
|
+
result: { successful, failed, errors: errors.length > 0 ? errors : undefined }
|
|
527
|
+
};
|
|
528
|
+
} catch (error) {
|
|
529
|
+
throw this.handleError(error, "deleteMany");
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async executeRaw<T = any>(query: string, params?: Record<string, any>): Promise<T> {
|
|
534
|
+
throw new DatabaseAdapterError(
|
|
535
|
+
"executeRaw is not implemented for DynamoDB. Use native DynamoDB syntax instead.",
|
|
536
|
+
"NOT_IMPLEMENTED"
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// ============ Private Helper Methods ============
|
|
541
|
+
|
|
542
|
+
private validateConfig(config: DynamoConfig): void {
|
|
543
|
+
if (!config) {
|
|
544
|
+
throw new DatabaseAdapterError(
|
|
545
|
+
"DynamoDB configuration is required",
|
|
546
|
+
"INVALID_CONFIG"
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (!config.region || config.region.trim() === "") {
|
|
551
|
+
throw new DatabaseAdapterError(
|
|
552
|
+
"DynamoDB region is required",
|
|
553
|
+
"MISSING_REGION"
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (config.accessKeyId && !config.secretAccessKey) {
|
|
558
|
+
throw new DatabaseAdapterError(
|
|
559
|
+
"secretAccessKey is required when accessKeyId is provided",
|
|
560
|
+
"INVALID_CREDENTIALS"
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (config.secretAccessKey && !config.accessKeyId) {
|
|
565
|
+
throw new DatabaseAdapterError(
|
|
566
|
+
"accessKeyId is required when secretAccessKey is provided",
|
|
567
|
+
"INVALID_CREDENTIALS"
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
private getClient(): DynamoDBDocumentClient {
|
|
573
|
+
if (!this.isConnectedFlag || !this.client) {
|
|
574
|
+
throw new DatabaseAdapterError(
|
|
575
|
+
"DynamoDB client is not connected. Call connect() first.",
|
|
576
|
+
"NOT_CONNECTED"
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return this.client;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
private buildKey(where: WhereClause): Record<string, any> {
|
|
584
|
+
const key: Record<string, any> = {};
|
|
585
|
+
|
|
586
|
+
for (const [field, condition] of Object.entries(where)) {
|
|
587
|
+
if (condition == null) {
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
if (typeof condition === "object" && "value" in condition) {
|
|
591
|
+
key[field] = condition.value;
|
|
592
|
+
} else {
|
|
593
|
+
key[field] = condition;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return key;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
private extractKey(item: Record<string, any>): WhereClause {
|
|
601
|
+
return {
|
|
602
|
+
id: {
|
|
603
|
+
operator: "=",
|
|
604
|
+
value: item.id
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
private isKeyQuery(where: WhereClause): boolean {
|
|
610
|
+
// Only route to Query when filtering by 'id' with an operator supported by KeyConditionExpression.
|
|
611
|
+
// IN and NOT_IN are not valid in KeyConditionExpression — fall back to Scan for those.
|
|
612
|
+
return Object.keys(where).some(key => {
|
|
613
|
+
if (key !== 'id') return false;
|
|
614
|
+
const cond = (where as any)[key];
|
|
615
|
+
if (typeof cond === 'object' && 'operator' in cond) {
|
|
616
|
+
const unsupportedInKey = ['IN', 'NOT_IN'];
|
|
617
|
+
return !unsupportedInKey.includes(cond.operator);
|
|
618
|
+
}
|
|
619
|
+
return true;
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
private buildProjection(fields: string[]): { projection: string; names: Record<string, string> } {
|
|
624
|
+
const names: Record<string, string> = {};
|
|
625
|
+
const projectionParts: string[] = [];
|
|
626
|
+
|
|
627
|
+
for (const field of fields) {
|
|
628
|
+
if (RESERVED_KEYWORDS.has(field.toUpperCase())) {
|
|
629
|
+
const placeholder = `#${field}`;
|
|
630
|
+
names[placeholder] = field;
|
|
631
|
+
projectionParts.push(placeholder);
|
|
632
|
+
} else {
|
|
633
|
+
projectionParts.push(field);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
return {
|
|
638
|
+
projection: projectionParts.join(", "),
|
|
639
|
+
names
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
private buildKeyConditionExpression(where: WhereClause): {
|
|
644
|
+
keyConditionExpression: string;
|
|
645
|
+
expressionAttributeValues: Record<string, any>;
|
|
646
|
+
names: Record<string, string>;
|
|
647
|
+
} {
|
|
648
|
+
const parts: string[] = [];
|
|
649
|
+
const values: Record<string, any> = {};
|
|
650
|
+
const names: Record<string, string> = {};
|
|
651
|
+
let valueIndex = 0;
|
|
652
|
+
|
|
653
|
+
for (const [field, condition] of Object.entries(where)) {
|
|
654
|
+
const valueKey = `:val${valueIndex}`;
|
|
655
|
+
const cond = condition as any;
|
|
656
|
+
const fieldPlaceholder = RESERVED_KEYWORDS.has(field.toUpperCase()) ? `#${field}` : field;
|
|
657
|
+
|
|
658
|
+
if (RESERVED_KEYWORDS.has(field.toUpperCase())) {
|
|
659
|
+
names[fieldPlaceholder] = field;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (typeof cond === "object" && "operator" in cond) {
|
|
663
|
+
const operator = cond.operator as QueryOperator;
|
|
664
|
+
|
|
665
|
+
switch (operator) {
|
|
666
|
+
case "=":
|
|
667
|
+
parts.push(`${fieldPlaceholder} = ${valueKey}`);
|
|
668
|
+
values[valueKey] = cond.value;
|
|
669
|
+
break;
|
|
670
|
+
case "!=":
|
|
671
|
+
parts.push(`${fieldPlaceholder} <> ${valueKey}`);
|
|
672
|
+
values[valueKey] = cond.value;
|
|
673
|
+
break;
|
|
674
|
+
case ">":
|
|
675
|
+
parts.push(`${fieldPlaceholder} > ${valueKey}`);
|
|
676
|
+
values[valueKey] = cond.value;
|
|
677
|
+
break;
|
|
678
|
+
case ">=":
|
|
679
|
+
parts.push(`${fieldPlaceholder} >= ${valueKey}`);
|
|
680
|
+
values[valueKey] = cond.value;
|
|
681
|
+
break;
|
|
682
|
+
case "<":
|
|
683
|
+
parts.push(`${fieldPlaceholder} < ${valueKey}`);
|
|
684
|
+
values[valueKey] = cond.value;
|
|
685
|
+
break;
|
|
686
|
+
case "<=":
|
|
687
|
+
parts.push(`${fieldPlaceholder} <= ${valueKey}`);
|
|
688
|
+
values[valueKey] = cond.value;
|
|
689
|
+
break;
|
|
690
|
+
case "BEGINS_WITH":
|
|
691
|
+
parts.push(`begins_with(${fieldPlaceholder}, ${valueKey})`);
|
|
692
|
+
values[valueKey] = cond.value;
|
|
693
|
+
break;
|
|
694
|
+
case "IN":
|
|
695
|
+
const inValues = (cond.value as any[]).map((v, i) => `:inval${valueIndex}_${i}`);
|
|
696
|
+
(cond.value as any[]).forEach((v, i) => {
|
|
697
|
+
values[`:inval${valueIndex}_${i}`] = v;
|
|
698
|
+
});
|
|
699
|
+
parts.push(`${fieldPlaceholder} IN (${inValues.join(", ")})`);
|
|
700
|
+
valueIndex += cond.value.length - 1;
|
|
701
|
+
break;
|
|
702
|
+
default:
|
|
703
|
+
throw new DatabaseAdapterError(
|
|
704
|
+
`Unsupported operator: ${operator}`,
|
|
705
|
+
"UNSUPPORTED_OPERATOR"
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
} else {
|
|
709
|
+
parts.push(`${fieldPlaceholder} = ${valueKey}`);
|
|
710
|
+
values[valueKey] = cond;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
valueIndex++;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
return {
|
|
717
|
+
keyConditionExpression: parts.join(" AND "),
|
|
718
|
+
expressionAttributeValues: Object.keys(values).length > 0 ? values : {},
|
|
719
|
+
names
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
private buildFilterExpression(where: WhereClause): {
|
|
724
|
+
filterExpression: string;
|
|
725
|
+
expressionAttributeValues: Record<string, any>;
|
|
726
|
+
names: Record<string, string>;
|
|
727
|
+
} {
|
|
728
|
+
const parts: string[] = [];
|
|
729
|
+
const values: Record<string, any> = {};
|
|
730
|
+
const names: Record<string, string> = {};
|
|
731
|
+
let valueIndex = 0;
|
|
732
|
+
|
|
733
|
+
for (const [field, condition] of Object.entries(where)) {
|
|
734
|
+
const valueKey = `:val${valueIndex}`;
|
|
735
|
+
const cond = condition as any;
|
|
736
|
+
const fieldPlaceholder = RESERVED_KEYWORDS.has(field.toUpperCase()) ? `#${field}` : field;
|
|
737
|
+
|
|
738
|
+
if (RESERVED_KEYWORDS.has(field.toUpperCase())) {
|
|
739
|
+
names[fieldPlaceholder] = field;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (typeof cond === "object" && "operator" in cond) {
|
|
743
|
+
const operator = cond.operator as QueryOperator;
|
|
744
|
+
|
|
745
|
+
switch (operator) {
|
|
746
|
+
case "=":
|
|
747
|
+
parts.push(`${fieldPlaceholder} = ${valueKey}`);
|
|
748
|
+
values[valueKey] = cond.value;
|
|
749
|
+
break;
|
|
750
|
+
case "!=":
|
|
751
|
+
parts.push(`${fieldPlaceholder} <> ${valueKey}`);
|
|
752
|
+
values[valueKey] = cond.value;
|
|
753
|
+
break;
|
|
754
|
+
case ">":
|
|
755
|
+
parts.push(`${fieldPlaceholder} > ${valueKey}`);
|
|
756
|
+
values[valueKey] = cond.value;
|
|
757
|
+
break;
|
|
758
|
+
case ">=":
|
|
759
|
+
parts.push(`${fieldPlaceholder} >= ${valueKey}`);
|
|
760
|
+
values[valueKey] = cond.value;
|
|
761
|
+
break;
|
|
762
|
+
case "<":
|
|
763
|
+
parts.push(`${fieldPlaceholder} < ${valueKey}`);
|
|
764
|
+
values[valueKey] = cond.value;
|
|
765
|
+
break;
|
|
766
|
+
case "<=":
|
|
767
|
+
parts.push(`${fieldPlaceholder} <= ${valueKey}`);
|
|
768
|
+
values[valueKey] = cond.value;
|
|
769
|
+
break;
|
|
770
|
+
case "BEGINS_WITH":
|
|
771
|
+
parts.push(`begins_with(${fieldPlaceholder}, ${valueKey})`);
|
|
772
|
+
values[valueKey] = cond.value;
|
|
773
|
+
break;
|
|
774
|
+
case "IN":
|
|
775
|
+
const inValues = (cond.value as any[]).map((v, i) => `:inval${valueIndex}_${i}`);
|
|
776
|
+
(cond.value as any[]).forEach((v, i) => {
|
|
777
|
+
values[`:inval${valueIndex}_${i}`] = v;
|
|
778
|
+
});
|
|
779
|
+
parts.push(`${fieldPlaceholder} IN (${inValues.join(", ")})`);
|
|
780
|
+
valueIndex += cond.value.length - 1;
|
|
781
|
+
break;
|
|
782
|
+
case "NOT_IN":
|
|
783
|
+
const notInValues = (cond.value as any[]).map((v, i) => `:notinval${valueIndex}_${i}`);
|
|
784
|
+
(cond.value as any[]).forEach((v, i) => {
|
|
785
|
+
values[`:notinval${valueIndex}_${i}`] = v;
|
|
786
|
+
});
|
|
787
|
+
parts.push(`NOT ${fieldPlaceholder} IN (${notInValues.join(", ")})`);
|
|
788
|
+
valueIndex += cond.value.length - 1;
|
|
789
|
+
break;
|
|
790
|
+
default:
|
|
791
|
+
throw new DatabaseAdapterError(
|
|
792
|
+
`Unsupported operator: ${operator}`,
|
|
793
|
+
"UNSUPPORTED_OPERATOR"
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
} else {
|
|
797
|
+
parts.push(`${fieldPlaceholder} = ${valueKey}`);
|
|
798
|
+
values[valueKey] = cond;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
valueIndex++;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
return {
|
|
805
|
+
filterExpression: parts.join(" AND "),
|
|
806
|
+
expressionAttributeValues: Object.keys(values).length > 0 ? values : {},
|
|
807
|
+
names
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
private buildUpdateExpression(update: UpdateClause): {
|
|
812
|
+
updateExpression: string;
|
|
813
|
+
expressionAttributeValues: Record<string, any>;
|
|
814
|
+
names: Record<string, string>;
|
|
815
|
+
} {
|
|
816
|
+
const setParts: string[] = [];
|
|
817
|
+
const values: Record<string, any> = {};
|
|
818
|
+
const names: Record<string, string> = {};
|
|
819
|
+
let valueIndex = 0;
|
|
820
|
+
|
|
821
|
+
for (const [field, value] of Object.entries(update)) {
|
|
822
|
+
const valueKey = `:val${valueIndex}`;
|
|
823
|
+
const fieldPlaceholder = RESERVED_KEYWORDS.has(field.toUpperCase()) ? `#${field}` : field;
|
|
824
|
+
|
|
825
|
+
if (RESERVED_KEYWORDS.has(field.toUpperCase())) {
|
|
826
|
+
names[fieldPlaceholder] = field;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
setParts.push(`${fieldPlaceholder} = ${valueKey}`);
|
|
830
|
+
values[valueKey] = value;
|
|
831
|
+
valueIndex++;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
return {
|
|
835
|
+
updateExpression: `SET ${setParts.join(", ")}`,
|
|
836
|
+
expressionAttributeValues: Object.keys(values).length > 0 ? values : {},
|
|
837
|
+
names
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
private handleError(error: unknown, operation: string): DatabaseAdapterError {
|
|
842
|
+
if (error instanceof DatabaseAdapterError) {
|
|
843
|
+
return error;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
if (error instanceof Error) {
|
|
847
|
+
return new DatabaseAdapterError(
|
|
848
|
+
`${operation} failed: ${error.message}`,
|
|
849
|
+
"OPERATION_FAILED",
|
|
850
|
+
{ originalError: error.message }
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
return new DatabaseAdapterError(
|
|
855
|
+
`${operation} failed: Unknown error`,
|
|
856
|
+
"OPERATION_FAILED"
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
}
|