@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,33 @@
|
|
|
1
|
+
import { OINODataModel, OINOMemoryDataset } from "@oino-ts/common";
|
|
2
|
+
import { OINOBlobApi } from "./OINOBlobApi.js";
|
|
3
|
+
import { OINOBlobEntry } from "./OINOBlobConstants.js";
|
|
4
|
+
/**
|
|
5
|
+
* Static data model for blob listings.
|
|
6
|
+
*
|
|
7
|
+
* Fields are added by the blob implementation's `initializeApiDatamodel`
|
|
8
|
+
* method, so the exact set depends on what the storage backend supports.
|
|
9
|
+
* The canonical order is:
|
|
10
|
+
* 1. `name` – full blob name (primary key, string)
|
|
11
|
+
* 2. `etag` – entity tag (string)
|
|
12
|
+
* 3. `lastModified` – last modification timestamp (datetime)
|
|
13
|
+
* 4. `contentLength` – size in bytes (number)
|
|
14
|
+
* 5. `contentType` – MIME type (string) – omitted when not supported
|
|
15
|
+
*/
|
|
16
|
+
export declare class OINOBlobDataModel extends OINODataModel {
|
|
17
|
+
/** Reference to the owning blob API */
|
|
18
|
+
readonly blobApi: OINOBlobApi;
|
|
19
|
+
/**
|
|
20
|
+
* Constructor. Fields are added externally by the blob implementation
|
|
21
|
+
* via `initializeApiDatamodel`.
|
|
22
|
+
*
|
|
23
|
+
* @param api the `OINOBlobApi` that owns this data model
|
|
24
|
+
*/
|
|
25
|
+
constructor(api: OINOBlobApi);
|
|
26
|
+
/**
|
|
27
|
+
* Convert an array of blob entries into an in-memory dataset whose
|
|
28
|
+
* columns match the fields present in this model.
|
|
29
|
+
*
|
|
30
|
+
* @param entries blob entries from the storage backend
|
|
31
|
+
*/
|
|
32
|
+
entriesToDataset(entries: OINOBlobEntry[]): OINOMemoryDataset;
|
|
33
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { OINOApiParams } from "@oino-ts/common";
|
|
2
|
+
import { OINOBlobParams, OINOBlobConstructor } from "./OINOBlobConstants.js";
|
|
3
|
+
import { OINOBlob } from "./OINOBlob.js";
|
|
4
|
+
import { OINOBlobApi } from "./OINOBlobApi.js";
|
|
5
|
+
/**
|
|
6
|
+
* Static factory for creating `OINOBlob` instances and `OINOBlobApi` instances
|
|
7
|
+
* from registered provider classes.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* ```ts
|
|
11
|
+
* OINOBlobFactory.registerBlob("OINOBlobAzureTable", OINOBlobAzureTable)
|
|
12
|
+
* const blob = await OINOBlobFactory.createBlob({ type: "OINOBlobAzureTable", ... })
|
|
13
|
+
* const api = await OINOBlobFactory.createApi(blob, { apiName: "files", tableName: "uploads/" })
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare class OINOBlobFactory {
|
|
17
|
+
private static _registry;
|
|
18
|
+
/**
|
|
19
|
+
* Register a blob provider class under the given name.
|
|
20
|
+
*
|
|
21
|
+
* @param name name used in `OINOBlobParams.type`
|
|
22
|
+
* @param blobClass constructor of the provider
|
|
23
|
+
*/
|
|
24
|
+
static registerBlob(name: string, blobClass: OINOBlobConstructor): void;
|
|
25
|
+
/**
|
|
26
|
+
* Create and optionally connect/validate a blob 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 createBlob(params: OINOBlobParams, connect?: boolean, validate?: boolean): Promise<OINOBlob>;
|
|
33
|
+
/**
|
|
34
|
+
* Create an `OINOBlobApi` and initialise its data model.
|
|
35
|
+
*
|
|
36
|
+
* @param blob blob backend to use
|
|
37
|
+
* @param params API parameters (`tableName` is used as the blob prefix)
|
|
38
|
+
*/
|
|
39
|
+
static createApi(blob: OINOBlob, params: OINOApiParams): Promise<OINOBlobApi>;
|
|
40
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { OINOBlob } from "./OINOBlob.js";
|
|
2
|
+
export { OINOBlobDataModel } from "./OINOBlobDataModel.js";
|
|
3
|
+
export { OINOBlobFactory } from "./OINOBlobFactory.js";
|
|
4
|
+
export { OINOBlobApi, OINOBlobApiResult } from "./OINOBlobApi.js";
|
|
5
|
+
export { type OINOBlobConstructor, type OINOBlobParams, type OINOBlobEntry, type OINOBlobFetchResult } from "./OINOBlobConstants.js";
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@oino-ts/blob",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "OINO TS library package for publishing blob 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
|
+
"blob",
|
|
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
|
+
}
|
package/src/OINOBlob.ts
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
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, OINOResult, OINOApi, OINOQueryFilter, OINOQueryBooleanOperation, OINOQueryComparison, OINOQueryNullCheck } from "@oino-ts/common"
|
|
8
|
+
import { OINOBlobParams, OINOBlobEntry, OINOBlobFetchResult } from "./OINOBlobConstants.js"
|
|
9
|
+
|
|
10
|
+
const BLOB_LIKE_ESCAPE_REGEX = /[.*+?^${}()|[\]\\]/g
|
|
11
|
+
const BLOB_LIKE_PERCENT_REGEX = /%/g
|
|
12
|
+
const BLOB_LIKE_UNDERSCORE_REGEX = /_/g
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Abstract base class for blob storage backends. Subclasses implement
|
|
16
|
+
* the two core operations (`listEntries` and `fetchEntry`) for a specific
|
|
17
|
+
* provider (e.g. Azure Blob Storage, S3, …).
|
|
18
|
+
*
|
|
19
|
+
* The SQL-formatting methods inherited from `OINODataSource` are not used
|
|
20
|
+
* by blob operations; they are implemented here as passthrough stubs so
|
|
21
|
+
* that the blob datasource can still be composed with `OINODataField`.
|
|
22
|
+
*/
|
|
23
|
+
export abstract class OINOBlob extends OINODataSource {
|
|
24
|
+
|
|
25
|
+
protected readonly blobParams: OINOBlobParams
|
|
26
|
+
|
|
27
|
+
/** Container / bucket name */
|
|
28
|
+
readonly name: string
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Constructor for `OINOBlob`.
|
|
32
|
+
* @param params blob storage connection parameters
|
|
33
|
+
*/
|
|
34
|
+
constructor(params: OINOBlobParams) {
|
|
35
|
+
super()
|
|
36
|
+
this.blobParams = { ...params }
|
|
37
|
+
this.name = this.blobParams.container
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── OINODataSource passthrough stubs ──────────────────────────────────
|
|
41
|
+
// These are required by the abstract base class but are not meaningful
|
|
42
|
+
// for blob storage. They return sensible no-op values so that
|
|
43
|
+
// OINODataField instances created by OINOBlobDataModel can function
|
|
44
|
+
// correctly for serialisation purposes.
|
|
45
|
+
|
|
46
|
+
printTableName(name: string): string {
|
|
47
|
+
return name
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
printColumnName(name: string): string {
|
|
51
|
+
return name
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
printCellAsValue(cellValue: OINODataCell, _sqlType: string): string {
|
|
55
|
+
if (cellValue === null || cellValue === undefined) {
|
|
56
|
+
return ""
|
|
57
|
+
}
|
|
58
|
+
if (cellValue instanceof Date) {
|
|
59
|
+
return cellValue.toISOString()
|
|
60
|
+
}
|
|
61
|
+
return String(cellValue)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
printStringValue(s: string): string {
|
|
65
|
+
return s
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
parseValueAsCell(v: OINODataCell, nativeType: string): OINODataCell {
|
|
69
|
+
if (nativeType === "DATETIME" && typeof v === "string" && v !== "") {
|
|
70
|
+
return new Date(v)
|
|
71
|
+
}
|
|
72
|
+
return v
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Blob-specific filter helper ───────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Test whether a blob entry matches an `OINOQueryFilter` predicate.
|
|
79
|
+
* Used for in-memory (result) filtering when the storage backend cannot
|
|
80
|
+
* translate the predicate to a native query.
|
|
81
|
+
*
|
|
82
|
+
* @param entry blob entry to test
|
|
83
|
+
* @param filter filter predicate to evaluate
|
|
84
|
+
*/
|
|
85
|
+
protected static matchesEntry(entry: OINOBlobEntry, filter: OINOQueryFilter): boolean {
|
|
86
|
+
if (filter.isEmpty()) return true
|
|
87
|
+
|
|
88
|
+
const op = filter.operator
|
|
89
|
+
|
|
90
|
+
if (op === OINOQueryBooleanOperation.and) {
|
|
91
|
+
return OINOBlob.matchesEntry(entry, filter.leftSide as OINOQueryFilter) &&
|
|
92
|
+
OINOBlob.matchesEntry(entry, filter.rightSide as OINOQueryFilter)
|
|
93
|
+
}
|
|
94
|
+
if (op === OINOQueryBooleanOperation.or) {
|
|
95
|
+
return OINOBlob.matchesEntry(entry, filter.leftSide as OINOQueryFilter) ||
|
|
96
|
+
OINOBlob.matchesEntry(entry, filter.rightSide as OINOQueryFilter)
|
|
97
|
+
}
|
|
98
|
+
if (op === OINOQueryBooleanOperation.not) {
|
|
99
|
+
return !OINOBlob.matchesEntry(entry, filter.rightSide as OINOQueryFilter)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const fieldName = filter.leftSide as string
|
|
103
|
+
const compareValue = filter.rightSide as string
|
|
104
|
+
|
|
105
|
+
let fieldValue: string | number | Date | null
|
|
106
|
+
switch (fieldName) {
|
|
107
|
+
case "name": fieldValue = entry.name; break
|
|
108
|
+
case "etag": fieldValue = entry.etag; break
|
|
109
|
+
case "lastModified": fieldValue = entry.lastModified; break
|
|
110
|
+
case "contentLength": fieldValue = entry.contentLength; break
|
|
111
|
+
case "contentType": fieldValue = entry.contentType; break
|
|
112
|
+
default: return true
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (op === OINOQueryNullCheck.isnull) return fieldValue === null
|
|
116
|
+
if (op === OINOQueryNullCheck.isNotNull) return fieldValue !== null
|
|
117
|
+
if (fieldValue === null) return false
|
|
118
|
+
|
|
119
|
+
if (fieldValue instanceof Date) {
|
|
120
|
+
const ms = fieldValue.getTime()
|
|
121
|
+
const cmpMs = new Date(compareValue).getTime()
|
|
122
|
+
switch (op) {
|
|
123
|
+
case OINOQueryComparison.lt: return ms < cmpMs
|
|
124
|
+
case OINOQueryComparison.le: return ms <= cmpMs
|
|
125
|
+
case OINOQueryComparison.eq: return ms === cmpMs
|
|
126
|
+
case OINOQueryComparison.ne: return ms !== cmpMs
|
|
127
|
+
case OINOQueryComparison.ge: return ms >= cmpMs
|
|
128
|
+
case OINOQueryComparison.gt: return ms > cmpMs
|
|
129
|
+
default: return true
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (typeof fieldValue === "number") {
|
|
134
|
+
const cmpNum = Number(compareValue)
|
|
135
|
+
switch (op) {
|
|
136
|
+
case OINOQueryComparison.lt: return fieldValue < cmpNum
|
|
137
|
+
case OINOQueryComparison.le: return fieldValue <= cmpNum
|
|
138
|
+
case OINOQueryComparison.eq: return fieldValue === cmpNum
|
|
139
|
+
case OINOQueryComparison.ne: return fieldValue !== cmpNum
|
|
140
|
+
case OINOQueryComparison.ge: return fieldValue >= cmpNum
|
|
141
|
+
case OINOQueryComparison.gt: return fieldValue > cmpNum
|
|
142
|
+
default: return true
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const strValue = String(fieldValue)
|
|
147
|
+
switch (op) {
|
|
148
|
+
case OINOQueryComparison.lt: return strValue < compareValue
|
|
149
|
+
case OINOQueryComparison.le: return strValue <= compareValue
|
|
150
|
+
case OINOQueryComparison.eq: return strValue === compareValue
|
|
151
|
+
case OINOQueryComparison.ne: return strValue !== compareValue
|
|
152
|
+
case OINOQueryComparison.ge: return strValue >= compareValue
|
|
153
|
+
case OINOQueryComparison.gt: return strValue > compareValue
|
|
154
|
+
case OINOQueryComparison.like: {
|
|
155
|
+
const escaped = compareValue
|
|
156
|
+
.replace(BLOB_LIKE_ESCAPE_REGEX, "\\$&")
|
|
157
|
+
.replace(BLOB_LIKE_PERCENT_REGEX, ".*")
|
|
158
|
+
.replace(BLOB_LIKE_UNDERSCORE_REGEX, ".")
|
|
159
|
+
return new RegExp("^" + escaped + "$", "i").test(strValue)
|
|
160
|
+
}
|
|
161
|
+
default: return true
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Extract a blob/object name prefix from the filter that can be forwarded
|
|
167
|
+
* to the storage backend as a server-side query optimisation.
|
|
168
|
+
*
|
|
169
|
+
* Only two cases translate to a prefix:
|
|
170
|
+
* - `(name)-eq(value)` → exact name match (use as prefix)
|
|
171
|
+
* - `(name)-like(prefix%)` → trailing-wildcard prefix match
|
|
172
|
+
*
|
|
173
|
+
* AND-combined filters are explored recursively so that a name constraint
|
|
174
|
+
* nested inside a larger AND predicate is still extracted.
|
|
175
|
+
*
|
|
176
|
+
* @param filter filter to inspect
|
|
177
|
+
*/
|
|
178
|
+
protected static extractNamePrefix(filter: OINOQueryFilter): string | undefined {
|
|
179
|
+
if (filter.isEmpty()) return undefined
|
|
180
|
+
|
|
181
|
+
const op = filter.operator
|
|
182
|
+
|
|
183
|
+
if (typeof filter.leftSide === "string" && filter.leftSide === "name") {
|
|
184
|
+
if (op === OINOQueryComparison.eq) {
|
|
185
|
+
return filter.rightSide as string
|
|
186
|
+
}
|
|
187
|
+
if (op === OINOQueryComparison.like) {
|
|
188
|
+
const pattern = filter.rightSide as string
|
|
189
|
+
const body = pattern.slice(0, -1)
|
|
190
|
+
if (pattern.endsWith("%") && !body.includes("%") && !body.includes("_")) {
|
|
191
|
+
return body
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (op === OINOQueryBooleanOperation.and) {
|
|
197
|
+
return OINOBlob.extractNamePrefix(filter.leftSide as OINOQueryFilter) ??
|
|
198
|
+
OINOBlob.extractNamePrefix(filter.rightSide as OINOQueryFilter)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return undefined
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── Blob-specific abstract interface ──────────────────────────────────
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* List blob entries, optionally filtered by a query filter. Implementations
|
|
208
|
+
* should apply native query filtering where possible and fall back to
|
|
209
|
+
* in-memory result filtering for predicates that cannot be expressed as a
|
|
210
|
+
* native query.
|
|
211
|
+
*
|
|
212
|
+
* @param filter optional query filter to apply to the results
|
|
213
|
+
*/
|
|
214
|
+
abstract listEntries(filter?: OINOQueryFilter): Promise<OINOBlobEntry[]>
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Fetch the binary content and content-type of a named blob.
|
|
218
|
+
*
|
|
219
|
+
* @param name full blob name (path within the container)
|
|
220
|
+
*/
|
|
221
|
+
abstract fetchEntry(name: string): Promise<OINOBlobFetchResult>
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Upload (create or replace) a blob with the given binary content.
|
|
225
|
+
*
|
|
226
|
+
* @param name full blob name (path within the container)
|
|
227
|
+
* @param content binary content to store
|
|
228
|
+
* @param contentType MIME type of the content (e.g. `"image/jpeg"`)
|
|
229
|
+
*/
|
|
230
|
+
abstract uploadEntry(name: string, content: Uint8Array, contentType: string): Promise<void>
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Delete a named blob.
|
|
234
|
+
*
|
|
235
|
+
* @param name full blob name (path within the container)
|
|
236
|
+
*/
|
|
237
|
+
abstract deleteEntry(name: string): Promise<void>
|
|
238
|
+
}
|