@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,414 @@
|
|
|
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 {
|
|
8
|
+
OINOApi,
|
|
9
|
+
OINOApiParams,
|
|
10
|
+
OINOApiRequest,
|
|
11
|
+
OINOApiResult,
|
|
12
|
+
OINOModelSet,
|
|
13
|
+
OINOContentType,
|
|
14
|
+
OINOQueryParams,
|
|
15
|
+
OINOHttpRequest,
|
|
16
|
+
type OINOApiData,
|
|
17
|
+
type OINODataField,
|
|
18
|
+
type OINODataRow,
|
|
19
|
+
OINOLog,
|
|
20
|
+
OINOConfig,
|
|
21
|
+
OINOParser,
|
|
22
|
+
OINOBenchmark,
|
|
23
|
+
OINO_ERROR_PREFIX
|
|
24
|
+
} from "@oino-ts/common"
|
|
25
|
+
import { OINONoSql } from "./OINONoSql.js"
|
|
26
|
+
import { OINONoSqlDataModel } from "./OINONoSqlDataModel.js"
|
|
27
|
+
import { OINONoSqlEntry } from "./OINONoSqlConstants.js"
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* REST API for NoSQL table storage.
|
|
31
|
+
*
|
|
32
|
+
* Supports the following HTTP methods:
|
|
33
|
+
* - **GET without id** – lists all entities and returns metadata as JSON.
|
|
34
|
+
* - **GET with id** – returns a single entity.
|
|
35
|
+
* - **POST / PUT with id** – upserts an entity; body must be a JSON object
|
|
36
|
+
* with a `properties` key containing the custom entity properties.
|
|
37
|
+
* - **DELETE with id** – deletes the named entity.
|
|
38
|
+
*
|
|
39
|
+
* The URL row ID format uses `OINOConfig.OINO_ID_SEPARATOR` to join the
|
|
40
|
+
* primary key field values, matching the number and order of primary key
|
|
41
|
+
* fields in the data model (same `_OINOID_` convention as `OINODbApi`).
|
|
42
|
+
*/
|
|
43
|
+
export class OINONoSqlApi extends OINOApi {
|
|
44
|
+
|
|
45
|
+
/** NoSQL storage backend */
|
|
46
|
+
readonly noSql: OINONoSql
|
|
47
|
+
|
|
48
|
+
/** NoSQL-specific data model (populated by `initializeDatamodel`) */
|
|
49
|
+
noSqlDatamodel: OINONoSqlDataModel | null = null
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Constructor.
|
|
53
|
+
*
|
|
54
|
+
* NOTE: `initializeDatamodel` (or `OINONoSqlFactory.createApi`) must be
|
|
55
|
+
* called before the first request is dispatched.
|
|
56
|
+
*
|
|
57
|
+
* @param noSql nosql storage backend
|
|
58
|
+
* @param params API parameters
|
|
59
|
+
*/
|
|
60
|
+
constructor(noSql: OINONoSql, params: OINOApiParams) {
|
|
61
|
+
if (params.hashidKey) {
|
|
62
|
+
throw new Error(OINO_ERROR_PREFIX + ": hashid is not supported by OINONoSqlApi (primary keys are strings, not numeric IDs)")
|
|
63
|
+
}
|
|
64
|
+
if (params.failOnUpdateOnAutoinc) {
|
|
65
|
+
throw new Error(OINO_ERROR_PREFIX + ": failOnUpdateOnAutoinc is not supported by OINONoSqlApi (no autoinc fields in NoSQL)")
|
|
66
|
+
}
|
|
67
|
+
if (params.returnInsertedIds) {
|
|
68
|
+
throw new Error(OINO_ERROR_PREFIX + ": returnInsertedIds is not supported by OINONoSqlApi")
|
|
69
|
+
}
|
|
70
|
+
super(noSql, params)
|
|
71
|
+
this.noSql = noSql
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Attach the static nosql data model and mark the API as initialised.
|
|
76
|
+
*
|
|
77
|
+
* @param datamodel `OINONoSqlDataModel` instance for this API
|
|
78
|
+
*/
|
|
79
|
+
initializeDatamodel(datamodel: OINONoSqlDataModel): void {
|
|
80
|
+
this.noSqlDatamodel = datamodel
|
|
81
|
+
this.datamodel = datamodel
|
|
82
|
+
this.initialized = true
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Parse a `_OINOID_`-formatted row ID into an ordered array of decoded
|
|
87
|
+
* primary key values using `OINOConfig.parseOINOId`. Returns `null` when
|
|
88
|
+
* the number of parts does not match the data model's primary key count.
|
|
89
|
+
*
|
|
90
|
+
* @param rowId `_OINOID_`-formatted row ID
|
|
91
|
+
*/
|
|
92
|
+
private _parseRowId(rowId: string): string[] | null {
|
|
93
|
+
if (!this.noSqlDatamodel) return null
|
|
94
|
+
const pk_count = this.noSqlDatamodel.filterFields((f: OINODataField) => f.fieldParams.isPrimaryKey).length
|
|
95
|
+
const parts = OINOConfig.parseOINOId(rowId)
|
|
96
|
+
if (parts.length !== pk_count) return null
|
|
97
|
+
return parts
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Private helpers ───────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Validate a data row against API parameters. Currently checks whether
|
|
104
|
+
* primary key fields are present when `requirePrimaryKey` is `true`.
|
|
105
|
+
*
|
|
106
|
+
* `requirePrimaryKey` is derived at the call-site from:
|
|
107
|
+
* - `this.params.failOnInsertWithoutKey` when explicitly set, or
|
|
108
|
+
* - `!this.noSql.supportsAutoKey` as the implementation-specific default.
|
|
109
|
+
*/
|
|
110
|
+
private _validateRow(result: OINOApiResult, row: OINODataRow, requirePrimaryKey: boolean): void {
|
|
111
|
+
if (!requirePrimaryKey) return
|
|
112
|
+
const pk_fields = this.noSqlDatamodel!.filterFields((f: OINODataField) => f.fieldParams.isPrimaryKey)
|
|
113
|
+
for (let i = 0; i < pk_fields.length; i++) {
|
|
114
|
+
const field_idx = this.noSqlDatamodel!.fields.indexOf(pk_fields[i])
|
|
115
|
+
const val = row[field_idx]
|
|
116
|
+
if (val === undefined || val === null || String(val) === "") {
|
|
117
|
+
result.setError(405, `Primary key '${pk_fields[i].name}' is missing from the data!`, "_validateRow")
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private _parseData(result: OINOApiResult, request: OINOApiRequest): OINODataRow[] {
|
|
124
|
+
let rows: OINODataRow[] = []
|
|
125
|
+
const data = request.rowData ?? request.body
|
|
126
|
+
try {
|
|
127
|
+
if (Array.isArray(data)) {
|
|
128
|
+
rows = data as OINODataRow[]
|
|
129
|
+
} else if (data != null) {
|
|
130
|
+
rows = OINOParser.createRows(this.datamodel!, data, request.requestType, request.multipartBoundary)
|
|
131
|
+
}
|
|
132
|
+
} catch (e: any) {
|
|
133
|
+
result.setError(400, "Invalid data: " + e.message, "_parseData")
|
|
134
|
+
}
|
|
135
|
+
return rows
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private _rowToEntry(row: OINODataRow, pkOverride?: string[]): OINONoSqlEntry {
|
|
139
|
+
const pk_fields = this.noSqlDatamodel!.fields.filter(f => f.fieldParams.isPrimaryKey)
|
|
140
|
+
const primary_key = pkOverride ?? pk_fields.map(f => {
|
|
141
|
+
const idx = this.noSqlDatamodel!.fields.indexOf(f)
|
|
142
|
+
return String(row[idx] ?? "")
|
|
143
|
+
})
|
|
144
|
+
const properties_idx = this.noSqlDatamodel!.fields.findIndex(f => f.name === "properties")
|
|
145
|
+
const raw = properties_idx >= 0 ? row[properties_idx] : undefined
|
|
146
|
+
let properties: Record<string, unknown> = {}
|
|
147
|
+
if (typeof raw === "string") {
|
|
148
|
+
properties = JSON.parse(raw) as Record<string, unknown>
|
|
149
|
+
} else if ((raw != null) && (typeof raw === "object") && !Array.isArray(raw) && !(raw instanceof Date) && !(raw instanceof Uint8Array)) {
|
|
150
|
+
properties = raw as Record<string, unknown>
|
|
151
|
+
}
|
|
152
|
+
return { primaryKey: primary_key, timestamp: new Date(), etag: "", properties }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── Private HTTP method handlers ──────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
private async _doGet(result: OINOApiResult, pkValues: string[] | null, request: OINOApiRequest): Promise<void> {
|
|
158
|
+
if (!pkValues) {
|
|
159
|
+
try {
|
|
160
|
+
const entries = await this.noSql.listEntries(request.queryParams?.filter)
|
|
161
|
+
const dataset = this.noSqlDatamodel!.entriesToDataset(entries)
|
|
162
|
+
result.data = new OINOModelSet(this.datamodel!, dataset, request.queryParams)
|
|
163
|
+
} catch (e: any) {
|
|
164
|
+
result.setError(500, "Error listing nosql entries: " + e.message, "DoGet")
|
|
165
|
+
OINOLog.exception("@oino-ts/nosql", "OINONoSqlApi", "_doGet",
|
|
166
|
+
"exception in list request", { message: e.message, stack: e.stack })
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
try {
|
|
170
|
+
const entry = await this.noSql.getEntry(pkValues)
|
|
171
|
+
if (entry === null) {
|
|
172
|
+
result.setError(404, `Entry '${pkValues.join("/")}' not found`, "DoGet")
|
|
173
|
+
} else {
|
|
174
|
+
const dataset = this.noSqlDatamodel!.entriesToDataset([entry])
|
|
175
|
+
result.data = new OINOModelSet(this.datamodel!, dataset, request.queryParams)
|
|
176
|
+
}
|
|
177
|
+
} catch (e: any) {
|
|
178
|
+
result.setError(500, "Error fetching nosql entry: " + e.message, "DoGet")
|
|
179
|
+
OINOLog.exception("@oino-ts/nosql", "OINONoSqlApi", "_doGet",
|
|
180
|
+
"exception in get request", { message: e.message, stack: e.stack })
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private async _doPut(result: OINOApiResult, pkValues: string[], row: OINODataRow): Promise<void> {
|
|
186
|
+
try {
|
|
187
|
+
await this.noSql.upsertEntry(this._rowToEntry(row, pkValues))
|
|
188
|
+
} catch (e: any) {
|
|
189
|
+
result.setError(500, "Error upserting nosql entry: " + e.message, "DoPut")
|
|
190
|
+
OINOLog.exception("@oino-ts/nosql", "OINONoSqlApi", "_doPut",
|
|
191
|
+
"exception in put request", { message: e.message, stack: e.stack })
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private async _doPost(result: OINOApiResult, rows: OINODataRow[], pkOverride?: string[]): Promise<void> {
|
|
196
|
+
// Validate all rows first and collect valid entries
|
|
197
|
+
const entries: OINONoSqlEntry[] = []
|
|
198
|
+
const require_pk = !pkOverride && (this.params.failOnInsertWithoutKey ?? !this.noSql.supportsAutoKey)
|
|
199
|
+
for (const row of rows) {
|
|
200
|
+
if (require_pk) {
|
|
201
|
+
this._validateRow(result, row, true)
|
|
202
|
+
if (!result.success) {
|
|
203
|
+
if (this.params.failOnAnyInvalidRows === false) {
|
|
204
|
+
result.setOk()
|
|
205
|
+
continue
|
|
206
|
+
}
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
entries.push(this._rowToEntry(row, pkOverride))
|
|
211
|
+
}
|
|
212
|
+
if (entries.length === 0 && result.success) {
|
|
213
|
+
result.setError(405, "No valid rows for POST!", "DoPost")
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
// Single batch call — implementations use native bulk APIs where possible
|
|
217
|
+
try {
|
|
218
|
+
await this.noSql.upsertEntries(entries)
|
|
219
|
+
} catch (e: any) {
|
|
220
|
+
result.setError(500, "Error upserting nosql entries: " + e.message, "DoPost")
|
|
221
|
+
OINOLog.exception("@oino-ts/nosql", "OINONoSqlApi", "_doPost",
|
|
222
|
+
"exception in post request", { message: e.message, stack: e.stack })
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private async _doDelete(result: OINOApiResult, pkValues: string[]): Promise<void> {
|
|
227
|
+
try {
|
|
228
|
+
await this.noSql.deleteEntry(pkValues)
|
|
229
|
+
} catch (e: any) {
|
|
230
|
+
result.setError(500, "Error deleting nosql entry: " + e.message, "DoDelete")
|
|
231
|
+
OINOLog.exception("@oino-ts/nosql", "OINONoSqlApi", "_doDelete",
|
|
232
|
+
"exception in delete request", { message: e.message, stack: e.stack })
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── OINOApi abstract implementations ─────────────────────────────────
|
|
237
|
+
|
|
238
|
+
async doApiRequest(request: OINOApiRequest): Promise<OINOApiResult> {
|
|
239
|
+
if (!this.initialized) {
|
|
240
|
+
throw new Error(OINO_ERROR_PREFIX + ": OINONoSqlApi is not initialized yet!")
|
|
241
|
+
}
|
|
242
|
+
OINOLog.debug("@oino-ts/nosql", "OINONoSqlApi", "doApiRequest", "Request",
|
|
243
|
+
{ method: request.method, id: request.rowId })
|
|
244
|
+
|
|
245
|
+
const result = new OINOApiResult(request)
|
|
246
|
+
let rows: OINODataRow[] = []
|
|
247
|
+
|
|
248
|
+
if (request.method === "PUT" || request.method === "POST") {
|
|
249
|
+
rows = this._parseData(result, request)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (request.method === "GET") {
|
|
253
|
+
if (request.rowId) {
|
|
254
|
+
const pk_values = this._parseRowId(request.rowId)
|
|
255
|
+
if (!pk_values) {
|
|
256
|
+
const pk_count = this.noSqlDatamodel!.filterFields((f: OINODataField) => f.fieldParams.isPrimaryKey).length
|
|
257
|
+
result.setError(400, `Invalid row ID; expected ${pk_count} key part(s) separated by '${OINOConfig.OINO_ID_SEPARATOR}'`, "DoRequest")
|
|
258
|
+
} else {
|
|
259
|
+
await this._doGet(result, pk_values, request)
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
await this._doGet(result, null, request)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
} else if (request.method === "PUT") {
|
|
266
|
+
if (!request.rowId) {
|
|
267
|
+
result.setError(400, "HTTP PUT method requires a URL ID!", "DoRequest")
|
|
268
|
+
} else if (rows.length !== 1) {
|
|
269
|
+
result.setError(400, "HTTP PUT method requires exactly one row in the body data!", "DoRequest")
|
|
270
|
+
} else {
|
|
271
|
+
const pk_values = this._parseRowId(request.rowId)
|
|
272
|
+
if (!pk_values) {
|
|
273
|
+
const pk_count = this.noSqlDatamodel!.filterFields((f: OINODataField) => f.fieldParams.isPrimaryKey).length
|
|
274
|
+
result.setError(400, `Invalid row ID; expected ${pk_count} key part(s) separated by '${OINOConfig.OINO_ID_SEPARATOR}'`, "DoRequest")
|
|
275
|
+
} else {
|
|
276
|
+
await this._doPut(result, pk_values, rows[0])
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
} else if (request.method === "POST") {
|
|
281
|
+
if (rows.length === 0) {
|
|
282
|
+
result.setError(400, "HTTP POST method requires at least one row in the body data!", "DoRequest")
|
|
283
|
+
} else {
|
|
284
|
+
let pk_override: string[] | undefined
|
|
285
|
+
if (request.rowId) {
|
|
286
|
+
if (rows.length !== 1) {
|
|
287
|
+
result.setError(400, "HTTP POST with a URL ID requires exactly one row in the body data!", "DoRequest")
|
|
288
|
+
} else {
|
|
289
|
+
const pk_values = this._parseRowId(request.rowId)
|
|
290
|
+
if (!pk_values) {
|
|
291
|
+
const pk_count = this.noSqlDatamodel!.filterFields((f: OINODataField) => f.fieldParams.isPrimaryKey).length
|
|
292
|
+
result.setError(400, `Invalid row ID; expected ${pk_count} key part(s) separated by '${OINOConfig.OINO_ID_SEPARATOR}'`, "DoRequest")
|
|
293
|
+
} else {
|
|
294
|
+
pk_override = pk_values
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (result.success) {
|
|
299
|
+
await this._doPost(result, rows, pk_override)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
} else if (request.method === "DELETE") {
|
|
304
|
+
if (!request.rowId) {
|
|
305
|
+
result.setError(400, "HTTP DELETE method requires a URL ID!", "DoRequest")
|
|
306
|
+
} else {
|
|
307
|
+
const pk_values = this._parseRowId(request.rowId)
|
|
308
|
+
if (!pk_values) {
|
|
309
|
+
const pk_count = this.noSqlDatamodel!.filterFields((f: OINODataField) => f.fieldParams.isPrimaryKey).length
|
|
310
|
+
result.setError(400, `Invalid row ID; expected ${pk_count} key part(s) separated by '${OINOConfig.OINO_ID_SEPARATOR}'`, "DoRequest")
|
|
311
|
+
} else {
|
|
312
|
+
await this._doDelete(result, pk_values)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
} else {
|
|
317
|
+
result.setError(405, "Unsupported HTTP method '" + request.method + "' for OINONoSqlApi", "DoRequest")
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return result
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async doBatchUpdate(method: string, rowId: string, rowData: OINOApiData, queryParams?: OINOQueryParams): Promise<OINOApiResult> {
|
|
324
|
+
return this.doBatchApiRequest(new OINOApiRequest({ method, rowId, rowData, queryParams }))
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async doBatchApiRequest(request: OINOApiRequest): Promise<OINOApiResult> {
|
|
328
|
+
if (!this.initialized) {
|
|
329
|
+
throw new Error(OINO_ERROR_PREFIX + ": OINONoSqlApi is not initialized yet!")
|
|
330
|
+
}
|
|
331
|
+
OINOLog.debug("@oino-ts/nosql", "OINONoSqlApi", "doBatchApiRequest", "Request",
|
|
332
|
+
{ method: request.method })
|
|
333
|
+
const result = new OINOApiResult(request)
|
|
334
|
+
if (request.method !== "PUT" && request.method !== "DELETE") {
|
|
335
|
+
result.setError(405, "Batch API only supports PUT and DELETE methods!", "DoBatchApiRequest")
|
|
336
|
+
return result
|
|
337
|
+
}
|
|
338
|
+
OINOBenchmark.startMetric("OINONoSqlApi", "doBatchApiRequest." + request.method)
|
|
339
|
+
const rows = this._parseData(result, request)
|
|
340
|
+
if (!result.success) {
|
|
341
|
+
OINOBenchmark.endMetric("OINONoSqlApi", "doBatchApiRequest." + request.method, false)
|
|
342
|
+
return result
|
|
343
|
+
}
|
|
344
|
+
if (request.method === "PUT") {
|
|
345
|
+
const entries: OINONoSqlEntry[] = []
|
|
346
|
+
const require_pk = this.params.failOnInsertWithoutKey ?? !this.noSql.supportsAutoKey
|
|
347
|
+
for (const row of rows) {
|
|
348
|
+
this._validateRow(result, row, require_pk)
|
|
349
|
+
if (!result.success) {
|
|
350
|
+
OINOBenchmark.endMetric("OINONoSqlApi", "doBatchApiRequest." + request.method, false)
|
|
351
|
+
return result
|
|
352
|
+
}
|
|
353
|
+
entries.push(this._rowToEntry(row))
|
|
354
|
+
}
|
|
355
|
+
if (entries.length === 0) {
|
|
356
|
+
result.setError(405, "No valid rows for batch PUT!", "DoBatchApiRequest")
|
|
357
|
+
OINOBenchmark.endMetric("OINONoSqlApi", "doBatchApiRequest." + request.method, false)
|
|
358
|
+
return result
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
await this.noSql.upsertEntries(entries)
|
|
362
|
+
} catch (e: any) {
|
|
363
|
+
result.setError(500, "Error batch upserting nosql entries: " + e.message, "DoBatchApiRequest")
|
|
364
|
+
OINOLog.exception("@oino-ts/nosql", "OINONoSqlApi", "doBatchApiRequest",
|
|
365
|
+
"exception in batch put request", { message: e.message, stack: e.stack })
|
|
366
|
+
}
|
|
367
|
+
} else {
|
|
368
|
+
const pk_fields = this.noSqlDatamodel!.fields.filter(f => f.fieldParams.isPrimaryKey)
|
|
369
|
+
for (const row of rows) {
|
|
370
|
+
const pk_values = pk_fields.map(f => {
|
|
371
|
+
const idx = this.noSqlDatamodel!.fields.indexOf(f)
|
|
372
|
+
return String(row[idx] ?? "")
|
|
373
|
+
})
|
|
374
|
+
try {
|
|
375
|
+
await this.noSql.deleteEntry(pk_values)
|
|
376
|
+
} catch (e: any) {
|
|
377
|
+
result.setError(500, "Error batch deleting nosql entry: " + e.message, "DoBatchApiRequest")
|
|
378
|
+
OINOLog.exception("@oino-ts/nosql", "OINONoSqlApi", "doBatchApiRequest",
|
|
379
|
+
"exception in batch delete request", { message: e.message, stack: e.stack })
|
|
380
|
+
OINOBenchmark.endMetric("OINONoSqlApi", "doBatchApiRequest." + request.method, false)
|
|
381
|
+
return result
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
OINOBenchmark.endMetric("OINONoSqlApi", "doBatchApiRequest." + request.method, result.success)
|
|
386
|
+
return result
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async doHttpRequest(
|
|
390
|
+
request: OINOHttpRequest,
|
|
391
|
+
rowId: string,
|
|
392
|
+
rowData: OINOApiData,
|
|
393
|
+
queryParams: OINOQueryParams
|
|
394
|
+
): Promise<OINOApiResult> {
|
|
395
|
+
const api_request = OINOApiRequest.fromHttpRequest(request, rowId, rowData, queryParams)
|
|
396
|
+
return this.doApiRequest(api_request)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async doRequest(
|
|
400
|
+
method: string,
|
|
401
|
+
rowId: string,
|
|
402
|
+
rowData: OINOApiData,
|
|
403
|
+
queryParams: OINOQueryParams,
|
|
404
|
+
contentType: OINOContentType = OINOContentType.json
|
|
405
|
+
): Promise<OINOApiResult> {
|
|
406
|
+
return this.doApiRequest(new OINOApiRequest({
|
|
407
|
+
method,
|
|
408
|
+
rowId,
|
|
409
|
+
rowData,
|
|
410
|
+
queryParams,
|
|
411
|
+
requestType: contentType
|
|
412
|
+
}))
|
|
413
|
+
}
|
|
414
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
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 type { OINONoSql } from "./OINONoSql.js"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* NoSQL class (constructor) type
|
|
11
|
+
* @param params nosql parameters
|
|
12
|
+
*/
|
|
13
|
+
export type OINONoSqlConstructor = new (params: OINONoSqlParams) => OINONoSql
|
|
14
|
+
|
|
15
|
+
/** NoSQL storage connection parameters */
|
|
16
|
+
export type OINONoSqlParams = {
|
|
17
|
+
/** Name of the nosql class (e.g. OINONoSqlAzureTable) */
|
|
18
|
+
type: string
|
|
19
|
+
/** Service endpoint URL */
|
|
20
|
+
url: string
|
|
21
|
+
/** Table name */
|
|
22
|
+
table: string
|
|
23
|
+
/** Provider-specific connection string (e.g. Azure Storage connection string) */
|
|
24
|
+
connectionStr?: string
|
|
25
|
+
/**
|
|
26
|
+
* Optional static partition key. When set, all read/write operations are
|
|
27
|
+
* automatically scoped to this partition key, allowing multiple logical
|
|
28
|
+
* tables to share a single Azure Table Storage table.
|
|
29
|
+
*/
|
|
30
|
+
staticPartitionKey?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A single NoSQL entity entry */
|
|
34
|
+
export type OINONoSqlEntry = {
|
|
35
|
+
/** Primary key values in the order defined by the implementation's data model */
|
|
36
|
+
primaryKey: string[]
|
|
37
|
+
/** Last modification timestamp */
|
|
38
|
+
timestamp: Date
|
|
39
|
+
/** Entity tag */
|
|
40
|
+
etag: string
|
|
41
|
+
/** All custom entity properties as a key-value map */
|
|
42
|
+
properties: Record<string, unknown>
|
|
43
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
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 {
|
|
8
|
+
OINODataModel,
|
|
9
|
+
OINOMemoryDataset,
|
|
10
|
+
OINODataRow,
|
|
11
|
+
} from "@oino-ts/common"
|
|
12
|
+
import { OINONoSqlApi } from "./OINONoSqlApi.js"
|
|
13
|
+
import { OINONoSqlEntry } from "./OINONoSqlConstants.js"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Static data model for NoSQL entity listings.
|
|
17
|
+
*
|
|
18
|
+
* The canonical field order is determined by the implementation's
|
|
19
|
+
* `initializeApiDatamodel` call. Primary key fields are mapped positionally
|
|
20
|
+
* to `OINONoSqlEntry.primaryKey`, while the remaining fields (`timestamp`,
|
|
21
|
+
* `etag`, `properties`) are matched by name.
|
|
22
|
+
*/
|
|
23
|
+
export class OINONoSqlDataModel extends OINODataModel {
|
|
24
|
+
|
|
25
|
+
/** Reference to the owning NoSQL API */
|
|
26
|
+
readonly noSqlApi: OINONoSqlApi
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Constructor. Fields are added externally by the nosql implementation
|
|
30
|
+
* via `initializeApiDatamodel`.
|
|
31
|
+
*
|
|
32
|
+
* @param api the `OINONoSqlApi` that owns this data model
|
|
33
|
+
*/
|
|
34
|
+
constructor(api: OINONoSqlApi) {
|
|
35
|
+
super(api)
|
|
36
|
+
this.noSqlApi = api
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Convert an array of NoSQL entries into an in-memory dataset whose
|
|
41
|
+
* columns match the fields present in this model.
|
|
42
|
+
*
|
|
43
|
+
* @param entries nosql entries from the storage backend
|
|
44
|
+
*/
|
|
45
|
+
entriesToDataset(entries: OINONoSqlEntry[]): OINOMemoryDataset {
|
|
46
|
+
const pk_fields = this.fields.filter(f => f.fieldParams.isPrimaryKey)
|
|
47
|
+
const rows: OINODataRow[] = entries.map(e => {
|
|
48
|
+
const row: OINODataRow = []
|
|
49
|
+
for (const field of this.fields) {
|
|
50
|
+
const pk_idx = pk_fields.indexOf(field)
|
|
51
|
+
if (pk_idx >= 0) {
|
|
52
|
+
row.push(e.primaryKey[pk_idx] ?? "")
|
|
53
|
+
} else {
|
|
54
|
+
switch (field.name) {
|
|
55
|
+
case "timestamp": row.push(e.timestamp); break
|
|
56
|
+
case "etag": row.push(e.etag); break
|
|
57
|
+
case "properties": row.push(JSON.stringify(e.properties)); break
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return row
|
|
62
|
+
})
|
|
63
|
+
return new OINOMemoryDataset(rows)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
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 { OINOApiParams } from "@oino-ts/common"
|
|
8
|
+
import { OINONoSqlParams, OINONoSqlConstructor } from "./OINONoSqlConstants.js"
|
|
9
|
+
import { OINONoSql } from "./OINONoSql.js"
|
|
10
|
+
import { OINONoSqlApi } from "./OINONoSqlApi.js"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Static factory for creating `OINONoSql` instances and `OINONoSqlApi` instances
|
|
14
|
+
* from registered provider classes.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* ```ts
|
|
18
|
+
* OINONoSqlFactory.registerNoSql("OINONoSqlAzureTable", OINONoSqlAzureTable)
|
|
19
|
+
* const nosql = await OINONoSqlFactory.createNoSql({ type: "OINONoSqlAzureTable", ... })
|
|
20
|
+
* const api = await OINONoSqlFactory.createApi(nosql, { apiName: "entities", tableName: "myTable" })
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export class OINONoSqlFactory {
|
|
24
|
+
private static _registry: Record<string, OINONoSqlConstructor> = {}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Register a nosql provider class under the given name.
|
|
28
|
+
*
|
|
29
|
+
* @param name name used in `OINONoSqlParams.type`
|
|
30
|
+
* @param noSqlClass constructor of the provider
|
|
31
|
+
*/
|
|
32
|
+
static registerNoSql(name: string, noSqlClass: OINONoSqlConstructor): void {
|
|
33
|
+
this._registry[name] = noSqlClass
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create and optionally connect/validate a nosql backend from params.
|
|
38
|
+
*
|
|
39
|
+
* @param params connection parameters
|
|
40
|
+
* @param connect if true, calls `connect()` on the backend
|
|
41
|
+
* @param validate if true, calls `validate()` on the backend
|
|
42
|
+
*/
|
|
43
|
+
static async createNoSql(
|
|
44
|
+
params: OINONoSqlParams,
|
|
45
|
+
connect: boolean = true,
|
|
46
|
+
validate: boolean = true
|
|
47
|
+
): Promise<OINONoSql> {
|
|
48
|
+
const no_sql_class = this._registry[params.type]
|
|
49
|
+
if (!no_sql_class) {
|
|
50
|
+
throw new Error("Unsupported nosql type: " + params.type)
|
|
51
|
+
}
|
|
52
|
+
const nosql: OINONoSql = new no_sql_class(params)
|
|
53
|
+
if (connect) {
|
|
54
|
+
const connect_res = await nosql.connect()
|
|
55
|
+
if (!connect_res.success) {
|
|
56
|
+
throw new Error("NoSql connection failed: " + connect_res.statusText)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (validate) {
|
|
60
|
+
const validate_res = await nosql.validate()
|
|
61
|
+
if (!validate_res.success) {
|
|
62
|
+
throw new Error("NoSql validation failed: " + validate_res.statusText)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return nosql
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Create an `OINONoSqlApi` and initialise its data model.
|
|
70
|
+
*
|
|
71
|
+
* @param noSql nosql backend to use
|
|
72
|
+
* @param params API parameters
|
|
73
|
+
*/
|
|
74
|
+
static async createApi(noSql: OINONoSql, params: OINOApiParams): Promise<OINONoSqlApi> {
|
|
75
|
+
const api = new OINONoSqlApi(noSql, params)
|
|
76
|
+
await noSql.initializeApiDatamodel(api)
|
|
77
|
+
return api
|
|
78
|
+
}
|
|
79
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { OINONoSql } from "./OINONoSql.js"
|
|
2
|
+
export { OINONoSqlDataModel } from "./OINONoSqlDataModel.js"
|
|
3
|
+
export { OINONoSqlFactory } from "./OINONoSqlFactory.js"
|
|
4
|
+
export { OINONoSqlApi } from "./OINONoSqlApi.js"
|
|
5
|
+
export { type OINONoSqlConstructor, type OINONoSqlParams, type OINONoSqlEntry } from "./OINONoSqlConstants.js"
|