@oino-ts/blob 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,194 @@
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.OINOBlob = void 0;
9
+ const common_1 = require("@oino-ts/common");
10
+ const BLOB_LIKE_ESCAPE_REGEX = /[.*+?^${}()|[\]\\]/g;
11
+ const BLOB_LIKE_PERCENT_REGEX = /%/g;
12
+ const BLOB_LIKE_UNDERSCORE_REGEX = /_/g;
13
+ /**
14
+ * Abstract base class for blob storage backends. Subclasses implement
15
+ * the two core operations (`listEntries` and `fetchEntry`) for a specific
16
+ * provider (e.g. Azure Blob Storage, S3, …).
17
+ *
18
+ * The SQL-formatting methods inherited from `OINODataSource` are not used
19
+ * by blob operations; they are implemented here as passthrough stubs so
20
+ * that the blob datasource can still be composed with `OINODataField`.
21
+ */
22
+ class OINOBlob extends common_1.OINODataSource {
23
+ blobParams;
24
+ /** Container / bucket name */
25
+ name;
26
+ /**
27
+ * Constructor for `OINOBlob`.
28
+ * @param params blob storage connection parameters
29
+ */
30
+ constructor(params) {
31
+ super();
32
+ this.blobParams = { ...params };
33
+ this.name = this.blobParams.container;
34
+ }
35
+ // ── OINODataSource passthrough stubs ──────────────────────────────────
36
+ // These are required by the abstract base class but are not meaningful
37
+ // for blob storage. They return sensible no-op values so that
38
+ // OINODataField instances created by OINOBlobDataModel can function
39
+ // correctly for serialisation purposes.
40
+ printTableName(name) {
41
+ return name;
42
+ }
43
+ printColumnName(name) {
44
+ return name;
45
+ }
46
+ printCellAsValue(cellValue, _sqlType) {
47
+ if (cellValue === null || cellValue === undefined) {
48
+ return "";
49
+ }
50
+ if (cellValue instanceof Date) {
51
+ return cellValue.toISOString();
52
+ }
53
+ return String(cellValue);
54
+ }
55
+ printStringValue(s) {
56
+ return s;
57
+ }
58
+ parseValueAsCell(v, nativeType) {
59
+ if (nativeType === "DATETIME" && typeof v === "string" && v !== "") {
60
+ return new Date(v);
61
+ }
62
+ return v;
63
+ }
64
+ // ── Blob-specific filter helper ───────────────────────────────────────
65
+ /**
66
+ * Test whether a blob entry matches an `OINOQueryFilter` predicate.
67
+ * Used for in-memory (result) filtering when the storage backend cannot
68
+ * translate the predicate to a native query.
69
+ *
70
+ * @param entry blob entry to test
71
+ * @param filter filter predicate to evaluate
72
+ */
73
+ static matchesEntry(entry, filter) {
74
+ if (filter.isEmpty())
75
+ return true;
76
+ const op = filter.operator;
77
+ if (op === common_1.OINOQueryBooleanOperation.and) {
78
+ return OINOBlob.matchesEntry(entry, filter.leftSide) &&
79
+ OINOBlob.matchesEntry(entry, filter.rightSide);
80
+ }
81
+ if (op === common_1.OINOQueryBooleanOperation.or) {
82
+ return OINOBlob.matchesEntry(entry, filter.leftSide) ||
83
+ OINOBlob.matchesEntry(entry, filter.rightSide);
84
+ }
85
+ if (op === common_1.OINOQueryBooleanOperation.not) {
86
+ return !OINOBlob.matchesEntry(entry, filter.rightSide);
87
+ }
88
+ const fieldName = filter.leftSide;
89
+ const compareValue = filter.rightSide;
90
+ let fieldValue;
91
+ switch (fieldName) {
92
+ case "name":
93
+ fieldValue = entry.name;
94
+ break;
95
+ case "etag":
96
+ fieldValue = entry.etag;
97
+ break;
98
+ case "lastModified":
99
+ fieldValue = entry.lastModified;
100
+ break;
101
+ case "contentLength":
102
+ fieldValue = entry.contentLength;
103
+ break;
104
+ case "contentType":
105
+ fieldValue = entry.contentType;
106
+ break;
107
+ default: return true;
108
+ }
109
+ if (op === common_1.OINOQueryNullCheck.isnull)
110
+ return fieldValue === null;
111
+ if (op === common_1.OINOQueryNullCheck.isNotNull)
112
+ return fieldValue !== null;
113
+ if (fieldValue === null)
114
+ return false;
115
+ if (fieldValue instanceof Date) {
116
+ const ms = fieldValue.getTime();
117
+ const cmpMs = new Date(compareValue).getTime();
118
+ switch (op) {
119
+ case common_1.OINOQueryComparison.lt: return ms < cmpMs;
120
+ case common_1.OINOQueryComparison.le: return ms <= cmpMs;
121
+ case common_1.OINOQueryComparison.eq: return ms === cmpMs;
122
+ case common_1.OINOQueryComparison.ne: return ms !== cmpMs;
123
+ case common_1.OINOQueryComparison.ge: return ms >= cmpMs;
124
+ case common_1.OINOQueryComparison.gt: return ms > cmpMs;
125
+ default: return true;
126
+ }
127
+ }
128
+ if (typeof fieldValue === "number") {
129
+ const cmpNum = Number(compareValue);
130
+ switch (op) {
131
+ case common_1.OINOQueryComparison.lt: return fieldValue < cmpNum;
132
+ case common_1.OINOQueryComparison.le: return fieldValue <= cmpNum;
133
+ case common_1.OINOQueryComparison.eq: return fieldValue === cmpNum;
134
+ case common_1.OINOQueryComparison.ne: return fieldValue !== cmpNum;
135
+ case common_1.OINOQueryComparison.ge: return fieldValue >= cmpNum;
136
+ case common_1.OINOQueryComparison.gt: return fieldValue > cmpNum;
137
+ default: return true;
138
+ }
139
+ }
140
+ const strValue = String(fieldValue);
141
+ switch (op) {
142
+ case common_1.OINOQueryComparison.lt: return strValue < compareValue;
143
+ case common_1.OINOQueryComparison.le: return strValue <= compareValue;
144
+ case common_1.OINOQueryComparison.eq: return strValue === compareValue;
145
+ case common_1.OINOQueryComparison.ne: return strValue !== compareValue;
146
+ case common_1.OINOQueryComparison.ge: return strValue >= compareValue;
147
+ case common_1.OINOQueryComparison.gt: return strValue > compareValue;
148
+ case common_1.OINOQueryComparison.like: {
149
+ const escaped = compareValue
150
+ .replace(BLOB_LIKE_ESCAPE_REGEX, "\\$&")
151
+ .replace(BLOB_LIKE_PERCENT_REGEX, ".*")
152
+ .replace(BLOB_LIKE_UNDERSCORE_REGEX, ".");
153
+ return new RegExp("^" + escaped + "$", "i").test(strValue);
154
+ }
155
+ default: return true;
156
+ }
157
+ }
158
+ /**
159
+ * Extract a blob/object name prefix from the filter that can be forwarded
160
+ * to the storage backend as a server-side query optimisation.
161
+ *
162
+ * Only two cases translate to a prefix:
163
+ * - `(name)-eq(value)` → exact name match (use as prefix)
164
+ * - `(name)-like(prefix%)` → trailing-wildcard prefix match
165
+ *
166
+ * AND-combined filters are explored recursively so that a name constraint
167
+ * nested inside a larger AND predicate is still extracted.
168
+ *
169
+ * @param filter filter to inspect
170
+ */
171
+ static extractNamePrefix(filter) {
172
+ if (filter.isEmpty())
173
+ return undefined;
174
+ const op = filter.operator;
175
+ if (typeof filter.leftSide === "string" && filter.leftSide === "name") {
176
+ if (op === common_1.OINOQueryComparison.eq) {
177
+ return filter.rightSide;
178
+ }
179
+ if (op === common_1.OINOQueryComparison.like) {
180
+ const pattern = filter.rightSide;
181
+ const body = pattern.slice(0, -1);
182
+ if (pattern.endsWith("%") && !body.includes("%") && !body.includes("_")) {
183
+ return body;
184
+ }
185
+ }
186
+ }
187
+ if (op === common_1.OINOQueryBooleanOperation.and) {
188
+ return OINOBlob.extractNamePrefix(filter.leftSide) ??
189
+ OINOBlob.extractNamePrefix(filter.rightSide);
190
+ }
191
+ return undefined;
192
+ }
193
+ }
194
+ exports.OINOBlob = OINOBlob;
@@ -0,0 +1,180 @@
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.OINOBlobApi = exports.OINOBlobApiResult = void 0;
9
+ const common_1 = require("@oino-ts/common");
10
+ class OINOBlobApiResult extends common_1.OINOApiResult {
11
+ /** Binary content of the blob (for GET with id) */
12
+ blobData;
13
+ /** Content-Type of the blob (for GET with id) */
14
+ blobDataType;
15
+ constructor(request, data, blobData, blobDataType) {
16
+ super(request, data);
17
+ this.blobData = blobData;
18
+ this.blobDataType = blobDataType;
19
+ }
20
+ async writeApiResponse(headers = {}) {
21
+ if (this.blobData) {
22
+ const response_headers = new Headers(headers);
23
+ response_headers.set("Content-Length", this.blobData.length.toString());
24
+ if (this.blobDataType) {
25
+ response_headers.set("Content-Type", this.blobDataType);
26
+ }
27
+ if (this.request.responseDownload) {
28
+ response_headers.set("Content-Disposition", `attachment; filename="${this.request.responseDownload}"`);
29
+ }
30
+ else {
31
+ response_headers.set("Content-Disposition", "inline");
32
+ }
33
+ return new Response(this.blobData, {
34
+ status: this.status,
35
+ statusText: this.statusText,
36
+ headers: response_headers
37
+ });
38
+ }
39
+ else {
40
+ return super.writeApiResponse(headers);
41
+ }
42
+ }
43
+ }
44
+ exports.OINOBlobApiResult = OINOBlobApiResult;
45
+ /**
46
+ * REST API for blob storage.
47
+ *
48
+ * Supports two GET variants:
49
+ * - **GET without id** – lists all blobs under the configured prefix and
50
+ * returns the metadata as JSON (or CSV) using `OINOModelSet`.
51
+ * - **GET with id** – downloads the named blob as a binary HTTP response
52
+ * with the blob's own `Content-Type`.
53
+ *
54
+ * All other HTTP methods return `405 Method Not Allowed`.
55
+ */
56
+ class OINOBlobApi extends common_1.OINOApi {
57
+ /** Blob storage backend */
58
+ blob;
59
+ /** Blob-specific data model (populated by `initializeDatamodel`) */
60
+ blobDatamodel = null;
61
+ /**
62
+ * Constructor.
63
+ *
64
+ * NOTE: `initializeDatamodel` (or `OINOBlobFactory.createApi`) must be
65
+ * called before the first request is dispatched.
66
+ *
67
+ * @param blob blob storage backend
68
+ * @param params API parameters (`tableName` is used as the blob prefix)
69
+ */
70
+ constructor(blob, params) {
71
+ super(blob, params);
72
+ this.blob = blob;
73
+ }
74
+ /**
75
+ * Attach the static blob data model and mark the API as initialised.
76
+ *
77
+ * @param datamodel `OINOBlobDataModel` instance for this API
78
+ */
79
+ initializeDatamodel(datamodel) {
80
+ this.blobDatamodel = datamodel;
81
+ this.datamodel = datamodel;
82
+ this.initialized = true;
83
+ }
84
+ // ── OINOApi abstract implementations ─────────────────────────────────
85
+ async doApiRequest(request) {
86
+ if (!this.initialized) {
87
+ throw new Error(common_1.OINO_ERROR_PREFIX + ": OINOBlobApi is not initialized yet!");
88
+ }
89
+ common_1.OINOLog.debug("@oino-ts/blob", "OINOBlobApi", "doApiRequest", "Request", { method: request.method, id: request.rowId });
90
+ const result = new OINOBlobApiResult(request);
91
+ if (request.method === "GET") {
92
+ if (!request.rowId) {
93
+ // ── List blobs ───────────────────────────────────────────────
94
+ try {
95
+ const entries = await this.blob.listEntries(request.queryParams?.filter);
96
+ const dataset = this.blobDatamodel.entriesToDataset(entries);
97
+ result.data = new common_1.OINOModelSet(this.datamodel, dataset, request.queryParams);
98
+ }
99
+ catch (e) {
100
+ result.setError(500, "Error listing blobs: " + e.message, "DoGet");
101
+ common_1.OINOLog.exception("@oino-ts/blob", "OINOBlobApi", "doApiRequest", "exception in list request", { message: e.message, stack: e.stack });
102
+ }
103
+ }
104
+ else {
105
+ // ── Download blob ────────────────────────────────────────────
106
+ try {
107
+ const name = decodeURIComponent(request.rowId);
108
+ const fetch_result = await this.blob.fetchEntry(name);
109
+ result.blobData = fetch_result.content;
110
+ result.blobDataType = fetch_result.contentType;
111
+ }
112
+ catch (e) {
113
+ result.setError(500, "Error fetching blob: " + e.message, "DoGet");
114
+ common_1.OINOLog.exception("@oino-ts/blob", "OINOBlobApi", "doApiRequest", "exception in fetch request", { message: e.message, stack: e.stack });
115
+ }
116
+ }
117
+ }
118
+ else if (request.method === "POST" || request.method === "PUT") {
119
+ if (!request.rowId) {
120
+ result.setError(400, "HTTP " + request.method + " method requires an URL ID (blob name)!", "DoRequest");
121
+ }
122
+ else {
123
+ try {
124
+ const name = decodeURIComponent(request.rowId);
125
+ const content_type = request.headers.get("content-type") ?? "application/octet-stream";
126
+ const data = request.rowData;
127
+ const content = data instanceof Uint8Array ? data : request.bodyAsBuffer();
128
+ await this.blob.uploadEntry(name, content, content_type);
129
+ }
130
+ catch (e) {
131
+ result.setError(500, "Error uploading blob: " + e.message, "DoPost");
132
+ common_1.OINOLog.exception("@oino-ts/blob", "OINOBlobApi", "doApiRequest", "exception in upload request", { message: e.message, stack: e.stack });
133
+ }
134
+ }
135
+ }
136
+ else if (request.method === "DELETE") {
137
+ if (!request.rowId) {
138
+ result.setError(400, "HTTP DELETE method requires an URL ID (blob name)!", "DoRequest");
139
+ }
140
+ else {
141
+ try {
142
+ const name = decodeURIComponent(request.rowId);
143
+ await this.blob.deleteEntry(name);
144
+ }
145
+ catch (e) {
146
+ result.setError(500, "Error deleting blob: " + e.message, "DoDelete");
147
+ common_1.OINOLog.exception("@oino-ts/blob", "OINOBlobApi", "doApiRequest", "exception in delete request", { message: e.message, stack: e.stack });
148
+ }
149
+ }
150
+ }
151
+ else {
152
+ result.setError(405, "Unsupported HTTP method '" + request.method + "' for OINOBlobApi", "DoRequest");
153
+ }
154
+ return result;
155
+ }
156
+ async doHttpRequest(request, rowId, rowData, queryParams) {
157
+ const api_request = common_1.OINOApiRequest.fromHttpRequest(request, rowId, rowData, queryParams);
158
+ return this.doApiRequest(api_request);
159
+ }
160
+ async doRequest(method, rowId, rowData, queryParams, contentType = common_1.OINOContentType.json) {
161
+ return this.doApiRequest(new common_1.OINOApiRequest({
162
+ method,
163
+ rowId,
164
+ rowData,
165
+ queryParams,
166
+ requestType: contentType
167
+ }));
168
+ }
169
+ async doBatchUpdate(method, _rowId, _rowData, _queryParams) {
170
+ const result = new common_1.OINOApiResult(new common_1.OINOApiRequest({ method }));
171
+ result.setError(405, "OINOBlobApi does not support batch updates", "DoBatchUpdate");
172
+ return result;
173
+ }
174
+ async doBatchApiRequest(request) {
175
+ const result = new common_1.OINOApiResult(request);
176
+ result.setError(405, "OINOBlobApi does not support batch updates", "DoBatchApiRequest");
177
+ return result;
178
+ }
179
+ }
180
+ exports.OINOBlobApi = OINOBlobApi;
@@ -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,69 @@
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.OINOBlobDataModel = void 0;
9
+ const common_1 = require("@oino-ts/common");
10
+ /**
11
+ * Static data model for blob listings.
12
+ *
13
+ * Fields are added by the blob implementation's `initializeApiDatamodel`
14
+ * method, so the exact set depends on what the storage backend supports.
15
+ * The canonical order is:
16
+ * 1. `name` – full blob name (primary key, string)
17
+ * 2. `etag` – entity tag (string)
18
+ * 3. `lastModified` – last modification timestamp (datetime)
19
+ * 4. `contentLength` – size in bytes (number)
20
+ * 5. `contentType` – MIME type (string) – omitted when not supported
21
+ */
22
+ class OINOBlobDataModel extends common_1.OINODataModel {
23
+ /** Reference to the owning blob API */
24
+ blobApi;
25
+ /**
26
+ * Constructor. Fields are added externally by the blob implementation
27
+ * via `initializeApiDatamodel`.
28
+ *
29
+ * @param api the `OINOBlobApi` that owns this data model
30
+ */
31
+ constructor(api) {
32
+ super(api);
33
+ this.blobApi = api;
34
+ }
35
+ /**
36
+ * Convert an array of blob entries into an in-memory dataset whose
37
+ * columns match the fields present in this model.
38
+ *
39
+ * @param entries blob entries from the storage backend
40
+ */
41
+ entriesToDataset(entries) {
42
+ const fieldNames = this.fields.map(f => f.name);
43
+ const rows = entries.map(e => {
44
+ const row = [];
45
+ for (const name of fieldNames) {
46
+ switch (name) {
47
+ case "name":
48
+ row.push(e.name);
49
+ break;
50
+ case "etag":
51
+ row.push(e.etag);
52
+ break;
53
+ case "lastModified":
54
+ row.push(e.lastModified);
55
+ break;
56
+ case "contentLength":
57
+ row.push(e.contentLength);
58
+ break;
59
+ case "contentType":
60
+ row.push(e.contentType);
61
+ break;
62
+ }
63
+ }
64
+ return row;
65
+ });
66
+ return new common_1.OINOMemoryDataset(rows);
67
+ }
68
+ }
69
+ exports.OINOBlobDataModel = OINOBlobDataModel;
@@ -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.OINOBlobFactory = void 0;
9
+ const OINOBlobApi_js_1 = require("./OINOBlobApi.js");
10
+ /**
11
+ * Static factory for creating `OINOBlob` instances and `OINOBlobApi` instances
12
+ * from registered provider classes.
13
+ *
14
+ * Usage:
15
+ * ```ts
16
+ * OINOBlobFactory.registerBlob("OINOBlobAzureTable", OINOBlobAzureTable)
17
+ * const blob = await OINOBlobFactory.createBlob({ type: "OINOBlobAzureTable", ... })
18
+ * const api = await OINOBlobFactory.createApi(blob, { apiName: "files", tableName: "uploads/" })
19
+ * ```
20
+ */
21
+ class OINOBlobFactory {
22
+ static _registry = {};
23
+ /**
24
+ * Register a blob provider class under the given name.
25
+ *
26
+ * @param name name used in `OINOBlobParams.type`
27
+ * @param blobClass constructor of the provider
28
+ */
29
+ static registerBlob(name, blobClass) {
30
+ this._registry[name] = blobClass;
31
+ }
32
+ /**
33
+ * Create and optionally connect/validate a blob 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 createBlob(params, connect = true, validate = true) {
40
+ const BlobClass = this._registry[params.type];
41
+ if (!BlobClass) {
42
+ throw new Error("Unsupported blob type: " + params.type);
43
+ }
44
+ const blob = new BlobClass(params);
45
+ if (connect) {
46
+ const connect_res = await blob.connect();
47
+ if (!connect_res.success) {
48
+ throw new Error("Blob connection failed: " + connect_res.statusText);
49
+ }
50
+ }
51
+ if (validate) {
52
+ const validate_res = await blob.validate();
53
+ if (!validate_res.success) {
54
+ throw new Error("Blob validation failed: " + validate_res.statusText);
55
+ }
56
+ }
57
+ return blob;
58
+ }
59
+ /**
60
+ * Create an `OINOBlobApi` and initialise its data model.
61
+ *
62
+ * @param blob blob backend to use
63
+ * @param params API parameters (`tableName` is used as the blob prefix)
64
+ */
65
+ static async createApi(blob, params) {
66
+ const api = new OINOBlobApi_js_1.OINOBlobApi(blob, params);
67
+ await blob.initializeApiDatamodel(api);
68
+ return api;
69
+ }
70
+ }
71
+ exports.OINOBlobFactory = OINOBlobFactory;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OINOBlobApiResult = exports.OINOBlobApi = exports.OINOBlobFactory = exports.OINOBlobDataModel = exports.OINOBlob = void 0;
4
+ var OINOBlob_js_1 = require("./OINOBlob.js");
5
+ Object.defineProperty(exports, "OINOBlob", { enumerable: true, get: function () { return OINOBlob_js_1.OINOBlob; } });
6
+ var OINOBlobDataModel_js_1 = require("./OINOBlobDataModel.js");
7
+ Object.defineProperty(exports, "OINOBlobDataModel", { enumerable: true, get: function () { return OINOBlobDataModel_js_1.OINOBlobDataModel; } });
8
+ var OINOBlobFactory_js_1 = require("./OINOBlobFactory.js");
9
+ Object.defineProperty(exports, "OINOBlobFactory", { enumerable: true, get: function () { return OINOBlobFactory_js_1.OINOBlobFactory; } });
10
+ var OINOBlobApi_js_1 = require("./OINOBlobApi.js");
11
+ Object.defineProperty(exports, "OINOBlobApi", { enumerable: true, get: function () { return OINOBlobApi_js_1.OINOBlobApi; } });
12
+ Object.defineProperty(exports, "OINOBlobApiResult", { enumerable: true, get: function () { return OINOBlobApi_js_1.OINOBlobApiResult; } });