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