@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.
- package/dist/cjs/OINONoSql.js +157 -0
- package/dist/cjs/OINONoSqlApi.js +378 -0
- package/dist/cjs/OINONoSqlConstants.js +7 -0
- package/dist/cjs/OINONoSqlDataModel.js +65 -0
- package/dist/cjs/OINONoSqlFactory.js +71 -0
- package/dist/cjs/index.js +11 -0
- package/dist/esm/OINONoSql.js +153 -0
- package/dist/esm/OINONoSqlApi.js +374 -0
- package/dist/esm/OINONoSqlConstants.js +6 -0
- package/dist/esm/OINONoSqlDataModel.js +61 -0
- package/dist/esm/OINONoSqlFactory.js +67 -0
- package/dist/esm/index.js +4 -0
- package/dist/types/OINONoSql.d.ts +81 -0
- package/dist/types/OINONoSqlApi.d.ts +67 -0
- package/dist/types/OINONoSqlConstants.d.ts +34 -0
- package/dist/types/OINONoSqlDataModel.d.ts +29 -0
- package/dist/types/OINONoSqlFactory.d.ts +40 -0
- package/dist/types/index.d.ts +5 -0
- package/package.json +37 -0
- package/src/OINONoSql.ts +202 -0
- package/src/OINONoSqlApi.test.ts +483 -0
- package/src/OINONoSqlApi.ts +414 -0
- package/src/OINONoSqlConstants.ts +43 -0
- package/src/OINONoSqlDataModel.ts +65 -0
- package/src/OINONoSqlFactory.ts +79 -0
- package/src/index.ts +5 -0
|
@@ -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
|
+
}
|