storybooker 0.19.4 → 0.22.0-canary.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/README.md +40 -18
- package/dist/adapters/_internal/queue.d.mts +127 -0
- package/dist/aws-dynamodb.d.mts +22 -0
- package/dist/aws-dynamodb.mjs +118 -0
- package/dist/aws-dynamodb.mjs.map +1 -0
- package/dist/aws-s3.d.mts +20 -0
- package/dist/aws-s3.mjs +96 -0
- package/dist/aws-s3.mjs.map +1 -0
- package/dist/azure-blob-storage.d.mts +20 -0
- package/dist/azure-blob-storage.mjs +126 -0
- package/dist/azure-blob-storage.mjs.map +1 -0
- package/dist/azure-cosmos-db.d.mts +23 -0
- package/dist/azure-cosmos-db.mjs +87 -0
- package/dist/azure-cosmos-db.mjs.map +1 -0
- package/dist/azure-data-tables.d.mts +23 -0
- package/dist/azure-data-tables.mjs +127 -0
- package/dist/azure-data-tables.mjs.map +1 -0
- package/dist/azure-easy-auth.d.mts +50 -0
- package/dist/azure-easy-auth.mjs +88 -0
- package/dist/azure-easy-auth.mjs.map +1 -0
- package/dist/azure-functions.d.mts +62 -0
- package/dist/azure-functions.mjs +147 -0
- package/dist/azure-functions.mjs.map +1 -0
- package/dist/fs.d.mts +37 -0
- package/dist/fs.mjs +240 -0
- package/dist/fs.mjs.map +1 -0
- package/dist/gcp-big-table.d.mts +23 -0
- package/dist/gcp-big-table.mjs +92 -0
- package/dist/gcp-big-table.mjs.map +1 -0
- package/dist/gcp-firestore.d.mts +22 -0
- package/dist/gcp-firestore.mjs +87 -0
- package/dist/gcp-firestore.mjs.map +1 -0
- package/dist/gcp-storage.d.mts +20 -0
- package/dist/gcp-storage.mjs +96 -0
- package/dist/gcp-storage.mjs.map +1 -0
- package/dist/handlers/handle-process-zip.mjs +90 -0
- package/dist/handlers/handle-process-zip.mjs.map +1 -0
- package/dist/handlers/handle-purge.d.mts +12 -0
- package/dist/handlers/handle-purge.mjs +36 -0
- package/dist/handlers/handle-purge.mjs.map +1 -0
- package/dist/handlers/handle-serve-storybook.mjs +94 -0
- package/dist/handlers/handle-serve-storybook.mjs.map +1 -0
- package/dist/index.d.mts +28 -0
- package/dist/index.mjs +62 -0
- package/dist/index.mjs.map +1 -0
- package/dist/models/builds-model.mjs +248 -0
- package/dist/models/builds-model.mjs.map +1 -0
- package/dist/models/builds-schema.d.mts +171 -0
- package/dist/models/builds-schema.mjs +67 -0
- package/dist/models/builds-schema.mjs.map +1 -0
- package/dist/models/projects-model.mjs +122 -0
- package/dist/models/projects-model.mjs.map +1 -0
- package/dist/models/projects-schema.d.mts +70 -0
- package/dist/models/projects-schema.mjs +37 -0
- package/dist/models/projects-schema.mjs.map +1 -0
- package/dist/models/tags-model.mjs +110 -0
- package/dist/models/tags-model.mjs.map +1 -0
- package/dist/models/tags-schema.d.mts +76 -0
- package/dist/models/tags-schema.mjs +34 -0
- package/dist/models/tags-schema.mjs.map +1 -0
- package/dist/models/~model.mjs +43 -0
- package/dist/models/~model.mjs.map +1 -0
- package/dist/models/~shared-schema.d.mts +1 -0
- package/dist/models/~shared-schema.mjs +20 -0
- package/dist/models/~shared-schema.mjs.map +1 -0
- package/dist/mysql.d.mts +39 -0
- package/dist/mysql.mjs +151 -0
- package/dist/mysql.mjs.map +1 -0
- package/dist/redis.d.mts +33 -0
- package/dist/redis.mjs +118 -0
- package/dist/redis.mjs.map +1 -0
- package/dist/routers/account-router.mjs +91 -0
- package/dist/routers/account-router.mjs.map +1 -0
- package/dist/routers/builds-router.mjs +347 -0
- package/dist/routers/builds-router.mjs.map +1 -0
- package/dist/routers/projects-router.mjs +236 -0
- package/dist/routers/projects-router.mjs.map +1 -0
- package/dist/routers/root-router.mjs +108 -0
- package/dist/routers/root-router.mjs.map +1 -0
- package/dist/routers/tags-router.mjs +269 -0
- package/dist/routers/tags-router.mjs.map +1 -0
- package/dist/routers/tasks-router.mjs +71 -0
- package/dist/routers/tasks-router.mjs.map +1 -0
- package/dist/urls.d.mts +47 -0
- package/dist/urls.mjs +208 -0
- package/dist/urls.mjs.map +1 -0
- package/dist/utils/adapter-utils.d.mts +14 -0
- package/dist/utils/adapter-utils.mjs +14 -0
- package/dist/utils/adapter-utils.mjs.map +1 -0
- package/dist/utils/auth.mjs +25 -0
- package/dist/utils/auth.mjs.map +1 -0
- package/dist/utils/error.d.mts +21 -0
- package/dist/utils/error.mjs +109 -0
- package/dist/utils/error.mjs.map +1 -0
- package/dist/utils/file-utils.mjs +16 -0
- package/dist/utils/file-utils.mjs.map +1 -0
- package/dist/utils/openapi-utils.mjs +45 -0
- package/dist/utils/openapi-utils.mjs.map +1 -0
- package/dist/utils/request.mjs +35 -0
- package/dist/utils/request.mjs.map +1 -0
- package/dist/utils/response.mjs +24 -0
- package/dist/utils/response.mjs.map +1 -0
- package/dist/utils/store.mjs +54 -0
- package/dist/utils/store.mjs.map +1 -0
- package/dist/utils/ui-utils.mjs +38 -0
- package/dist/utils/ui-utils.mjs.map +1 -0
- package/dist/utils/url-utils.d.mts +10 -0
- package/dist/utils/url-utils.mjs +54 -0
- package/dist/utils/url-utils.mjs.map +1 -0
- package/dist/~internal/adapter/auth.d.mts +123 -0
- package/dist/~internal/adapter/auth.mjs +20 -0
- package/dist/~internal/adapter/auth.mjs.map +1 -0
- package/dist/~internal/adapter/database.d.mts +240 -0
- package/dist/~internal/adapter/database.mjs +63 -0
- package/dist/~internal/adapter/database.mjs.map +1 -0
- package/dist/~internal/adapter/logger.d.mts +34 -0
- package/dist/~internal/adapter/logger.mjs +13 -0
- package/dist/~internal/adapter/logger.mjs.map +1 -0
- package/dist/~internal/adapter/storage.d.mts +208 -0
- package/dist/~internal/adapter/storage.mjs +63 -0
- package/dist/~internal/adapter/storage.mjs.map +1 -0
- package/dist/~internal/adapter/ui.d.mts +109 -0
- package/dist/~internal/adapter/ui.mjs +1 -0
- package/dist/~internal/adapter.d.mts +8 -0
- package/dist/~internal/adapter.mjs +6 -0
- package/dist/~internal/constants.d.mts +24 -0
- package/dist/~internal/constants.mjs +32 -0
- package/dist/~internal/constants.mjs.map +1 -0
- package/dist/~internal/mimes.d.mts +449 -0
- package/dist/~internal/mimes.mjs +454 -0
- package/dist/~internal/mimes.mjs.map +1 -0
- package/dist/~internal/router.d.mts +1651 -0
- package/dist/~internal/router.mjs +39 -0
- package/dist/~internal/router.mjs.map +1 -0
- package/dist/~internal/types.d.mts +77 -0
- package/dist/~internal/types.mjs +1 -0
- package/dist/~internal/utils.d.mts +4 -0
- package/dist/~internal/utils.mjs +5 -0
- package/openapi.json +3162 -0
- package/package.json +148 -27
- package/src/adapters/_internal/auth.ts +135 -0
- package/src/adapters/_internal/database.ts +241 -0
- package/src/adapters/_internal/index.ts +8 -0
- package/src/adapters/_internal/logger.ts +41 -0
- package/src/adapters/_internal/queue.ts +151 -0
- package/src/adapters/_internal/storage.ts +197 -0
- package/src/adapters/_internal/ui.ts +103 -0
- package/src/adapters/aws-dynamodb.ts +201 -0
- package/src/adapters/aws-s3.ts +160 -0
- package/src/adapters/azure-blob-storage.ts +223 -0
- package/src/adapters/azure-cosmos-db.ts +158 -0
- package/src/adapters/azure-data-tables.ts +223 -0
- package/src/adapters/azure-easy-auth.ts +174 -0
- package/src/adapters/azure-functions.ts +242 -0
- package/src/adapters/fs.ts +398 -0
- package/src/adapters/gcp-big-table.ts +157 -0
- package/src/adapters/gcp-firestore.ts +146 -0
- package/src/adapters/gcp-storage.ts +141 -0
- package/src/adapters/mysql.ts +296 -0
- package/src/adapters/redis.ts +242 -0
- package/src/handlers/handle-process-zip.ts +117 -0
- package/src/handlers/handle-purge.ts +65 -0
- package/src/handlers/handle-serve-storybook.ts +101 -0
- package/src/index.ts +81 -16
- package/src/mocks/mock-auth-service.ts +51 -0
- package/src/mocks/mock-store.ts +26 -0
- package/src/models/builds-model.ts +373 -0
- package/src/models/builds-schema.ts +84 -0
- package/src/models/projects-model.ts +177 -0
- package/src/models/projects-schema.ts +69 -0
- package/src/models/tags-model.ts +138 -0
- package/src/models/tags-schema.ts +45 -0
- package/src/models/~model.ts +79 -0
- package/src/models/~shared-schema.ts +14 -0
- package/src/routers/_app-router.ts +57 -0
- package/src/routers/account-router.ts +136 -0
- package/src/routers/builds-router.ts +464 -0
- package/src/routers/projects-router.ts +309 -0
- package/src/routers/root-router.ts +127 -0
- package/src/routers/tags-router.ts +339 -0
- package/src/routers/tasks-router.ts +75 -0
- package/src/types.ts +107 -0
- package/src/urls.ts +327 -0
- package/src/utils/adapter-utils.ts +26 -0
- package/src/utils/auth.test.ts +71 -0
- package/src/utils/auth.ts +39 -0
- package/src/utils/constants.ts +31 -0
- package/src/utils/date-utils.ts +10 -0
- package/src/utils/error.test.ts +86 -0
- package/src/utils/error.ts +140 -0
- package/src/utils/file-utils.test.ts +65 -0
- package/src/utils/file-utils.ts +43 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/mime-utils.ts +457 -0
- package/src/utils/openapi-utils.ts +49 -0
- package/src/utils/request.ts +97 -0
- package/src/utils/response.ts +20 -0
- package/src/utils/store.ts +85 -0
- package/src/utils/story-utils.ts +42 -0
- package/src/utils/text-utils.ts +10 -0
- package/src/utils/ui-utils.ts +57 -0
- package/src/utils/url-utils.ts +113 -0
- package/dist/index.js +0 -554
- package/src/commands/create.ts +0 -263
- package/src/commands/purge.ts +0 -70
- package/src/commands/test.ts +0 -42
- package/src/service-schema.d.ts +0 -2023
- package/src/utils/auth-utils.ts +0 -31
- package/src/utils/pkg-utils.ts +0 -37
- package/src/utils/sb-build.ts +0 -55
- package/src/utils/sb-test.ts +0 -115
- package/src/utils/schema-utils.ts +0 -123
- package/src/utils/stream-utils.ts +0 -72
- package/src/utils/types.ts +0 -4
- package/src/utils/zip.ts +0 -77
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
// oxlint-disable no-await-in-loop
|
|
2
|
+
// oxlint-disable max-params
|
|
3
|
+
// oxlint-disable require-await
|
|
4
|
+
// oxlint-disable no-unsafe-assignment
|
|
5
|
+
|
|
6
|
+
import type { Buffer } from "node:buffer";
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as fsp from "node:fs/promises";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import { Readable, type Stream } from "node:stream";
|
|
11
|
+
import type { ReadableStream as WebReadableStream } from "node:stream/web";
|
|
12
|
+
import {
|
|
13
|
+
DatabaseAdapterErrors,
|
|
14
|
+
StorageAdapterErrors,
|
|
15
|
+
type DatabaseAdapter,
|
|
16
|
+
type DatabaseAdapterOptions,
|
|
17
|
+
type StorageAdapter,
|
|
18
|
+
type StoryBookerDatabaseDocument,
|
|
19
|
+
} from "./_internal/index.ts";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Database adapter for StoryBooker while uses a file (json) in
|
|
23
|
+
* the local filesystem to read from and write entries to.
|
|
24
|
+
* It uses NodeJS FS API to read/write to filesystem.
|
|
25
|
+
*
|
|
26
|
+
* It is useful for testing and playground
|
|
27
|
+
* but not recommended for heavy traffic.
|
|
28
|
+
*
|
|
29
|
+
* Usage:
|
|
30
|
+
* ```ts
|
|
31
|
+
* const database = createLocalFileDatabaseAdapter("./db.json");
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function createLocalFileDatabaseAdapter(filename = "db.json"): DatabaseAdapter {
|
|
35
|
+
const filepath = path.resolve(filename);
|
|
36
|
+
let db: Record<string, Record<string, StoryBookerDatabaseDocument>> | undefined = undefined;
|
|
37
|
+
|
|
38
|
+
const readFromFile = async (options: DatabaseAdapterOptions): Promise<void> => {
|
|
39
|
+
try {
|
|
40
|
+
const newDB = await fsp.readFile(filepath, {
|
|
41
|
+
encoding: "utf8",
|
|
42
|
+
signal: options.abortSignal,
|
|
43
|
+
});
|
|
44
|
+
db = newDB ? JSON.parse(newDB) : {};
|
|
45
|
+
} catch {
|
|
46
|
+
db = {};
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const saveToFile = async (options: DatabaseAdapterOptions): Promise<void> => {
|
|
51
|
+
if (!db) {
|
|
52
|
+
throw new DatabaseAdapterErrors.DatabaseNotInitializedError();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
await fsp.writeFile(filepath, JSON.stringify(db, null, 2), {
|
|
56
|
+
encoding: "utf8",
|
|
57
|
+
signal: options.abortSignal,
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
metadata: {
|
|
63
|
+
name: "Local File",
|
|
64
|
+
description: "A file based database stored in a single JSON file.",
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
async init(options) {
|
|
68
|
+
if (fs.existsSync(filepath)) {
|
|
69
|
+
const stat = await fsp.stat(filepath);
|
|
70
|
+
if (stat.isFile()) {
|
|
71
|
+
await readFromFile(options);
|
|
72
|
+
} else {
|
|
73
|
+
throw new DatabaseAdapterErrors.DatabaseNotInitializedError(
|
|
74
|
+
`Path "${filepath}" is not a file`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
db = {}; // Initialize empty DB
|
|
79
|
+
const basedir = path.dirname(filepath);
|
|
80
|
+
await fsp.mkdir(basedir, { recursive: true }).catch(() => {
|
|
81
|
+
// ignore error
|
|
82
|
+
});
|
|
83
|
+
await saveToFile(options);
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
// Collections
|
|
88
|
+
|
|
89
|
+
async createCollection(collectionId, options) {
|
|
90
|
+
if (!db) {
|
|
91
|
+
throw new DatabaseAdapterErrors.DatabaseNotInitializedError();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (Object.hasOwn(db, collectionId)) {
|
|
95
|
+
throw new DatabaseAdapterErrors.CollectionAlreadyExistsError(collectionId);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
db[collectionId] ??= {};
|
|
99
|
+
await saveToFile(options);
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
async listCollections() {
|
|
103
|
+
if (!db) {
|
|
104
|
+
throw new DatabaseAdapterErrors.DatabaseNotInitializedError();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return Object.keys(db);
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
async deleteCollection(collectionId, options) {
|
|
111
|
+
if (!db) {
|
|
112
|
+
throw new DatabaseAdapterErrors.DatabaseNotInitializedError();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!Object.hasOwn(db, collectionId)) {
|
|
116
|
+
throw new DatabaseAdapterErrors.CollectionDoesNotExistError(collectionId);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// oxlint-disable-next-line no-dynamic-delete
|
|
120
|
+
delete db[collectionId];
|
|
121
|
+
await saveToFile(options);
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
async hasCollection(collectionId, _options) {
|
|
125
|
+
if (!db) {
|
|
126
|
+
throw new DatabaseAdapterErrors.DatabaseNotInitializedError();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return Object.hasOwn(db, collectionId);
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
// Documents
|
|
133
|
+
|
|
134
|
+
async listDocuments(collectionId, listOptions, _options) {
|
|
135
|
+
if (!db) {
|
|
136
|
+
throw new DatabaseAdapterErrors.DatabaseNotInitializedError();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!Object.hasOwn(db, collectionId)) {
|
|
140
|
+
throw new DatabaseAdapterErrors.CollectionDoesNotExistError(collectionId);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const { limit = Number.POSITIVE_INFINITY, sort, filter } = listOptions || {};
|
|
144
|
+
|
|
145
|
+
// oxlint-disable-next-line no-non-null-assertion
|
|
146
|
+
const collection = db[collectionId]!;
|
|
147
|
+
const items = Object.values(collection);
|
|
148
|
+
if (sort) {
|
|
149
|
+
if (typeof sort === "function") {
|
|
150
|
+
items.sort(sort);
|
|
151
|
+
} else if (sort === "latest") {
|
|
152
|
+
items.sort((itemA, itemB) => {
|
|
153
|
+
return new Date(itemB.updatedAt).getTime() - new Date(itemA.updatedAt).getTime();
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (filter && typeof filter === "function") {
|
|
159
|
+
return items.filter((item) => filter(item)).slice(0, limit);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return items.slice(0, limit);
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
async getDocument(collectionId, documentId, _options) {
|
|
166
|
+
if (!db) {
|
|
167
|
+
throw new DatabaseAdapterErrors.DatabaseNotInitializedError();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!Object.hasOwn(db, collectionId)) {
|
|
171
|
+
throw new DatabaseAdapterErrors.CollectionDoesNotExistError(collectionId);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const item = db[collectionId]?.[documentId];
|
|
175
|
+
if (!item) {
|
|
176
|
+
throw new DatabaseAdapterErrors.DocumentDoesNotExistError(collectionId, documentId);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return item;
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
async hasDocument(collectionId, documentId, options) {
|
|
183
|
+
return Boolean(await this.getDocument(collectionId, documentId, options));
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
async createDocument(collectionId, documentData, options) {
|
|
187
|
+
if (!db) {
|
|
188
|
+
throw new DatabaseAdapterErrors.DatabaseNotInitializedError();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!Object.hasOwn(db, collectionId)) {
|
|
192
|
+
throw new DatabaseAdapterErrors.CollectionDoesNotExistError(collectionId);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// oxlint-disable-next-line no-non-null-assertion
|
|
196
|
+
const collection = db[collectionId]!;
|
|
197
|
+
if (collection[documentData.id]) {
|
|
198
|
+
throw new DatabaseAdapterErrors.DocumentAlreadyExistsError(collectionId, documentData.id);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
collection[documentData.id] = documentData;
|
|
202
|
+
await saveToFile(options);
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
async deleteDocument(collectionId, documentId, options) {
|
|
206
|
+
if (!db) {
|
|
207
|
+
throw new DatabaseAdapterErrors.DatabaseNotInitializedError();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!Object.hasOwn(db, collectionId)) {
|
|
211
|
+
throw new DatabaseAdapterErrors.CollectionDoesNotExistError(collectionId);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!(await this.hasDocument(collectionId, documentId, options))) {
|
|
215
|
+
throw new DatabaseAdapterErrors.DocumentDoesNotExistError(collectionId, documentId);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// oxlint-disable-next-line no-non-null-assertion
|
|
219
|
+
const collection = db[collectionId]!;
|
|
220
|
+
// oxlint-disable-next-line no-dynamic-delete
|
|
221
|
+
delete collection[documentId];
|
|
222
|
+
await saveToFile(options);
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
async updateDocument(collectionId, documentId, documentData, options) {
|
|
226
|
+
if (!db) {
|
|
227
|
+
throw new DatabaseAdapterErrors.DatabaseNotInitializedError();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!Object.hasOwn(db, collectionId)) {
|
|
231
|
+
throw new DatabaseAdapterErrors.CollectionDoesNotExistError(collectionId);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const prevItem = await this.getDocument(collectionId, documentId, options);
|
|
235
|
+
if (!prevItem) {
|
|
236
|
+
throw new DatabaseAdapterErrors.DocumentDoesNotExistError(collectionId, documentId);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// oxlint-disable-next-line no-non-null-assertion
|
|
240
|
+
const collection = db[collectionId]!;
|
|
241
|
+
collection[documentId] = { ...prevItem, ...documentData, id: documentId };
|
|
242
|
+
await saveToFile(options);
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Storage adapter for StoryBooker while uses
|
|
249
|
+
* the local filesystem to read from and write files to.
|
|
250
|
+
* It uses NodeJS FS API to read/write to filesystem.
|
|
251
|
+
*
|
|
252
|
+
* It is useful for testing and playground
|
|
253
|
+
* but not recommended for heavy traffic.
|
|
254
|
+
*
|
|
255
|
+
* Usage:
|
|
256
|
+
* ```ts
|
|
257
|
+
* const storage = createLocalFileStorageAdapter("./store/");
|
|
258
|
+
* ```
|
|
259
|
+
*/
|
|
260
|
+
export function createLocalFileStorageAdapter(pathPrefix = "."): StorageAdapter {
|
|
261
|
+
const basePath = path.resolve(pathPrefix);
|
|
262
|
+
|
|
263
|
+
function genPath(...pathParts: (string | undefined)[]): string {
|
|
264
|
+
return path.join(basePath, ...pathParts.filter((part) => part !== undefined));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Containers
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
metadata: {
|
|
271
|
+
name: "Local File System",
|
|
272
|
+
description: "A storage adapter that uses the local file system to store files.",
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
init: async (_options) => {
|
|
276
|
+
try {
|
|
277
|
+
await fsp.mkdir(basePath, { recursive: true });
|
|
278
|
+
} catch (error) {
|
|
279
|
+
throw new StorageAdapterErrors.StorageNotInitializedError({ cause: error });
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
async createContainer(containerId, options) {
|
|
284
|
+
if (await this.hasContainer(containerId, options)) {
|
|
285
|
+
throw new StorageAdapterErrors.ContainerAlreadyExistsError(containerId);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
await fsp.mkdir(genPath(containerId), { recursive: true });
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
async deleteContainer(containerId, options) {
|
|
292
|
+
if (!(await this.hasContainer(containerId, options))) {
|
|
293
|
+
throw new StorageAdapterErrors.ContainerDoesNotExistError(containerId);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
await fsp.rm(genPath(containerId), { force: true, recursive: true });
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
async hasContainer(containerId) {
|
|
300
|
+
return fs.existsSync(genPath(containerId));
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
async listContainers() {
|
|
304
|
+
const dirPath = genPath();
|
|
305
|
+
if (!fs.existsSync(dirPath)) {
|
|
306
|
+
throw new StorageAdapterErrors.StorageNotInitializedError(
|
|
307
|
+
`Dir "${dirPath}" does not exist`,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const containers: string[] = [];
|
|
312
|
+
const entries = await fsp.readdir(dirPath, {
|
|
313
|
+
withFileTypes: true,
|
|
314
|
+
});
|
|
315
|
+
for (const entry of entries) {
|
|
316
|
+
if (entry.isDirectory()) {
|
|
317
|
+
containers.push(entry.name);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return containers;
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
// Files
|
|
324
|
+
|
|
325
|
+
async deleteFiles(containerId, filePathsOrPrefix) {
|
|
326
|
+
if (typeof filePathsOrPrefix === "string") {
|
|
327
|
+
await fsp.rm(genPath(containerId, filePathsOrPrefix), {
|
|
328
|
+
force: true,
|
|
329
|
+
recursive: true,
|
|
330
|
+
});
|
|
331
|
+
} else {
|
|
332
|
+
for (const filepath of filePathsOrPrefix) {
|
|
333
|
+
// oxlint-disable-next-line no-await-in-loop
|
|
334
|
+
await fsp.rm(filepath, { force: true, recursive: true });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
async hasFile(containerId, filepath) {
|
|
340
|
+
const path = genPath(containerId, filepath);
|
|
341
|
+
return fs.existsSync(path);
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
async downloadFile(containerId, filepath, options) {
|
|
345
|
+
if (!(await this.hasFile(containerId, filepath, options))) {
|
|
346
|
+
throw new StorageAdapterErrors.FileDoesNotExistError(containerId, filepath);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const path = genPath(containerId, filepath);
|
|
350
|
+
const buffer = await fsp.readFile(path);
|
|
351
|
+
const content = new Blob([buffer as Buffer<ArrayBuffer>]);
|
|
352
|
+
return { content, path };
|
|
353
|
+
},
|
|
354
|
+
|
|
355
|
+
async uploadFiles(containerId, files, options) {
|
|
356
|
+
for (const file of files) {
|
|
357
|
+
const filepath = genPath(containerId, file.path);
|
|
358
|
+
const dirpath = path.dirname(filepath);
|
|
359
|
+
|
|
360
|
+
await fsp.mkdir(dirpath, { recursive: true });
|
|
361
|
+
if (file.content instanceof ReadableStream) {
|
|
362
|
+
await writeWebStreamToFile(file.content, filepath);
|
|
363
|
+
} else {
|
|
364
|
+
const data: string | Stream =
|
|
365
|
+
// oxlint-disable-next-line no-nested-ternary
|
|
366
|
+
typeof file.content === "string" ? file.content : await file.content.text();
|
|
367
|
+
|
|
368
|
+
await fsp.writeFile(filepath, data, {
|
|
369
|
+
encoding: "utf8",
|
|
370
|
+
signal: options.abortSignal,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function writeWebStreamToFile(
|
|
379
|
+
webReadableStream: ReadableStream,
|
|
380
|
+
outputPath: string,
|
|
381
|
+
): Promise<null> {
|
|
382
|
+
// Convert WebReadableStream to Node.js Readable stream
|
|
383
|
+
const nodeReadableStream = Readable.fromWeb(webReadableStream as WebReadableStream);
|
|
384
|
+
|
|
385
|
+
// Create a writable file stream
|
|
386
|
+
const fileWritableStream = fs.createWriteStream(outputPath);
|
|
387
|
+
|
|
388
|
+
// Pipe the Node.js readable stream to the writable file stream
|
|
389
|
+
nodeReadableStream.pipe(fileWritableStream);
|
|
390
|
+
|
|
391
|
+
// Return a promise that resolves when writing is finished
|
|
392
|
+
return new Promise((resolve, reject) => {
|
|
393
|
+
fileWritableStream.on("finish", () => {
|
|
394
|
+
resolve(null);
|
|
395
|
+
});
|
|
396
|
+
fileWritableStream.on("error", reject);
|
|
397
|
+
});
|
|
398
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { Bigtable, Instance } from "@google-cloud/bigtable";
|
|
2
|
+
import {
|
|
3
|
+
DatabaseAdapterErrors,
|
|
4
|
+
type DatabaseAdapter,
|
|
5
|
+
type DatabaseAdapterOptions,
|
|
6
|
+
type DatabaseDocumentListOptions,
|
|
7
|
+
type StoryBookerDatabaseDocument,
|
|
8
|
+
} from "./_internal/database.ts";
|
|
9
|
+
|
|
10
|
+
type ColumnFamily = "cf1";
|
|
11
|
+
const COLUMN_FAMILY: ColumnFamily = "cf1";
|
|
12
|
+
|
|
13
|
+
export class GcpBigtableDatabaseAdapter implements DatabaseAdapter {
|
|
14
|
+
#instance: Instance;
|
|
15
|
+
|
|
16
|
+
constructor(client: Bigtable, instanceName = "StoryBooker") {
|
|
17
|
+
this.#instance = client.instance(instanceName);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
metadata: DatabaseAdapter["metadata"] = { name: "Google Cloud Bigtable" };
|
|
21
|
+
|
|
22
|
+
init: DatabaseAdapter["init"] = async (_options) => {
|
|
23
|
+
// Bigtable instances are typically created outside of app code (via console/IaC)
|
|
24
|
+
// Optionally, check if instance exists
|
|
25
|
+
const [exists] = await this.#instance.exists();
|
|
26
|
+
if (!exists) {
|
|
27
|
+
throw new DatabaseAdapterErrors.DatabaseNotInitializedError(
|
|
28
|
+
`Bigtable instance '${this.#instance.id}' does not exist.`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
listCollections: DatabaseAdapter["listCollections"] = async (_options) => {
|
|
34
|
+
const [tables] = await this.#instance.getTables();
|
|
35
|
+
return tables.map((table) => table.id);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
createCollection: DatabaseAdapter["createCollection"] = async (collectionId, _options) => {
|
|
39
|
+
try {
|
|
40
|
+
await this.#instance.createTable(collectionId, {
|
|
41
|
+
families: [COLUMN_FAMILY],
|
|
42
|
+
});
|
|
43
|
+
} catch (error) {
|
|
44
|
+
throw new DatabaseAdapterErrors.CollectionAlreadyExistsError(collectionId, error);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
hasCollection: DatabaseAdapter["hasCollection"] = async (collectionId, _options) => {
|
|
49
|
+
const table = this.#instance.table(collectionId);
|
|
50
|
+
const [exists] = await table.exists();
|
|
51
|
+
return exists;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
deleteCollection: DatabaseAdapter["deleteCollection"] = async (collectionId, _options) => {
|
|
55
|
+
try {
|
|
56
|
+
const table = this.#instance.table(collectionId);
|
|
57
|
+
await table.delete();
|
|
58
|
+
} catch (error) {
|
|
59
|
+
throw new DatabaseAdapterErrors.CollectionDoesNotExistError(collectionId, error);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
listDocuments: DatabaseAdapter["listDocuments"] = async <
|
|
64
|
+
Document extends StoryBookerDatabaseDocument,
|
|
65
|
+
>(
|
|
66
|
+
collectionId: string,
|
|
67
|
+
_listOptions: DatabaseDocumentListOptions<Document>,
|
|
68
|
+
_options: DatabaseAdapterOptions,
|
|
69
|
+
) => {
|
|
70
|
+
try {
|
|
71
|
+
const table = this.#instance.table(collectionId);
|
|
72
|
+
const [rows] = await table.getRows();
|
|
73
|
+
const list: Document[] = [];
|
|
74
|
+
for (const row of rows) {
|
|
75
|
+
const data = (row.data as Record<ColumnFamily, Document>)[COLUMN_FAMILY];
|
|
76
|
+
const document: Document = { ...data, id: row.id };
|
|
77
|
+
list.push(document);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return list;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
throw new DatabaseAdapterErrors.CollectionDoesNotExistError(collectionId, error);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
getDocument: DatabaseAdapter["getDocument"] = async <
|
|
87
|
+
Document extends StoryBookerDatabaseDocument,
|
|
88
|
+
>(
|
|
89
|
+
collectionId: string,
|
|
90
|
+
documentId: string,
|
|
91
|
+
_options: DatabaseAdapterOptions,
|
|
92
|
+
) => {
|
|
93
|
+
const table = this.#instance.table(collectionId);
|
|
94
|
+
const row = table.row(documentId);
|
|
95
|
+
const [exists] = await row.exists();
|
|
96
|
+
if (!exists) {
|
|
97
|
+
throw new DatabaseAdapterErrors.DocumentDoesNotExistError(collectionId, documentId);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const [rowData] = await row.get<Document>([COLUMN_FAMILY]);
|
|
101
|
+
|
|
102
|
+
return { ...rowData, id: documentId };
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
createDocument: DatabaseAdapter["createDocument"] = async (
|
|
106
|
+
collectionId,
|
|
107
|
+
documentData,
|
|
108
|
+
_options,
|
|
109
|
+
) => {
|
|
110
|
+
try {
|
|
111
|
+
const table = this.#instance.table(collectionId);
|
|
112
|
+
const row = table.row(documentData.id);
|
|
113
|
+
await row.create({ entry: { [COLUMN_FAMILY]: documentData } });
|
|
114
|
+
} catch (error) {
|
|
115
|
+
throw new DatabaseAdapterErrors.DocumentAlreadyExistsError(
|
|
116
|
+
collectionId,
|
|
117
|
+
documentData.id,
|
|
118
|
+
error,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
hasDocument: DatabaseAdapter["hasDocument"] = async (collectionId, documentId, _options) => {
|
|
124
|
+
const table = this.#instance.table(collectionId);
|
|
125
|
+
const row = table.row(documentId);
|
|
126
|
+
const [exists] = await row.exists();
|
|
127
|
+
return exists;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
deleteDocument: DatabaseAdapter["deleteDocument"] = async (
|
|
131
|
+
collectionId,
|
|
132
|
+
documentId,
|
|
133
|
+
_options,
|
|
134
|
+
) => {
|
|
135
|
+
try {
|
|
136
|
+
const table = this.#instance.table(collectionId);
|
|
137
|
+
const row = table.row(documentId);
|
|
138
|
+
await row.delete();
|
|
139
|
+
} catch (error) {
|
|
140
|
+
throw new DatabaseAdapterErrors.DocumentDoesNotExistError(collectionId, documentId, error);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
updateDocument: DatabaseAdapter["updateDocument"] = async (
|
|
145
|
+
collectionId,
|
|
146
|
+
documentId,
|
|
147
|
+
documentData,
|
|
148
|
+
) => {
|
|
149
|
+
try {
|
|
150
|
+
const table = this.#instance.table(collectionId);
|
|
151
|
+
const row = table.row(documentId);
|
|
152
|
+
await row.save({ [COLUMN_FAMILY]: documentData });
|
|
153
|
+
} catch (error) {
|
|
154
|
+
throw new DatabaseAdapterErrors.DocumentDoesNotExistError(collectionId, documentId, error);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { Firestore } from "@google-cloud/firestore";
|
|
2
|
+
import {
|
|
3
|
+
DatabaseAdapterErrors,
|
|
4
|
+
type DatabaseAdapter,
|
|
5
|
+
type DatabaseAdapterOptions,
|
|
6
|
+
type DatabaseDocumentListOptions,
|
|
7
|
+
type StoryBookerDatabaseDocument,
|
|
8
|
+
} from "./_internal/database.ts";
|
|
9
|
+
|
|
10
|
+
export class GcpFirestoreDatabaseAdapter implements DatabaseAdapter {
|
|
11
|
+
#instance: Firestore;
|
|
12
|
+
|
|
13
|
+
constructor(instance: Firestore) {
|
|
14
|
+
this.#instance = instance;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
metadata: DatabaseAdapter["metadata"] = { name: "Google Cloud Firestore" };
|
|
18
|
+
|
|
19
|
+
listCollections: DatabaseAdapter["listCollections"] = async (_options) => {
|
|
20
|
+
try {
|
|
21
|
+
const collections = await this.#instance.listCollections();
|
|
22
|
+
return collections.map((col) => col.id);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
throw new DatabaseAdapterErrors.DatabaseNotInitializedError(error);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// oxlint-disable-next-line class-methods-use-this --- NOOP
|
|
29
|
+
createCollection: DatabaseAdapter["createCollection"] = async (_collectionId, _options) => {
|
|
30
|
+
// Firestore creates collections implicitly when you add a document.
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
hasCollection: DatabaseAdapter["hasCollection"] = async (collectionId, _options) => {
|
|
34
|
+
const col = this.#instance.collection(collectionId);
|
|
35
|
+
const snapshot = await col.limit(1).get();
|
|
36
|
+
return !snapshot.empty;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
deleteCollection: DatabaseAdapter["deleteCollection"] = async (collectionId, _options) => {
|
|
40
|
+
// Firestore doesn't have a direct way to delete a collection
|
|
41
|
+
// We need to delete all documents in the collection
|
|
42
|
+
try {
|
|
43
|
+
const col = this.#instance.collection(collectionId);
|
|
44
|
+
const snapshot = await col.get();
|
|
45
|
+
if (snapshot.empty) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const batch = this.#instance.batch();
|
|
49
|
+
for (const doc of snapshot.docs) {
|
|
50
|
+
batch.delete(doc.ref);
|
|
51
|
+
}
|
|
52
|
+
await batch.commit();
|
|
53
|
+
} catch (error) {
|
|
54
|
+
throw new DatabaseAdapterErrors.CollectionDoesNotExistError(collectionId, error);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
listDocuments: DatabaseAdapter["listDocuments"] = async <
|
|
59
|
+
Document extends StoryBookerDatabaseDocument,
|
|
60
|
+
>(
|
|
61
|
+
collectionId: string,
|
|
62
|
+
_listOptions: DatabaseDocumentListOptions<Document>,
|
|
63
|
+
_options: DatabaseAdapterOptions,
|
|
64
|
+
) => {
|
|
65
|
+
try {
|
|
66
|
+
const col = this.#instance.collection(collectionId);
|
|
67
|
+
const snapshot = await col.get();
|
|
68
|
+
const list: Document[] = [];
|
|
69
|
+
for (const doc of snapshot.docs) {
|
|
70
|
+
const data = doc.data() as Document;
|
|
71
|
+
list.push({ ...data, id: doc.id });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return list;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
throw new DatabaseAdapterErrors.CollectionDoesNotExistError(collectionId, error);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
getDocument: DatabaseAdapter["getDocument"] = async <
|
|
81
|
+
Document extends StoryBookerDatabaseDocument,
|
|
82
|
+
>(
|
|
83
|
+
collectionId: string,
|
|
84
|
+
documentId: string,
|
|
85
|
+
_options: DatabaseAdapterOptions,
|
|
86
|
+
) => {
|
|
87
|
+
const docRef = this.#instance.collection(collectionId).doc(documentId);
|
|
88
|
+
const doc = await docRef.get();
|
|
89
|
+
if (!doc.exists) {
|
|
90
|
+
throw new DatabaseAdapterErrors.DocumentDoesNotExistError(collectionId, documentId);
|
|
91
|
+
}
|
|
92
|
+
return { ...doc.data(), id: doc.id } as Document;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
createDocument: DatabaseAdapter["createDocument"] = async (
|
|
96
|
+
collectionId,
|
|
97
|
+
documentData,
|
|
98
|
+
_options,
|
|
99
|
+
) => {
|
|
100
|
+
try {
|
|
101
|
+
const docRef = this.#instance.collection(collectionId).doc(documentData.id);
|
|
102
|
+
await docRef.create(documentData);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
throw new DatabaseAdapterErrors.DocumentAlreadyExistsError(
|
|
105
|
+
collectionId,
|
|
106
|
+
documentData.id,
|
|
107
|
+
error,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
hasDocument: DatabaseAdapter["hasDocument"] = async (collectionId, documentId, _options) => {
|
|
113
|
+
const docRef = this.#instance.collection(collectionId).doc(documentId);
|
|
114
|
+
const doc = await docRef.get();
|
|
115
|
+
return doc.exists;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
deleteDocument: DatabaseAdapter["deleteDocument"] = async (
|
|
119
|
+
collectionId,
|
|
120
|
+
documentId,
|
|
121
|
+
_options,
|
|
122
|
+
) => {
|
|
123
|
+
try {
|
|
124
|
+
const docRef = this.#instance.collection(collectionId).doc(documentId);
|
|
125
|
+
await docRef.delete();
|
|
126
|
+
} catch (error) {
|
|
127
|
+
throw new DatabaseAdapterErrors.DocumentDoesNotExistError(collectionId, documentId, error);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
updateDocument: DatabaseAdapter["updateDocument"] = async (
|
|
132
|
+
collectionId,
|
|
133
|
+
documentId,
|
|
134
|
+
documentData,
|
|
135
|
+
) => {
|
|
136
|
+
try {
|
|
137
|
+
const docRef = this.#instance.collection(collectionId).doc(documentId);
|
|
138
|
+
await docRef.set(documentData, {
|
|
139
|
+
merge: true,
|
|
140
|
+
mergeFields: Object.keys(documentData),
|
|
141
|
+
});
|
|
142
|
+
} catch (error) {
|
|
143
|
+
throw new DatabaseAdapterErrors.DocumentDoesNotExistError(collectionId, documentId, error);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|