@oino-ts/blob-azure 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,208 @@
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.OINOBlobAzureTable = void 0;
9
+ const node_buffer_1 = require("node:buffer");
10
+ const storage_blob_1 = require("@azure/storage-blob");
11
+ const common_1 = require("@oino-ts/common");
12
+ const blob_1 = require("@oino-ts/blob");
13
+ /**
14
+ * Azure Blob Storage implementation of `OINOBlob`.
15
+ *
16
+ * Authenticates using an Azure Storage connection string. Connection parameters map as:
17
+ * - `params.url` → blob service endpoint, e.g. `https://<account>.blob.core.windows.net`
18
+ * - `params.container` → container name
19
+ * - `params.connectionStr` → Azure Storage connection string (e.g. `DefaultEndpointsProtocol=https;AccountName=...`)
20
+ *
21
+ * Register and use via the factory:
22
+ * ```ts
23
+ * import { OINOBlobFactory } from "@oino-ts/blob"
24
+ * import { OINOBlobAzureTable } from "@oino-ts/blob-azure"
25
+ *
26
+ * OINOBlobFactory.registerBlob("OINOBlobAzureTable", OINOBlobAzureTable)
27
+ *
28
+ * const blob = await OINOBlobFactory.createBlob({
29
+ * type: "OINOBlobAzureTable",
30
+ * url: "https://myaccount.blob.core.windows.net",
31
+ * container: "my-container",
32
+ * connectionStr: process.env.AZURE_STORAGE_CONNECTION_STRING
33
+ * })
34
+ * const api = await OINOBlobFactory.createApi(blob, {
35
+ * apiName: "files",
36
+ * tableName: "uploads/" // blob prefix / folder
37
+ * })
38
+ * ```
39
+ */
40
+ class OINOBlobAzureTable extends blob_1.OINOBlob {
41
+ _containerClient = null;
42
+ // ── OINODataSource lifecycle ──────────────────────────────────────────
43
+ /**
44
+ * Initialise the Azure SDK client. Does not perform any network call.
45
+ */
46
+ async connect() {
47
+ const result = new common_1.OINOResult();
48
+ try {
49
+ let serviceClient;
50
+ if (this.blobParams.connectionStr) {
51
+ serviceClient = storage_blob_1.BlobServiceClient.fromConnectionString(this.blobParams.connectionStr);
52
+ }
53
+ else {
54
+ return new common_1.OINOResult({
55
+ success: false,
56
+ status: 400,
57
+ statusText: "OINOBlobAzureTable: params.connectionStr is required"
58
+ });
59
+ }
60
+ this._containerClient = serviceClient.getContainerClient(this.blobParams.container);
61
+ this.isConnected = true;
62
+ }
63
+ catch (e) {
64
+ return new common_1.OINOResult({ success: false, status: 500, statusText: "OINOBlobAzureTable connect failed: " + e.message });
65
+ }
66
+ return result;
67
+ }
68
+ /**
69
+ * Verify that the target container exists and is accessible.
70
+ */
71
+ async validate() {
72
+ if (!this._containerClient) {
73
+ return new common_1.OINOResult({ success: false, status: 500, statusText: "OINOBlobAzureTable: not connected" });
74
+ }
75
+ try {
76
+ const exists = await this._containerClient.exists();
77
+ if (!exists) {
78
+ return new common_1.OINOResult({
79
+ success: false,
80
+ status: 404,
81
+ statusText: "OINOBlobAzureTable: container '" + this.blobParams.container + "' not found"
82
+ });
83
+ }
84
+ this.isValidated = true;
85
+ }
86
+ catch (e) {
87
+ return new common_1.OINOResult({ success: false, status: 500, statusText: "OINOBlobAzureTable validate failed: " + e.message });
88
+ }
89
+ return new common_1.OINOResult();
90
+ }
91
+ /**
92
+ * Release the client reference (Azure SDK is stateless per-request so nothing to close).
93
+ */
94
+ async disconnect() {
95
+ this._containerClient = null;
96
+ this.isConnected = false;
97
+ this.isValidated = false;
98
+ }
99
+ // ── OINOBlob operations ───────────────────────────────────────────────
100
+ /**
101
+ * List all blobs, applying native Azure query filtering where possible and
102
+ * in-memory result filtering for predicates that cannot be expressed as a
103
+ * native query.
104
+ *
105
+ * - The `name` field supports server-side prefix filtering via the Azure
106
+ * `listBlobsFlat` `prefix` option (query filtering).
107
+ * - All other field predicates (`etag`, `lastModified`, `contentLength`,
108
+ * `contentType`) are evaluated in-memory after the listing (result
109
+ * filtering).
110
+ *
111
+ * @param filter optional query filter to apply
112
+ */
113
+ async listEntries(filter) {
114
+ if (!this._containerClient) {
115
+ throw new Error("OINOBlobAzureTable: not connected");
116
+ }
117
+ const queryPrefix = (filter && !filter.isEmpty())
118
+ ? blob_1.OINOBlob.extractNamePrefix(filter)
119
+ : undefined;
120
+ const entries = [];
121
+ for await (const blob of this._containerClient.listBlobsFlat({ prefix: queryPrefix })) {
122
+ entries.push({
123
+ name: blob.name,
124
+ etag: blob.properties.etag ?? "",
125
+ lastModified: blob.properties.lastModified,
126
+ contentLength: blob.properties.contentLength ?? 0,
127
+ contentType: blob.properties.contentType ?? "application/octet-stream"
128
+ });
129
+ }
130
+ if (!filter || filter.isEmpty()) {
131
+ return entries;
132
+ }
133
+ return entries.filter(e => blob_1.OINOBlob.matchesEntry(e, filter));
134
+ }
135
+ /**
136
+ * Download the raw content of a named blob.
137
+ *
138
+ * @param name full blob name (path within the container)
139
+ */
140
+ async fetchEntry(name) {
141
+ if (!this._containerClient) {
142
+ throw new Error("OINOBlobAzureTable: not connected");
143
+ }
144
+ const blobClient = this._containerClient.getBlobClient(name);
145
+ const downloadResponse = await blobClient.download(0);
146
+ const contentType = downloadResponse.contentType ?? "application/octet-stream";
147
+ const stream = downloadResponse.readableStreamBody;
148
+ if (!stream) {
149
+ throw new Error("OINOBlobAzureTable: no readable stream returned for blob '" + name + "'");
150
+ }
151
+ const chunks = [];
152
+ for await (const chunk of stream) {
153
+ chunks.push(chunk instanceof node_buffer_1.Buffer ? chunk : node_buffer_1.Buffer.from(chunk));
154
+ }
155
+ return {
156
+ content: new Uint8Array(node_buffer_1.Buffer.concat(chunks)),
157
+ contentType
158
+ };
159
+ }
160
+ /**
161
+ * Upload (create or replace) a blob with the given binary content.
162
+ *
163
+ * @param name full blob name (path within the container)
164
+ * @param content binary content to store
165
+ * @param contentType MIME type of the content (e.g. `"image/jpeg"`)
166
+ */
167
+ async uploadEntry(name, content, contentType) {
168
+ if (!this._containerClient) {
169
+ throw new Error("OINOBlobAzureTable: not connected");
170
+ }
171
+ const blockBlobClient = this._containerClient.getBlockBlobClient(name);
172
+ const headers = { blobDataType: contentType };
173
+ await blockBlobClient.upload(content, content.length, { blobHTTPHeaders: headers });
174
+ }
175
+ /**
176
+ * Delete a named blob.
177
+ *
178
+ * @param name full blob name (path within the container)
179
+ */
180
+ async deleteEntry(name) {
181
+ if (!this._containerClient) {
182
+ throw new Error("OINOBlobAzureTable: not connected");
183
+ }
184
+ const blobClient = this._containerClient.getBlobClient(name);
185
+ await blobClient.delete();
186
+ }
187
+ // ── OINODataSource datamodel initialisation ───────────────────────────
188
+ /**
189
+ * Attach a static `OINOBlobDataModel` to the given API, adding all five
190
+ * standard fields that Azure Blob Storage returns in a listing.
191
+ *
192
+ * @param api the `OINOBlobApi` whose data model is to be initialised
193
+ */
194
+ async initializeApiDatamodel(api) {
195
+ const blobApi = api;
196
+ const datamodel = new blob_1.OINOBlobDataModel(blobApi);
197
+ const ds = this;
198
+ const FIELD = { isPrimaryKey: false, isForeignKey: false, isAutoInc: false, isNotNull: false };
199
+ const PK = { isPrimaryKey: true, isForeignKey: false, isAutoInc: false, isNotNull: true };
200
+ datamodel.addField(new common_1.OINOStringDataField(ds, "name", "TEXT", PK, 1024));
201
+ datamodel.addField(new common_1.OINOStringDataField(ds, "etag", "TEXT", FIELD, 256));
202
+ datamodel.addField(new common_1.OINODatetimeDataField(ds, "lastModified", "DATETIME", FIELD));
203
+ datamodel.addField(new common_1.OINONumberDataField(ds, "contentLength", "INTEGER", FIELD));
204
+ datamodel.addField(new common_1.OINOStringDataField(ds, "contentType", "TEXT", FIELD, 256));
205
+ blobApi.initializeDatamodel(datamodel);
206
+ }
207
+ }
208
+ exports.OINOBlobAzureTable = OINOBlobAzureTable;
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OINOBlobAzureTable = void 0;
4
+ var OINOBlobAzureTable_js_1 = require("./OINOBlobAzureTable.js");
5
+ Object.defineProperty(exports, "OINOBlobAzureTable", { enumerable: true, get: function () { return OINOBlobAzureTable_js_1.OINOBlobAzureTable; } });
@@ -0,0 +1,204 @@
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 { Buffer } from "node:buffer";
7
+ import { BlobServiceClient } from "@azure/storage-blob";
8
+ import { OINOResult, OINOStringDataField, OINONumberDataField, OINODatetimeDataField } from "@oino-ts/common";
9
+ import { OINOBlob, OINOBlobDataModel } from "@oino-ts/blob";
10
+ /**
11
+ * Azure Blob Storage implementation of `OINOBlob`.
12
+ *
13
+ * Authenticates using an Azure Storage connection string. Connection parameters map as:
14
+ * - `params.url` → blob service endpoint, e.g. `https://<account>.blob.core.windows.net`
15
+ * - `params.container` → container name
16
+ * - `params.connectionStr` → Azure Storage connection string (e.g. `DefaultEndpointsProtocol=https;AccountName=...`)
17
+ *
18
+ * Register and use via the factory:
19
+ * ```ts
20
+ * import { OINOBlobFactory } from "@oino-ts/blob"
21
+ * import { OINOBlobAzureTable } from "@oino-ts/blob-azure"
22
+ *
23
+ * OINOBlobFactory.registerBlob("OINOBlobAzureTable", OINOBlobAzureTable)
24
+ *
25
+ * const blob = await OINOBlobFactory.createBlob({
26
+ * type: "OINOBlobAzureTable",
27
+ * url: "https://myaccount.blob.core.windows.net",
28
+ * container: "my-container",
29
+ * connectionStr: process.env.AZURE_STORAGE_CONNECTION_STRING
30
+ * })
31
+ * const api = await OINOBlobFactory.createApi(blob, {
32
+ * apiName: "files",
33
+ * tableName: "uploads/" // blob prefix / folder
34
+ * })
35
+ * ```
36
+ */
37
+ export class OINOBlobAzureTable extends OINOBlob {
38
+ _containerClient = null;
39
+ // ── OINODataSource lifecycle ──────────────────────────────────────────
40
+ /**
41
+ * Initialise the Azure SDK client. Does not perform any network call.
42
+ */
43
+ async connect() {
44
+ const result = new OINOResult();
45
+ try {
46
+ let serviceClient;
47
+ if (this.blobParams.connectionStr) {
48
+ serviceClient = BlobServiceClient.fromConnectionString(this.blobParams.connectionStr);
49
+ }
50
+ else {
51
+ return new OINOResult({
52
+ success: false,
53
+ status: 400,
54
+ statusText: "OINOBlobAzureTable: params.connectionStr is required"
55
+ });
56
+ }
57
+ this._containerClient = serviceClient.getContainerClient(this.blobParams.container);
58
+ this.isConnected = true;
59
+ }
60
+ catch (e) {
61
+ return new OINOResult({ success: false, status: 500, statusText: "OINOBlobAzureTable connect failed: " + e.message });
62
+ }
63
+ return result;
64
+ }
65
+ /**
66
+ * Verify that the target container exists and is accessible.
67
+ */
68
+ async validate() {
69
+ if (!this._containerClient) {
70
+ return new OINOResult({ success: false, status: 500, statusText: "OINOBlobAzureTable: not connected" });
71
+ }
72
+ try {
73
+ const exists = await this._containerClient.exists();
74
+ if (!exists) {
75
+ return new OINOResult({
76
+ success: false,
77
+ status: 404,
78
+ statusText: "OINOBlobAzureTable: container '" + this.blobParams.container + "' not found"
79
+ });
80
+ }
81
+ this.isValidated = true;
82
+ }
83
+ catch (e) {
84
+ return new OINOResult({ success: false, status: 500, statusText: "OINOBlobAzureTable validate failed: " + e.message });
85
+ }
86
+ return new OINOResult();
87
+ }
88
+ /**
89
+ * Release the client reference (Azure SDK is stateless per-request so nothing to close).
90
+ */
91
+ async disconnect() {
92
+ this._containerClient = null;
93
+ this.isConnected = false;
94
+ this.isValidated = false;
95
+ }
96
+ // ── OINOBlob operations ───────────────────────────────────────────────
97
+ /**
98
+ * List all blobs, applying native Azure query filtering where possible and
99
+ * in-memory result filtering for predicates that cannot be expressed as a
100
+ * native query.
101
+ *
102
+ * - The `name` field supports server-side prefix filtering via the Azure
103
+ * `listBlobsFlat` `prefix` option (query filtering).
104
+ * - All other field predicates (`etag`, `lastModified`, `contentLength`,
105
+ * `contentType`) are evaluated in-memory after the listing (result
106
+ * filtering).
107
+ *
108
+ * @param filter optional query filter to apply
109
+ */
110
+ async listEntries(filter) {
111
+ if (!this._containerClient) {
112
+ throw new Error("OINOBlobAzureTable: not connected");
113
+ }
114
+ const queryPrefix = (filter && !filter.isEmpty())
115
+ ? OINOBlob.extractNamePrefix(filter)
116
+ : undefined;
117
+ const entries = [];
118
+ for await (const blob of this._containerClient.listBlobsFlat({ prefix: queryPrefix })) {
119
+ entries.push({
120
+ name: blob.name,
121
+ etag: blob.properties.etag ?? "",
122
+ lastModified: blob.properties.lastModified,
123
+ contentLength: blob.properties.contentLength ?? 0,
124
+ contentType: blob.properties.contentType ?? "application/octet-stream"
125
+ });
126
+ }
127
+ if (!filter || filter.isEmpty()) {
128
+ return entries;
129
+ }
130
+ return entries.filter(e => OINOBlob.matchesEntry(e, filter));
131
+ }
132
+ /**
133
+ * Download the raw content of a named blob.
134
+ *
135
+ * @param name full blob name (path within the container)
136
+ */
137
+ async fetchEntry(name) {
138
+ if (!this._containerClient) {
139
+ throw new Error("OINOBlobAzureTable: not connected");
140
+ }
141
+ const blobClient = this._containerClient.getBlobClient(name);
142
+ const downloadResponse = await blobClient.download(0);
143
+ const contentType = downloadResponse.contentType ?? "application/octet-stream";
144
+ const stream = downloadResponse.readableStreamBody;
145
+ if (!stream) {
146
+ throw new Error("OINOBlobAzureTable: no readable stream returned for blob '" + name + "'");
147
+ }
148
+ const chunks = [];
149
+ for await (const chunk of stream) {
150
+ chunks.push(chunk instanceof Buffer ? chunk : Buffer.from(chunk));
151
+ }
152
+ return {
153
+ content: new Uint8Array(Buffer.concat(chunks)),
154
+ contentType
155
+ };
156
+ }
157
+ /**
158
+ * Upload (create or replace) a blob with the given binary content.
159
+ *
160
+ * @param name full blob name (path within the container)
161
+ * @param content binary content to store
162
+ * @param contentType MIME type of the content (e.g. `"image/jpeg"`)
163
+ */
164
+ async uploadEntry(name, content, contentType) {
165
+ if (!this._containerClient) {
166
+ throw new Error("OINOBlobAzureTable: not connected");
167
+ }
168
+ const blockBlobClient = this._containerClient.getBlockBlobClient(name);
169
+ const headers = { blobDataType: contentType };
170
+ await blockBlobClient.upload(content, content.length, { blobHTTPHeaders: headers });
171
+ }
172
+ /**
173
+ * Delete a named blob.
174
+ *
175
+ * @param name full blob name (path within the container)
176
+ */
177
+ async deleteEntry(name) {
178
+ if (!this._containerClient) {
179
+ throw new Error("OINOBlobAzureTable: not connected");
180
+ }
181
+ const blobClient = this._containerClient.getBlobClient(name);
182
+ await blobClient.delete();
183
+ }
184
+ // ── OINODataSource datamodel initialisation ───────────────────────────
185
+ /**
186
+ * Attach a static `OINOBlobDataModel` to the given API, adding all five
187
+ * standard fields that Azure Blob Storage returns in a listing.
188
+ *
189
+ * @param api the `OINOBlobApi` whose data model is to be initialised
190
+ */
191
+ async initializeApiDatamodel(api) {
192
+ const blobApi = api;
193
+ const datamodel = new OINOBlobDataModel(blobApi);
194
+ const ds = this;
195
+ const FIELD = { isPrimaryKey: false, isForeignKey: false, isAutoInc: false, isNotNull: false };
196
+ const PK = { isPrimaryKey: true, isForeignKey: false, isAutoInc: false, isNotNull: true };
197
+ datamodel.addField(new OINOStringDataField(ds, "name", "TEXT", PK, 1024));
198
+ datamodel.addField(new OINOStringDataField(ds, "etag", "TEXT", FIELD, 256));
199
+ datamodel.addField(new OINODatetimeDataField(ds, "lastModified", "DATETIME", FIELD));
200
+ datamodel.addField(new OINONumberDataField(ds, "contentLength", "INTEGER", FIELD));
201
+ datamodel.addField(new OINOStringDataField(ds, "contentType", "TEXT", FIELD, 256));
202
+ blobApi.initializeDatamodel(datamodel);
203
+ }
204
+ }
@@ -0,0 +1 @@
1
+ export { OINOBlobAzureTable } from "./OINOBlobAzureTable.js";
@@ -0,0 +1,86 @@
1
+ import { OINOApi, OINOResult, OINOQueryFilter } from "@oino-ts/common";
2
+ import { OINOBlob } from "@oino-ts/blob";
3
+ import { type OINOBlobEntry, type OINOBlobFetchResult } from "@oino-ts/blob";
4
+ /**
5
+ * Azure Blob Storage implementation of `OINOBlob`.
6
+ *
7
+ * Authenticates using an Azure Storage connection string. Connection parameters map as:
8
+ * - `params.url` → blob service endpoint, e.g. `https://<account>.blob.core.windows.net`
9
+ * - `params.container` → container name
10
+ * - `params.connectionStr` → Azure Storage connection string (e.g. `DefaultEndpointsProtocol=https;AccountName=...`)
11
+ *
12
+ * Register and use via the factory:
13
+ * ```ts
14
+ * import { OINOBlobFactory } from "@oino-ts/blob"
15
+ * import { OINOBlobAzureTable } from "@oino-ts/blob-azure"
16
+ *
17
+ * OINOBlobFactory.registerBlob("OINOBlobAzureTable", OINOBlobAzureTable)
18
+ *
19
+ * const blob = await OINOBlobFactory.createBlob({
20
+ * type: "OINOBlobAzureTable",
21
+ * url: "https://myaccount.blob.core.windows.net",
22
+ * container: "my-container",
23
+ * connectionStr: process.env.AZURE_STORAGE_CONNECTION_STRING
24
+ * })
25
+ * const api = await OINOBlobFactory.createApi(blob, {
26
+ * apiName: "files",
27
+ * tableName: "uploads/" // blob prefix / folder
28
+ * })
29
+ * ```
30
+ */
31
+ export declare class OINOBlobAzureTable extends OINOBlob {
32
+ private _containerClient;
33
+ /**
34
+ * Initialise the Azure SDK client. Does not perform any network call.
35
+ */
36
+ connect(): Promise<OINOResult>;
37
+ /**
38
+ * Verify that the target container exists and is accessible.
39
+ */
40
+ validate(): Promise<OINOResult>;
41
+ /**
42
+ * Release the client reference (Azure SDK is stateless per-request so nothing to close).
43
+ */
44
+ disconnect(): Promise<void>;
45
+ /**
46
+ * List all blobs, applying native Azure query filtering where possible and
47
+ * in-memory result filtering for predicates that cannot be expressed as a
48
+ * native query.
49
+ *
50
+ * - The `name` field supports server-side prefix filtering via the Azure
51
+ * `listBlobsFlat` `prefix` option (query filtering).
52
+ * - All other field predicates (`etag`, `lastModified`, `contentLength`,
53
+ * `contentType`) are evaluated in-memory after the listing (result
54
+ * filtering).
55
+ *
56
+ * @param filter optional query filter to apply
57
+ */
58
+ listEntries(filter?: OINOQueryFilter): Promise<OINOBlobEntry[]>;
59
+ /**
60
+ * Download the raw content of a named blob.
61
+ *
62
+ * @param name full blob name (path within the container)
63
+ */
64
+ fetchEntry(name: string): Promise<OINOBlobFetchResult>;
65
+ /**
66
+ * Upload (create or replace) a blob with the given binary content.
67
+ *
68
+ * @param name full blob name (path within the container)
69
+ * @param content binary content to store
70
+ * @param contentType MIME type of the content (e.g. `"image/jpeg"`)
71
+ */
72
+ uploadEntry(name: string, content: Uint8Array, contentType: string): Promise<void>;
73
+ /**
74
+ * Delete a named blob.
75
+ *
76
+ * @param name full blob name (path within the container)
77
+ */
78
+ deleteEntry(name: string): Promise<void>;
79
+ /**
80
+ * Attach a static `OINOBlobDataModel` to the given API, adding all five
81
+ * standard fields that Azure Blob Storage returns in a listing.
82
+ *
83
+ * @param api the `OINOBlobApi` whose data model is to be initialised
84
+ */
85
+ initializeApiDatamodel(api: OINOApi): Promise<void>;
86
+ }
@@ -0,0 +1 @@
1
+ export { OINOBlobAzureTable } from "./OINOBlobAzureTable.js";
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@oino-ts/blob-azure",
3
+ "version": "1.0.0",
4
+ "description": "OINO TS package for using Azure 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
+ "azure"
18
+ ],
19
+ "main": "./dist/cjs/index.js",
20
+ "module": "./dist/esm/index.js",
21
+ "types": "./dist/types/index.d.ts",
22
+ "dependencies": {
23
+ "@azure/storage-blob": "^12.0.0",
24
+ "@oino-ts/blob": "1.0.0",
25
+ "@oino-ts/common": "1.0.0"
26
+ },
27
+ "devDependencies": {
28
+ "@oino-ts/types": "1.0.0",
29
+ "@types/bun": "^1.1.14",
30
+ "@types/node": "^22.0.00",
31
+ "typescript": "~5.9.0"
32
+ },
33
+ "files": [
34
+ "src/*.ts",
35
+ "dist/cjs/*.js",
36
+ "dist/esm/*.js",
37
+ "dist/types/*.d.ts"
38
+ ]
39
+ }
@@ -0,0 +1,225 @@
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 { Buffer } from "node:buffer"
8
+
9
+ import {
10
+ BlobServiceClient,
11
+ type ContainerClient
12
+ } from "@azure/storage-blob"
13
+
14
+ import { OINOApi, OINOResult, OINOQueryFilter, OINOStringDataField, OINONumberDataField, OINODatetimeDataField, type OINODataFieldParams } from "@oino-ts/common"
15
+ import { OINOBlob, OINOBlobDataModel, OINOBlobApi } from "@oino-ts/blob"
16
+ import { type OINOBlobEntry, type OINOBlobFetchResult } from "@oino-ts/blob"
17
+
18
+ /**
19
+ * Azure Blob Storage implementation of `OINOBlob`.
20
+ *
21
+ * Authenticates using an Azure Storage connection string. Connection parameters map as:
22
+ * - `params.url` → blob service endpoint, e.g. `https://<account>.blob.core.windows.net`
23
+ * - `params.container` → container name
24
+ * - `params.connectionStr` → Azure Storage connection string (e.g. `DefaultEndpointsProtocol=https;AccountName=...`)
25
+ *
26
+ * Register and use via the factory:
27
+ * ```ts
28
+ * import { OINOBlobFactory } from "@oino-ts/blob"
29
+ * import { OINOBlobAzureTable } from "@oino-ts/blob-azure"
30
+ *
31
+ * OINOBlobFactory.registerBlob("OINOBlobAzureTable", OINOBlobAzureTable)
32
+ *
33
+ * const blob = await OINOBlobFactory.createBlob({
34
+ * type: "OINOBlobAzureTable",
35
+ * url: "https://myaccount.blob.core.windows.net",
36
+ * container: "my-container",
37
+ * connectionStr: process.env.AZURE_STORAGE_CONNECTION_STRING
38
+ * })
39
+ * const api = await OINOBlobFactory.createApi(blob, {
40
+ * apiName: "files",
41
+ * tableName: "uploads/" // blob prefix / folder
42
+ * })
43
+ * ```
44
+ */
45
+ export class OINOBlobAzureTable extends OINOBlob {
46
+ private _containerClient: ContainerClient | null = null
47
+
48
+ // ── OINODataSource lifecycle ──────────────────────────────────────────
49
+
50
+ /**
51
+ * Initialise the Azure SDK client. Does not perform any network call.
52
+ */
53
+ async connect(): Promise<OINOResult> {
54
+ const result = new OINOResult()
55
+ try {
56
+ let serviceClient: BlobServiceClient
57
+ if (this.blobParams.connectionStr) {
58
+ serviceClient = BlobServiceClient.fromConnectionString(this.blobParams.connectionStr)
59
+ } else {
60
+ return new OINOResult({
61
+ success: false,
62
+ status: 400,
63
+ statusText: "OINOBlobAzureTable: params.connectionStr is required"
64
+ })
65
+ }
66
+ this._containerClient = serviceClient.getContainerClient(this.blobParams.container)
67
+ this.isConnected = true
68
+ } catch (e: any) {
69
+ return new OINOResult({ success: false, status: 500, statusText: "OINOBlobAzureTable connect failed: " + e.message })
70
+ }
71
+ return result
72
+ }
73
+
74
+ /**
75
+ * Verify that the target container exists and is accessible.
76
+ */
77
+ async validate(): Promise<OINOResult> {
78
+ if (!this._containerClient) {
79
+ return new OINOResult({ success: false, status: 500, statusText: "OINOBlobAzureTable: not connected" })
80
+ }
81
+ try {
82
+ const exists = await this._containerClient.exists()
83
+ if (!exists) {
84
+ return new OINOResult({
85
+ success: false,
86
+ status: 404,
87
+ statusText: "OINOBlobAzureTable: container '" + this.blobParams.container + "' not found"
88
+ })
89
+ }
90
+ this.isValidated = true
91
+ } catch (e: any) {
92
+ return new OINOResult({ success: false, status: 500, statusText: "OINOBlobAzureTable validate failed: " + e.message })
93
+ }
94
+ return new OINOResult()
95
+ }
96
+
97
+ /**
98
+ * Release the client reference (Azure SDK is stateless per-request so nothing to close).
99
+ */
100
+ async disconnect(): Promise<void> {
101
+ this._containerClient = null
102
+ this.isConnected = false
103
+ this.isValidated = false
104
+ }
105
+
106
+ // ── OINOBlob operations ───────────────────────────────────────────────
107
+
108
+ /**
109
+ * List all blobs, applying native Azure query filtering where possible and
110
+ * in-memory result filtering for predicates that cannot be expressed as a
111
+ * native query.
112
+ *
113
+ * - The `name` field supports server-side prefix filtering via the Azure
114
+ * `listBlobsFlat` `prefix` option (query filtering).
115
+ * - All other field predicates (`etag`, `lastModified`, `contentLength`,
116
+ * `contentType`) are evaluated in-memory after the listing (result
117
+ * filtering).
118
+ *
119
+ * @param filter optional query filter to apply
120
+ */
121
+ async listEntries(filter?: OINOQueryFilter): Promise<OINOBlobEntry[]> {
122
+ if (!this._containerClient) {
123
+ throw new Error("OINOBlobAzureTable: not connected")
124
+ }
125
+
126
+ const queryPrefix = (filter && !filter.isEmpty())
127
+ ? OINOBlob.extractNamePrefix(filter)
128
+ : undefined
129
+
130
+
131
+ const entries: OINOBlobEntry[] = []
132
+ for await (const blob of this._containerClient.listBlobsFlat({ prefix: queryPrefix })) {
133
+ entries.push({
134
+ name: blob.name,
135
+ etag: blob.properties.etag ?? "",
136
+ lastModified: blob.properties.lastModified,
137
+ contentLength: blob.properties.contentLength ?? 0,
138
+ contentType: blob.properties.contentType ?? "application/octet-stream"
139
+ })
140
+ }
141
+
142
+ if (!filter || filter.isEmpty()) {
143
+ return entries
144
+ }
145
+ return entries.filter(e => OINOBlob.matchesEntry(e, filter))
146
+
147
+ }
148
+
149
+ /**
150
+ * Download the raw content of a named blob.
151
+ *
152
+ * @param name full blob name (path within the container)
153
+ */
154
+ async fetchEntry(name: string): Promise<OINOBlobFetchResult> {
155
+ if (!this._containerClient) {
156
+ throw new Error("OINOBlobAzureTable: not connected")
157
+ }
158
+ const blobClient = this._containerClient.getBlobClient(name)
159
+ const downloadResponse = await blobClient.download(0)
160
+ const contentType = downloadResponse.contentType ?? "application/octet-stream"
161
+ const stream = downloadResponse.readableStreamBody
162
+ if (!stream) {
163
+ throw new Error("OINOBlobAzureTable: no readable stream returned for blob '" + name + "'")
164
+ }
165
+ const chunks: Buffer[] = []
166
+ for await (const chunk of stream) {
167
+ chunks.push(chunk instanceof Buffer ? chunk : Buffer.from(chunk as Uint8Array|string))
168
+ }
169
+ return {
170
+ content: new Uint8Array(Buffer.concat(chunks)),
171
+ contentType
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Upload (create or replace) a blob with the given binary content.
177
+ *
178
+ * @param name full blob name (path within the container)
179
+ * @param content binary content to store
180
+ * @param contentType MIME type of the content (e.g. `"image/jpeg"`)
181
+ */
182
+ async uploadEntry(name: string, content: Uint8Array, contentType: string): Promise<void> {
183
+ if (!this._containerClient) {
184
+ throw new Error("OINOBlobAzureTable: not connected")
185
+ }
186
+ const blockBlobClient = this._containerClient.getBlockBlobClient(name)
187
+ const headers:any = { blobDataType: contentType }
188
+ await blockBlobClient.upload(content, content.length, { blobHTTPHeaders: headers })
189
+ }
190
+
191
+ /**
192
+ * Delete a named blob.
193
+ *
194
+ * @param name full blob name (path within the container)
195
+ */
196
+ async deleteEntry(name: string): Promise<void> {
197
+ if (!this._containerClient) {
198
+ throw new Error("OINOBlobAzureTable: not connected")
199
+ }
200
+ const blobClient = this._containerClient.getBlobClient(name)
201
+ await blobClient.delete()
202
+ }
203
+
204
+ // ── OINODataSource datamodel initialisation ───────────────────────────
205
+
206
+ /**
207
+ * Attach a static `OINOBlobDataModel` to the given API, adding all five
208
+ * standard fields that Azure Blob Storage returns in a listing.
209
+ *
210
+ * @param api the `OINOBlobApi` whose data model is to be initialised
211
+ */
212
+ async initializeApiDatamodel(api: OINOApi): Promise<void> {
213
+ const blobApi = api as OINOBlobApi
214
+ const datamodel = new OINOBlobDataModel(blobApi)
215
+ const ds = this
216
+ const FIELD: OINODataFieldParams = { isPrimaryKey: false, isForeignKey: false, isAutoInc: false, isNotNull: false }
217
+ const PK: OINODataFieldParams = { isPrimaryKey: true, isForeignKey: false, isAutoInc: false, isNotNull: true }
218
+ datamodel.addField(new OINOStringDataField(ds, "name", "TEXT", PK, 1024))
219
+ datamodel.addField(new OINOStringDataField(ds, "etag", "TEXT", FIELD, 256))
220
+ datamodel.addField(new OINODatetimeDataField(ds, "lastModified", "DATETIME", FIELD))
221
+ datamodel.addField(new OINONumberDataField(ds, "contentLength", "INTEGER", FIELD))
222
+ datamodel.addField(new OINOStringDataField(ds, "contentType", "TEXT", FIELD, 256))
223
+ blobApi.initializeDatamodel(datamodel)
224
+ }
225
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { OINOBlobAzureTable } from "./OINOBlobAzureTable.js"