storybooker 0.19.3 → 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.
Files changed (215) hide show
  1. package/README.md +40 -18
  2. package/dist/adapters/_internal/queue.d.mts +127 -0
  3. package/dist/aws-dynamodb.d.mts +22 -0
  4. package/dist/aws-dynamodb.mjs +118 -0
  5. package/dist/aws-dynamodb.mjs.map +1 -0
  6. package/dist/aws-s3.d.mts +20 -0
  7. package/dist/aws-s3.mjs +96 -0
  8. package/dist/aws-s3.mjs.map +1 -0
  9. package/dist/azure-blob-storage.d.mts +20 -0
  10. package/dist/azure-blob-storage.mjs +126 -0
  11. package/dist/azure-blob-storage.mjs.map +1 -0
  12. package/dist/azure-cosmos-db.d.mts +23 -0
  13. package/dist/azure-cosmos-db.mjs +87 -0
  14. package/dist/azure-cosmos-db.mjs.map +1 -0
  15. package/dist/azure-data-tables.d.mts +23 -0
  16. package/dist/azure-data-tables.mjs +127 -0
  17. package/dist/azure-data-tables.mjs.map +1 -0
  18. package/dist/azure-easy-auth.d.mts +50 -0
  19. package/dist/azure-easy-auth.mjs +88 -0
  20. package/dist/azure-easy-auth.mjs.map +1 -0
  21. package/dist/azure-functions.d.mts +62 -0
  22. package/dist/azure-functions.mjs +147 -0
  23. package/dist/azure-functions.mjs.map +1 -0
  24. package/dist/fs.d.mts +37 -0
  25. package/dist/fs.mjs +240 -0
  26. package/dist/fs.mjs.map +1 -0
  27. package/dist/gcp-big-table.d.mts +23 -0
  28. package/dist/gcp-big-table.mjs +92 -0
  29. package/dist/gcp-big-table.mjs.map +1 -0
  30. package/dist/gcp-firestore.d.mts +22 -0
  31. package/dist/gcp-firestore.mjs +87 -0
  32. package/dist/gcp-firestore.mjs.map +1 -0
  33. package/dist/gcp-storage.d.mts +20 -0
  34. package/dist/gcp-storage.mjs +96 -0
  35. package/dist/gcp-storage.mjs.map +1 -0
  36. package/dist/handlers/handle-process-zip.mjs +90 -0
  37. package/dist/handlers/handle-process-zip.mjs.map +1 -0
  38. package/dist/handlers/handle-purge.d.mts +12 -0
  39. package/dist/handlers/handle-purge.mjs +36 -0
  40. package/dist/handlers/handle-purge.mjs.map +1 -0
  41. package/dist/handlers/handle-serve-storybook.mjs +94 -0
  42. package/dist/handlers/handle-serve-storybook.mjs.map +1 -0
  43. package/dist/index.d.mts +28 -0
  44. package/dist/index.mjs +62 -0
  45. package/dist/index.mjs.map +1 -0
  46. package/dist/models/builds-model.mjs +248 -0
  47. package/dist/models/builds-model.mjs.map +1 -0
  48. package/dist/models/builds-schema.d.mts +171 -0
  49. package/dist/models/builds-schema.mjs +67 -0
  50. package/dist/models/builds-schema.mjs.map +1 -0
  51. package/dist/models/projects-model.mjs +122 -0
  52. package/dist/models/projects-model.mjs.map +1 -0
  53. package/dist/models/projects-schema.d.mts +70 -0
  54. package/dist/models/projects-schema.mjs +37 -0
  55. package/dist/models/projects-schema.mjs.map +1 -0
  56. package/dist/models/tags-model.mjs +110 -0
  57. package/dist/models/tags-model.mjs.map +1 -0
  58. package/dist/models/tags-schema.d.mts +76 -0
  59. package/dist/models/tags-schema.mjs +34 -0
  60. package/dist/models/tags-schema.mjs.map +1 -0
  61. package/dist/models/~model.mjs +43 -0
  62. package/dist/models/~model.mjs.map +1 -0
  63. package/dist/models/~shared-schema.d.mts +1 -0
  64. package/dist/models/~shared-schema.mjs +20 -0
  65. package/dist/models/~shared-schema.mjs.map +1 -0
  66. package/dist/mysql.d.mts +39 -0
  67. package/dist/mysql.mjs +151 -0
  68. package/dist/mysql.mjs.map +1 -0
  69. package/dist/redis.d.mts +33 -0
  70. package/dist/redis.mjs +118 -0
  71. package/dist/redis.mjs.map +1 -0
  72. package/dist/routers/account-router.mjs +91 -0
  73. package/dist/routers/account-router.mjs.map +1 -0
  74. package/dist/routers/builds-router.mjs +347 -0
  75. package/dist/routers/builds-router.mjs.map +1 -0
  76. package/dist/routers/projects-router.mjs +236 -0
  77. package/dist/routers/projects-router.mjs.map +1 -0
  78. package/dist/routers/root-router.mjs +108 -0
  79. package/dist/routers/root-router.mjs.map +1 -0
  80. package/dist/routers/tags-router.mjs +269 -0
  81. package/dist/routers/tags-router.mjs.map +1 -0
  82. package/dist/routers/tasks-router.mjs +71 -0
  83. package/dist/routers/tasks-router.mjs.map +1 -0
  84. package/dist/urls.d.mts +47 -0
  85. package/dist/urls.mjs +208 -0
  86. package/dist/urls.mjs.map +1 -0
  87. package/dist/utils/adapter-utils.d.mts +14 -0
  88. package/dist/utils/adapter-utils.mjs +14 -0
  89. package/dist/utils/adapter-utils.mjs.map +1 -0
  90. package/dist/utils/auth.mjs +25 -0
  91. package/dist/utils/auth.mjs.map +1 -0
  92. package/dist/utils/error.d.mts +21 -0
  93. package/dist/utils/error.mjs +109 -0
  94. package/dist/utils/error.mjs.map +1 -0
  95. package/dist/utils/file-utils.mjs +16 -0
  96. package/dist/utils/file-utils.mjs.map +1 -0
  97. package/dist/utils/openapi-utils.mjs +45 -0
  98. package/dist/utils/openapi-utils.mjs.map +1 -0
  99. package/dist/utils/request.mjs +35 -0
  100. package/dist/utils/request.mjs.map +1 -0
  101. package/dist/utils/response.mjs +24 -0
  102. package/dist/utils/response.mjs.map +1 -0
  103. package/dist/utils/store.mjs +54 -0
  104. package/dist/utils/store.mjs.map +1 -0
  105. package/dist/utils/ui-utils.mjs +38 -0
  106. package/dist/utils/ui-utils.mjs.map +1 -0
  107. package/dist/utils/url-utils.d.mts +10 -0
  108. package/dist/utils/url-utils.mjs +54 -0
  109. package/dist/utils/url-utils.mjs.map +1 -0
  110. package/dist/~internal/adapter/auth.d.mts +123 -0
  111. package/dist/~internal/adapter/auth.mjs +20 -0
  112. package/dist/~internal/adapter/auth.mjs.map +1 -0
  113. package/dist/~internal/adapter/database.d.mts +240 -0
  114. package/dist/~internal/adapter/database.mjs +63 -0
  115. package/dist/~internal/adapter/database.mjs.map +1 -0
  116. package/dist/~internal/adapter/logger.d.mts +34 -0
  117. package/dist/~internal/adapter/logger.mjs +13 -0
  118. package/dist/~internal/adapter/logger.mjs.map +1 -0
  119. package/dist/~internal/adapter/storage.d.mts +208 -0
  120. package/dist/~internal/adapter/storage.mjs +63 -0
  121. package/dist/~internal/adapter/storage.mjs.map +1 -0
  122. package/dist/~internal/adapter/ui.d.mts +109 -0
  123. package/dist/~internal/adapter/ui.mjs +1 -0
  124. package/dist/~internal/adapter.d.mts +8 -0
  125. package/dist/~internal/adapter.mjs +6 -0
  126. package/dist/~internal/constants.d.mts +24 -0
  127. package/dist/~internal/constants.mjs +32 -0
  128. package/dist/~internal/constants.mjs.map +1 -0
  129. package/dist/~internal/mimes.d.mts +449 -0
  130. package/dist/~internal/mimes.mjs +454 -0
  131. package/dist/~internal/mimes.mjs.map +1 -0
  132. package/dist/~internal/router.d.mts +1651 -0
  133. package/dist/~internal/router.mjs +39 -0
  134. package/dist/~internal/router.mjs.map +1 -0
  135. package/dist/~internal/types.d.mts +77 -0
  136. package/dist/~internal/types.mjs +1 -0
  137. package/dist/~internal/utils.d.mts +4 -0
  138. package/dist/~internal/utils.mjs +5 -0
  139. package/openapi.json +3162 -0
  140. package/package.json +148 -27
  141. package/src/adapters/_internal/auth.ts +135 -0
  142. package/src/adapters/_internal/database.ts +241 -0
  143. package/src/adapters/_internal/index.ts +8 -0
  144. package/src/adapters/_internal/logger.ts +41 -0
  145. package/src/adapters/_internal/queue.ts +151 -0
  146. package/src/adapters/_internal/storage.ts +197 -0
  147. package/src/adapters/_internal/ui.ts +103 -0
  148. package/src/adapters/aws-dynamodb.ts +201 -0
  149. package/src/adapters/aws-s3.ts +160 -0
  150. package/src/adapters/azure-blob-storage.ts +223 -0
  151. package/src/adapters/azure-cosmos-db.ts +158 -0
  152. package/src/adapters/azure-data-tables.ts +223 -0
  153. package/src/adapters/azure-easy-auth.ts +174 -0
  154. package/src/adapters/azure-functions.ts +242 -0
  155. package/src/adapters/fs.ts +398 -0
  156. package/src/adapters/gcp-big-table.ts +157 -0
  157. package/src/adapters/gcp-firestore.ts +146 -0
  158. package/src/adapters/gcp-storage.ts +141 -0
  159. package/src/adapters/mysql.ts +296 -0
  160. package/src/adapters/redis.ts +242 -0
  161. package/src/handlers/handle-process-zip.ts +117 -0
  162. package/src/handlers/handle-purge.ts +65 -0
  163. package/src/handlers/handle-serve-storybook.ts +101 -0
  164. package/src/index.ts +81 -16
  165. package/src/mocks/mock-auth-service.ts +51 -0
  166. package/src/mocks/mock-store.ts +26 -0
  167. package/src/models/builds-model.ts +373 -0
  168. package/src/models/builds-schema.ts +84 -0
  169. package/src/models/projects-model.ts +177 -0
  170. package/src/models/projects-schema.ts +69 -0
  171. package/src/models/tags-model.ts +138 -0
  172. package/src/models/tags-schema.ts +45 -0
  173. package/src/models/~model.ts +79 -0
  174. package/src/models/~shared-schema.ts +14 -0
  175. package/src/routers/_app-router.ts +57 -0
  176. package/src/routers/account-router.ts +136 -0
  177. package/src/routers/builds-router.ts +464 -0
  178. package/src/routers/projects-router.ts +309 -0
  179. package/src/routers/root-router.ts +127 -0
  180. package/src/routers/tags-router.ts +339 -0
  181. package/src/routers/tasks-router.ts +75 -0
  182. package/src/types.ts +107 -0
  183. package/src/urls.ts +327 -0
  184. package/src/utils/adapter-utils.ts +26 -0
  185. package/src/utils/auth.test.ts +71 -0
  186. package/src/utils/auth.ts +39 -0
  187. package/src/utils/constants.ts +31 -0
  188. package/src/utils/date-utils.ts +10 -0
  189. package/src/utils/error.test.ts +86 -0
  190. package/src/utils/error.ts +140 -0
  191. package/src/utils/file-utils.test.ts +65 -0
  192. package/src/utils/file-utils.ts +43 -0
  193. package/src/utils/index.ts +3 -0
  194. package/src/utils/mime-utils.ts +457 -0
  195. package/src/utils/openapi-utils.ts +49 -0
  196. package/src/utils/request.ts +97 -0
  197. package/src/utils/response.ts +20 -0
  198. package/src/utils/store.ts +85 -0
  199. package/src/utils/story-utils.ts +42 -0
  200. package/src/utils/text-utils.ts +10 -0
  201. package/src/utils/ui-utils.ts +57 -0
  202. package/src/utils/url-utils.ts +113 -0
  203. package/dist/index.js +0 -554
  204. package/src/commands/create.ts +0 -263
  205. package/src/commands/purge.ts +0 -70
  206. package/src/commands/test.ts +0 -42
  207. package/src/service-schema.d.ts +0 -2023
  208. package/src/utils/auth-utils.ts +0 -31
  209. package/src/utils/pkg-utils.ts +0 -37
  210. package/src/utils/sb-build.ts +0 -55
  211. package/src/utils/sb-test.ts +0 -115
  212. package/src/utils/schema-utils.ts +0 -123
  213. package/src/utils/stream-utils.ts +0 -72
  214. package/src/utils/types.ts +0 -4
  215. 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
+ }