@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,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
+ }
@@ -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
+ }