@oino-ts/nosql 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,483 @@
1
+ /*
2
+ * This Source Code Form is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
+ */
6
+
7
+ import { expect, test } from "bun:test"
8
+
9
+ import { OINONoSqlAzureTable } from "@oino-ts/nosql-azure"
10
+ import { OINONoSqlAwsDynamo } from "@oino-ts/nosql-aws"
11
+ import { OINOQueryFilter, OINOApiRequest, OINOApiResult, OINOConsoleLog, OINOLogLevel, OINOLog, OINOBenchmark, OINOContentType, OINOConfig, type OINODataField, type OINODataRow } from "@oino-ts/common"
12
+
13
+ import { OINONoSql, OINONoSqlApi, OINONoSqlFactory, type OINONoSqlParams } from "./index.js"
14
+
15
+ const OINOCLOUD_TEST_BLOB_AZURE_CONSTR = process.env.OINOCLOUD_TEST_BLOB_AZURE_CONSTR || console.error("OINOCLOUD_TEST_BLOB_AZURE_CONSTR not set") || ""
16
+ const OINOCLOUD_TEST_BLOB_S3_CONSTR = process.env.OINOCLOUD_TEST_BLOB_S3_CONSTR || console.error("OINOCLOUD_TEST_BLOB_S3_CONSTR not set") || ""
17
+
18
+ type OINONoSqlStorageParams = {
19
+ /** Connection params passed to OINONoSqlFactory.createNoSql */
20
+ noSqlParams: OINONoSqlParams
21
+ /** API name exposed via OINONoSqlFactory.createApi */
22
+ apiName: string
23
+ }
24
+
25
+ type OINONoSqlTestParams = {
26
+ name: string
27
+ /** OINOID-formatted primary key of a known existing entry, e.g. "Orders_10248" */
28
+ existingRowId: string
29
+ /** Optional filter for the list-with-filter test; undefined means no filter */
30
+ listFilter: OINOQueryFilter | undefined
31
+ /** OINOID for the scratch insert / update / delete entry */
32
+ testRowId: string
33
+ /** Properties to write on insert */
34
+ insertProperties: Record<string, unknown>
35
+ /** Properties to write on update – must differ from insert in at least one field */
36
+ updateProperties: Record<string, unknown>
37
+ /** A value that only appears in updateProperties (used to verify update was applied) */
38
+ updateVerifyValue: string
39
+ /** A value that only appears in insertProperties (used to verify batch restore) */
40
+ insertVerifyValue: string
41
+ }
42
+
43
+ const NOSQL_STORAGES: OINONoSqlStorageParams[] = [
44
+ {
45
+ noSqlParams: {
46
+ type: "OINONoSqlAzureTable",
47
+ url: "https://oinocloudteststor.table.core.windows.net",
48
+ table: "NorthwindOrders",
49
+ connectionStr: OINOCLOUD_TEST_BLOB_AZURE_CONSTR
50
+ },
51
+ apiName: "azure-northwind-nosql"
52
+ },
53
+ {
54
+ noSqlParams: {
55
+ type: "OINONoSqlAwsDynamo",
56
+ url: "",
57
+ table: "NorthwindOrders",
58
+ connectionStr: OINOCLOUD_TEST_BLOB_S3_CONSTR
59
+ },
60
+ apiName: "aws-northwind-nosql"
61
+ }
62
+ ]
63
+
64
+ const NOSQL_TESTS: OINONoSqlTestParams[] = [
65
+ {
66
+ name: "NOSQL 1",
67
+ existingRowId: "Orders_10248",
68
+ listFilter: undefined,
69
+ testRowId: "OINOTest_nosql1-test",
70
+ insertProperties: { CustomerID: "VINET", Freight: 32.38, ShipCity: "Reims" },
71
+ updateProperties: { CustomerID: "VINET", Freight: 99.99, ShipCity: "Updated City" },
72
+ updateVerifyValue: "Updated City",
73
+ insertVerifyValue: "Reims"
74
+ },
75
+ {
76
+ name: "NOSQL 2",
77
+ existingRowId: "Orders_10248",
78
+ listFilter: OINOQueryFilter.parse("(partitionKey)-eq(Orders)"),
79
+ testRowId: "OINOTest_nosql2-test",
80
+ insertProperties: { CustomerID: "VINET", Freight: 32.38, ShipCity: "Reims" },
81
+ updateProperties: { CustomerID: "VINET", Freight: 99.99, ShipCity: "Updated City" },
82
+ updateVerifyValue: "Updated City",
83
+ insertVerifyValue: "Reims"
84
+ }
85
+ ]
86
+
87
+ /**
88
+ * Snapshot keys cross-checked between adjacent storage implementations.
89
+ */
90
+ const NOSQL_CROSSCHECKS: string[] = [
91
+ "[LIST ALL] list all: LIST JSON 1",
92
+ "[LIST FILTERED] list with filter: LIST FILTERED JSON 1",
93
+ "[HTTP GET] fetch single entry: SINGLE JSON 1",
94
+ "[HTTP GET] fetch missing entry: GET MISSING 1",
95
+ "[BATCH UPDATE] reversed values: GET reversed data 1",
96
+ "[BATCH UPDATE] reversed values: GET restored data 1"
97
+ ]
98
+
99
+ OINOLog.setInstance(new OINOConsoleLog(OINOLogLevel.warning))
100
+ OINOBenchmark.setEnabled(["doApiRequest"])
101
+ OINOBenchmark.reset()
102
+
103
+ OINONoSqlFactory.registerNoSql("OINONoSqlAzureTable", OINONoSqlAzureTable)
104
+ OINONoSqlFactory.registerNoSql("OINONoSqlAwsDynamo", OINONoSqlAwsDynamo)
105
+
106
+ function encodeResult(o: unknown): string {
107
+ return JSON.stringify(o ?? {}, null, 3)
108
+ .replaceAll(/`/g, "'")
109
+ .replaceAll(/(\\[nrt"\\]?)/g, (_match, p1) => encodeURIComponent(p1 as string))
110
+ }
111
+
112
+ /** Same as encodeResult but strips volatile request IDs and timestamps from error messages */
113
+ function encodeResultStable(o: unknown): string {
114
+ return encodeResult(o)
115
+ .replaceAll(/RequestId:[a-z0-9-]+/g, "RequestId:REQUESTID")
116
+ .replaceAll(/Time:[0-9\-TZ:.]+/g, "Time:TIME")
117
+ .replaceAll(/"url":\s*"[^"]*"/g, '"url": "URL"')
118
+ }
119
+
120
+ /**
121
+ * Strip volatile fields (timestamp, etag) from a JSON nosql listing so that
122
+ * snapshot comparisons are stable across runs.
123
+ */
124
+ function stableNoSqlListing(json: string | undefined): string {
125
+ if (!json) return ""
126
+ return json
127
+ .replaceAll(/"timestamp":\s*"[^"]*"/g, '"timestamp": "TIMESTAMP"')
128
+ .replaceAll(/"etag":\s*"(?:[^"\\]|\\.)*"/g, '"etag": "ETAG"')
129
+ }
130
+
131
+ export async function OINOTestNoSql(storageParams: OINONoSqlStorageParams, testParams: OINONoSqlTestParams): Promise<void> {
132
+ const target_name = "[" + testParams.name + "]"
133
+ const target_storage = "[" + storageParams.noSqlParams.type + "]"
134
+
135
+ // ── CONNECTION ────────────────────────────────────────────────────────
136
+ // Connect and validate BEFORE registering any tests so that createApi
137
+ // can rely on _hashKeyAttr / _rangeKeyAttr being set by validate().
138
+
139
+ let target_group = "[CONNECTION]"
140
+
141
+ const wrong_constr_params: OINONoSqlParams = { ...storageParams.noSqlParams, connectionStr: "WRONG_CONNECTION_STRING" }
142
+ const wrong_nosql: OINONoSql = await OINONoSqlFactory.createNoSql(wrong_constr_params, false, false)
143
+ const wrong_connect_res = await wrong_nosql.connect()
144
+ // Azure parses the connection string format and may throw during connect;
145
+ // AWS only discovers bad credentials at request time – either way we expect failure.
146
+ const wrong_validate_res = wrong_connect_res.success ? await wrong_nosql.validate() : wrong_connect_res
147
+ await test(target_name + target_storage + target_group + " connection error", () => {
148
+ expect(wrong_validate_res.success).toBe(false)
149
+ expect(wrong_validate_res.statusText).toMatchSnapshot("CONNECTION ERROR")
150
+ })
151
+
152
+ const nosql: OINONoSql = await OINONoSqlFactory.createNoSql(storageParams.noSqlParams, false, false)
153
+ const connect_res = await nosql.connect()
154
+ const validate_res = connect_res.success ? await nosql.validate() : connect_res
155
+ await test(target_name + target_storage + target_group + " connection success", () => {
156
+ expect(connect_res.success).toBe(true)
157
+ expect(validate_res.success).toBe(true)
158
+ expect(nosql.isConnected).toBe(true)
159
+ expect(nosql.isValidated).toBe(true)
160
+ })
161
+
162
+ if (!validate_res.success) return
163
+
164
+ const api: OINONoSqlApi = await OINONoSqlFactory.createApi(nosql, {
165
+ apiName: storageParams.apiName,
166
+ tableName: storageParams.noSqlParams.table
167
+ })
168
+
169
+ const base_url = new URL("http://localhost/" + storageParams.apiName)
170
+
171
+ // ── LIST ALL ──────────────────────────────────────────────────────────
172
+
173
+ target_group = "[LIST ALL]"
174
+
175
+ const list_all_request = new OINOApiRequest({ url: base_url, method: "GET" })
176
+ await test(target_name + target_storage + target_group + " list all", async () => {
177
+ const result: OINOApiResult = await api.doApiRequest(list_all_request)
178
+ expect(result.success).toBe(true)
179
+ expect(result.data).toBeDefined()
180
+ const json = await result.data!.writeString(OINOContentType.json)
181
+ expect(stableNoSqlListing(json)).toMatchSnapshot("LIST JSON")
182
+ }, 30_000)
183
+
184
+ // ── LIST WITH FILTER ──────────────────────────────────────────────────
185
+
186
+ target_group = "[LIST FILTER]"
187
+
188
+ const list_filter_request = new OINOApiRequest({
189
+ url: base_url,
190
+ method: "GET",
191
+ filter: testParams.listFilter
192
+ })
193
+ await test(target_name + target_storage + target_group + " list with filter", async () => {
194
+ const result: OINOApiResult = await api.doApiRequest(list_filter_request)
195
+ expect(result.success).toBe(true)
196
+ expect(result.data).toBeDefined()
197
+ const json = await result.data!.writeString(OINOContentType.json)
198
+ expect(stableNoSqlListing(json)).toMatchSnapshot("LIST FILTERED JSON")
199
+ }, 30_000)
200
+
201
+ // ── GET SINGLE ────────────────────────────────────────────────────────
202
+
203
+ target_group = "[HTTP GET]"
204
+
205
+ const get_single_request = new OINOApiRequest({
206
+ url: base_url,
207
+ method: "GET",
208
+ rowId: testParams.existingRowId
209
+ })
210
+ await test(target_name + target_storage + target_group + " fetch single entry", async () => {
211
+ const result: OINOApiResult = await api.doApiRequest(get_single_request)
212
+ expect(result.success).toBe(true)
213
+ expect(result.data).toBeDefined()
214
+ const json = await result.data!.writeString(OINOContentType.json)
215
+ expect(stableNoSqlListing(json)).toMatchSnapshot("SINGLE JSON")
216
+ }, 30_000)
217
+
218
+ const get_missing_request = new OINOApiRequest({
219
+ url: base_url,
220
+ method: "GET",
221
+ rowId: "OINOTest_does-not-exist"
222
+ })
223
+ await test(target_name + target_storage + target_group + " fetch missing entry", async () => {
224
+ const result: OINOApiResult = await api.doApiRequest(get_missing_request)
225
+ expect(result.success).toBe(false)
226
+ expect(encodeResultStable(result)).toMatchSnapshot("GET MISSING")
227
+ }, 30_000)
228
+
229
+ // ── INSERT (POST) ─────────────────────────────────────────────────────
230
+
231
+ target_group = "[HTTP POST]"
232
+
233
+ const post_no_id_request = new OINOApiRequest({
234
+ url: base_url,
235
+ method: "POST",
236
+ body: JSON.stringify({ properties: testParams.insertProperties }),
237
+ headers: { "content-type": "application/json" }
238
+ })
239
+ await test(target_name + target_storage + target_group + " insert without id", async () => {
240
+ const result: OINOApiResult = await api.doApiRequest(post_no_id_request)
241
+ expect(result.success).toBe(false)
242
+ expect(encodeResult(result)).toMatchSnapshot("POST NO ID")
243
+ }, 30_000)
244
+
245
+ const post_request = new OINOApiRequest({
246
+ url: base_url,
247
+ method: "POST",
248
+ rowId: testParams.testRowId,
249
+ body: JSON.stringify({ properties: testParams.insertProperties }),
250
+ headers: { "content-type": "application/json" }
251
+ })
252
+ await test(target_name + target_storage + target_group + " insert", async () => {
253
+ const result: OINOApiResult = await api.doApiRequest(post_request)
254
+ expect(result.success).toBe(true)
255
+ expect(encodeResult(result)).toMatchSnapshot("POST")
256
+
257
+ // Verify the entry was actually stored
258
+ const verify_request = new OINOApiRequest({ url: base_url, method: "GET", rowId: testParams.testRowId })
259
+ const verify_result: OINOApiResult = await api.doApiRequest(verify_request)
260
+ expect(verify_result.success).toBe(true)
261
+ expect(verify_result.data).toBeDefined()
262
+ }, 30_000)
263
+
264
+ // ── UPDATE (PUT) ──────────────────────────────────────────────────────
265
+
266
+ target_group = "[HTTP PUT]"
267
+
268
+ const put_no_id_request = new OINOApiRequest({
269
+ url: base_url,
270
+ method: "PUT",
271
+ body: JSON.stringify({ properties: testParams.updateProperties }),
272
+ headers: { "content-type": "application/json" }
273
+ })
274
+ await test(target_name + target_storage + target_group + " update without id", async () => {
275
+ const result: OINOApiResult = await api.doApiRequest(put_no_id_request)
276
+ expect(result.success).toBe(false)
277
+ expect(encodeResult(result)).toMatchSnapshot("PUT NO ID")
278
+ }, 30_000)
279
+
280
+ const put_request = new OINOApiRequest({
281
+ url: base_url,
282
+ method: "PUT",
283
+ rowId: testParams.testRowId,
284
+ body: JSON.stringify({ properties: testParams.updateProperties }),
285
+ headers: { "content-type": "application/json" }
286
+ })
287
+ await test(target_name + target_storage + target_group + " update", async () => {
288
+ const result: OINOApiResult = await api.doApiRequest(put_request)
289
+ expect(result.success).toBe(true)
290
+ expect(encodeResult(result)).toMatchSnapshot("PUT")
291
+
292
+ // Verify updated content is reflected in a subsequent read
293
+ const verify_request = new OINOApiRequest({ url: base_url, method: "GET", rowId: testParams.testRowId })
294
+ const verify_result: OINOApiResult = await api.doApiRequest(verify_request)
295
+ expect(verify_result.success).toBe(true)
296
+ const json = await verify_result.data!.writeString(OINOContentType.json)
297
+ expect(json).toContain(testParams.updateVerifyValue)
298
+ }, 30_000)
299
+
300
+ // ── BATCH UPDATE ──────────────────────────────────────────────────────
301
+ // NoSQL backends reject duplicate keys within a single batch, so use
302
+ // 3 *distinct* row IDs (derived from testRowId with -b1/-b2/-b3 suffixes)
303
+ // that all share the same partition key as testRowId.
304
+
305
+ target_group = "[BATCH UPDATE]"
306
+
307
+ const batch_pk_fields = api.noSqlDatamodel!.filterFields((f: OINODataField) => f.fieldParams.isPrimaryKey)
308
+ const batch_props_idx = api.noSqlDatamodel!.findFieldIndexByName("properties")
309
+
310
+ // Derive 3 distinct IDs that share the same partition key
311
+ const base_parts = OINOConfig.parseOINOId(testParams.testRowId)
312
+ const batch_ids = ["-b1", "-b2", "-b3"].map(suffix =>
313
+ OINOConfig.printOINOId([base_parts[0], base_parts.slice(1).join(OINOConfig.OINO_ID_SEPARATOR) + suffix])
314
+ )
315
+
316
+ const makeBatchRow = (rowId: string, props: Record<string, unknown>): OINODataRow => {
317
+ const pk_values = OINOConfig.parseOINOId(rowId)
318
+ const row: OINODataRow = new Array(api.noSqlDatamodel!.fields.length).fill(null) as OINODataRow
319
+ for (let i = 0; i < batch_pk_fields.length; i++) {
320
+ row[api.noSqlDatamodel!.fields.indexOf(batch_pk_fields[i])] = pk_values[i]
321
+ }
322
+ row[batch_props_idx] = JSON.stringify(props)
323
+ return row
324
+ }
325
+
326
+ await test(target_name + target_storage + target_group + " reversed values", async () => {
327
+ // Write updateProperties to all 3 distinct batch entries
328
+ const batch_rows_update = batch_ids.map(bid => makeBatchRow(bid, testParams.updateProperties))
329
+ const batch_update_result = await api.doBatchApiRequest(
330
+ new OINOApiRequest({ url: base_url, method: "PUT", rowData: batch_rows_update })
331
+ )
332
+ expect(batch_update_result.success).toBe(true)
333
+ expect(encodeResult(batch_update_result)).toMatchSnapshot("PUT reversed data")
334
+
335
+ // Verify the last entry has the update value
336
+ const batch_get_request = new OINOApiRequest({ url: base_url, method: "GET", rowId: batch_ids[2] })
337
+ const reversed_result: OINOApiResult = await api.doApiRequest(batch_get_request)
338
+ expect(reversed_result.success).toBe(true)
339
+ const reversed_json = await reversed_result.data!.writeString(OINOContentType.json)
340
+ expect(reversed_json).toContain(testParams.updateVerifyValue)
341
+ expect(stableNoSqlListing(reversed_json)).toMatchSnapshot("GET reversed data")
342
+
343
+ // Restore all 3 entries to insertProperties
344
+ const batch_rows_restore = batch_ids.map(bid => makeBatchRow(bid, testParams.insertProperties))
345
+ const batch_restore_result = await api.doBatchApiRequest(
346
+ new OINOApiRequest({ url: base_url, method: "PUT", rowData: batch_rows_restore })
347
+ )
348
+ expect(batch_restore_result.success).toBe(true)
349
+ expect(encodeResult(batch_restore_result)).toMatchSnapshot("PUT restored data")
350
+
351
+ const restored_result: OINOApiResult = await api.doApiRequest(batch_get_request)
352
+ expect(restored_result.success).toBe(true)
353
+ const restored_json = await restored_result.data!.writeString(OINOContentType.json)
354
+ expect(restored_json).toContain(testParams.insertVerifyValue)
355
+ expect(stableNoSqlListing(restored_json)).toMatchSnapshot("GET restored data")
356
+
357
+ // Clean up batch entries
358
+ for (const bid of batch_ids) {
359
+ await api.doApiRequest(new OINOApiRequest({ url: base_url, method: "DELETE", rowId: bid }))
360
+ }
361
+ }, 60_000)
362
+
363
+ // ── DELETE ────────────────────────────────────────────────────────────
364
+
365
+ target_group = "[HTTP DELETE]"
366
+
367
+ const delete_no_id_request = new OINOApiRequest({ url: base_url, method: "DELETE" })
368
+ await test(target_name + target_storage + target_group + " delete without id", async () => {
369
+ const result: OINOApiResult = await api.doApiRequest(delete_no_id_request)
370
+ expect(result.success).toBe(false)
371
+ expect(encodeResult(result)).toMatchSnapshot("DELETE NO ID")
372
+ }, 30_000)
373
+
374
+ const delete_request = new OINOApiRequest({
375
+ url: base_url,
376
+ method: "DELETE",
377
+ rowId: testParams.testRowId
378
+ })
379
+ await test(target_name + target_storage + target_group + " delete", async () => {
380
+ const result: OINOApiResult = await api.doApiRequest(delete_request)
381
+ expect(result.success).toBe(true)
382
+ expect(encodeResult(result)).toMatchSnapshot("DELETE")
383
+
384
+ // Verify the entry is gone
385
+ const verify_request = new OINOApiRequest({ url: base_url, method: "GET", rowId: testParams.testRowId })
386
+ const verify_result: OINOApiResult = await api.doApiRequest(verify_request)
387
+ expect(verify_result.success).toBe(false)
388
+ }, 30_000)
389
+ }
390
+
391
+ for (const storage of NOSQL_STORAGES) {
392
+ for (const nosql_test of NOSQL_TESTS) {
393
+ await OINOTestNoSql(storage, nosql_test)
394
+ }
395
+ }
396
+
397
+ // ── CROSS-CHECK snapshots between adjacent storages ───────────────────────────
398
+
399
+ /** Parse the top-level JSON and also parse any `properties` fields that are stored as JSON strings. */
400
+ function parseSnapshotValue(val: string | undefined): unknown {
401
+ if (val === undefined) return undefined
402
+ // Bun snapshot files store string values as `"content"` inside template literals where
403
+ // the outer double-quotes are Bun's string delimiter but inner content is NOT JSON-escaped
404
+ // (literal newlines and unescaped double-quotes are kept as-is). Stripping the outer
405
+ // `"..."` wrapper gives the raw value that can then be JSON-parsed normally.
406
+ const trimmed = val.trim()
407
+ const inner = trimmed.startsWith('"') && trimmed.endsWith('"') ? trimmed.slice(1, -1) : trimmed
408
+ let parsed: any
409
+ try {
410
+ parsed = JSON.parse(inner, (key, value) => {
411
+ // Bun snapshot files may contain nested JSON strings (e.g. the "properties" field of a NoSQL entry) that also need parsing; these are not automatically parsed by JSON.parse and require a second pass.
412
+ if (key == "properties" && typeof value === "string") {
413
+ return JSON.parse(value)
414
+ } else {
415
+ return value
416
+ }
417
+ })
418
+ // console.debug("Parsed snapshot value:",parsed, typeof (parsed["properties"]))
419
+ } catch (e) {
420
+ console.warn("Failed to parse snapshot value as JSON, keeping as string:", e, inner)
421
+ return inner
422
+ }
423
+ return parsed
424
+ }
425
+
426
+ /**
427
+ * Recursively compare two values ignoring property order.
428
+ * Returns a human-readable description of the first difference found, or null if equal.
429
+ */
430
+ function deepDiff(a: unknown, b: unknown, path = ""): string[] {
431
+ const label = path || "<root>"
432
+ if (a === b) return []
433
+ if (a === null || b === null) return [`${label}: ${JSON.stringify(a)} !== ${JSON.stringify(b)}`]
434
+ if (typeof a !== typeof b) return [`${label}: type ${typeof a} !== ${typeof b}`]
435
+ if (typeof a !== "object") return [`${label}: ${JSON.stringify(a)} !== ${JSON.stringify(b)}`]
436
+ if (Array.isArray(a) !== Array.isArray(b)) return [`${label}: one is array, other is object`]
437
+ if (Array.isArray(a) && Array.isArray(b)) {
438
+ const diffs: string[] = []
439
+ if (a.length !== b.length) diffs.push(`${label}[]: length ${a.length} !== ${b.length}`)
440
+ for (let i = 0; i < Math.min(a.length, b.length); i++) {
441
+ diffs.push(...deepDiff(a[i], b[i], `${label}[${i}]`))
442
+ }
443
+ return diffs
444
+ }
445
+ const aObj = a as Record<string, unknown>
446
+ const bObj = b as Record<string, unknown>
447
+ const diffs: string[] = []
448
+ for (const key of Object.keys(aObj).sort()) {
449
+ if (!(key in bObj)) diffs.push(`${label}.${key}: present in first but missing in second`)
450
+ else diffs.push(...deepDiff(aObj[key], bObj[key], `${label}.${key}`))
451
+ }
452
+ for (const key of Object.keys(bObj)) {
453
+ if (!(key in aObj)) diffs.push(`${label}.${key}: missing in first but present in second`)
454
+ }
455
+ return diffs
456
+ }
457
+
458
+ const snapshot_file = Bun.file("./node_modules/@oino-ts/nosql/src/__snapshots__/OINONoSqlApi.test.ts.snap")
459
+ const snap_exists = await snapshot_file.exists()
460
+ if (snap_exists) {
461
+ await Bun.write("./node_modules/@oino-ts/nosql/src/__snapshots__/OINONoSqlApi.test.ts.snap.js", snapshot_file) // copy snapshots as .js so require works (note! if run with --update-snapshots, it's still the old file)
462
+ }
463
+ const snapshots = snap_exists ? require("./__snapshots__/OINONoSqlApi.test.ts.snap.js") : {}
464
+
465
+ for (let i = 0; i < NOSQL_STORAGES.length - 1; i++) {
466
+ const storage1 = NOSQL_STORAGES[i]
467
+ const storage2 = NOSQL_STORAGES[i + 1]
468
+ for (const nosql_test of NOSQL_TESTS) {
469
+ for (const crosscheck of NOSQL_CROSSCHECKS) {
470
+ test(
471
+ "cross check {" + storage1.noSqlParams.type + "} and {" + storage2.noSqlParams.type + "} test {" + nosql_test.name + "} snapshots on {" + crosscheck + "}",
472
+ () => {
473
+ const key1 = "[" + nosql_test.name + "][" + storage1.noSqlParams.type + "]" + crosscheck
474
+ const key2 = "[" + nosql_test.name + "][" + storage2.noSqlParams.type + "]" + crosscheck
475
+ const parsed1 = parseSnapshotValue(snapshots[key1] as string | undefined)
476
+ const parsed2 = parseSnapshotValue(snapshots[key2] as string | undefined)
477
+ const diffs = deepDiff(parsed1, parsed2)
478
+ expect(diffs).toEqual([])
479
+ }
480
+ )
481
+ }
482
+ }
483
+ }