storybooker 0.19.4 → 0.22.0-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -18
- package/dist/adapters/_internal/queue.d.mts +127 -0
- package/dist/aws-dynamodb.d.mts +22 -0
- package/dist/aws-dynamodb.mjs +118 -0
- package/dist/aws-dynamodb.mjs.map +1 -0
- package/dist/aws-s3.d.mts +20 -0
- package/dist/aws-s3.mjs +96 -0
- package/dist/aws-s3.mjs.map +1 -0
- package/dist/azure-blob-storage.d.mts +20 -0
- package/dist/azure-blob-storage.mjs +126 -0
- package/dist/azure-blob-storage.mjs.map +1 -0
- package/dist/azure-cosmos-db.d.mts +23 -0
- package/dist/azure-cosmos-db.mjs +87 -0
- package/dist/azure-cosmos-db.mjs.map +1 -0
- package/dist/azure-data-tables.d.mts +23 -0
- package/dist/azure-data-tables.mjs +127 -0
- package/dist/azure-data-tables.mjs.map +1 -0
- package/dist/azure-easy-auth.d.mts +50 -0
- package/dist/azure-easy-auth.mjs +88 -0
- package/dist/azure-easy-auth.mjs.map +1 -0
- package/dist/azure-functions.d.mts +62 -0
- package/dist/azure-functions.mjs +147 -0
- package/dist/azure-functions.mjs.map +1 -0
- package/dist/fs.d.mts +37 -0
- package/dist/fs.mjs +240 -0
- package/dist/fs.mjs.map +1 -0
- package/dist/gcp-big-table.d.mts +23 -0
- package/dist/gcp-big-table.mjs +92 -0
- package/dist/gcp-big-table.mjs.map +1 -0
- package/dist/gcp-firestore.d.mts +22 -0
- package/dist/gcp-firestore.mjs +87 -0
- package/dist/gcp-firestore.mjs.map +1 -0
- package/dist/gcp-storage.d.mts +20 -0
- package/dist/gcp-storage.mjs +96 -0
- package/dist/gcp-storage.mjs.map +1 -0
- package/dist/handlers/handle-process-zip.mjs +90 -0
- package/dist/handlers/handle-process-zip.mjs.map +1 -0
- package/dist/handlers/handle-purge.d.mts +12 -0
- package/dist/handlers/handle-purge.mjs +36 -0
- package/dist/handlers/handle-purge.mjs.map +1 -0
- package/dist/handlers/handle-serve-storybook.mjs +94 -0
- package/dist/handlers/handle-serve-storybook.mjs.map +1 -0
- package/dist/index.d.mts +28 -0
- package/dist/index.mjs +62 -0
- package/dist/index.mjs.map +1 -0
- package/dist/models/builds-model.mjs +248 -0
- package/dist/models/builds-model.mjs.map +1 -0
- package/dist/models/builds-schema.d.mts +171 -0
- package/dist/models/builds-schema.mjs +67 -0
- package/dist/models/builds-schema.mjs.map +1 -0
- package/dist/models/projects-model.mjs +122 -0
- package/dist/models/projects-model.mjs.map +1 -0
- package/dist/models/projects-schema.d.mts +70 -0
- package/dist/models/projects-schema.mjs +37 -0
- package/dist/models/projects-schema.mjs.map +1 -0
- package/dist/models/tags-model.mjs +110 -0
- package/dist/models/tags-model.mjs.map +1 -0
- package/dist/models/tags-schema.d.mts +76 -0
- package/dist/models/tags-schema.mjs +34 -0
- package/dist/models/tags-schema.mjs.map +1 -0
- package/dist/models/~model.mjs +43 -0
- package/dist/models/~model.mjs.map +1 -0
- package/dist/models/~shared-schema.d.mts +1 -0
- package/dist/models/~shared-schema.mjs +20 -0
- package/dist/models/~shared-schema.mjs.map +1 -0
- package/dist/mysql.d.mts +39 -0
- package/dist/mysql.mjs +151 -0
- package/dist/mysql.mjs.map +1 -0
- package/dist/redis.d.mts +33 -0
- package/dist/redis.mjs +118 -0
- package/dist/redis.mjs.map +1 -0
- package/dist/routers/account-router.mjs +91 -0
- package/dist/routers/account-router.mjs.map +1 -0
- package/dist/routers/builds-router.mjs +347 -0
- package/dist/routers/builds-router.mjs.map +1 -0
- package/dist/routers/projects-router.mjs +236 -0
- package/dist/routers/projects-router.mjs.map +1 -0
- package/dist/routers/root-router.mjs +108 -0
- package/dist/routers/root-router.mjs.map +1 -0
- package/dist/routers/tags-router.mjs +269 -0
- package/dist/routers/tags-router.mjs.map +1 -0
- package/dist/routers/tasks-router.mjs +71 -0
- package/dist/routers/tasks-router.mjs.map +1 -0
- package/dist/urls.d.mts +47 -0
- package/dist/urls.mjs +208 -0
- package/dist/urls.mjs.map +1 -0
- package/dist/utils/adapter-utils.d.mts +14 -0
- package/dist/utils/adapter-utils.mjs +14 -0
- package/dist/utils/adapter-utils.mjs.map +1 -0
- package/dist/utils/auth.mjs +25 -0
- package/dist/utils/auth.mjs.map +1 -0
- package/dist/utils/error.d.mts +21 -0
- package/dist/utils/error.mjs +109 -0
- package/dist/utils/error.mjs.map +1 -0
- package/dist/utils/file-utils.mjs +16 -0
- package/dist/utils/file-utils.mjs.map +1 -0
- package/dist/utils/openapi-utils.mjs +45 -0
- package/dist/utils/openapi-utils.mjs.map +1 -0
- package/dist/utils/request.mjs +35 -0
- package/dist/utils/request.mjs.map +1 -0
- package/dist/utils/response.mjs +24 -0
- package/dist/utils/response.mjs.map +1 -0
- package/dist/utils/store.mjs +54 -0
- package/dist/utils/store.mjs.map +1 -0
- package/dist/utils/ui-utils.mjs +38 -0
- package/dist/utils/ui-utils.mjs.map +1 -0
- package/dist/utils/url-utils.d.mts +10 -0
- package/dist/utils/url-utils.mjs +54 -0
- package/dist/utils/url-utils.mjs.map +1 -0
- package/dist/~internal/adapter/auth.d.mts +123 -0
- package/dist/~internal/adapter/auth.mjs +20 -0
- package/dist/~internal/adapter/auth.mjs.map +1 -0
- package/dist/~internal/adapter/database.d.mts +240 -0
- package/dist/~internal/adapter/database.mjs +63 -0
- package/dist/~internal/adapter/database.mjs.map +1 -0
- package/dist/~internal/adapter/logger.d.mts +34 -0
- package/dist/~internal/adapter/logger.mjs +13 -0
- package/dist/~internal/adapter/logger.mjs.map +1 -0
- package/dist/~internal/adapter/storage.d.mts +208 -0
- package/dist/~internal/adapter/storage.mjs +63 -0
- package/dist/~internal/adapter/storage.mjs.map +1 -0
- package/dist/~internal/adapter/ui.d.mts +109 -0
- package/dist/~internal/adapter/ui.mjs +1 -0
- package/dist/~internal/adapter.d.mts +8 -0
- package/dist/~internal/adapter.mjs +6 -0
- package/dist/~internal/constants.d.mts +24 -0
- package/dist/~internal/constants.mjs +32 -0
- package/dist/~internal/constants.mjs.map +1 -0
- package/dist/~internal/mimes.d.mts +449 -0
- package/dist/~internal/mimes.mjs +454 -0
- package/dist/~internal/mimes.mjs.map +1 -0
- package/dist/~internal/router.d.mts +1651 -0
- package/dist/~internal/router.mjs +39 -0
- package/dist/~internal/router.mjs.map +1 -0
- package/dist/~internal/types.d.mts +77 -0
- package/dist/~internal/types.mjs +1 -0
- package/dist/~internal/utils.d.mts +4 -0
- package/dist/~internal/utils.mjs +5 -0
- package/openapi.json +3162 -0
- package/package.json +148 -27
- package/src/adapters/_internal/auth.ts +135 -0
- package/src/adapters/_internal/database.ts +241 -0
- package/src/adapters/_internal/index.ts +8 -0
- package/src/adapters/_internal/logger.ts +41 -0
- package/src/adapters/_internal/queue.ts +151 -0
- package/src/adapters/_internal/storage.ts +197 -0
- package/src/adapters/_internal/ui.ts +103 -0
- package/src/adapters/aws-dynamodb.ts +201 -0
- package/src/adapters/aws-s3.ts +160 -0
- package/src/adapters/azure-blob-storage.ts +223 -0
- package/src/adapters/azure-cosmos-db.ts +158 -0
- package/src/adapters/azure-data-tables.ts +223 -0
- package/src/adapters/azure-easy-auth.ts +174 -0
- package/src/adapters/azure-functions.ts +242 -0
- package/src/adapters/fs.ts +398 -0
- package/src/adapters/gcp-big-table.ts +157 -0
- package/src/adapters/gcp-firestore.ts +146 -0
- package/src/adapters/gcp-storage.ts +141 -0
- package/src/adapters/mysql.ts +296 -0
- package/src/adapters/redis.ts +242 -0
- package/src/handlers/handle-process-zip.ts +117 -0
- package/src/handlers/handle-purge.ts +65 -0
- package/src/handlers/handle-serve-storybook.ts +101 -0
- package/src/index.ts +81 -16
- package/src/mocks/mock-auth-service.ts +51 -0
- package/src/mocks/mock-store.ts +26 -0
- package/src/models/builds-model.ts +373 -0
- package/src/models/builds-schema.ts +84 -0
- package/src/models/projects-model.ts +177 -0
- package/src/models/projects-schema.ts +69 -0
- package/src/models/tags-model.ts +138 -0
- package/src/models/tags-schema.ts +45 -0
- package/src/models/~model.ts +79 -0
- package/src/models/~shared-schema.ts +14 -0
- package/src/routers/_app-router.ts +57 -0
- package/src/routers/account-router.ts +136 -0
- package/src/routers/builds-router.ts +464 -0
- package/src/routers/projects-router.ts +309 -0
- package/src/routers/root-router.ts +127 -0
- package/src/routers/tags-router.ts +339 -0
- package/src/routers/tasks-router.ts +75 -0
- package/src/types.ts +107 -0
- package/src/urls.ts +327 -0
- package/src/utils/adapter-utils.ts +26 -0
- package/src/utils/auth.test.ts +71 -0
- package/src/utils/auth.ts +39 -0
- package/src/utils/constants.ts +31 -0
- package/src/utils/date-utils.ts +10 -0
- package/src/utils/error.test.ts +86 -0
- package/src/utils/error.ts +140 -0
- package/src/utils/file-utils.test.ts +65 -0
- package/src/utils/file-utils.ts +43 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/mime-utils.ts +457 -0
- package/src/utils/openapi-utils.ts +49 -0
- package/src/utils/request.ts +97 -0
- package/src/utils/response.ts +20 -0
- package/src/utils/store.ts +85 -0
- package/src/utils/story-utils.ts +42 -0
- package/src/utils/text-utils.ts +10 -0
- package/src/utils/ui-utils.ts +57 -0
- package/src/utils/url-utils.ts +113 -0
- package/dist/index.js +0 -554
- package/src/commands/create.ts +0 -263
- package/src/commands/purge.ts +0 -70
- package/src/commands/test.ts +0 -42
- package/src/service-schema.d.ts +0 -2023
- package/src/utils/auth-utils.ts +0 -31
- package/src/utils/pkg-utils.ts +0 -37
- package/src/utils/sb-build.ts +0 -55
- package/src/utils/sb-test.ts +0 -115
- package/src/utils/schema-utils.ts +0 -123
- package/src/utils/stream-utils.ts +0 -72
- package/src/utils/types.ts +0 -4
- package/src/utils/zip.ts +0 -77
|
@@ -0,0 +1,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
|
+
}
|