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.
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,151 @@
1
+ import type { StoryBookerAdapterMetadata } from "../../utils/adapter-utils.ts";
2
+ import type { LoggerAdapter } from "./logger.ts";
3
+
4
+ /**
5
+ * Service adapter to interact with queueing service.
6
+ *
7
+ * @description
8
+ * The adapter should provide callbacks to perform operations
9
+ * to an existing queue like send and receive messages.
10
+ *
11
+ * - `topic`: A topic/queue to hold messages.
12
+ * - `message`: A single message in topic which contains data and metadata.
13
+ * Each message has a unique identifier.
14
+ */
15
+ export interface QueueAdapter {
16
+ /**
17
+ * Metadata about the adapter.
18
+ */
19
+ metadata: StoryBookerAdapterMetadata;
20
+
21
+ /**
22
+ * An optional method that is called on app boot-up
23
+ * to run async setup functions.
24
+ * @param options Common options like abortSignal.
25
+ * @throws if an error occur during initialisation.
26
+ */
27
+ init?: (options: QueueAdapterOptions) => Promise<void>;
28
+
29
+ // Topics (message queues)
30
+
31
+ /**
32
+ * List all topics available in the queue service.
33
+ * @param options Common options like abortSignal.
34
+ * @returns A list of names/IDs of the topics.
35
+ * @throws If the queue service is not connected.
36
+ */
37
+ listTopics: (options: QueueAdapterOptions) => Promise<string[]>;
38
+
39
+ /**
40
+ * Create a topic used for different message types.
41
+ * @param topicId ID of the topic
42
+ * @param options Common options like abortSignal.
43
+ * @throws if topic with ID already exists.
44
+ */
45
+ createTopic: (topicId: string, options: QueueAdapterOptions) => Promise<void>;
46
+
47
+ /**
48
+ * Delete an existing topic.
49
+ * @param topicId ID of the topic
50
+ * @param options Common options like abortSignal.
51
+ * @throws if topic with ID does not exist.
52
+ */
53
+ deleteTopic: (topicId: string, options: QueueAdapterOptions) => Promise<void>;
54
+
55
+ /**
56
+ * Check if topic exists.
57
+ * @param topicId ID of the topic
58
+ * @param options Common options like abortSignal.
59
+ * @returns if topic is available or not
60
+ * @throws never.
61
+ */
62
+ hasTopic: (topicId: string, options: QueueAdapterOptions) => Promise<boolean>;
63
+
64
+ // Messages
65
+
66
+ /**
67
+ * Send a message to the specified topic.
68
+ * @param topicId ID of the topic
69
+ * @param message Message data to be sent
70
+ * @param options Common options like abortSignal.
71
+ * @returns Message ID if successful
72
+ * @throws if the topic does not exist.
73
+ */
74
+ sendMessage: <Message extends StoryBookerQueueMessage>(
75
+ topicId: string,
76
+ message: Omit<Message, "id" | "timestamp">,
77
+ options: QueueAdapterOptions,
78
+ ) => Promise<string>;
79
+
80
+ /**
81
+ * Receive messages from the specified topic.
82
+ * @param topicId ID of the topic
83
+ * @param receiveOptions Options to configure message receiving
84
+ * @param options Common options like abortSignal.
85
+ * @returns List of messages
86
+ * @throws if the topic does not exist.
87
+ */
88
+ receiveMessages: <Message extends StoryBookerQueueMessage>(
89
+ topicId: string,
90
+ receiveOptions: QueueMessageReceiveOptions,
91
+ options: QueueAdapterOptions,
92
+ ) => Promise<Message[]>;
93
+
94
+ /**
95
+ * Acknowledge that a message has been processed.
96
+ * @param topicId ID of the topic
97
+ * @param messageId ID of the message
98
+ * @param options Common options like abortSignal.
99
+ * @throws if the topic or message does not exist.
100
+ */
101
+ acknowledgeMessage: (
102
+ topicId: string,
103
+ messageId: string,
104
+ options: QueueAdapterOptions,
105
+ ) => Promise<void>;
106
+
107
+ /**
108
+ * Get message count in a topic.
109
+ * @param topicId ID of the topic
110
+ * @param options Common options like abortSignal.
111
+ * @returns Number of messages in the topic
112
+ * @throws if the topic does not exist.
113
+ */
114
+ getMessageCount: (topicId: string, options: QueueAdapterOptions) => Promise<number>;
115
+
116
+ /**
117
+ * Purge all messages from a topic.
118
+ * @param topicId ID of the topic
119
+ * @param options Common options like abortSignal.
120
+ * @throws if the topic does not exist.
121
+ */
122
+ purgeMessages: (topicId: string, options: QueueAdapterOptions) => Promise<void>;
123
+ }
124
+
125
+ /**
126
+ * Base Message shape used in StoryBooker Queue.
127
+ * Should always contain fields 'id' and 'timestamp'.
128
+ */
129
+ export interface StoryBookerQueueMessage {
130
+ id: string;
131
+ timestamp: number;
132
+ data: Record<string, unknown>;
133
+ attributes?: Record<string, string>;
134
+ }
135
+
136
+ /** Common Queue adapter options. */
137
+ export interface QueueAdapterOptions {
138
+ /** A signal that can be used to cancel the request handling. */
139
+ abortSignal?: AbortSignal;
140
+ /** Logger */
141
+ logger: LoggerAdapter;
142
+ }
143
+
144
+ export interface QueueMessageReceiveOptions {
145
+ /** Maximum number of messages to receive */
146
+ maxMessages?: number;
147
+ /** Visibility timeout in seconds */
148
+ visibilityTimeout?: number;
149
+ /** Wait time for long polling in seconds */
150
+ waitTimeSeconds?: number;
151
+ }
@@ -0,0 +1,197 @@
1
+ // oxlint-disable max-classes-per-file
2
+
3
+ import { HTTPException } from "hono/http-exception";
4
+ import type { ContentfulStatusCode } from "hono/utils/http-status";
5
+ import type { StoryBookerAdapterMetadata } from "../../utils/adapter-utils.ts";
6
+ import type { LoggerAdapter } from "./logger.ts";
7
+
8
+ /**
9
+ * Service adapter to interact with file-storage.
10
+ *
11
+ * @description
12
+ * The adapter should provide callbacks to perform operations
13
+ * to an existing storage like upload and download files.
14
+ *
15
+ * - `container`: A container/group/bucket to hold files. Each project has one container.
16
+ * - `file`: A single binary that can individually stored and retrieved.
17
+ *
18
+ * @throws {StorageNotInitializedError} if the Storage service is not connected.
19
+ * @throws {ContainerAlreadyExistsError} if the container already exists.
20
+ * @throws {ContainerDoesNotExistError} if the container does not exist.
21
+ * @throws {FileDoesNotExistError} if the file does not exist in the container.
22
+ * @throws {CustomError} if some other error occurs.
23
+ */
24
+ export interface StorageAdapter {
25
+ /**
26
+ * Metadata about the adapter.
27
+ */
28
+ metadata: StoryBookerAdapterMetadata;
29
+
30
+ /**
31
+ * An optional method that is called on app boot-up
32
+ * to run async setup functions.
33
+ * @param options Common options like abortSignal.
34
+ * @throws If the Storage service fails to initialize.
35
+ */
36
+ init?: (options: StorageAdapterOptions) => Promise<void>;
37
+
38
+ // Containers
39
+
40
+ /**
41
+ * List all containers available in the storage.
42
+ * @param options Common options like abortSignal.
43
+ * @returns A list of names/IDs of the containers.
44
+ * @throws {StorageNotInitializedError} if the Storage service is not connected.
45
+ */
46
+ listContainers: (options: StorageAdapterOptions) => Promise<string[]>;
47
+
48
+ /**
49
+ * Create a container used for different projects.
50
+ * @param containerId ID of the container
51
+ * @param options Common options like abortSignal.
52
+ * @throws if container with ID already exists.
53
+ */
54
+ createContainer: (containerId: string, options: StorageAdapterOptions) => Promise<void>;
55
+
56
+ /**
57
+ * Delete an existing container.
58
+ * @param containerId ID of the container
59
+ * @param options Common options like abortSignal.
60
+ * @throws if container with ID does not exist.
61
+ */
62
+ deleteContainer: (containerId: string, options: StorageAdapterOptions) => Promise<void>;
63
+
64
+ /**
65
+ * Check if container exists.
66
+ * @param containerId ID of the container
67
+ * @param options Common options like abortSignal.
68
+ * @returns if container is available of not
69
+ * @throws never.
70
+ */
71
+ hasContainer: (containerId: string, options: StorageAdapterOptions) => Promise<boolean>;
72
+
73
+ // Files
74
+
75
+ /**
76
+ * Upload multiple files to the storage container
77
+ * @param containerId ID of the container
78
+ * @param files List of files with path, data and metadata
79
+ * @param options Common options like abortSignal
80
+ * @throws if the the container does not exists
81
+ */
82
+ uploadFiles: (
83
+ containerId: string,
84
+ files: StoryBookerFile[],
85
+ options: StorageAdapterOptions,
86
+ ) => Promise<void>;
87
+
88
+ /**
89
+ * Delete multiple files by their paths or a shared prefix.
90
+ * @param containerId ID of the container
91
+ * @param filePathsOrPrefix
92
+ * Either a list of complete filepaths or
93
+ * a single string representing the shared path (prefix).
94
+ * @param options Common options like abortSignal
95
+ * @throws if the the container does not exists but NOT if file(s) does not exists
96
+ */
97
+ deleteFiles: (
98
+ containerId: string,
99
+ filePathsOrPrefix: string | string[],
100
+ options: StorageAdapterOptions,
101
+ ) => Promise<void>;
102
+
103
+ /**
104
+ * Check if a file exists in the storage container
105
+ * @param containerId ID of the container
106
+ * @param filepath Path of the file
107
+ * @param options Common options like abortSignal
108
+ * @returns if the file exists or not
109
+ * @throws if the the container does not exists
110
+ */
111
+ hasFile: (
112
+ containerId: string,
113
+ filepath: string,
114
+ options: StorageAdapterOptions,
115
+ ) => Promise<boolean>;
116
+
117
+ /**
118
+ * Download a single file from the storage container
119
+ * @param containerId ID of the container
120
+ * @param filepath Path of the file
121
+ * @param options Common options like abortSignal
122
+ * @returns StoryBooker file data
123
+ * @throws if the the container or file does not exists
124
+ */
125
+ downloadFile: (
126
+ containerId: string,
127
+ filepath: string,
128
+ options: StorageAdapterOptions,
129
+ ) => Promise<Partial<StoryBookerFile>>;
130
+ }
131
+
132
+ /** Common Storage adapter options. */
133
+ export interface StorageAdapterOptions {
134
+ /** A signal that can be used to cancel the request handling. */
135
+ abortSignal?: AbortSignal;
136
+ /** Logger */
137
+ logger: LoggerAdapter;
138
+ }
139
+
140
+ /** Shape of file/blob */
141
+ export interface StoryBookerFile {
142
+ content: Blob | ReadableStream | string;
143
+ mimeType: string;
144
+ path: string;
145
+ }
146
+
147
+ /**
148
+ * Pre-defined Storage adapter errors
149
+ * that can be used across different adapters.
150
+ *
151
+ * Throws {HTTPException} with relevant status codes.
152
+ */
153
+ export const StorageAdapterErrors = {
154
+ StorageNotInitializedError: class extends HTTPException {
155
+ constructor(cause?: unknown) {
156
+ super(500, { cause, message: "Storage adapter is not initialized." });
157
+ }
158
+ },
159
+ ContainerAlreadyExistsError: class extends HTTPException {
160
+ constructor(containerId: string, cause?: unknown) {
161
+ super(409, {
162
+ cause,
163
+ message: `Storage container '${containerId}' already exists.`,
164
+ });
165
+ }
166
+ },
167
+ ContainerDoesNotExistError: class extends HTTPException {
168
+ constructor(containerId: string, cause?: unknown) {
169
+ super(404, {
170
+ cause,
171
+ message: `Storage container '${containerId}' does not exist.`,
172
+ });
173
+ }
174
+ },
175
+ FileDoesNotExistError: class extends HTTPException {
176
+ constructor(containerId: string, filepath: string, cause?: unknown) {
177
+ super(404, {
178
+ cause,
179
+ message: `Storage file '${filepath}' does not exist in container '${containerId}'.`,
180
+ });
181
+ }
182
+ },
183
+ FileMalformedError: class extends HTTPException {
184
+ constructor(containerId: string, filepath: string, cause?: unknown) {
185
+ super(415, {
186
+ cause,
187
+ message: `Storage file '${filepath}' is malformed in container '${containerId}'.`,
188
+ });
189
+ }
190
+ },
191
+ CustomError: class extends HTTPException {
192
+ constructor(status: number | undefined, message: string, cause?: unknown) {
193
+ super(status as ContentfulStatusCode, { cause, message });
194
+ }
195
+ },
196
+ // oxlint-disable-next-line no-explicit-any
197
+ } satisfies Record<string, new (...args: any[]) => HTTPException>;
@@ -0,0 +1,103 @@
1
+ import type {
2
+ BuildStoryType,
3
+ BuildType,
4
+ BuildUploadVariant,
5
+ ParsedError,
6
+ ProjectType,
7
+ StoryBookerUser,
8
+ TagType,
9
+ } from "../../types.ts";
10
+ import type { UrlBuilder } from "../../urls.ts";
11
+ import type { StoryBookerAdapterMetadata } from "../../utils/adapter-utils.ts";
12
+ import type { LoggerAdapter } from "./logger.ts";
13
+
14
+ export type RenderedContent = string | Promise<string>;
15
+ export type UIResult = Response | Promise<Response> | RenderedContent;
16
+
17
+ /**
18
+ * Adapter for creating UI for StoryBooker service.
19
+ *
20
+ * The render methods are called asynchronously and can return promise of HTML.
21
+ */
22
+ export interface UIAdapter {
23
+ /**
24
+ * Metadata about the adapter.
25
+ */
26
+ metadata: StoryBookerAdapterMetadata;
27
+
28
+ /**
29
+ * A special handler that is invoked when no existing StoryBooker route is matched.
30
+ *
31
+ * This can be used to serve special routes and/or static files from disk.
32
+ */
33
+ handleUnhandledRoute?(filepath: string, options: UIAdapterOptions): Response | Promise<Response>;
34
+
35
+ renderHomePage?(props: { projects: ProjectType[] }, options: UIAdapterOptions): UIResult;
36
+ renderErrorPage?(props: ParsedError, options: UIAdapterOptions): UIResult;
37
+ renderAccountsPage?(props: { children: string | undefined }, options: UIAdapterOptions): UIResult;
38
+
39
+ // Projects
40
+ renderProjectsListPage?(props: { projects: ProjectType[] }, options: UIAdapterOptions): UIResult;
41
+ renderProjectDetailsPage?(
42
+ props: { project: ProjectType; recentBuilds: BuildType[]; recentTags: TagType[] },
43
+ options: UIAdapterOptions,
44
+ ): UIResult;
45
+ renderProjectCreatePage?(props: unknown, options: UIAdapterOptions): UIResult;
46
+ renderProjectUpdatePage?(props: { project: ProjectType }, options: UIAdapterOptions): UIResult;
47
+
48
+ // Tags
49
+ renderTagsListPage?(
50
+ props: { tags: TagType[]; project: ProjectType; defaultType?: string | null },
51
+ options: UIAdapterOptions,
52
+ ): UIResult;
53
+ renderTagDetailsPage?(
54
+ props: { tag: TagType; project: ProjectType; builds: BuildType[] },
55
+ options: UIAdapterOptions,
56
+ ): UIResult;
57
+ renderTagCreatePage?(props: { project: ProjectType }, options: UIAdapterOptions): UIResult;
58
+ renderTagUpdatePage?(
59
+ props: { tag: TagType; project: ProjectType },
60
+ options: UIAdapterOptions,
61
+ ): UIResult;
62
+
63
+ // Builds
64
+ renderBuildsListPage?(
65
+ props: { builds: BuildType[]; project: ProjectType },
66
+ options: UIAdapterOptions,
67
+ ): UIResult;
68
+ renderBuildDetailsPage?(
69
+ props: { build: BuildType; project: ProjectType; stories: BuildStoryType[] | null },
70
+ options: UIAdapterOptions,
71
+ ): UIResult;
72
+ renderBuildCreatePage?(
73
+ props: { project: ProjectType; tagId?: string },
74
+ options: UIAdapterOptions,
75
+ ): UIResult;
76
+ renderBuildUploadPage?(
77
+ props: { build: BuildType; project: ProjectType; uploadVariant?: BuildUploadVariant },
78
+ options: UIAdapterOptions,
79
+ ): UIResult;
80
+ }
81
+
82
+ /** Common UI adapter options. */
83
+ export interface UIAdapterOptions {
84
+ isAuthEnabled: boolean;
85
+ /** Logger */
86
+ logger: LoggerAdapter;
87
+ /** Logged-in user */
88
+ user: StoryBookerUser | null | undefined;
89
+ /** Current url */
90
+ url: string;
91
+ /** Current locale */
92
+ locale: string;
93
+ /** URL builder */
94
+ urlBuilder: UrlBuilder;
95
+ /** Metadata about all adapters */
96
+ adaptersMetadata: {
97
+ auth?: StoryBookerAdapterMetadata;
98
+ database?: StoryBookerAdapterMetadata;
99
+ logger?: StoryBookerAdapterMetadata;
100
+ storage?: StoryBookerAdapterMetadata;
101
+ ui?: StoryBookerAdapterMetadata;
102
+ };
103
+ }
@@ -0,0 +1,201 @@
1
+ // oxlint-disable id-length
2
+
3
+ import * as Dynamo from "@aws-sdk/client-dynamodb";
4
+ import {
5
+ DatabaseAdapterErrors,
6
+ type DatabaseAdapter,
7
+ type DatabaseAdapterOptions,
8
+ type DatabaseDocumentListOptions,
9
+ type StoryBookerDatabaseDocument,
10
+ } from "./_internal/database.ts";
11
+
12
+ export class AwsDynamoDatabaseService implements DatabaseAdapter {
13
+ #client: Dynamo.DynamoDBClient;
14
+
15
+ constructor(client: Dynamo.DynamoDBClient) {
16
+ this.#client = client;
17
+ }
18
+
19
+ metadata: DatabaseAdapter["metadata"] = { name: "AWS DynamoDB" };
20
+
21
+ listCollections: DatabaseAdapter["listCollections"] = async (options) => {
22
+ const response = await this.#client.send(new Dynamo.ListTablesCommand({}), {
23
+ abortSignal: options.abortSignal,
24
+ });
25
+ return response.TableNames ?? [];
26
+ };
27
+
28
+ createCollection: DatabaseAdapter["createCollection"] = async (collectionId, options) => {
29
+ try {
30
+ await this.#client.send(
31
+ new Dynamo.CreateTableCommand({
32
+ AttributeDefinitions: [{ AttributeName: "id", AttributeType: "S" }],
33
+ BillingMode: "PAY_PER_REQUEST",
34
+ KeySchema: [{ AttributeName: "id", KeyType: "HASH" }],
35
+ TableName: collectionId,
36
+ }),
37
+ { abortSignal: options.abortSignal },
38
+ );
39
+ } catch (error) {
40
+ throw new DatabaseAdapterErrors.CollectionAlreadyExistsError(collectionId, error);
41
+ }
42
+ };
43
+
44
+ hasCollection: DatabaseAdapter["hasCollection"] = async (collectionId, options) => {
45
+ try {
46
+ const response = await this.#client.send(
47
+ new Dynamo.DescribeTableCommand({ TableName: collectionId }),
48
+ { abortSignal: options.abortSignal },
49
+ );
50
+ return Boolean(response.Table);
51
+ } catch {
52
+ return false;
53
+ }
54
+ };
55
+
56
+ deleteCollection: DatabaseAdapter["deleteCollection"] = async (collectionId, options) => {
57
+ try {
58
+ await this.#client.send(new Dynamo.DeleteTableCommand({ TableName: collectionId }), {
59
+ abortSignal: options.abortSignal,
60
+ });
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 response = await this.#client.send(new Dynamo.ScanCommand({ TableName: collectionId }), {
74
+ abortSignal: options.abortSignal,
75
+ });
76
+ return (response.Items ?? []).map((item) => {
77
+ const doc: Record<string, unknown> = {};
78
+ for (const [key, value] of Object.entries(item)) {
79
+ doc[key] = value.S ?? value.N ?? value.BOOL ?? value.NULL ?? value;
80
+ }
81
+
82
+ return doc as Document;
83
+ });
84
+ };
85
+
86
+ getDocument: DatabaseAdapter["getDocument"] = async <
87
+ Document extends StoryBookerDatabaseDocument,
88
+ >(
89
+ collectionId: string,
90
+ documentId: string,
91
+ options: DatabaseAdapterOptions,
92
+ ): Promise<Document> => {
93
+ try {
94
+ const response = await this.#client.send(
95
+ new Dynamo.GetItemCommand({
96
+ Key: { id: { S: documentId } },
97
+ TableName: collectionId,
98
+ }),
99
+ { abortSignal: options.abortSignal },
100
+ );
101
+ const document = response.Item
102
+ ? (Object.fromEntries(
103
+ Object.entries(response.Item).map(([key, value]) => [
104
+ key,
105
+ value.S ?? value.N ?? value.BOOL ?? value.NULL ?? value,
106
+ ]),
107
+ ) as Record<string, unknown>)
108
+ : undefined;
109
+
110
+ if (!document) {
111
+ throw new Error("Document not found");
112
+ }
113
+
114
+ document["id"] = documentId;
115
+ return document as Document;
116
+ } catch (error) {
117
+ throw new DatabaseAdapterErrors.DocumentDoesNotExistError(collectionId, documentId, error);
118
+ }
119
+ };
120
+
121
+ createDocument: DatabaseAdapter["createDocument"] = async (
122
+ collectionId,
123
+ documentData,
124
+ options,
125
+ ) => {
126
+ try {
127
+ await this.#client.send(
128
+ new Dynamo.PutItemCommand({
129
+ Item: Object.fromEntries(
130
+ Object.entries(documentData).map(([key, value]) => [key, { S: String(value) }]),
131
+ ),
132
+ TableName: collectionId,
133
+ }),
134
+ { abortSignal: options.abortSignal },
135
+ );
136
+ } catch (error) {
137
+ throw new DatabaseAdapterErrors.DocumentAlreadyExistsError(
138
+ collectionId,
139
+ documentData.id,
140
+ error,
141
+ );
142
+ }
143
+ };
144
+
145
+ hasDocument: DatabaseAdapter["hasDocument"] = async (collectionId, documentId, options) => {
146
+ const response = await this.#client.send(
147
+ new Dynamo.GetItemCommand({
148
+ Key: { id: { S: documentId } },
149
+ TableName: collectionId,
150
+ }),
151
+ { abortSignal: options.abortSignal },
152
+ );
153
+ return Boolean(response.Item);
154
+ };
155
+
156
+ deleteDocument: DatabaseAdapter["deleteDocument"] = async (collectionId, documentId, options) => {
157
+ try {
158
+ await this.#client.send(
159
+ new Dynamo.DeleteItemCommand({
160
+ Key: { id: { S: documentId } },
161
+ TableName: collectionId,
162
+ }),
163
+ { abortSignal: options.abortSignal },
164
+ );
165
+ } catch (error) {
166
+ throw new DatabaseAdapterErrors.DocumentDoesNotExistError(collectionId, documentId, error);
167
+ }
168
+ };
169
+
170
+ // oxlint-disable-next-line max-params
171
+ updateDocument: DatabaseAdapter["updateDocument"] = async (
172
+ collectionId,
173
+ documentId,
174
+ documentData,
175
+ options,
176
+ ) => {
177
+ const updateExpr: string[] = [];
178
+ const exprAttrValues: Record<string, Dynamo.AttributeValue> = {};
179
+ for (const [key, value] of Object.entries(documentData)) {
180
+ updateExpr.push(`#${key} = :${key}`);
181
+ exprAttrValues[`:${key}`] = { S: String(value) };
182
+ }
183
+
184
+ try {
185
+ await this.#client.send(
186
+ new Dynamo.UpdateItemCommand({
187
+ ExpressionAttributeNames: Object.fromEntries(
188
+ Object.keys(documentData).map((k) => [`#${k}`, k]),
189
+ ),
190
+ ExpressionAttributeValues: exprAttrValues,
191
+ Key: { id: { S: documentId } },
192
+ TableName: collectionId,
193
+ UpdateExpression: `SET ${updateExpr.join(", ")}`,
194
+ }),
195
+ { abortSignal: options.abortSignal },
196
+ );
197
+ } catch (error) {
198
+ throw new DatabaseAdapterErrors.DocumentDoesNotExistError(collectionId, documentId, error);
199
+ }
200
+ };
201
+ }