@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,81 @@
1
+ import { OINODataSource, OINODataCell, OINOQueryFilter } from "@oino-ts/common";
2
+ import { OINONoSqlParams, OINONoSqlEntry } from "./OINONoSqlConstants.js";
3
+ /**
4
+ * Abstract base class for NoSQL storage backends. Subclasses implement
5
+ * the core CRUD operations for a specific provider (e.g. Azure Table Storage).
6
+ *
7
+ * The data model exposed by the API has a fixed set of fields:
8
+ * 1. `partitionKey` – partition key (primary key component, string)
9
+ * 2. `rowKey` – row key (primary key component, string)
10
+ * 3. `timestamp` – last modification timestamp (datetime)
11
+ * 4. `etag` – entity tag (string)
12
+ * 5. `properties` – all custom entity properties as a JSON string
13
+ *
14
+ * The SQL-formatting methods inherited from `OINODataSource` are not used
15
+ * by nosql operations; they are implemented here as passthrough stubs.
16
+ */
17
+ export declare abstract class OINONoSql extends OINODataSource {
18
+ protected readonly nosqlParams: OINONoSqlParams;
19
+ /** Table name */
20
+ readonly name: string;
21
+ /**
22
+ * Whether this backend can auto-generate primary key values when none are
23
+ * supplied in a POST request. Defaults to `false`; override in subclasses
24
+ * that support server-side key generation (e.g. UUID on insert).
25
+ */
26
+ readonly supportsAutoKey: boolean;
27
+ /**
28
+ * Constructor for `OINONoSql`.
29
+ * @param params nosql storage connection parameters
30
+ */
31
+ constructor(params: OINONoSqlParams);
32
+ printTableName(name: string): string;
33
+ printColumnName(name: string): string;
34
+ printCellAsValue(cellValue: OINODataCell, nativeType: string): string;
35
+ printStringValue(s: string): string;
36
+ parseValueAsCell(v: OINODataCell, nativeType: string): OINODataCell;
37
+ /**
38
+ * Test whether a NoSQL entry matches an `OINOQueryFilter` predicate.
39
+ * Used for in-memory (result) filtering when the storage backend cannot
40
+ * translate the predicate to a native query.
41
+ *
42
+ * @param entry nosql entry to test
43
+ * @param filter filter predicate to evaluate
44
+ */
45
+ protected static matchesEntry(entry: OINONoSqlEntry, filter: OINOQueryFilter): boolean;
46
+ /**
47
+ * List all entities in the table, applying storage-level filtering where
48
+ * possible and in-memory result filtering for remaining predicates.
49
+ *
50
+ * @param filter optional query filter to apply
51
+ */
52
+ abstract listEntries(filter?: OINOQueryFilter): Promise<OINONoSqlEntry[]>;
53
+ /**
54
+ * Fetch a single entity by its primary key values.
55
+ *
56
+ * Returns `null` when no entity with the given primary key exists.
57
+ *
58
+ * @param primaryKey ordered primary key values as defined by the implementation's data model
59
+ */
60
+ abstract getEntry(primaryKey: string[]): Promise<OINONoSqlEntry | null>;
61
+ /**
62
+ * Upsert (insert or replace) an entity.
63
+ *
64
+ * @param entry entity to upsert
65
+ */
66
+ abstract upsertEntry(entry: OINONoSqlEntry): Promise<void>;
67
+ /**
68
+ * Upsert (insert or replace) multiple entities in the most efficient way
69
+ * the backend supports. The default implementation loops over
70
+ * `upsertEntry`; override in subclasses that support native batch writes.
71
+ *
72
+ * @param entries entities to upsert
73
+ */
74
+ upsertEntries(entries: OINONoSqlEntry[]): Promise<void>;
75
+ /**
76
+ * Delete an entity.
77
+ *
78
+ * @param primaryKey ordered primary key values as defined by the implementation's data model
79
+ */
80
+ abstract deleteEntry(primaryKey: string[]): Promise<void>;
81
+ }
@@ -0,0 +1,67 @@
1
+ import { OINOApi, OINOApiParams, OINOApiRequest, OINOApiResult, OINOContentType, OINOQueryParams, OINOHttpRequest, type OINOApiData } from "@oino-ts/common";
2
+ import { OINONoSql } from "./OINONoSql.js";
3
+ import { OINONoSqlDataModel } from "./OINONoSqlDataModel.js";
4
+ /**
5
+ * REST API for NoSQL table storage.
6
+ *
7
+ * Supports the following HTTP methods:
8
+ * - **GET without id** – lists all entities and returns metadata as JSON.
9
+ * - **GET with id** – returns a single entity.
10
+ * - **POST / PUT with id** – upserts an entity; body must be a JSON object
11
+ * with a `properties` key containing the custom entity properties.
12
+ * - **DELETE with id** – deletes the named entity.
13
+ *
14
+ * The URL row ID format uses `OINOConfig.OINO_ID_SEPARATOR` to join the
15
+ * primary key field values, matching the number and order of primary key
16
+ * fields in the data model (same `_OINOID_` convention as `OINODbApi`).
17
+ */
18
+ export declare class OINONoSqlApi extends OINOApi {
19
+ /** NoSQL storage backend */
20
+ readonly noSql: OINONoSql;
21
+ /** NoSQL-specific data model (populated by `initializeDatamodel`) */
22
+ noSqlDatamodel: OINONoSqlDataModel | null;
23
+ /**
24
+ * Constructor.
25
+ *
26
+ * NOTE: `initializeDatamodel` (or `OINONoSqlFactory.createApi`) must be
27
+ * called before the first request is dispatched.
28
+ *
29
+ * @param noSql nosql storage backend
30
+ * @param params API parameters
31
+ */
32
+ constructor(noSql: OINONoSql, params: OINOApiParams);
33
+ /**
34
+ * Attach the static nosql data model and mark the API as initialised.
35
+ *
36
+ * @param datamodel `OINONoSqlDataModel` instance for this API
37
+ */
38
+ initializeDatamodel(datamodel: OINONoSqlDataModel): void;
39
+ /**
40
+ * Parse a `_OINOID_`-formatted row ID into an ordered array of decoded
41
+ * primary key values using `OINOConfig.parseOINOId`. Returns `null` when
42
+ * the number of parts does not match the data model's primary key count.
43
+ *
44
+ * @param rowId `_OINOID_`-formatted row ID
45
+ */
46
+ private _parseRowId;
47
+ /**
48
+ * Validate a data row against API parameters. Currently checks whether
49
+ * primary key fields are present when `requirePrimaryKey` is `true`.
50
+ *
51
+ * `requirePrimaryKey` is derived at the call-site from:
52
+ * - `this.params.failOnInsertWithoutKey` when explicitly set, or
53
+ * - `!this.noSql.supportsAutoKey` as the implementation-specific default.
54
+ */
55
+ private _validateRow;
56
+ private _parseData;
57
+ private _rowToEntry;
58
+ private _doGet;
59
+ private _doPut;
60
+ private _doPost;
61
+ private _doDelete;
62
+ doApiRequest(request: OINOApiRequest): Promise<OINOApiResult>;
63
+ doBatchUpdate(method: string, rowId: string, rowData: OINOApiData, queryParams?: OINOQueryParams): Promise<OINOApiResult>;
64
+ doBatchApiRequest(request: OINOApiRequest): Promise<OINOApiResult>;
65
+ doHttpRequest(request: OINOHttpRequest, rowId: string, rowData: OINOApiData, queryParams: OINOQueryParams): Promise<OINOApiResult>;
66
+ doRequest(method: string, rowId: string, rowData: OINOApiData, queryParams: OINOQueryParams, contentType?: OINOContentType): Promise<OINOApiResult>;
67
+ }
@@ -0,0 +1,34 @@
1
+ import type { OINONoSql } from "./OINONoSql.js";
2
+ /**
3
+ * NoSQL class (constructor) type
4
+ * @param params nosql parameters
5
+ */
6
+ export type OINONoSqlConstructor = new (params: OINONoSqlParams) => OINONoSql;
7
+ /** NoSQL storage connection parameters */
8
+ export type OINONoSqlParams = {
9
+ /** Name of the nosql class (e.g. OINONoSqlAzureTable) */
10
+ type: string;
11
+ /** Service endpoint URL */
12
+ url: string;
13
+ /** Table name */
14
+ table: string;
15
+ /** Provider-specific connection string (e.g. Azure Storage connection string) */
16
+ connectionStr?: string;
17
+ /**
18
+ * Optional static partition key. When set, all read/write operations are
19
+ * automatically scoped to this partition key, allowing multiple logical
20
+ * tables to share a single Azure Table Storage table.
21
+ */
22
+ staticPartitionKey?: string;
23
+ };
24
+ /** A single NoSQL entity entry */
25
+ export type OINONoSqlEntry = {
26
+ /** Primary key values in the order defined by the implementation's data model */
27
+ primaryKey: string[];
28
+ /** Last modification timestamp */
29
+ timestamp: Date;
30
+ /** Entity tag */
31
+ etag: string;
32
+ /** All custom entity properties as a key-value map */
33
+ properties: Record<string, unknown>;
34
+ };
@@ -0,0 +1,29 @@
1
+ import { OINODataModel, OINOMemoryDataset } from "@oino-ts/common";
2
+ import { OINONoSqlApi } from "./OINONoSqlApi.js";
3
+ import { OINONoSqlEntry } from "./OINONoSqlConstants.js";
4
+ /**
5
+ * Static data model for NoSQL entity listings.
6
+ *
7
+ * The canonical field order is determined by the implementation's
8
+ * `initializeApiDatamodel` call. Primary key fields are mapped positionally
9
+ * to `OINONoSqlEntry.primaryKey`, while the remaining fields (`timestamp`,
10
+ * `etag`, `properties`) are matched by name.
11
+ */
12
+ export declare class OINONoSqlDataModel extends OINODataModel {
13
+ /** Reference to the owning NoSQL API */
14
+ readonly noSqlApi: OINONoSqlApi;
15
+ /**
16
+ * Constructor. Fields are added externally by the nosql implementation
17
+ * via `initializeApiDatamodel`.
18
+ *
19
+ * @param api the `OINONoSqlApi` that owns this data model
20
+ */
21
+ constructor(api: OINONoSqlApi);
22
+ /**
23
+ * Convert an array of NoSQL entries into an in-memory dataset whose
24
+ * columns match the fields present in this model.
25
+ *
26
+ * @param entries nosql entries from the storage backend
27
+ */
28
+ entriesToDataset(entries: OINONoSqlEntry[]): OINOMemoryDataset;
29
+ }
@@ -0,0 +1,40 @@
1
+ import { OINOApiParams } from "@oino-ts/common";
2
+ import { OINONoSqlParams, OINONoSqlConstructor } from "./OINONoSqlConstants.js";
3
+ import { OINONoSql } from "./OINONoSql.js";
4
+ import { OINONoSqlApi } from "./OINONoSqlApi.js";
5
+ /**
6
+ * Static factory for creating `OINONoSql` instances and `OINONoSqlApi` instances
7
+ * from registered provider classes.
8
+ *
9
+ * Usage:
10
+ * ```ts
11
+ * OINONoSqlFactory.registerNoSql("OINONoSqlAzureTable", OINONoSqlAzureTable)
12
+ * const nosql = await OINONoSqlFactory.createNoSql({ type: "OINONoSqlAzureTable", ... })
13
+ * const api = await OINONoSqlFactory.createApi(nosql, { apiName: "entities", tableName: "myTable" })
14
+ * ```
15
+ */
16
+ export declare class OINONoSqlFactory {
17
+ private static _registry;
18
+ /**
19
+ * Register a nosql provider class under the given name.
20
+ *
21
+ * @param name name used in `OINONoSqlParams.type`
22
+ * @param noSqlClass constructor of the provider
23
+ */
24
+ static registerNoSql(name: string, noSqlClass: OINONoSqlConstructor): void;
25
+ /**
26
+ * Create and optionally connect/validate a nosql backend from params.
27
+ *
28
+ * @param params connection parameters
29
+ * @param connect if true, calls `connect()` on the backend
30
+ * @param validate if true, calls `validate()` on the backend
31
+ */
32
+ static createNoSql(params: OINONoSqlParams, connect?: boolean, validate?: boolean): Promise<OINONoSql>;
33
+ /**
34
+ * Create an `OINONoSqlApi` and initialise its data model.
35
+ *
36
+ * @param noSql nosql backend to use
37
+ * @param params API parameters
38
+ */
39
+ static createApi(noSql: OINONoSql, params: OINOApiParams): Promise<OINONoSqlApi>;
40
+ }
@@ -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";
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@oino-ts/nosql",
3
+ "version": "1.0.0",
4
+ "description": "OINO TS library package for publishing NoSQL storage as a REST API.",
5
+ "author": "Matias Kiviniemi (pragmatta)",
6
+ "license": "MPL-2.0",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/pragmatta/oino-ts.git"
10
+ },
11
+ "keywords": [
12
+ "nosql",
13
+ "storage",
14
+ "rest-api",
15
+ "typescript",
16
+ "library"
17
+ ],
18
+ "main": "./dist/cjs/index.js",
19
+ "module": "./dist/esm/index.js",
20
+ "types": "./dist/types/index.d.ts",
21
+ "dependencies": {
22
+ "@oino-ts/common": "1.0.0",
23
+ "oino-ts": "file:.."
24
+ },
25
+ "devDependencies": {
26
+ "@oino-ts/types": "1.0.0",
27
+ "@types/bun": "^1.1.14",
28
+ "@types/node": "^21.0.00",
29
+ "typescript": "~5.9.0"
30
+ },
31
+ "files": [
32
+ "src/*.ts",
33
+ "dist/cjs/*.js",
34
+ "dist/esm/*.js",
35
+ "dist/types/*.d.ts"
36
+ ]
37
+ }
@@ -0,0 +1,202 @@
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 { OINODataSource, OINODataCell, OINOQueryFilter, OINOQueryBooleanOperation, OINOQueryComparison, OINOQueryNullCheck } from "@oino-ts/common"
8
+ import { OINONoSqlParams, OINONoSqlEntry } from "./OINONoSqlConstants.js"
9
+
10
+ const NOSQL_LIKE_ESCAPE_REGEX = /[.*+?^${}()|[\]\\]/g
11
+ const NOSQL_LIKE_PERCENT_REGEX = /%/g
12
+ const NOSQL_LIKE_UNDERSCORE_REGEX = /_/g
13
+
14
+ /**
15
+ * Abstract base class for NoSQL storage backends. Subclasses implement
16
+ * the core CRUD operations for a specific provider (e.g. Azure Table Storage).
17
+ *
18
+ * The data model exposed by the API has a fixed set of fields:
19
+ * 1. `partitionKey` – partition key (primary key component, string)
20
+ * 2. `rowKey` – row key (primary key component, string)
21
+ * 3. `timestamp` – last modification timestamp (datetime)
22
+ * 4. `etag` – entity tag (string)
23
+ * 5. `properties` – all custom entity properties as a JSON string
24
+ *
25
+ * The SQL-formatting methods inherited from `OINODataSource` are not used
26
+ * by nosql operations; they are implemented here as passthrough stubs.
27
+ */
28
+ export abstract class OINONoSql extends OINODataSource {
29
+
30
+ protected readonly nosqlParams: OINONoSqlParams
31
+
32
+ /** Table name */
33
+ readonly name: string
34
+
35
+ /**
36
+ * Whether this backend can auto-generate primary key values when none are
37
+ * supplied in a POST request. Defaults to `false`; override in subclasses
38
+ * that support server-side key generation (e.g. UUID on insert).
39
+ */
40
+ readonly supportsAutoKey: boolean = false
41
+
42
+ /**
43
+ * Constructor for `OINONoSql`.
44
+ * @param params nosql storage connection parameters
45
+ */
46
+ constructor(params: OINONoSqlParams) {
47
+ super()
48
+ this.nosqlParams = { ...params }
49
+ this.name = this.nosqlParams.table
50
+ }
51
+
52
+ // ── OINODataSource passthrough stubs ──────────────────────────────────
53
+
54
+ printTableName(name: string): string {
55
+ return name
56
+ }
57
+
58
+ printColumnName(name: string): string {
59
+ return name
60
+ }
61
+
62
+ printCellAsValue(cellValue: OINODataCell, nativeType: string): string {
63
+ if (cellValue === null || cellValue === undefined) {
64
+ return ""
65
+ }
66
+ if (cellValue instanceof Date) {
67
+ return cellValue.toISOString()
68
+ }
69
+ return String(cellValue)
70
+ }
71
+
72
+ printStringValue(s: string): string {
73
+ return s
74
+ }
75
+
76
+ parseValueAsCell(v: OINODataCell, nativeType: string): OINODataCell {
77
+ if (nativeType === "DATETIME" && typeof v === "string" && v !== "") {
78
+ return new Date(v)
79
+ }
80
+ return v
81
+ }
82
+
83
+ // ── NoSQL-specific filter helpers ─────────────────────────────────────
84
+
85
+ /**
86
+ * Test whether a NoSQL entry matches an `OINOQueryFilter` predicate.
87
+ * Used for in-memory (result) filtering when the storage backend cannot
88
+ * translate the predicate to a native query.
89
+ *
90
+ * @param entry nosql entry to test
91
+ * @param filter filter predicate to evaluate
92
+ */
93
+ protected static matchesEntry(entry: OINONoSqlEntry, filter: OINOQueryFilter): boolean {
94
+ if (filter.isEmpty()) return true
95
+
96
+ const op = filter.operator
97
+
98
+ if (op === OINOQueryBooleanOperation.and) {
99
+ return OINONoSql.matchesEntry(entry, filter.leftSide as OINOQueryFilter) &&
100
+ OINONoSql.matchesEntry(entry, filter.rightSide as OINOQueryFilter)
101
+ }
102
+ if (op === OINOQueryBooleanOperation.or) {
103
+ return OINONoSql.matchesEntry(entry, filter.leftSide as OINOQueryFilter) ||
104
+ OINONoSql.matchesEntry(entry, filter.rightSide as OINOQueryFilter)
105
+ }
106
+ if (op === OINOQueryBooleanOperation.not) {
107
+ return !OINONoSql.matchesEntry(entry, filter.rightSide as OINOQueryFilter)
108
+ }
109
+
110
+ const field_name = filter.leftSide as string
111
+ const compare_value = filter.rightSide as string
112
+
113
+ let field_value: string | number | Date | null
114
+ switch (field_name) {
115
+ case "timestamp": field_value = entry.timestamp; break
116
+ case "etag": field_value = entry.etag; break
117
+ default: return true
118
+ }
119
+
120
+ if (op === OINOQueryNullCheck.isnull) return field_value === null
121
+ if (op === OINOQueryNullCheck.isNotNull) return field_value !== null
122
+ if (field_value === null) return false
123
+
124
+ if (field_value instanceof Date) {
125
+ const ms = field_value.getTime()
126
+ const cmp_ms = new Date(compare_value).getTime()
127
+ switch (op) {
128
+ case OINOQueryComparison.lt: return ms < cmp_ms
129
+ case OINOQueryComparison.le: return ms <= cmp_ms
130
+ case OINOQueryComparison.eq: return ms === cmp_ms
131
+ case OINOQueryComparison.ne: return ms !== cmp_ms
132
+ case OINOQueryComparison.ge: return ms >= cmp_ms
133
+ case OINOQueryComparison.gt: return ms > cmp_ms
134
+ default: return true
135
+ }
136
+ }
137
+
138
+ const str_value = String(field_value)
139
+ switch (op) {
140
+ case OINOQueryComparison.lt: return str_value < compare_value
141
+ case OINOQueryComparison.le: return str_value <= compare_value
142
+ case OINOQueryComparison.eq: return str_value === compare_value
143
+ case OINOQueryComparison.ne: return str_value !== compare_value
144
+ case OINOQueryComparison.ge: return str_value >= compare_value
145
+ case OINOQueryComparison.gt: return str_value > compare_value
146
+ case OINOQueryComparison.like: {
147
+ const escaped = compare_value
148
+ .replace(NOSQL_LIKE_ESCAPE_REGEX, "\\$&")
149
+ .replace(NOSQL_LIKE_PERCENT_REGEX, ".*")
150
+ .replace(NOSQL_LIKE_UNDERSCORE_REGEX, ".")
151
+ return new RegExp("^" + escaped + "$", "i").test(str_value)
152
+ }
153
+ default: return true
154
+ }
155
+ }
156
+
157
+ // ── Abstract NoSQL operations ─────────────────────────────────────────
158
+
159
+ /**
160
+ * List all entities in the table, applying storage-level filtering where
161
+ * possible and in-memory result filtering for remaining predicates.
162
+ *
163
+ * @param filter optional query filter to apply
164
+ */
165
+ abstract listEntries(filter?: OINOQueryFilter): Promise<OINONoSqlEntry[]>
166
+
167
+ /**
168
+ * Fetch a single entity by its primary key values.
169
+ *
170
+ * Returns `null` when no entity with the given primary key exists.
171
+ *
172
+ * @param primaryKey ordered primary key values as defined by the implementation's data model
173
+ */
174
+ abstract getEntry(primaryKey: string[]): Promise<OINONoSqlEntry | null>
175
+
176
+ /**
177
+ * Upsert (insert or replace) an entity.
178
+ *
179
+ * @param entry entity to upsert
180
+ */
181
+ abstract upsertEntry(entry: OINONoSqlEntry): Promise<void>
182
+
183
+ /**
184
+ * Upsert (insert or replace) multiple entities in the most efficient way
185
+ * the backend supports. The default implementation loops over
186
+ * `upsertEntry`; override in subclasses that support native batch writes.
187
+ *
188
+ * @param entries entities to upsert
189
+ */
190
+ async upsertEntries(entries: OINONoSqlEntry[]): Promise<void> {
191
+ for (const entry of entries) {
192
+ await this.upsertEntry(entry)
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Delete an entity.
198
+ *
199
+ * @param primaryKey ordered primary key values as defined by the implementation's data model
200
+ */
201
+ abstract deleteEntry(primaryKey: string[]): Promise<void>
202
+ }