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,160 @@
1
+ import {
2
+ type S3Client,
3
+ CreateBucketCommand,
4
+ DeleteBucketCommand,
5
+ ListBucketsCommand,
6
+ ListObjectsV2Command,
7
+ DeleteObjectsCommand,
8
+ PutObjectCommand,
9
+ HeadObjectCommand,
10
+ GetObjectCommand,
11
+ } from "@aws-sdk/client-s3";
12
+ import { Buffer } from "node:buffer";
13
+ import { StorageAdapterErrors, type StorageAdapter } from "./_internal/storage.ts";
14
+
15
+ export class AwsS3StorageService implements StorageAdapter {
16
+ #client: S3Client;
17
+
18
+ constructor(client: S3Client) {
19
+ this.#client = client;
20
+ }
21
+
22
+ metadata: StorageAdapter["metadata"] = { name: "AWS S3" };
23
+
24
+ createContainer: StorageAdapter["createContainer"] = async (containerId, options) => {
25
+ try {
26
+ await this.#client.send(
27
+ new CreateBucketCommand({ Bucket: genBucketNameFromContainerId(containerId) }),
28
+ { abortSignal: options.abortSignal },
29
+ );
30
+ } catch (error) {
31
+ throw new StorageAdapterErrors.ContainerAlreadyExistsError(containerId, error);
32
+ }
33
+ };
34
+
35
+ deleteContainer: StorageAdapter["deleteContainer"] = async (containerId, options) => {
36
+ try {
37
+ await this.#client.send(
38
+ new DeleteBucketCommand({ Bucket: genBucketNameFromContainerId(containerId) }),
39
+ { abortSignal: options.abortSignal },
40
+ );
41
+ } catch (error) {
42
+ throw new StorageAdapterErrors.ContainerDoesNotExistError(containerId, error);
43
+ }
44
+ };
45
+
46
+ hasContainer: StorageAdapter["hasContainer"] = async (containerId, options) => {
47
+ const buckets = await this.#client.send(new ListBucketsCommand({}), {
48
+ abortSignal: options.abortSignal,
49
+ });
50
+ return Boolean(
51
+ buckets.Buckets?.some((bucket) => bucket.Name === genBucketNameFromContainerId(containerId)),
52
+ );
53
+ };
54
+
55
+ listContainers: StorageAdapter["listContainers"] = async (options) => {
56
+ const buckets = await this.#client.send(new ListBucketsCommand({}), {
57
+ abortSignal: options.abortSignal,
58
+ });
59
+ // oxlint-disable-next-line no-non-null-assertion
60
+ return buckets.Buckets?.map((bucket) => bucket.Name!) ?? [];
61
+ };
62
+
63
+ deleteFiles: StorageAdapter["deleteFiles"] = async (containerId, filePathsOrPrefix, options) => {
64
+ try {
65
+ const bucket = genBucketNameFromContainerId(containerId);
66
+ let objects: { Key: string }[] = [];
67
+ if (typeof filePathsOrPrefix === "string") {
68
+ const resp = await this.#client.send(
69
+ new ListObjectsV2Command({
70
+ Bucket: bucket,
71
+ Prefix: filePathsOrPrefix,
72
+ }),
73
+ { abortSignal: options.abortSignal },
74
+ );
75
+ // oxlint-disable-next-line no-non-null-assertion
76
+ objects = (resp.Contents ?? []).map((obj) => ({ Key: obj.Key! }));
77
+ } else {
78
+ objects = filePathsOrPrefix.map((path) => ({ Key: path }));
79
+ }
80
+ if (objects.length === 0) {
81
+ return;
82
+ }
83
+
84
+ await this.#client.send(
85
+ new DeleteObjectsCommand({
86
+ Bucket: bucket,
87
+ Delete: { Objects: objects },
88
+ }),
89
+ { abortSignal: options.abortSignal },
90
+ );
91
+ } catch (error) {
92
+ throw new StorageAdapterErrors.CustomError(
93
+ undefined,
94
+ `Failed to delete files in container ${containerId}.`,
95
+ error,
96
+ );
97
+ }
98
+ };
99
+
100
+ uploadFiles: StorageAdapter["uploadFiles"] = async (containerId, files, options) => {
101
+ const bucket = genBucketNameFromContainerId(containerId);
102
+
103
+ const promises = files.map(async ({ content, path, mimeType }) => {
104
+ await this.#client
105
+ .send(
106
+ new PutObjectCommand({
107
+ Body: typeof content === "string" ? Buffer.from(content) : content,
108
+ Bucket: bucket,
109
+ ContentType: mimeType,
110
+ Key: path,
111
+ }),
112
+ { abortSignal: options.abortSignal },
113
+ )
114
+ .then((error: unknown) => {
115
+ options.logger.error(`Failed to upload file ${path} to bucket ${bucket}:`, error);
116
+ });
117
+ });
118
+
119
+ await Promise.allSettled(promises);
120
+ };
121
+
122
+ hasFile: StorageAdapter["hasFile"] = async (containerId, filepath, options) => {
123
+ try {
124
+ await this.#client.send(
125
+ new HeadObjectCommand({
126
+ Bucket: genBucketNameFromContainerId(containerId),
127
+ Key: filepath,
128
+ }),
129
+ { abortSignal: options.abortSignal },
130
+ );
131
+ return true;
132
+ } catch {
133
+ return false;
134
+ }
135
+ };
136
+
137
+ downloadFile: StorageAdapter["downloadFile"] = async (containerId, filepath, options) => {
138
+ const bucket = genBucketNameFromContainerId(containerId);
139
+ const resp = await this.#client.send(new GetObjectCommand({ Bucket: bucket, Key: filepath }), {
140
+ abortSignal: options.abortSignal,
141
+ });
142
+
143
+ if (!resp.Body) {
144
+ throw new StorageAdapterErrors.FileDoesNotExistError(containerId, filepath);
145
+ }
146
+
147
+ return {
148
+ content: resp.Body as ReadableStream,
149
+ mimeType: resp.ContentType,
150
+ path: filepath,
151
+ };
152
+ };
153
+ }
154
+
155
+ function genBucketNameFromContainerId(containerId: string): string {
156
+ return containerId
157
+ .replaceAll(/[^\w-]+/g, "-")
158
+ .slice(0, 63)
159
+ .toLowerCase();
160
+ }
@@ -0,0 +1,223 @@
1
+ import type { BlobClient, BlobServiceClient, BlockBlobClient } from "@azure/storage-blob";
2
+ import { Readable } from "node:stream";
3
+ import type streamWeb from "node:stream/web";
4
+ import { StorageAdapterErrors, type StorageAdapter } from "./_internal/storage.ts";
5
+
6
+ export class AzureBlobStorageService implements StorageAdapter {
7
+ #client: BlobServiceClient;
8
+
9
+ constructor(client: BlobServiceClient) {
10
+ this.#client = client;
11
+ }
12
+
13
+ metadata: StorageAdapter["metadata"] = { name: "Azure Blob Storage" };
14
+
15
+ createContainer: StorageAdapter["createContainer"] = async (containerId, options) => {
16
+ try {
17
+ const containerName = genContainerNameFromContainerId(containerId);
18
+ await this.#client.createContainer(containerName, {
19
+ abortSignal: options.abortSignal,
20
+ });
21
+ } catch (error) {
22
+ throw new StorageAdapterErrors.ContainerAlreadyExistsError(containerId, error);
23
+ }
24
+ };
25
+
26
+ deleteContainer: StorageAdapter["deleteContainer"] = async (containerId, options) => {
27
+ try {
28
+ const containerName = genContainerNameFromContainerId(containerId);
29
+ await this.#client.getContainerClient(containerName).deleteIfExists({
30
+ abortSignal: options.abortSignal,
31
+ });
32
+ } catch (error) {
33
+ throw new StorageAdapterErrors.ContainerDoesNotExistError(containerId, error);
34
+ }
35
+ };
36
+
37
+ hasContainer: StorageAdapter["hasContainer"] = async (containerId, options) => {
38
+ const containerName = genContainerNameFromContainerId(containerId);
39
+ return await this.#client.getContainerClient(containerName).exists({
40
+ abortSignal: options.abortSignal,
41
+ });
42
+ };
43
+
44
+ listContainers: StorageAdapter["listContainers"] = async (options) => {
45
+ const containers: string[] = [];
46
+ for await (const item of this.#client.listContainers({
47
+ abortSignal: options.abortSignal,
48
+ })) {
49
+ containers.push(item.name);
50
+ }
51
+
52
+ return containers;
53
+ };
54
+
55
+ deleteFiles: StorageAdapter["deleteFiles"] = async (containerId, filePathsOrPrefix, options) => {
56
+ const containerName = genContainerNameFromContainerId(containerId);
57
+ const containerClient = this.#client.getContainerClient(containerName);
58
+ const blobClientsToDelete: BlobClient[] = [];
59
+
60
+ if (typeof filePathsOrPrefix === "string") {
61
+ for await (const blob of containerClient.listBlobsFlat({
62
+ abortSignal: options.abortSignal,
63
+ prefix: filePathsOrPrefix,
64
+ })) {
65
+ blobClientsToDelete.push(containerClient.getBlobClient(blob.name));
66
+ }
67
+ } else {
68
+ for (const filepath of filePathsOrPrefix) {
69
+ blobClientsToDelete.push(containerClient.getBlobClient(filepath));
70
+ }
71
+ }
72
+
73
+ if (blobClientsToDelete.length === 0) {
74
+ return;
75
+ }
76
+
77
+ const response = await containerClient.getBlobBatchClient().deleteBlobs(blobClientsToDelete, {
78
+ abortSignal: options.abortSignal,
79
+ });
80
+
81
+ if (response.errorCode) {
82
+ throw new StorageAdapterErrors.CustomError(
83
+ undefined,
84
+ `Failed to delete ${response.subResponsesFailedCount} blobs in container ${containerId}: ${response.errorCode}`,
85
+ );
86
+ }
87
+ };
88
+
89
+ uploadFiles: StorageAdapter["uploadFiles"] = async (containerId, files, options) => {
90
+ const containerName = genContainerNameFromContainerId(containerId);
91
+ const containerClient = this.#client.getContainerClient(containerName);
92
+
93
+ const { errors } = await promisePool(
94
+ files.map(({ content, path, mimeType }) => async (): Promise<void> => {
95
+ await uploadFileToBlobStorage(
96
+ containerClient.getBlockBlobClient(path),
97
+ content,
98
+ mimeType,
99
+ options.abortSignal,
100
+ );
101
+ }),
102
+ 20,
103
+ );
104
+
105
+ if (errors.length > 0) {
106
+ options.logger.error(`Failed to upload ${errors.length} files. Errors:`, errors);
107
+ }
108
+ };
109
+
110
+ hasFile: StorageAdapter["hasFile"] = async (containerId, filepath, options) => {
111
+ const containerName = genContainerNameFromContainerId(containerId);
112
+ const containerClient = this.#client.getContainerClient(containerName);
113
+ const blockBlobClient = containerClient.getBlockBlobClient(filepath);
114
+ return await blockBlobClient.exists({ abortSignal: options.abortSignal });
115
+ };
116
+
117
+ downloadFile: StorageAdapter["downloadFile"] = async (containerId, filepath, options) => {
118
+ const containerName = genContainerNameFromContainerId(containerId);
119
+ const containerClient = this.#client.getContainerClient(containerName);
120
+ const blockBlobClient = containerClient.getBlockBlobClient(filepath);
121
+
122
+ if (!(await blockBlobClient.exists())) {
123
+ throw new StorageAdapterErrors.FileDoesNotExistError(containerId, filepath);
124
+ }
125
+
126
+ const downloadResponse = await blockBlobClient.download(0, undefined, {
127
+ abortSignal: options.abortSignal,
128
+ });
129
+
130
+ if (!downloadResponse.readableStreamBody) {
131
+ throw new StorageAdapterErrors.FileMalformedError(
132
+ containerId,
133
+ filepath,
134
+ "No readable stream body found.",
135
+ );
136
+ }
137
+
138
+ return {
139
+ content: downloadResponse.readableStreamBody as unknown as ReadableStream,
140
+ mimeType: downloadResponse.contentType,
141
+ path: filepath,
142
+ };
143
+ };
144
+ }
145
+
146
+ function genContainerNameFromContainerId(containerId: string): string {
147
+ return containerId
148
+ .replaceAll(/[^\w-]+/g, "-")
149
+ .slice(0, 255)
150
+ .toLowerCase();
151
+ }
152
+
153
+ // oxlint-disable-next-line max-params
154
+ async function uploadFileToBlobStorage(
155
+ client: BlockBlobClient,
156
+ data: Blob | string | ReadableStream,
157
+ mimeType: string,
158
+ abortSignal?: AbortSignal,
159
+ ): Promise<void> {
160
+ if (typeof data === "string") {
161
+ const blob = new Blob([data], { type: mimeType });
162
+ await client.uploadData(blob, {
163
+ abortSignal,
164
+ blobHTTPHeaders: { blobContentType: mimeType },
165
+ });
166
+ return;
167
+ }
168
+
169
+ if (data instanceof Blob) {
170
+ await client.uploadData(data, {
171
+ abortSignal,
172
+ blobHTTPHeaders: { blobContentType: mimeType },
173
+ });
174
+ return;
175
+ }
176
+
177
+ if (data instanceof ReadableStream) {
178
+ const stream = data as unknown as streamWeb.ReadableStream;
179
+ await client.uploadStream(Readable.fromWeb(stream), undefined, undefined, {
180
+ abortSignal,
181
+ blobHTTPHeaders: { blobContentType: mimeType },
182
+ });
183
+ return;
184
+ }
185
+
186
+ throw new Error(`Unknown file type`);
187
+ }
188
+
189
+ async function promisePool<Result>(
190
+ tasks: (() => Promise<Result>)[],
191
+ concurrencyLimit: number,
192
+ ): Promise<{ errors: unknown[]; results: Result[] }> {
193
+ const promises: Promise<Result>[] = [];
194
+ const errors: unknown[] = [];
195
+ const executing = new Set();
196
+
197
+ for (const task of tasks) {
198
+ // Start the taskPromise
199
+ const promise = Promise.resolve().then(task);
200
+ promises.push(promise);
201
+
202
+ // Add to executing set
203
+ executing.add(promise);
204
+
205
+ // When the promise settles, remove it from executing
206
+ const cleanup = (): boolean => executing.delete(promise);
207
+ promise.then(cleanup).catch((error: unknown) => {
208
+ errors.push(error);
209
+ cleanup();
210
+ });
211
+
212
+ // If the number of running promises hit concurrencyLimit, wait for one to finish
213
+ if (executing.size >= concurrencyLimit) {
214
+ // oxlint-disable-next-line no-await-in-loop
215
+ await Promise.race(executing);
216
+ }
217
+ }
218
+
219
+ // Wait for all remaining tasks to finish
220
+ const results = await Promise.all(promises);
221
+
222
+ return { errors, results };
223
+ }
@@ -0,0 +1,158 @@
1
+ import type { CosmosClient, Database } from "@azure/cosmos";
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 AzureCosmosDatabaseService implements DatabaseAdapter {
11
+ #db: Database;
12
+
13
+ constructor(client: CosmosClient, dbName = "StoryBooker") {
14
+ this.#db = client.database(dbName);
15
+ }
16
+
17
+ metadata: DatabaseAdapter["metadata"] = { name: "Azure Cosmos DB" };
18
+
19
+ init: DatabaseAdapter["init"] = async (options) => {
20
+ await this.#db.client.databases.createIfNotExists(
21
+ { id: this.#db.id },
22
+ { abortSignal: options.abortSignal },
23
+ );
24
+ };
25
+
26
+ listCollections: DatabaseAdapter["listCollections"] = async (options) => {
27
+ try {
28
+ const response = await this.#db.containers
29
+ .readAll({ abortSignal: options.abortSignal })
30
+ .fetchAll();
31
+ const collections: string[] = response.resources.map((resource) => resource.id);
32
+
33
+ return collections;
34
+ } catch (error) {
35
+ throw new DatabaseAdapterErrors.DatabaseNotInitializedError(error);
36
+ }
37
+ };
38
+
39
+ createCollection: DatabaseAdapter["createCollection"] = async (collectionId, options) => {
40
+ try {
41
+ await this.#db.containers.create({ id: collectionId }, { abortSignal: options.abortSignal });
42
+ } catch (error) {
43
+ throw new DatabaseAdapterErrors.CollectionAlreadyExistsError(collectionId, error);
44
+ }
45
+ };
46
+
47
+ hasCollection: DatabaseAdapter["hasCollection"] = async (collectionId, options) => {
48
+ try {
49
+ const response = await this.#db
50
+ .container(collectionId)
51
+ .read({ abortSignal: options.abortSignal });
52
+ return Boolean(response.resource);
53
+ } catch {
54
+ return false;
55
+ }
56
+ };
57
+
58
+ deleteCollection: DatabaseAdapter["deleteCollection"] = async (collectionId, options) => {
59
+ try {
60
+ await this.#db.container(collectionId).delete({ abortSignal: options.abortSignal });
61
+ } catch (error) {
62
+ throw new DatabaseAdapterErrors.CollectionDoesNotExistError(collectionId, error);
63
+ }
64
+ };
65
+
66
+ listDocuments: DatabaseAdapter["listDocuments"] = async <
67
+ Document extends StoryBookerDatabaseDocument,
68
+ >(
69
+ collectionId: string,
70
+ _listOptions: DatabaseDocumentListOptions<Document>,
71
+ options: DatabaseAdapterOptions,
72
+ ) => {
73
+ const items = await this.#db
74
+ .container(collectionId)
75
+ .items.readAll({ abortSignal: options.abortSignal })
76
+ .fetchAll();
77
+ return items.resources as Document[];
78
+ };
79
+
80
+ getDocument: DatabaseAdapter["getDocument"] = async <
81
+ Document extends StoryBookerDatabaseDocument,
82
+ >(
83
+ collectionId: string,
84
+ documentId: string,
85
+ options: DatabaseAdapterOptions,
86
+ ) => {
87
+ try {
88
+ const item = this.#db.container(collectionId).item(documentId);
89
+ const response = await item.read({ abortSignal: options.abortSignal });
90
+ const document: Document = response.resource;
91
+ document.id = documentId;
92
+ return document;
93
+ } catch (error) {
94
+ throw new DatabaseAdapterErrors.DocumentDoesNotExistError(collectionId, documentId, error);
95
+ }
96
+ };
97
+
98
+ createDocument: DatabaseAdapter["createDocument"] = async (
99
+ collectionId,
100
+ documentData,
101
+ options,
102
+ ) => {
103
+ try {
104
+ await this.#db
105
+ .container(collectionId)
106
+ .items.create(documentData, { abortSignal: options.abortSignal });
107
+ } catch (error) {
108
+ throw new DatabaseAdapterErrors.DocumentAlreadyExistsError(
109
+ collectionId,
110
+ documentData.id,
111
+ error,
112
+ );
113
+ }
114
+ };
115
+
116
+ hasDocument: DatabaseAdapter["hasDocument"] = async (collectionId, documentId, options) => {
117
+ const item = this.#db.container(collectionId).item(documentId);
118
+ const response = await item.read({ abortSignal: options.abortSignal });
119
+ return Boolean(response.resource);
120
+ };
121
+
122
+ deleteDocument: DatabaseAdapter["deleteDocument"] = async (collectionId, documentId, options) => {
123
+ try {
124
+ await this.#db
125
+ .container(collectionId)
126
+ .item(documentId)
127
+ .delete({ abortSignal: options.abortSignal });
128
+ } catch (error) {
129
+ throw new DatabaseAdapterErrors.DocumentDoesNotExistError(collectionId, documentId, error);
130
+ }
131
+ };
132
+
133
+ // oxlint-disable-next-line max-params
134
+ updateDocument: DatabaseAdapter["updateDocument"] = async (
135
+ collectionId,
136
+ documentId,
137
+ documentData,
138
+ options,
139
+ ) => {
140
+ try {
141
+ await this.#db
142
+ .container(collectionId)
143
+ .item(documentId)
144
+ .patch<Document>(
145
+ {
146
+ operations: Object.entries(documentData).map(([key, value]) => ({
147
+ op: "replace",
148
+ path: `/${key}`,
149
+ value,
150
+ })),
151
+ },
152
+ { abortSignal: options.abortSignal },
153
+ );
154
+ } catch (error) {
155
+ throw new DatabaseAdapterErrors.DocumentDoesNotExistError(collectionId, documentId, error);
156
+ }
157
+ };
158
+ }