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.
@@ -0,0 +1,486 @@
1
+ import { MongoService } from "./../src/mongo/mongo";
2
+ import { DatabaseAdapterError } from "./../src/types/error";
3
+
4
+ const DB_NAME = "fluxor_db_tests";
5
+ const MONGO_URI = `mongodb://localhost:27017/${DB_NAME}`;
6
+ const tableName = "test_collection";
7
+
8
+ let service: MongoService;
9
+
10
+ beforeEach(async () => {
11
+ service = new MongoService();
12
+ service.setConfig({ uri: MONGO_URI, dbName: DB_NAME });
13
+ await service.connect();
14
+
15
+ // Clean slate before each test
16
+ const db = (service as any).db;
17
+ await db.collection(tableName).deleteMany({});
18
+ });
19
+
20
+ afterEach(async () => {
21
+ await service.disconnect();
22
+ });
23
+
24
+ // ─── helpers ────────────────────────────────────────────────────────────────
25
+
26
+ function makeRecord(id: number, overrides: Record<string, any> = {}) {
27
+ return { id, name: `Record ${id}`, status: "active", score: id * 10, ...overrides };
28
+ }
29
+
30
+ async function seedRecords(count: number, overrides: Record<string, any> = {}) {
31
+ const records = Array.from({ length: count }, (_, i) => makeRecord(i + 1, overrides));
32
+ await service.createMany(tableName, records);
33
+ return records;
34
+ }
35
+
36
+ // ─── tests ───────────────────────────────────────────────────────────────────
37
+
38
+ describe("MongoService", () => {
39
+
40
+ // ── Connection Management ────────────────────────────────────────────────
41
+
42
+ describe("Connection Management", () => {
43
+ it("should connect successfully with valid config", async () => {
44
+ const s = new MongoService();
45
+ s.setConfig({ uri: MONGO_URI, dbName: DB_NAME });
46
+ await expect(s.connect()).resolves.toBeUndefined();
47
+ expect(s.isConnected()).toBe(true);
48
+ await s.disconnect();
49
+ });
50
+
51
+ it("should reject connection when no config is set", async () => {
52
+ const s = new MongoService();
53
+ await expect(s.connect()).rejects.toThrow(DatabaseAdapterError);
54
+ });
55
+
56
+ it("should reject connection with invalid uri", async () => {
57
+ const s = new MongoService();
58
+ s.setConfig({ uri: "", dbName: DB_NAME });
59
+ await expect(s.connect()).rejects.toThrow(DatabaseAdapterError);
60
+ });
61
+
62
+ it("should be disconnected after calling disconnect", async () => {
63
+ await service.disconnect();
64
+ expect(service.isConnected()).toBe(false);
65
+ });
66
+
67
+ it("should return false for healthCheck on disconnected service", async () => {
68
+ await service.disconnect();
69
+ const result = await service.healthCheck();
70
+ expect(result).toBe(false);
71
+ });
72
+ });
73
+
74
+ // ── createOne ────────────────────────────────────────────────────────────
75
+
76
+ describe("createOne", () => {
77
+ it("should insert a single record", async () => {
78
+ const record = makeRecord(1);
79
+ const result = await service.createOne(tableName, record);
80
+ expect(result).toMatchObject(record);
81
+ });
82
+
83
+ it("should return inserted data with correct types", async () => {
84
+ const record = { id: 1, name: "Test", score: 42, active: true };
85
+ const result: any = await service.createOne(tableName, record);
86
+ expect(typeof result.id).toBe("number");
87
+ expect(typeof result.name).toBe("string");
88
+ expect(typeof result.score).toBe("number");
89
+ expect(typeof result.active).toBe("boolean");
90
+ });
91
+
92
+ it("should handle nested objects", async () => {
93
+ const record = { id: 1, name: "Nested", meta: { tags: ["a", "b"], count: 2 } };
94
+ const result: any = await service.createOne(tableName, record);
95
+ expect(result.meta).toEqual({ tags: ["a", "b"], count: 2 });
96
+ });
97
+ });
98
+
99
+ // ── createMany ───────────────────────────────────────────────────────────
100
+
101
+ describe("createMany", () => {
102
+ it("should insert multiple records", async () => {
103
+ const records = [makeRecord(1), makeRecord(2), makeRecord(3)];
104
+ const { items, result } = await service.createMany(tableName, records);
105
+ expect(items).toHaveLength(3);
106
+ expect(result.successful).toBe(3);
107
+ expect(result.failed).toBe(0);
108
+ });
109
+
110
+ it("should handle empty array", async () => {
111
+ const { items, result } = await service.createMany(tableName, []);
112
+ expect(items).toHaveLength(0);
113
+ expect(result.successful).toBe(0);
114
+ expect(result.failed).toBe(0);
115
+ });
116
+
117
+ it("should throw error if data is not array", async () => {
118
+ await expect(
119
+ service.createMany(tableName, "not-an-array" as any)
120
+ ).rejects.toThrow(DatabaseAdapterError);
121
+ });
122
+
123
+ it("should insert large batches correctly", async () => {
124
+ const records = Array.from({ length: 50 }, (_, i) => makeRecord(i + 1));
125
+ const { items, result } = await service.createMany(tableName, records);
126
+ expect(result.successful).toBe(50);
127
+ expect(items).toHaveLength(50);
128
+ });
129
+ });
130
+
131
+ // ── selectOne ────────────────────────────────────────────────────────────
132
+
133
+ describe("selectOne", () => {
134
+ it("should select a single record by id", async () => {
135
+ await seedRecords(3);
136
+ const result: any = await service.selectOne(tableName, { id: { operator: "=", value: 2 } });
137
+ expect(result).not.toBeNull();
138
+ expect(result.id).toBe(2);
139
+ });
140
+
141
+ it("should return null when record not found", async () => {
142
+ const result = await service.selectOne(tableName, { id: { operator: "=", value: 9999 } });
143
+ expect(result).toBeNull();
144
+ });
145
+
146
+ it("should support projection", async () => {
147
+ await seedRecords(1);
148
+ const result: any = await service.selectOne(
149
+ tableName,
150
+ { id: { operator: "=", value: 1 } },
151
+ { projection: ["id", "name"] }
152
+ );
153
+ expect(result).not.toBeNull();
154
+ expect(result.id).toBeDefined();
155
+ expect(result.name).toBeDefined();
156
+ expect(result.score).toBeUndefined();
157
+ });
158
+ });
159
+
160
+ // ── selectMany ───────────────────────────────────────────────────────────
161
+
162
+ describe("selectMany", () => {
163
+ it("should select multiple records with condition", async () => {
164
+ await seedRecords(5);
165
+ const result = await service.selectMany(tableName, { status: { operator: "=", value: "active" } });
166
+ expect(result.items.length).toBeGreaterThan(0);
167
+ result.items.forEach((item: any) => {
168
+ expect(item.status).toBe("active");
169
+ });
170
+ });
171
+
172
+ it("should handle empty result set", async () => {
173
+ const result = await service.selectMany(tableName, { id: { operator: "=", value: 9999 } });
174
+ expect(result.items).toHaveLength(0);
175
+ expect(result.total).toBe(0);
176
+ });
177
+
178
+ it("should support limit and offset", async () => {
179
+ await seedRecords(10);
180
+ const result = await service.selectMany(tableName, undefined, { limit: 3, offset: 2 });
181
+ expect(result.items).toHaveLength(3);
182
+ });
183
+
184
+ it("should support ordering", async () => {
185
+ await seedRecords(5);
186
+ const result = await service.selectMany(tableName, undefined, {
187
+ orderBy: [{ field: "id", direction: "DESC" }]
188
+ });
189
+
190
+ if (result.items.length > 1) {
191
+ const ids = result.items.map((item: any) => item.id);
192
+ for (let i = 1; i < ids.length; i++) {
193
+ expect(ids[i]).toBeLessThanOrEqual(ids[i - 1]);
194
+ }
195
+ }
196
+ });
197
+
198
+ it("should support projection", async () => {
199
+ await seedRecords(3);
200
+ const result = await service.selectMany(tableName, undefined, {
201
+ projection: ["id", "name"]
202
+ });
203
+ expect(result.items.length).toBeGreaterThan(0);
204
+ result.items.forEach((item: any) => {
205
+ expect(item.id).toBeDefined();
206
+ expect(item.name).toBeDefined();
207
+ expect(item.score).toBeUndefined();
208
+ });
209
+ });
210
+
211
+ it("should return pagination info", async () => {
212
+ await seedRecords(10);
213
+ const result = await service.selectMany(tableName, undefined, { limit: 3 });
214
+ expect(result.hasMore).toBe(true);
215
+ expect(typeof result.total).toBe("number");
216
+ });
217
+
218
+ it("should support greater than operator", async () => {
219
+ await seedRecords(5);
220
+ const result = await service.selectMany(tableName, {
221
+ score: { operator: ">", value: 30 }
222
+ });
223
+ result.items.forEach((item: any) => {
224
+ expect(item.score).toBeGreaterThan(30);
225
+ });
226
+ });
227
+
228
+ it("should support less than operator", async () => {
229
+ await seedRecords(5);
230
+ const result = await service.selectMany(tableName, {
231
+ score: { operator: "<", value: 40 }
232
+ });
233
+ result.items.forEach((item: any) => {
234
+ expect(item.score).toBeLessThan(40);
235
+ });
236
+ });
237
+ });
238
+
239
+ // ── updateOne ────────────────────────────────────────────────────────────
240
+
241
+ describe("updateOne", () => {
242
+ it("should update a single record", async () => {
243
+ await seedRecords(3);
244
+ const updated: any = await service.updateOne(
245
+ tableName,
246
+ { name: "Updated Name" },
247
+ { id: { operator: "=", value: 2 } }
248
+ );
249
+ expect(updated.name).toBe("Updated Name");
250
+ expect(updated.id).toBe(2);
251
+ });
252
+
253
+ it("should throw error when record not found", async () => {
254
+ await expect(
255
+ service.updateOne(
256
+ tableName,
257
+ { name: "Updated" },
258
+ { id: { operator: "=", value: 9999 } }
259
+ )
260
+ ).rejects.toThrow(DatabaseAdapterError);
261
+ });
262
+
263
+ it("should preserve other fields during update", async () => {
264
+ await seedRecords(1);
265
+ const updated: any = await service.updateOne(
266
+ tableName,
267
+ { name: "Changed" },
268
+ { id: { operator: "=", value: 1 } }
269
+ );
270
+ expect(updated.name).toBe("Changed");
271
+ expect(updated.score).toBe(10);
272
+ expect(updated.status).toBe("active");
273
+ });
274
+ });
275
+
276
+ // ── updateMany ───────────────────────────────────────────────────────────
277
+
278
+ describe("updateMany", () => {
279
+ it("should update multiple records matching condition", async () => {
280
+ await seedRecords(5);
281
+ const { result } = await service.updateMany(
282
+ tableName,
283
+ { status: "inactive" },
284
+ { score: { operator: ">", value: 20 } }
285
+ );
286
+ expect(result.successful).toBeGreaterThan(0);
287
+ expect(result.failed).toBe(0);
288
+ });
289
+
290
+ it("should respect limit option", async () => {
291
+ await seedRecords(5);
292
+ const { result } = await service.updateMany(
293
+ tableName,
294
+ { status: "archived" },
295
+ undefined,
296
+ { limit: 2 }
297
+ );
298
+ expect(result.successful).toBeLessThanOrEqual(2);
299
+ });
300
+
301
+ it("should return empty result when no matches found", async () => {
302
+ const { items, result } = await service.updateMany(
303
+ tableName,
304
+ { name: "Ghost" },
305
+ { id: { operator: "=", value: 9999 } }
306
+ );
307
+ expect(items).toHaveLength(0);
308
+ expect(result.successful).toBe(0);
309
+ });
310
+ });
311
+
312
+ // ── deleteOne ────────────────────────────────────────────────────────────
313
+
314
+ describe("deleteOne", () => {
315
+ it("should delete a single record", async () => {
316
+ await seedRecords(3);
317
+ const { success, deletedCount } = await service.deleteOne(
318
+ tableName,
319
+ { id: { operator: "=", value: 2 } }
320
+ );
321
+ expect(success).toBe(true);
322
+ expect(deletedCount).toBe(1);
323
+
324
+ const check = await service.selectOne(tableName, { id: { operator: "=", value: 2 } });
325
+ expect(check).toBeNull();
326
+ });
327
+
328
+ it("should handle deletion of non-existent record", async () => {
329
+ const { success, deletedCount } = await service.deleteOne(
330
+ tableName,
331
+ { id: { operator: "=", value: 9999 } }
332
+ );
333
+ expect(success).toBe(false);
334
+ expect(deletedCount).toBe(0);
335
+ });
336
+ });
337
+
338
+ // ── deleteMany ───────────────────────────────────────────────────────────
339
+
340
+ describe("deleteMany", () => {
341
+ it("should delete multiple records matching condition", async () => {
342
+ await seedRecords(5);
343
+ const { success, deletedCount } = await service.deleteMany(
344
+ tableName,
345
+ { score: { operator: ">", value: 20 } }
346
+ );
347
+ expect(success).toBe(true);
348
+ expect(deletedCount).toBeGreaterThan(0);
349
+ });
350
+
351
+ it("should respect limit option", async () => {
352
+ await seedRecords(5);
353
+ const { deletedCount } = await service.deleteMany(tableName, undefined, { limit: 2 });
354
+ expect(deletedCount).toBe(2);
355
+ });
356
+
357
+ it("should handle no matches", async () => {
358
+ const { success, deletedCount } = await service.deleteMany(
359
+ tableName,
360
+ { id: { operator: "=", value: 9999 } }
361
+ );
362
+ expect(success).toBe(true);
363
+ expect(deletedCount).toBe(0);
364
+ });
365
+ });
366
+
367
+ // ── Query Operators ──────────────────────────────────────────────────────
368
+
369
+ describe("Query Operators", () => {
370
+ it("should support IN operator", async () => {
371
+ await seedRecords(5);
372
+ const result = await service.selectMany(tableName, {
373
+ id: { operator: "IN", value: [1, 3, 5] }
374
+ });
375
+ expect(result.items).toHaveLength(3);
376
+ const ids = result.items.map((item: any) => item.id).sort();
377
+ expect(ids).toEqual([1, 3, 5]);
378
+ });
379
+
380
+ it("should support NOT_IN operator", async () => {
381
+ await seedRecords(5);
382
+ const result = await service.selectMany(tableName, {
383
+ id: { operator: "NOT_IN", value: [1, 2] }
384
+ });
385
+ expect(result.items).toHaveLength(3);
386
+ result.items.forEach((item: any) => {
387
+ expect([1, 2]).not.toContain(item.id);
388
+ });
389
+ });
390
+
391
+ it("should support LIKE operator", async () => {
392
+ await service.createMany(tableName, [
393
+ { id: 1, name: "Alice" },
394
+ { id: 2, name: "Bob" },
395
+ { id: 3, name: "Albert" }
396
+ ]);
397
+ const result = await service.selectMany(tableName, {
398
+ name: { operator: "LIKE", value: "al" }
399
+ });
400
+ expect(result.items.length).toBeGreaterThanOrEqual(2);
401
+ result.items.forEach((item: any) => {
402
+ expect(item.name.toLowerCase()).toContain("al");
403
+ });
404
+ });
405
+
406
+ it("should support BEGINS_WITH operator", async () => {
407
+ await service.createMany(tableName, [
408
+ { id: 1, name: "Alpha" },
409
+ { id: 2, name: "Beta" },
410
+ { id: 3, name: "Alphabet" }
411
+ ]);
412
+ const result = await service.selectMany(tableName, {
413
+ name: { operator: "BEGINS_WITH", value: "Alpha" }
414
+ });
415
+ expect(result.items.length).toBeGreaterThanOrEqual(2);
416
+ result.items.forEach((item: any) => {
417
+ expect(item.name.toLowerCase()).toMatch(/^alpha/i);
418
+ });
419
+ });
420
+
421
+ it("should support BETWEEN operator", async () => {
422
+ await seedRecords(10);
423
+ const result = await service.selectMany(tableName, {
424
+ score: { operator: "BETWEEN", value: [30, 70] }
425
+ });
426
+ result.items.forEach((item: any) => {
427
+ expect(item.score).toBeGreaterThanOrEqual(30);
428
+ expect(item.score).toBeLessThanOrEqual(70);
429
+ });
430
+ });
431
+
432
+ it("should support != operator", async () => {
433
+ await seedRecords(5);
434
+ const result = await service.selectMany(tableName, {
435
+ id: { operator: "!=", value: 3 }
436
+ });
437
+ result.items.forEach((item: any) => {
438
+ expect(item.id).not.toBe(3);
439
+ });
440
+ });
441
+
442
+ it("should support >= operator", async () => {
443
+ await seedRecords(5);
444
+ const result = await service.selectMany(tableName, {
445
+ score: { operator: ">=", value: 30 }
446
+ });
447
+ result.items.forEach((item: any) => {
448
+ expect(item.score).toBeGreaterThanOrEqual(30);
449
+ });
450
+ });
451
+
452
+ it("should support <= operator", async () => {
453
+ await seedRecords(5);
454
+ const result = await service.selectMany(tableName, {
455
+ score: { operator: "<=", value: 30 }
456
+ });
457
+ result.items.forEach((item: any) => {
458
+ expect(item.score).toBeLessThanOrEqual(30);
459
+ });
460
+ });
461
+ });
462
+
463
+ // ── Error Handling ───────────────────────────────────────────────────────
464
+
465
+ describe("Error Handling", () => {
466
+ it("should throw error on operation without connection", async () => {
467
+ const s = new MongoService();
468
+ s.setConfig({ uri: MONGO_URI, dbName: DB_NAME });
469
+ await expect(
470
+ s.selectOne(tableName, { id: { operator: "=", value: 1 } })
471
+ ).rejects.toThrow(DatabaseAdapterError);
472
+ });
473
+
474
+ it("should throw error on executeRaw", async () => {
475
+ await expect(
476
+ service.executeRaw("any query")
477
+ ).rejects.toThrow(DatabaseAdapterError);
478
+ });
479
+
480
+ it("should throw error on invalid table name", async () => {
481
+ await expect(
482
+ service.selectOne("", { id: { operator: "=", value: 1 } })
483
+ ).rejects.toThrow(DatabaseAdapterError);
484
+ });
485
+ });
486
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2019",
4
+ "module": "CommonJS",
5
+ "strict": true,
6
+ "esModuleInterop": true,
7
+ "forceConsistentCasingInFileNames": true,
8
+ "skipLibCheck": true,
9
+ "moduleResolution": "node",
10
+ "sourceMap": true,
11
+
12
+ "declaration": true,
13
+ "outDir": "dist",
14
+ "emitDecoratorMetadata": true,
15
+ "experimentalDecorators": true,
16
+
17
+ "baseUrl": "./src",
18
+ "paths": {
19
+ }
20
+ },
21
+ "include": ["src/**/*"],
22
+ "exclude": ["node_modules", "dist"]
23
+ }