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,53 @@
|
|
|
1
|
+
export type QueryOperator = "=" | "!=" | ">" | ">=" | "<" | "<=" | "IN" | "NOT_IN" | "BETWEEN" | "LIKE" | "EXISTS" | "BEGINS_WITH";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Represents a single condition in a WHERE clause
|
|
5
|
+
*/
|
|
6
|
+
export interface QueryCondition {
|
|
7
|
+
operator: QueryOperator;
|
|
8
|
+
value: any;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Represents a single field condition or nested conditions
|
|
13
|
+
*/
|
|
14
|
+
export interface WhereClause {
|
|
15
|
+
[key: string]: QueryCondition | QueryCondition[] | WhereClause;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Represents update operations on specific fields
|
|
20
|
+
*/
|
|
21
|
+
export interface UpdateClause {
|
|
22
|
+
[key: string]: any;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Configuration for query operations
|
|
27
|
+
*/
|
|
28
|
+
export interface QueryOptions {
|
|
29
|
+
limit?: number;
|
|
30
|
+
offset?: number;
|
|
31
|
+
orderBy?: { field: string; direction: "ASC" | "DESC" }[];
|
|
32
|
+
projection?: string[]; // Select specific fields
|
|
33
|
+
consistent?: boolean; // For databases that support consistency levels
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Result wrapper for paginated responses
|
|
38
|
+
*/
|
|
39
|
+
export type PaginatedResult<T> = {
|
|
40
|
+
items: T[];
|
|
41
|
+
total: number;
|
|
42
|
+
hasMore: boolean;
|
|
43
|
+
nextOffset?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Result wrapper for batch operations
|
|
48
|
+
*/
|
|
49
|
+
export interface BatchResult {
|
|
50
|
+
successful: number;
|
|
51
|
+
failed: number;
|
|
52
|
+
errors?: { index: number; error: string }[];
|
|
53
|
+
}
|
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
import { DynamoService } from "../src/dynamo/dynamo";
|
|
2
|
+
import { DatabaseAdapterError } from "../src/types/error";
|
|
3
|
+
import { DynamoConfig } from "../src/dynamo/dynamo.types";
|
|
4
|
+
import { CreateTableCommand, DeleteTableCommand, DynamoDBClient, DynamoDBClientConfig } from "@aws-sdk/client-dynamodb";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* DynamoDB Unit Tests using LocalStack
|
|
8
|
+
* Requires LocalStack running on localhost:4566
|
|
9
|
+
*/
|
|
10
|
+
describe("DynamoService", () => {
|
|
11
|
+
let service: DynamoService;
|
|
12
|
+
let tableManagementClient: DynamoDBClient;
|
|
13
|
+
const tableName = "test_table";
|
|
14
|
+
|
|
15
|
+
const config: DynamoConfig = {
|
|
16
|
+
region: "us-east-1",
|
|
17
|
+
endpoint: "http://localhost:4566",
|
|
18
|
+
accessKeyId: "test",
|
|
19
|
+
secretAccessKey: "test"
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
beforeAll(async () => {
|
|
23
|
+
// Service manages its own connection
|
|
24
|
+
service = new DynamoService();
|
|
25
|
+
service.setConfig(config);
|
|
26
|
+
await service.connect();
|
|
27
|
+
|
|
28
|
+
// Separate client only for table management
|
|
29
|
+
tableManagementClient = new DynamoDBClient({
|
|
30
|
+
region: config.region,
|
|
31
|
+
endpoint: config.endpoint,
|
|
32
|
+
credentials: {
|
|
33
|
+
accessKeyId: config.accessKeyId,
|
|
34
|
+
secretAccessKey: config.secretAccessKey
|
|
35
|
+
}
|
|
36
|
+
} as DynamoDBClientConfig);
|
|
37
|
+
|
|
38
|
+
// Create table once for all tests
|
|
39
|
+
try {
|
|
40
|
+
await tableManagementClient.send(new DeleteTableCommand({ TableName: tableName }));
|
|
41
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
42
|
+
} catch (error) {
|
|
43
|
+
// Ignore if table doesn't exist
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await tableManagementClient.send(
|
|
47
|
+
new CreateTableCommand({
|
|
48
|
+
TableName: tableName,
|
|
49
|
+
KeySchema: [{ AttributeName: "id", KeyType: "HASH" }],
|
|
50
|
+
AttributeDefinitions: [{ AttributeName: "id", AttributeType: "N" }],
|
|
51
|
+
BillingMode: "PAY_PER_REQUEST"
|
|
52
|
+
})
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
56
|
+
}, 15000);
|
|
57
|
+
|
|
58
|
+
afterAll(async () => {
|
|
59
|
+
await service.disconnect();
|
|
60
|
+
tableManagementClient.destroy();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
beforeEach(async () => {
|
|
64
|
+
// Clear all data from table before each test
|
|
65
|
+
try {
|
|
66
|
+
const result = await service.selectMany(tableName);
|
|
67
|
+
for (const item of result.items) {
|
|
68
|
+
await service.deleteOne(tableName, { id: { operator: "=", value: (item as any).id } });
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
// Ignore errors during cleanup
|
|
72
|
+
}
|
|
73
|
+
}, 5000);
|
|
74
|
+
|
|
75
|
+
// ============ CONNECTION TESTS ============
|
|
76
|
+
|
|
77
|
+
describe("Connection Management", () => {
|
|
78
|
+
it("should connect successfully with valid config", async () => {
|
|
79
|
+
const testService = new DynamoService();
|
|
80
|
+
testService.setConfig(config);
|
|
81
|
+
await testService.connect();
|
|
82
|
+
|
|
83
|
+
expect(testService.isConnected()).toBe(true);
|
|
84
|
+
|
|
85
|
+
await testService.disconnect();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should reject connection when no config is set", async () => {
|
|
89
|
+
const testService = new DynamoService();
|
|
90
|
+
|
|
91
|
+
await expect(testService.connect()).rejects.toThrow(DatabaseAdapterError);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should reject connection with invalid region", async () => {
|
|
95
|
+
const testService = new DynamoService();
|
|
96
|
+
testService.setConfig({
|
|
97
|
+
...config,
|
|
98
|
+
region: ""
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await expect(testService.connect()).rejects.toThrow(DatabaseAdapterError);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should be disconnected after calling disconnect", async () => {
|
|
105
|
+
const testService = new DynamoService();
|
|
106
|
+
testService.setConfig(config);
|
|
107
|
+
await testService.connect();
|
|
108
|
+
|
|
109
|
+
expect(testService.isConnected()).toBe(true);
|
|
110
|
+
|
|
111
|
+
await testService.disconnect();
|
|
112
|
+
expect(testService.isConnected()).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("should return false for healthCheck on disconnected service", async () => {
|
|
116
|
+
const testService = new DynamoService();
|
|
117
|
+
const health = await testService.healthCheck();
|
|
118
|
+
expect(health).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ============ CREATE OPERATIONS ============
|
|
123
|
+
|
|
124
|
+
describe("createOne", () => {
|
|
125
|
+
it("should insert a single record", async () => {
|
|
126
|
+
const data = { id: 1, name: "Test User", email: "test@example.com" };
|
|
127
|
+
const result = await service.createOne(tableName, data);
|
|
128
|
+
|
|
129
|
+
expect(result).toEqual(data);
|
|
130
|
+
expect(result.id).toBe(1);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("should return inserted data with correct types", async () => {
|
|
134
|
+
const data = {
|
|
135
|
+
id: 2,
|
|
136
|
+
name: "John Doe",
|
|
137
|
+
age: 30,
|
|
138
|
+
active: true,
|
|
139
|
+
tags: ["admin", "user"]
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const result = await service.createOne(tableName, data);
|
|
143
|
+
|
|
144
|
+
expect(result.id).toBe(2);
|
|
145
|
+
expect(result.name).toBe("John Doe");
|
|
146
|
+
expect(result.age).toBe(30);
|
|
147
|
+
expect(result.active).toBe(true);
|
|
148
|
+
expect(result.tags).toEqual(["admin", "user"]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("should handle nested objects", async () => {
|
|
152
|
+
const data = {
|
|
153
|
+
id: 3,
|
|
154
|
+
user: {
|
|
155
|
+
firstName: "Jane",
|
|
156
|
+
lastName: "Doe",
|
|
157
|
+
contact: {
|
|
158
|
+
email: "jane@example.com",
|
|
159
|
+
phone: "123-456-7890"
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const result = await service.createOne(tableName, data);
|
|
165
|
+
|
|
166
|
+
expect(result.user.firstName).toBe("Jane");
|
|
167
|
+
expect(result.user.contact.email).toBe("jane@example.com");
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("createMany", () => {
|
|
172
|
+
it("should insert multiple records", async () => {
|
|
173
|
+
const data = [
|
|
174
|
+
{ id: 1, name: "User 1" },
|
|
175
|
+
{ id: 2, name: "User 2" },
|
|
176
|
+
{ id: 3, name: "User 3" }
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
const result = await service.createMany(tableName, data);
|
|
180
|
+
|
|
181
|
+
expect(result.items).toHaveLength(3);
|
|
182
|
+
expect(result.result.successful).toBe(3);
|
|
183
|
+
expect(result.result.failed).toBe(0);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("should handle empty array", async () => {
|
|
187
|
+
const result = await service.createMany(tableName, []);
|
|
188
|
+
|
|
189
|
+
expect(result.items).toHaveLength(0);
|
|
190
|
+
expect(result.result.successful).toBe(0);
|
|
191
|
+
expect(result.result.failed).toBe(0);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("should throw error if data is not array", async () => {
|
|
195
|
+
await expect(
|
|
196
|
+
service.createMany(tableName, { id: 1 } as any)
|
|
197
|
+
).rejects.toThrow();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("should batch inserts in groups of 25", async () => {
|
|
201
|
+
const data: Array<{ id: number; name: string }> = [];
|
|
202
|
+
for (let i = 1; i <= 50; i++) {
|
|
203
|
+
data.push({ id: i, name: `User ${i}` });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const result = await service.createMany(tableName, data);
|
|
207
|
+
|
|
208
|
+
expect(result.items.length).toBe(50);
|
|
209
|
+
expect(result.result.successful).toBe(50);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// ============ SELECT OPERATIONS ============
|
|
214
|
+
|
|
215
|
+
describe("selectOne", () => {
|
|
216
|
+
beforeEach(async () => {
|
|
217
|
+
await service.createOne(tableName, { id: 1, name: "Test", status: "active" });
|
|
218
|
+
await service.createOne(tableName, { id: 2, name: "Another", status: "inactive" });
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("should select a single record by id", async () => {
|
|
222
|
+
const result = await service.selectOne(tableName, {
|
|
223
|
+
id: { operator: "=", value: 1 }
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
expect(result).not.toBeNull();
|
|
227
|
+
expect(result?.id).toBe(1);
|
|
228
|
+
expect(result?.name).toBe("Test");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("should return null when record not found", async () => {
|
|
232
|
+
const result = await service.selectOne(tableName, {
|
|
233
|
+
id: { operator: "=", value: 999 }
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
expect(result).toBeNull();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("should support projection", async () => {
|
|
240
|
+
const result = await service.selectOne(
|
|
241
|
+
tableName,
|
|
242
|
+
{ id: { operator: "=", value: 1 } },
|
|
243
|
+
{ projection: ["id", "name"] }
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
expect(result?.id).toBe(1);
|
|
247
|
+
expect(result?.name).toBe("Test");
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe("selectMany", () => {
|
|
252
|
+
beforeEach(async () => {
|
|
253
|
+
const data = [
|
|
254
|
+
{ id: 1, name: "User 1", status: "active", score: 100 },
|
|
255
|
+
{ id: 2, name: "User 2", status: "active", score: 85 },
|
|
256
|
+
{ id: 3, name: "User 3", status: "inactive", score: 90 },
|
|
257
|
+
{ id: 4, name: "User 4", status: "active", score: 75 },
|
|
258
|
+
{ id: 5, name: "User 5", status: "inactive", score: 95 }
|
|
259
|
+
];
|
|
260
|
+
await service.createMany(tableName, data);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("should select multiple records with condition", async () => {
|
|
264
|
+
const result = await service.selectMany(tableName, {
|
|
265
|
+
status: { operator: "=", value: "active" }
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
expect(result.items.length).toBeGreaterThan(0);
|
|
269
|
+
expect(result.items.every((item: any) => item.status === "active")).toBe(true);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("should handle empty result set", async () => {
|
|
273
|
+
const result = await service.selectMany(tableName, {
|
|
274
|
+
status: { operator: "=", value: "nonexistent" }
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
expect(result.items).toHaveLength(0);
|
|
278
|
+
expect(result.total).toBe(0);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("should support limit and offset", async () => {
|
|
282
|
+
const result = await service.selectMany(tableName, undefined, {
|
|
283
|
+
limit: 2,
|
|
284
|
+
offset: 0
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
expect(result.items.length).toBeLessThanOrEqual(2);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("should support ordering", async () => {
|
|
291
|
+
const result = await service.selectMany(tableName, undefined, {
|
|
292
|
+
orderBy: [{ field: "id", direction: "DESC" }],
|
|
293
|
+
limit: 5
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
if (result.items.length > 1) {
|
|
297
|
+
const ids = result.items.map((item: any) => item.id);
|
|
298
|
+
for (let i = 1; i < ids.length; i++) {
|
|
299
|
+
expect(ids[i]).toBeLessThanOrEqual(ids[i - 1]);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("should support projection", async () => {
|
|
305
|
+
const result = await service.selectMany(tableName, undefined, {
|
|
306
|
+
projection: ["id", "name"],
|
|
307
|
+
limit: 3
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
result.items.forEach((item: any) => {
|
|
311
|
+
expect(item).toHaveProperty("id");
|
|
312
|
+
expect(item).toHaveProperty("name");
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("should return pagination info", async () => {
|
|
317
|
+
const result = await service.selectMany(tableName, undefined, { limit: 2 });
|
|
318
|
+
|
|
319
|
+
expect(result).toHaveProperty("total");
|
|
320
|
+
expect(result).toHaveProperty("hasMore");
|
|
321
|
+
expect(result).toHaveProperty("items");
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("should support greater than operator", async () => {
|
|
325
|
+
const result = await service.selectMany(tableName, {
|
|
326
|
+
score: { operator: ">", value: 80 }
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
expect(result.items.every((item: any) => item.score > 80)).toBe(true);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("should support less than operator", async () => {
|
|
333
|
+
const result = await service.selectMany(tableName, {
|
|
334
|
+
score: { operator: "<", value: 80 }
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
expect(result.items.every((item: any) => item.score < 80)).toBe(true);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// ============ UPDATE OPERATIONS ============
|
|
342
|
+
|
|
343
|
+
describe("updateOne", () => {
|
|
344
|
+
beforeEach(async () => {
|
|
345
|
+
await service.createOne(tableName, { id: 1, name: "Original", status: "active" });
|
|
346
|
+
await service.createOne(tableName, { id: 2, name: "Another", status: "inactive" });
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("should update a single record", async () => {
|
|
350
|
+
const result = await service.updateOne(
|
|
351
|
+
tableName,
|
|
352
|
+
{ name: "Updated", status: "inactive" },
|
|
353
|
+
{ id: { operator: "=", value: 1 } }
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
expect(result.name).toBe("Updated");
|
|
357
|
+
expect(result.status).toBe("inactive");
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("should throw error when record not found", async () => {
|
|
361
|
+
await expect(
|
|
362
|
+
service.updateOne(
|
|
363
|
+
tableName,
|
|
364
|
+
{ name: "Updated" },
|
|
365
|
+
{ id: { operator: "=", value: 999 } }
|
|
366
|
+
)
|
|
367
|
+
).rejects.toThrow(DatabaseAdapterError);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("should preserve other fields during update", async () => {
|
|
371
|
+
const result = await service.updateOne(
|
|
372
|
+
tableName,
|
|
373
|
+
{ name: "Updated" },
|
|
374
|
+
{ id: { operator: "=", value: 1 } }
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
expect(result.id).toBe(1);
|
|
378
|
+
expect(result.name).toBe("Updated");
|
|
379
|
+
expect(result.status).toBe("active");
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
describe("updateMany", () => {
|
|
384
|
+
beforeEach(async () => {
|
|
385
|
+
const data = [
|
|
386
|
+
{ id: 1, name: "User 1", status: "active" },
|
|
387
|
+
{ id: 2, name: "User 2", status: "active" },
|
|
388
|
+
{ id: 3, name: "User 3", status: "inactive" },
|
|
389
|
+
{ id: 4, name: "User 4", status: "active" }
|
|
390
|
+
];
|
|
391
|
+
await service.createMany(tableName, data);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it("should update multiple records matching condition", async () => {
|
|
395
|
+
const result = await service.updateMany(
|
|
396
|
+
tableName,
|
|
397
|
+
{ status: "archived" },
|
|
398
|
+
{ status: { operator: "=", value: "inactive" } }
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
expect(result.result.successful).toBeGreaterThan(0);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it("should respect limit option", async () => {
|
|
405
|
+
const result = await service.updateMany(
|
|
406
|
+
tableName,
|
|
407
|
+
{ status: "archived" },
|
|
408
|
+
{ status: { operator: "=", value: "active" } },
|
|
409
|
+
{ limit: 2 }
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
expect(result.items.length).toBeLessThanOrEqual(2);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it("should return empty result when no matches found", async () => {
|
|
416
|
+
const result = await service.updateMany(
|
|
417
|
+
tableName,
|
|
418
|
+
{ status: "archived" },
|
|
419
|
+
{ status: { operator: "=", value: "nonexistent" } }
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
expect(result.items).toHaveLength(0);
|
|
423
|
+
expect(result.result.successful).toBe(0);
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// ============ DELETE OPERATIONS ============
|
|
428
|
+
|
|
429
|
+
describe("deleteOne", () => {
|
|
430
|
+
beforeEach(async () => {
|
|
431
|
+
await service.createOne(tableName, { id: 1, name: "User 1" });
|
|
432
|
+
await service.createOne(tableName, { id: 2, name: "User 2" });
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it("should delete a single record", async () => {
|
|
436
|
+
const result = await service.deleteOne(tableName, {
|
|
437
|
+
id: { operator: "=", value: 1 }
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
expect(result.success).toBe(true);
|
|
441
|
+
expect(result.deletedCount).toBe(1);
|
|
442
|
+
|
|
443
|
+
const check = await service.selectOne(tableName, {
|
|
444
|
+
id: { operator: "=", value: 1 }
|
|
445
|
+
});
|
|
446
|
+
expect(check).toBeNull();
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("should handle deletion of non-existent record", async () => {
|
|
450
|
+
const result = await service.deleteOne(tableName, {
|
|
451
|
+
id: { operator: "=", value: 999 }
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
expect(result.success).toBe(false);
|
|
455
|
+
expect(result.deletedCount).toBe(0);
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
describe("deleteMany", () => {
|
|
460
|
+
beforeEach(async () => {
|
|
461
|
+
const data = [
|
|
462
|
+
{ id: 1, name: "User 1", status: "active" },
|
|
463
|
+
{ id: 2, name: "User 2", status: "inactive" },
|
|
464
|
+
{ id: 3, name: "User 3", status: "active" },
|
|
465
|
+
{ id: 4, name: "User 4", status: "inactive" }
|
|
466
|
+
];
|
|
467
|
+
await service.createMany(tableName, data);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it("should delete multiple records matching condition", async () => {
|
|
471
|
+
const result = await service.deleteMany(tableName, {
|
|
472
|
+
status: { operator: "=", value: "inactive" }
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
expect(result.success).toBe(true);
|
|
476
|
+
expect(result.deletedCount).toBeGreaterThan(0);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it("should respect limit option", async () => {
|
|
480
|
+
const result = await service.deleteMany(
|
|
481
|
+
tableName,
|
|
482
|
+
{ status: { operator: "=", value: "active" } },
|
|
483
|
+
{ limit: 1 }
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
expect(result.deletedCount).toBeLessThanOrEqual(1);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it("should handle no matches", async () => {
|
|
490
|
+
const result = await service.deleteMany(tableName, {
|
|
491
|
+
status: { operator: "=", value: "nonexistent" }
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
expect(result.success).toBe(true);
|
|
495
|
+
expect(result.deletedCount).toBe(0);
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// ============ OPERATOR TESTS ============
|
|
500
|
+
|
|
501
|
+
describe("Query Operators", () => {
|
|
502
|
+
beforeEach(async () => {
|
|
503
|
+
const data = [
|
|
504
|
+
{ id: 1, price: 100, category: "electronics" },
|
|
505
|
+
{ id: 2, price: 200, category: "electronics" },
|
|
506
|
+
{ id: 3, price: 50, category: "books" },
|
|
507
|
+
{ id: 4, price: 150, category: "books" }
|
|
508
|
+
];
|
|
509
|
+
await service.createMany(tableName, data);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it("should support IN operator", async () => {
|
|
513
|
+
const result = await service.selectMany(tableName, {
|
|
514
|
+
id: { operator: "IN", value: [1, 2] }
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
expect(result.items.length).toBeGreaterThan(0);
|
|
518
|
+
expect(result.items.every((item: any) => [1, 2].includes(item.id))).toBe(true);
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// ============ ERROR HANDLING ============
|
|
523
|
+
|
|
524
|
+
describe("Error Handling", () => {
|
|
525
|
+
it("should throw error on operation without connection", async () => {
|
|
526
|
+
const disconnectedService = new DynamoService();
|
|
527
|
+
|
|
528
|
+
await expect(
|
|
529
|
+
disconnectedService.selectOne(tableName, { id: { operator: "=", value: 1 } })
|
|
530
|
+
).rejects.toThrow(DatabaseAdapterError);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it("should throw error on unsupported operator", async () => {
|
|
534
|
+
await expect(
|
|
535
|
+
service.selectMany(tableName, {
|
|
536
|
+
id: { operator: "UNSUPPORTED" as any, value: 1 }
|
|
537
|
+
})
|
|
538
|
+
).rejects.toThrow();
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it("should throw error on executeRaw", async () => {
|
|
542
|
+
await expect(
|
|
543
|
+
service.executeRaw("raw query")
|
|
544
|
+
).rejects.toThrow(DatabaseAdapterError);
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
});
|