@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,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
4
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
5
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.OINONoSql = void 0;
|
|
9
|
+
const common_1 = require("@oino-ts/common");
|
|
10
|
+
const NOSQL_LIKE_ESCAPE_REGEX = /[.*+?^${}()|[\]\\]/g;
|
|
11
|
+
const NOSQL_LIKE_PERCENT_REGEX = /%/g;
|
|
12
|
+
const NOSQL_LIKE_UNDERSCORE_REGEX = /_/g;
|
|
13
|
+
/**
|
|
14
|
+
* Abstract base class for NoSQL storage backends. Subclasses implement
|
|
15
|
+
* the core CRUD operations for a specific provider (e.g. Azure Table Storage).
|
|
16
|
+
*
|
|
17
|
+
* The data model exposed by the API has a fixed set of fields:
|
|
18
|
+
* 1. `partitionKey` – partition key (primary key component, string)
|
|
19
|
+
* 2. `rowKey` – row key (primary key component, string)
|
|
20
|
+
* 3. `timestamp` – last modification timestamp (datetime)
|
|
21
|
+
* 4. `etag` – entity tag (string)
|
|
22
|
+
* 5. `properties` – all custom entity properties as a JSON string
|
|
23
|
+
*
|
|
24
|
+
* The SQL-formatting methods inherited from `OINODataSource` are not used
|
|
25
|
+
* by nosql operations; they are implemented here as passthrough stubs.
|
|
26
|
+
*/
|
|
27
|
+
class OINONoSql extends common_1.OINODataSource {
|
|
28
|
+
nosqlParams;
|
|
29
|
+
/** Table name */
|
|
30
|
+
name;
|
|
31
|
+
/**
|
|
32
|
+
* Whether this backend can auto-generate primary key values when none are
|
|
33
|
+
* supplied in a POST request. Defaults to `false`; override in subclasses
|
|
34
|
+
* that support server-side key generation (e.g. UUID on insert).
|
|
35
|
+
*/
|
|
36
|
+
supportsAutoKey = false;
|
|
37
|
+
/**
|
|
38
|
+
* Constructor for `OINONoSql`.
|
|
39
|
+
* @param params nosql storage connection parameters
|
|
40
|
+
*/
|
|
41
|
+
constructor(params) {
|
|
42
|
+
super();
|
|
43
|
+
this.nosqlParams = { ...params };
|
|
44
|
+
this.name = this.nosqlParams.table;
|
|
45
|
+
}
|
|
46
|
+
// ── OINODataSource passthrough stubs ──────────────────────────────────
|
|
47
|
+
printTableName(name) {
|
|
48
|
+
return name;
|
|
49
|
+
}
|
|
50
|
+
printColumnName(name) {
|
|
51
|
+
return name;
|
|
52
|
+
}
|
|
53
|
+
printCellAsValue(cellValue, nativeType) {
|
|
54
|
+
if (cellValue === null || cellValue === undefined) {
|
|
55
|
+
return "";
|
|
56
|
+
}
|
|
57
|
+
if (cellValue instanceof Date) {
|
|
58
|
+
return cellValue.toISOString();
|
|
59
|
+
}
|
|
60
|
+
return String(cellValue);
|
|
61
|
+
}
|
|
62
|
+
printStringValue(s) {
|
|
63
|
+
return s;
|
|
64
|
+
}
|
|
65
|
+
parseValueAsCell(v, nativeType) {
|
|
66
|
+
if (nativeType === "DATETIME" && typeof v === "string" && v !== "") {
|
|
67
|
+
return new Date(v);
|
|
68
|
+
}
|
|
69
|
+
return v;
|
|
70
|
+
}
|
|
71
|
+
// ── NoSQL-specific filter helpers ─────────────────────────────────────
|
|
72
|
+
/**
|
|
73
|
+
* Test whether a NoSQL entry matches an `OINOQueryFilter` predicate.
|
|
74
|
+
* Used for in-memory (result) filtering when the storage backend cannot
|
|
75
|
+
* translate the predicate to a native query.
|
|
76
|
+
*
|
|
77
|
+
* @param entry nosql entry to test
|
|
78
|
+
* @param filter filter predicate to evaluate
|
|
79
|
+
*/
|
|
80
|
+
static matchesEntry(entry, filter) {
|
|
81
|
+
if (filter.isEmpty())
|
|
82
|
+
return true;
|
|
83
|
+
const op = filter.operator;
|
|
84
|
+
if (op === common_1.OINOQueryBooleanOperation.and) {
|
|
85
|
+
return OINONoSql.matchesEntry(entry, filter.leftSide) &&
|
|
86
|
+
OINONoSql.matchesEntry(entry, filter.rightSide);
|
|
87
|
+
}
|
|
88
|
+
if (op === common_1.OINOQueryBooleanOperation.or) {
|
|
89
|
+
return OINONoSql.matchesEntry(entry, filter.leftSide) ||
|
|
90
|
+
OINONoSql.matchesEntry(entry, filter.rightSide);
|
|
91
|
+
}
|
|
92
|
+
if (op === common_1.OINOQueryBooleanOperation.not) {
|
|
93
|
+
return !OINONoSql.matchesEntry(entry, filter.rightSide);
|
|
94
|
+
}
|
|
95
|
+
const field_name = filter.leftSide;
|
|
96
|
+
const compare_value = filter.rightSide;
|
|
97
|
+
let field_value;
|
|
98
|
+
switch (field_name) {
|
|
99
|
+
case "timestamp":
|
|
100
|
+
field_value = entry.timestamp;
|
|
101
|
+
break;
|
|
102
|
+
case "etag":
|
|
103
|
+
field_value = entry.etag;
|
|
104
|
+
break;
|
|
105
|
+
default: return true;
|
|
106
|
+
}
|
|
107
|
+
if (op === common_1.OINOQueryNullCheck.isnull)
|
|
108
|
+
return field_value === null;
|
|
109
|
+
if (op === common_1.OINOQueryNullCheck.isNotNull)
|
|
110
|
+
return field_value !== null;
|
|
111
|
+
if (field_value === null)
|
|
112
|
+
return false;
|
|
113
|
+
if (field_value instanceof Date) {
|
|
114
|
+
const ms = field_value.getTime();
|
|
115
|
+
const cmp_ms = new Date(compare_value).getTime();
|
|
116
|
+
switch (op) {
|
|
117
|
+
case common_1.OINOQueryComparison.lt: return ms < cmp_ms;
|
|
118
|
+
case common_1.OINOQueryComparison.le: return ms <= cmp_ms;
|
|
119
|
+
case common_1.OINOQueryComparison.eq: return ms === cmp_ms;
|
|
120
|
+
case common_1.OINOQueryComparison.ne: return ms !== cmp_ms;
|
|
121
|
+
case common_1.OINOQueryComparison.ge: return ms >= cmp_ms;
|
|
122
|
+
case common_1.OINOQueryComparison.gt: return ms > cmp_ms;
|
|
123
|
+
default: return true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const str_value = String(field_value);
|
|
127
|
+
switch (op) {
|
|
128
|
+
case common_1.OINOQueryComparison.lt: return str_value < compare_value;
|
|
129
|
+
case common_1.OINOQueryComparison.le: return str_value <= compare_value;
|
|
130
|
+
case common_1.OINOQueryComparison.eq: return str_value === compare_value;
|
|
131
|
+
case common_1.OINOQueryComparison.ne: return str_value !== compare_value;
|
|
132
|
+
case common_1.OINOQueryComparison.ge: return str_value >= compare_value;
|
|
133
|
+
case common_1.OINOQueryComparison.gt: return str_value > compare_value;
|
|
134
|
+
case common_1.OINOQueryComparison.like: {
|
|
135
|
+
const escaped = compare_value
|
|
136
|
+
.replace(NOSQL_LIKE_ESCAPE_REGEX, "\\$&")
|
|
137
|
+
.replace(NOSQL_LIKE_PERCENT_REGEX, ".*")
|
|
138
|
+
.replace(NOSQL_LIKE_UNDERSCORE_REGEX, ".");
|
|
139
|
+
return new RegExp("^" + escaped + "$", "i").test(str_value);
|
|
140
|
+
}
|
|
141
|
+
default: return true;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Upsert (insert or replace) multiple entities in the most efficient way
|
|
146
|
+
* the backend supports. The default implementation loops over
|
|
147
|
+
* `upsertEntry`; override in subclasses that support native batch writes.
|
|
148
|
+
*
|
|
149
|
+
* @param entries entities to upsert
|
|
150
|
+
*/
|
|
151
|
+
async upsertEntries(entries) {
|
|
152
|
+
for (const entry of entries) {
|
|
153
|
+
await this.upsertEntry(entry);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
exports.OINONoSql = OINONoSql;
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
4
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
5
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.OINONoSqlApi = void 0;
|
|
9
|
+
const common_1 = require("@oino-ts/common");
|
|
10
|
+
/**
|
|
11
|
+
* REST API for NoSQL table storage.
|
|
12
|
+
*
|
|
13
|
+
* Supports the following HTTP methods:
|
|
14
|
+
* - **GET without id** – lists all entities and returns metadata as JSON.
|
|
15
|
+
* - **GET with id** – returns a single entity.
|
|
16
|
+
* - **POST / PUT with id** – upserts an entity; body must be a JSON object
|
|
17
|
+
* with a `properties` key containing the custom entity properties.
|
|
18
|
+
* - **DELETE with id** – deletes the named entity.
|
|
19
|
+
*
|
|
20
|
+
* The URL row ID format uses `OINOConfig.OINO_ID_SEPARATOR` to join the
|
|
21
|
+
* primary key field values, matching the number and order of primary key
|
|
22
|
+
* fields in the data model (same `_OINOID_` convention as `OINODbApi`).
|
|
23
|
+
*/
|
|
24
|
+
class OINONoSqlApi extends common_1.OINOApi {
|
|
25
|
+
/** NoSQL storage backend */
|
|
26
|
+
noSql;
|
|
27
|
+
/** NoSQL-specific data model (populated by `initializeDatamodel`) */
|
|
28
|
+
noSqlDatamodel = null;
|
|
29
|
+
/**
|
|
30
|
+
* Constructor.
|
|
31
|
+
*
|
|
32
|
+
* NOTE: `initializeDatamodel` (or `OINONoSqlFactory.createApi`) must be
|
|
33
|
+
* called before the first request is dispatched.
|
|
34
|
+
*
|
|
35
|
+
* @param noSql nosql storage backend
|
|
36
|
+
* @param params API parameters
|
|
37
|
+
*/
|
|
38
|
+
constructor(noSql, params) {
|
|
39
|
+
if (params.hashidKey) {
|
|
40
|
+
throw new Error(common_1.OINO_ERROR_PREFIX + ": hashid is not supported by OINONoSqlApi (primary keys are strings, not numeric IDs)");
|
|
41
|
+
}
|
|
42
|
+
if (params.failOnUpdateOnAutoinc) {
|
|
43
|
+
throw new Error(common_1.OINO_ERROR_PREFIX + ": failOnUpdateOnAutoinc is not supported by OINONoSqlApi (no autoinc fields in NoSQL)");
|
|
44
|
+
}
|
|
45
|
+
if (params.returnInsertedIds) {
|
|
46
|
+
throw new Error(common_1.OINO_ERROR_PREFIX + ": returnInsertedIds is not supported by OINONoSqlApi");
|
|
47
|
+
}
|
|
48
|
+
super(noSql, params);
|
|
49
|
+
this.noSql = noSql;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Attach the static nosql data model and mark the API as initialised.
|
|
53
|
+
*
|
|
54
|
+
* @param datamodel `OINONoSqlDataModel` instance for this API
|
|
55
|
+
*/
|
|
56
|
+
initializeDatamodel(datamodel) {
|
|
57
|
+
this.noSqlDatamodel = datamodel;
|
|
58
|
+
this.datamodel = datamodel;
|
|
59
|
+
this.initialized = true;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Parse a `_OINOID_`-formatted row ID into an ordered array of decoded
|
|
63
|
+
* primary key values using `OINOConfig.parseOINOId`. Returns `null` when
|
|
64
|
+
* the number of parts does not match the data model's primary key count.
|
|
65
|
+
*
|
|
66
|
+
* @param rowId `_OINOID_`-formatted row ID
|
|
67
|
+
*/
|
|
68
|
+
_parseRowId(rowId) {
|
|
69
|
+
if (!this.noSqlDatamodel)
|
|
70
|
+
return null;
|
|
71
|
+
const pk_count = this.noSqlDatamodel.filterFields((f) => f.fieldParams.isPrimaryKey).length;
|
|
72
|
+
const parts = common_1.OINOConfig.parseOINOId(rowId);
|
|
73
|
+
if (parts.length !== pk_count)
|
|
74
|
+
return null;
|
|
75
|
+
return parts;
|
|
76
|
+
}
|
|
77
|
+
// ── Private helpers ───────────────────────────────────────────────────
|
|
78
|
+
/**
|
|
79
|
+
* Validate a data row against API parameters. Currently checks whether
|
|
80
|
+
* primary key fields are present when `requirePrimaryKey` is `true`.
|
|
81
|
+
*
|
|
82
|
+
* `requirePrimaryKey` is derived at the call-site from:
|
|
83
|
+
* - `this.params.failOnInsertWithoutKey` when explicitly set, or
|
|
84
|
+
* - `!this.noSql.supportsAutoKey` as the implementation-specific default.
|
|
85
|
+
*/
|
|
86
|
+
_validateRow(result, row, requirePrimaryKey) {
|
|
87
|
+
if (!requirePrimaryKey)
|
|
88
|
+
return;
|
|
89
|
+
const pk_fields = this.noSqlDatamodel.filterFields((f) => f.fieldParams.isPrimaryKey);
|
|
90
|
+
for (let i = 0; i < pk_fields.length; i++) {
|
|
91
|
+
const field_idx = this.noSqlDatamodel.fields.indexOf(pk_fields[i]);
|
|
92
|
+
const val = row[field_idx];
|
|
93
|
+
if (val === undefined || val === null || String(val) === "") {
|
|
94
|
+
result.setError(405, `Primary key '${pk_fields[i].name}' is missing from the data!`, "_validateRow");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
_parseData(result, request) {
|
|
100
|
+
let rows = [];
|
|
101
|
+
const data = request.rowData ?? request.body;
|
|
102
|
+
try {
|
|
103
|
+
if (Array.isArray(data)) {
|
|
104
|
+
rows = data;
|
|
105
|
+
}
|
|
106
|
+
else if (data != null) {
|
|
107
|
+
rows = common_1.OINOParser.createRows(this.datamodel, data, request.requestType, request.multipartBoundary);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
result.setError(400, "Invalid data: " + e.message, "_parseData");
|
|
112
|
+
}
|
|
113
|
+
return rows;
|
|
114
|
+
}
|
|
115
|
+
_rowToEntry(row, pkOverride) {
|
|
116
|
+
const pk_fields = this.noSqlDatamodel.fields.filter(f => f.fieldParams.isPrimaryKey);
|
|
117
|
+
const primary_key = pkOverride ?? pk_fields.map(f => {
|
|
118
|
+
const idx = this.noSqlDatamodel.fields.indexOf(f);
|
|
119
|
+
return String(row[idx] ?? "");
|
|
120
|
+
});
|
|
121
|
+
const properties_idx = this.noSqlDatamodel.fields.findIndex(f => f.name === "properties");
|
|
122
|
+
const raw = properties_idx >= 0 ? row[properties_idx] : undefined;
|
|
123
|
+
let properties = {};
|
|
124
|
+
if (typeof raw === "string") {
|
|
125
|
+
properties = JSON.parse(raw);
|
|
126
|
+
}
|
|
127
|
+
else if ((raw != null) && (typeof raw === "object") && !Array.isArray(raw) && !(raw instanceof Date) && !(raw instanceof Uint8Array)) {
|
|
128
|
+
properties = raw;
|
|
129
|
+
}
|
|
130
|
+
return { primaryKey: primary_key, timestamp: new Date(), etag: "", properties };
|
|
131
|
+
}
|
|
132
|
+
// ── Private HTTP method handlers ──────────────────────────────────────
|
|
133
|
+
async _doGet(result, pkValues, request) {
|
|
134
|
+
if (!pkValues) {
|
|
135
|
+
try {
|
|
136
|
+
const entries = await this.noSql.listEntries(request.queryParams?.filter);
|
|
137
|
+
const dataset = this.noSqlDatamodel.entriesToDataset(entries);
|
|
138
|
+
result.data = new common_1.OINOModelSet(this.datamodel, dataset, request.queryParams);
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
result.setError(500, "Error listing nosql entries: " + e.message, "DoGet");
|
|
142
|
+
common_1.OINOLog.exception("@oino-ts/nosql", "OINONoSqlApi", "_doGet", "exception in list request", { message: e.message, stack: e.stack });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
try {
|
|
147
|
+
const entry = await this.noSql.getEntry(pkValues);
|
|
148
|
+
if (entry === null) {
|
|
149
|
+
result.setError(404, `Entry '${pkValues.join("/")}' not found`, "DoGet");
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
const dataset = this.noSqlDatamodel.entriesToDataset([entry]);
|
|
153
|
+
result.data = new common_1.OINOModelSet(this.datamodel, dataset, request.queryParams);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
result.setError(500, "Error fetching nosql entry: " + e.message, "DoGet");
|
|
158
|
+
common_1.OINOLog.exception("@oino-ts/nosql", "OINONoSqlApi", "_doGet", "exception in get request", { message: e.message, stack: e.stack });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async _doPut(result, pkValues, row) {
|
|
163
|
+
try {
|
|
164
|
+
await this.noSql.upsertEntry(this._rowToEntry(row, pkValues));
|
|
165
|
+
}
|
|
166
|
+
catch (e) {
|
|
167
|
+
result.setError(500, "Error upserting nosql entry: " + e.message, "DoPut");
|
|
168
|
+
common_1.OINOLog.exception("@oino-ts/nosql", "OINONoSqlApi", "_doPut", "exception in put request", { message: e.message, stack: e.stack });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async _doPost(result, rows, pkOverride) {
|
|
172
|
+
// Validate all rows first and collect valid entries
|
|
173
|
+
const entries = [];
|
|
174
|
+
const require_pk = !pkOverride && (this.params.failOnInsertWithoutKey ?? !this.noSql.supportsAutoKey);
|
|
175
|
+
for (const row of rows) {
|
|
176
|
+
if (require_pk) {
|
|
177
|
+
this._validateRow(result, row, true);
|
|
178
|
+
if (!result.success) {
|
|
179
|
+
if (this.params.failOnAnyInvalidRows === false) {
|
|
180
|
+
result.setOk();
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
entries.push(this._rowToEntry(row, pkOverride));
|
|
187
|
+
}
|
|
188
|
+
if (entries.length === 0 && result.success) {
|
|
189
|
+
result.setError(405, "No valid rows for POST!", "DoPost");
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
// Single batch call — implementations use native bulk APIs where possible
|
|
193
|
+
try {
|
|
194
|
+
await this.noSql.upsertEntries(entries);
|
|
195
|
+
}
|
|
196
|
+
catch (e) {
|
|
197
|
+
result.setError(500, "Error upserting nosql entries: " + e.message, "DoPost");
|
|
198
|
+
common_1.OINOLog.exception("@oino-ts/nosql", "OINONoSqlApi", "_doPost", "exception in post request", { message: e.message, stack: e.stack });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async _doDelete(result, pkValues) {
|
|
202
|
+
try {
|
|
203
|
+
await this.noSql.deleteEntry(pkValues);
|
|
204
|
+
}
|
|
205
|
+
catch (e) {
|
|
206
|
+
result.setError(500, "Error deleting nosql entry: " + e.message, "DoDelete");
|
|
207
|
+
common_1.OINOLog.exception("@oino-ts/nosql", "OINONoSqlApi", "_doDelete", "exception in delete request", { message: e.message, stack: e.stack });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// ── OINOApi abstract implementations ─────────────────────────────────
|
|
211
|
+
async doApiRequest(request) {
|
|
212
|
+
if (!this.initialized) {
|
|
213
|
+
throw new Error(common_1.OINO_ERROR_PREFIX + ": OINONoSqlApi is not initialized yet!");
|
|
214
|
+
}
|
|
215
|
+
common_1.OINOLog.debug("@oino-ts/nosql", "OINONoSqlApi", "doApiRequest", "Request", { method: request.method, id: request.rowId });
|
|
216
|
+
const result = new common_1.OINOApiResult(request);
|
|
217
|
+
let rows = [];
|
|
218
|
+
if (request.method === "PUT" || request.method === "POST") {
|
|
219
|
+
rows = this._parseData(result, request);
|
|
220
|
+
}
|
|
221
|
+
if (request.method === "GET") {
|
|
222
|
+
if (request.rowId) {
|
|
223
|
+
const pk_values = this._parseRowId(request.rowId);
|
|
224
|
+
if (!pk_values) {
|
|
225
|
+
const pk_count = this.noSqlDatamodel.filterFields((f) => f.fieldParams.isPrimaryKey).length;
|
|
226
|
+
result.setError(400, `Invalid row ID; expected ${pk_count} key part(s) separated by '${common_1.OINOConfig.OINO_ID_SEPARATOR}'`, "DoRequest");
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
await this._doGet(result, pk_values, request);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
await this._doGet(result, null, request);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
else if (request.method === "PUT") {
|
|
237
|
+
if (!request.rowId) {
|
|
238
|
+
result.setError(400, "HTTP PUT method requires a URL ID!", "DoRequest");
|
|
239
|
+
}
|
|
240
|
+
else if (rows.length !== 1) {
|
|
241
|
+
result.setError(400, "HTTP PUT method requires exactly one row in the body data!", "DoRequest");
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
const pk_values = this._parseRowId(request.rowId);
|
|
245
|
+
if (!pk_values) {
|
|
246
|
+
const pk_count = this.noSqlDatamodel.filterFields((f) => f.fieldParams.isPrimaryKey).length;
|
|
247
|
+
result.setError(400, `Invalid row ID; expected ${pk_count} key part(s) separated by '${common_1.OINOConfig.OINO_ID_SEPARATOR}'`, "DoRequest");
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
await this._doPut(result, pk_values, rows[0]);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
else if (request.method === "POST") {
|
|
255
|
+
if (rows.length === 0) {
|
|
256
|
+
result.setError(400, "HTTP POST method requires at least one row in the body data!", "DoRequest");
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
let pk_override;
|
|
260
|
+
if (request.rowId) {
|
|
261
|
+
if (rows.length !== 1) {
|
|
262
|
+
result.setError(400, "HTTP POST with a URL ID requires exactly one row in the body data!", "DoRequest");
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
const pk_values = this._parseRowId(request.rowId);
|
|
266
|
+
if (!pk_values) {
|
|
267
|
+
const pk_count = this.noSqlDatamodel.filterFields((f) => f.fieldParams.isPrimaryKey).length;
|
|
268
|
+
result.setError(400, `Invalid row ID; expected ${pk_count} key part(s) separated by '${common_1.OINOConfig.OINO_ID_SEPARATOR}'`, "DoRequest");
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
pk_override = pk_values;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (result.success) {
|
|
276
|
+
await this._doPost(result, rows, pk_override);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
else if (request.method === "DELETE") {
|
|
281
|
+
if (!request.rowId) {
|
|
282
|
+
result.setError(400, "HTTP DELETE method requires a URL ID!", "DoRequest");
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
const pk_values = this._parseRowId(request.rowId);
|
|
286
|
+
if (!pk_values) {
|
|
287
|
+
const pk_count = this.noSqlDatamodel.filterFields((f) => f.fieldParams.isPrimaryKey).length;
|
|
288
|
+
result.setError(400, `Invalid row ID; expected ${pk_count} key part(s) separated by '${common_1.OINOConfig.OINO_ID_SEPARATOR}'`, "DoRequest");
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
await this._doDelete(result, pk_values);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
result.setError(405, "Unsupported HTTP method '" + request.method + "' for OINONoSqlApi", "DoRequest");
|
|
297
|
+
}
|
|
298
|
+
return result;
|
|
299
|
+
}
|
|
300
|
+
async doBatchUpdate(method, rowId, rowData, queryParams) {
|
|
301
|
+
return this.doBatchApiRequest(new common_1.OINOApiRequest({ method, rowId, rowData, queryParams }));
|
|
302
|
+
}
|
|
303
|
+
async doBatchApiRequest(request) {
|
|
304
|
+
if (!this.initialized) {
|
|
305
|
+
throw new Error(common_1.OINO_ERROR_PREFIX + ": OINONoSqlApi is not initialized yet!");
|
|
306
|
+
}
|
|
307
|
+
common_1.OINOLog.debug("@oino-ts/nosql", "OINONoSqlApi", "doBatchApiRequest", "Request", { method: request.method });
|
|
308
|
+
const result = new common_1.OINOApiResult(request);
|
|
309
|
+
if (request.method !== "PUT" && request.method !== "DELETE") {
|
|
310
|
+
result.setError(405, "Batch API only supports PUT and DELETE methods!", "DoBatchApiRequest");
|
|
311
|
+
return result;
|
|
312
|
+
}
|
|
313
|
+
common_1.OINOBenchmark.startMetric("OINONoSqlApi", "doBatchApiRequest." + request.method);
|
|
314
|
+
const rows = this._parseData(result, request);
|
|
315
|
+
if (!result.success) {
|
|
316
|
+
common_1.OINOBenchmark.endMetric("OINONoSqlApi", "doBatchApiRequest." + request.method, false);
|
|
317
|
+
return result;
|
|
318
|
+
}
|
|
319
|
+
if (request.method === "PUT") {
|
|
320
|
+
const entries = [];
|
|
321
|
+
const require_pk = this.params.failOnInsertWithoutKey ?? !this.noSql.supportsAutoKey;
|
|
322
|
+
for (const row of rows) {
|
|
323
|
+
this._validateRow(result, row, require_pk);
|
|
324
|
+
if (!result.success) {
|
|
325
|
+
common_1.OINOBenchmark.endMetric("OINONoSqlApi", "doBatchApiRequest." + request.method, false);
|
|
326
|
+
return result;
|
|
327
|
+
}
|
|
328
|
+
entries.push(this._rowToEntry(row));
|
|
329
|
+
}
|
|
330
|
+
if (entries.length === 0) {
|
|
331
|
+
result.setError(405, "No valid rows for batch PUT!", "DoBatchApiRequest");
|
|
332
|
+
common_1.OINOBenchmark.endMetric("OINONoSqlApi", "doBatchApiRequest." + request.method, false);
|
|
333
|
+
return result;
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
await this.noSql.upsertEntries(entries);
|
|
337
|
+
}
|
|
338
|
+
catch (e) {
|
|
339
|
+
result.setError(500, "Error batch upserting nosql entries: " + e.message, "DoBatchApiRequest");
|
|
340
|
+
common_1.OINOLog.exception("@oino-ts/nosql", "OINONoSqlApi", "doBatchApiRequest", "exception in batch put request", { message: e.message, stack: e.stack });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
const pk_fields = this.noSqlDatamodel.fields.filter(f => f.fieldParams.isPrimaryKey);
|
|
345
|
+
for (const row of rows) {
|
|
346
|
+
const pk_values = pk_fields.map(f => {
|
|
347
|
+
const idx = this.noSqlDatamodel.fields.indexOf(f);
|
|
348
|
+
return String(row[idx] ?? "");
|
|
349
|
+
});
|
|
350
|
+
try {
|
|
351
|
+
await this.noSql.deleteEntry(pk_values);
|
|
352
|
+
}
|
|
353
|
+
catch (e) {
|
|
354
|
+
result.setError(500, "Error batch deleting nosql entry: " + e.message, "DoBatchApiRequest");
|
|
355
|
+
common_1.OINOLog.exception("@oino-ts/nosql", "OINONoSqlApi", "doBatchApiRequest", "exception in batch delete request", { message: e.message, stack: e.stack });
|
|
356
|
+
common_1.OINOBenchmark.endMetric("OINONoSqlApi", "doBatchApiRequest." + request.method, false);
|
|
357
|
+
return result;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
common_1.OINOBenchmark.endMetric("OINONoSqlApi", "doBatchApiRequest." + request.method, result.success);
|
|
362
|
+
return result;
|
|
363
|
+
}
|
|
364
|
+
async doHttpRequest(request, rowId, rowData, queryParams) {
|
|
365
|
+
const api_request = common_1.OINOApiRequest.fromHttpRequest(request, rowId, rowData, queryParams);
|
|
366
|
+
return this.doApiRequest(api_request);
|
|
367
|
+
}
|
|
368
|
+
async doRequest(method, rowId, rowData, queryParams, contentType = common_1.OINOContentType.json) {
|
|
369
|
+
return this.doApiRequest(new common_1.OINOApiRequest({
|
|
370
|
+
method,
|
|
371
|
+
rowId,
|
|
372
|
+
rowData,
|
|
373
|
+
queryParams,
|
|
374
|
+
requestType: contentType
|
|
375
|
+
}));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
exports.OINONoSqlApi = OINONoSqlApi;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
4
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
5
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
4
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
5
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.OINONoSqlDataModel = void 0;
|
|
9
|
+
const common_1 = require("@oino-ts/common");
|
|
10
|
+
/**
|
|
11
|
+
* Static data model for NoSQL entity listings.
|
|
12
|
+
*
|
|
13
|
+
* The canonical field order is determined by the implementation's
|
|
14
|
+
* `initializeApiDatamodel` call. Primary key fields are mapped positionally
|
|
15
|
+
* to `OINONoSqlEntry.primaryKey`, while the remaining fields (`timestamp`,
|
|
16
|
+
* `etag`, `properties`) are matched by name.
|
|
17
|
+
*/
|
|
18
|
+
class OINONoSqlDataModel extends common_1.OINODataModel {
|
|
19
|
+
/** Reference to the owning NoSQL API */
|
|
20
|
+
noSqlApi;
|
|
21
|
+
/**
|
|
22
|
+
* Constructor. Fields are added externally by the nosql implementation
|
|
23
|
+
* via `initializeApiDatamodel`.
|
|
24
|
+
*
|
|
25
|
+
* @param api the `OINONoSqlApi` that owns this data model
|
|
26
|
+
*/
|
|
27
|
+
constructor(api) {
|
|
28
|
+
super(api);
|
|
29
|
+
this.noSqlApi = api;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Convert an array of NoSQL entries into an in-memory dataset whose
|
|
33
|
+
* columns match the fields present in this model.
|
|
34
|
+
*
|
|
35
|
+
* @param entries nosql entries from the storage backend
|
|
36
|
+
*/
|
|
37
|
+
entriesToDataset(entries) {
|
|
38
|
+
const pk_fields = this.fields.filter(f => f.fieldParams.isPrimaryKey);
|
|
39
|
+
const rows = entries.map(e => {
|
|
40
|
+
const row = [];
|
|
41
|
+
for (const field of this.fields) {
|
|
42
|
+
const pk_idx = pk_fields.indexOf(field);
|
|
43
|
+
if (pk_idx >= 0) {
|
|
44
|
+
row.push(e.primaryKey[pk_idx] ?? "");
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
switch (field.name) {
|
|
48
|
+
case "timestamp":
|
|
49
|
+
row.push(e.timestamp);
|
|
50
|
+
break;
|
|
51
|
+
case "etag":
|
|
52
|
+
row.push(e.etag);
|
|
53
|
+
break;
|
|
54
|
+
case "properties":
|
|
55
|
+
row.push(JSON.stringify(e.properties));
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return row;
|
|
61
|
+
});
|
|
62
|
+
return new common_1.OINOMemoryDataset(rows);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
exports.OINONoSqlDataModel = OINONoSqlDataModel;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
4
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
5
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.OINONoSqlFactory = void 0;
|
|
9
|
+
const OINONoSqlApi_js_1 = require("./OINONoSqlApi.js");
|
|
10
|
+
/**
|
|
11
|
+
* Static factory for creating `OINONoSql` instances and `OINONoSqlApi` instances
|
|
12
|
+
* from registered provider classes.
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* ```ts
|
|
16
|
+
* OINONoSqlFactory.registerNoSql("OINONoSqlAzureTable", OINONoSqlAzureTable)
|
|
17
|
+
* const nosql = await OINONoSqlFactory.createNoSql({ type: "OINONoSqlAzureTable", ... })
|
|
18
|
+
* const api = await OINONoSqlFactory.createApi(nosql, { apiName: "entities", tableName: "myTable" })
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
class OINONoSqlFactory {
|
|
22
|
+
static _registry = {};
|
|
23
|
+
/**
|
|
24
|
+
* Register a nosql provider class under the given name.
|
|
25
|
+
*
|
|
26
|
+
* @param name name used in `OINONoSqlParams.type`
|
|
27
|
+
* @param noSqlClass constructor of the provider
|
|
28
|
+
*/
|
|
29
|
+
static registerNoSql(name, noSqlClass) {
|
|
30
|
+
this._registry[name] = noSqlClass;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Create and optionally connect/validate a nosql backend from params.
|
|
34
|
+
*
|
|
35
|
+
* @param params connection parameters
|
|
36
|
+
* @param connect if true, calls `connect()` on the backend
|
|
37
|
+
* @param validate if true, calls `validate()` on the backend
|
|
38
|
+
*/
|
|
39
|
+
static async createNoSql(params, connect = true, validate = true) {
|
|
40
|
+
const no_sql_class = this._registry[params.type];
|
|
41
|
+
if (!no_sql_class) {
|
|
42
|
+
throw new Error("Unsupported nosql type: " + params.type);
|
|
43
|
+
}
|
|
44
|
+
const nosql = new no_sql_class(params);
|
|
45
|
+
if (connect) {
|
|
46
|
+
const connect_res = await nosql.connect();
|
|
47
|
+
if (!connect_res.success) {
|
|
48
|
+
throw new Error("NoSql connection failed: " + connect_res.statusText);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (validate) {
|
|
52
|
+
const validate_res = await nosql.validate();
|
|
53
|
+
if (!validate_res.success) {
|
|
54
|
+
throw new Error("NoSql validation failed: " + validate_res.statusText);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return nosql;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Create an `OINONoSqlApi` and initialise its data model.
|
|
61
|
+
*
|
|
62
|
+
* @param noSql nosql backend to use
|
|
63
|
+
* @param params API parameters
|
|
64
|
+
*/
|
|
65
|
+
static async createApi(noSql, params) {
|
|
66
|
+
const api = new OINONoSqlApi_js_1.OINONoSqlApi(noSql, params);
|
|
67
|
+
await noSql.initializeApiDatamodel(api);
|
|
68
|
+
return api;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
exports.OINONoSqlFactory = OINONoSqlFactory;
|