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,373 @@
|
|
|
1
|
+
// oxlint-disable switch-case-braces
|
|
2
|
+
|
|
3
|
+
import { HTTPException } from "hono/http-exception";
|
|
4
|
+
import type { StoryBookerPermissionAction } from "../adapters/_internal/auth.ts";
|
|
5
|
+
import { handleProcessZip } from "../handlers/handle-process-zip.ts";
|
|
6
|
+
import { urlBuilder } from "../urls.ts";
|
|
7
|
+
import {
|
|
8
|
+
generateDatabaseCollectionId,
|
|
9
|
+
generateStorageContainerId,
|
|
10
|
+
} from "../utils/adapter-utils.ts";
|
|
11
|
+
import { checkAuthorisation } from "../utils/auth.ts";
|
|
12
|
+
import { mimes } from "../utils/mime-utils.ts";
|
|
13
|
+
import { getStore } from "../utils/store.ts";
|
|
14
|
+
import {
|
|
15
|
+
BuildSchema,
|
|
16
|
+
type BuildCreateType,
|
|
17
|
+
type BuildStoryType,
|
|
18
|
+
type BuildType,
|
|
19
|
+
type BuildUpdateType,
|
|
20
|
+
type BuildUploadVariant,
|
|
21
|
+
} from "./builds-schema.ts";
|
|
22
|
+
import { ProjectsModel } from "./projects-model.ts";
|
|
23
|
+
import { TagsModel } from "./tags-model.ts";
|
|
24
|
+
import type { TagVariant } from "./tags-schema.ts";
|
|
25
|
+
import { Model, type BaseModel, type ListOptions } from "./~model.ts";
|
|
26
|
+
|
|
27
|
+
export class BuildsModel extends Model<BuildType> {
|
|
28
|
+
constructor(projectId: string) {
|
|
29
|
+
super(projectId, generateDatabaseCollectionId(projectId, "Builds"));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async list(options: ListOptions<BuildType> = {}): Promise<BuildType[]> {
|
|
33
|
+
if (options) {
|
|
34
|
+
this.log("List builds with options (%o)...", { ...options });
|
|
35
|
+
} else {
|
|
36
|
+
this.log("List builds...");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const items = await this.database.listDocuments(
|
|
40
|
+
this.collectionId,
|
|
41
|
+
{ sort: "latest", ...options },
|
|
42
|
+
this.dbOptions,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return BuildSchema.array().parse(items);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async create(data: BuildCreateType): Promise<BuildType> {
|
|
49
|
+
const { tags: parsedTags, id, ...rest } = data;
|
|
50
|
+
this.log("Create build '%s'...", id);
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
if (await this.has(id)) {
|
|
54
|
+
throw new HTTPException(409, {
|
|
55
|
+
message: `Build '${id}' already exists.`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const tags = Array.isArray(parsedTags) ? parsedTags : parsedTags.split(",");
|
|
60
|
+
const tagIds = await Promise.all(
|
|
61
|
+
tags.filter(Boolean).map(async (tagId) => {
|
|
62
|
+
return await this.#updateOrCreateTag(tagId, id);
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const now = new Date().toISOString();
|
|
67
|
+
// oxlint-disable-next-line sort-keys
|
|
68
|
+
const build: BuildType = {
|
|
69
|
+
...rest,
|
|
70
|
+
createdAt: now,
|
|
71
|
+
coverage: "none",
|
|
72
|
+
screenshots: "none",
|
|
73
|
+
storybook: "none",
|
|
74
|
+
testReport: "none",
|
|
75
|
+
id,
|
|
76
|
+
message: rest.message ?? "",
|
|
77
|
+
tagIds: tagIds.filter(Boolean).join(","),
|
|
78
|
+
updatedAt: now,
|
|
79
|
+
};
|
|
80
|
+
await this.database.createDocument(this.collectionId, build, this.dbOptions);
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const projectsModel = new ProjectsModel();
|
|
84
|
+
const project = await projectsModel.get(this.projectId);
|
|
85
|
+
if (tags.includes(project.gitHubDefaultBranch)) {
|
|
86
|
+
await projectsModel.update(this.projectId, { latestBuildId: id });
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
this.error("Error updating project with latest build ID:", error);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return build;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
throw new HTTPException(500, {
|
|
95
|
+
cause: error,
|
|
96
|
+
message: `Failed to create build '${id}'.`,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async get(id: string): Promise<BuildType> {
|
|
102
|
+
this.log("Get build '%s'...", id);
|
|
103
|
+
|
|
104
|
+
const item = await this.database.getDocument(this.collectionId, id, this.dbOptions);
|
|
105
|
+
|
|
106
|
+
return BuildSchema.parse(item);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async has(id: string): Promise<boolean> {
|
|
110
|
+
this.log("Check build '%s'...", id);
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
return await this.database.hasDocument(this.collectionId, id, this.dbOptions);
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async update(id: string, data: BuildUpdateType): Promise<void> {
|
|
120
|
+
this.log("Update build '%s''...", id);
|
|
121
|
+
|
|
122
|
+
await this.database.updateDocument(
|
|
123
|
+
this.collectionId,
|
|
124
|
+
id,
|
|
125
|
+
{ ...data, updatedAt: new Date().toISOString() },
|
|
126
|
+
this.dbOptions,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async delete(buildId: string, updateTag = true): Promise<void> {
|
|
131
|
+
this.log("Delete build '%s'...", buildId);
|
|
132
|
+
|
|
133
|
+
const build = await this.get(buildId);
|
|
134
|
+
|
|
135
|
+
this.debug("Delete document '%s'", buildId);
|
|
136
|
+
await this.database.deleteDocument(this.collectionId, buildId, this.dbOptions);
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
this.debug("Delete files '%s'", buildId);
|
|
140
|
+
await this.storage.deleteFiles(
|
|
141
|
+
generateStorageContainerId(this.projectId),
|
|
142
|
+
buildId,
|
|
143
|
+
this.storageOptions,
|
|
144
|
+
);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
this.error("Cannot delete container:", error);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (updateTag) {
|
|
150
|
+
this.debug("Update tags for build '%s'", buildId);
|
|
151
|
+
const tagIds = build.tagIds?.split(",") ?? [];
|
|
152
|
+
const tagsModel = new TagsModel(this.projectId);
|
|
153
|
+
await Promise.allSettled(
|
|
154
|
+
tagIds.map(async (tagId) => {
|
|
155
|
+
const tag = await tagsModel.get(tagId);
|
|
156
|
+
if (tag.latestBuildId === buildId) {
|
|
157
|
+
await tagsModel.update(tagId, {
|
|
158
|
+
buildsCount: Math.max(tag.buildsCount - 1, 0),
|
|
159
|
+
latestBuildId: "",
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}),
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const projectsModel = new ProjectsModel();
|
|
168
|
+
const project = await projectsModel.get(this.projectId);
|
|
169
|
+
if (project.latestBuildId === buildId) {
|
|
170
|
+
this.debug("Update project for build '%s'", buildId);
|
|
171
|
+
await projectsModel.update(this.projectId, {
|
|
172
|
+
latestBuildId: "",
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
} catch (error) {
|
|
176
|
+
this.error("Cannot unset build ID from project:", error);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async upload(buildId: string, variant: BuildUploadVariant, zipFile?: File): Promise<void> {
|
|
181
|
+
const { config, request } = getStore();
|
|
182
|
+
this.log("Upload build '%s' (%s)...", buildId, variant);
|
|
183
|
+
const variantCopy = variant; // for switch fallthrough/default
|
|
184
|
+
|
|
185
|
+
switch (variant) {
|
|
186
|
+
case "coverage":
|
|
187
|
+
case "testReport":
|
|
188
|
+
case "screenshots":
|
|
189
|
+
case "storybook": {
|
|
190
|
+
const size = await this.#uploadZipFile(buildId, variant, zipFile);
|
|
191
|
+
await this.update(buildId, { [variant]: "uploaded" });
|
|
192
|
+
|
|
193
|
+
const {
|
|
194
|
+
maxInlineUploadProcessingSizeInBytes = 5 * 1024 * 1024,
|
|
195
|
+
queueLargeZipFileProcessing = false,
|
|
196
|
+
} = config ?? {};
|
|
197
|
+
|
|
198
|
+
// Automatically process zip if feature is enabled and size is below limit
|
|
199
|
+
if (size !== undefined && size <= maxInlineUploadProcessingSizeInBytes) {
|
|
200
|
+
await handleProcessZip(this.projectId, buildId, variant).catch((error: unknown) => {
|
|
201
|
+
this.error("Error processing zip file:", error);
|
|
202
|
+
});
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Otherwise queue processing task if enabled
|
|
207
|
+
if (queueLargeZipFileProcessing) {
|
|
208
|
+
this.log("Queue processing for build '%s' (%s)...", buildId, variant);
|
|
209
|
+
const url = urlBuilder.taskProcessZip(this.projectId, buildId, variant);
|
|
210
|
+
// Do not await fetch to avoid blocking
|
|
211
|
+
fetch(url, { headers: request.headers, method: "POST" }).catch((error: unknown) => {
|
|
212
|
+
this.error("Error queuing zip file processing:", error);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
default:
|
|
220
|
+
throw new Error(`Unsupported upload variant: ${variantCopy}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
id: BaseModel<BuildType>["id"] = (id: string) => {
|
|
225
|
+
return {
|
|
226
|
+
checkAuth: (action) =>
|
|
227
|
+
checkAuthorisation({
|
|
228
|
+
action,
|
|
229
|
+
projectId: this.projectId,
|
|
230
|
+
resource: "build",
|
|
231
|
+
}),
|
|
232
|
+
delete: this.delete.bind(this, id),
|
|
233
|
+
get: this.get.bind(this, id),
|
|
234
|
+
has: this.has.bind(this, id),
|
|
235
|
+
id,
|
|
236
|
+
update: this.update.bind(this, id),
|
|
237
|
+
};
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
checkAuth(action: StoryBookerPermissionAction): boolean {
|
|
241
|
+
return checkAuthorisation({
|
|
242
|
+
action,
|
|
243
|
+
projectId: this.projectId,
|
|
244
|
+
resource: "build",
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async getStories(idOrBuild: string | BuildType): Promise<BuildStoryType[] | null> {
|
|
249
|
+
const { storybook, id } = typeof idOrBuild === "string" ? await this.get(idOrBuild) : idOrBuild;
|
|
250
|
+
|
|
251
|
+
if (storybook !== "ready") {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const { logger } = getStore();
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
this.log("List stories '%s'...", id);
|
|
259
|
+
|
|
260
|
+
const buildIndexJsonPath = `${id}/storybook/index.json`;
|
|
261
|
+
const { content } = await this.storage.downloadFile(
|
|
262
|
+
generateStorageContainerId(this.projectId),
|
|
263
|
+
buildIndexJsonPath,
|
|
264
|
+
{ logger },
|
|
265
|
+
);
|
|
266
|
+
const data: unknown =
|
|
267
|
+
typeof content === "string" ? JSON.parse(content) : await new Response(content).json();
|
|
268
|
+
|
|
269
|
+
if (!data || typeof data !== "object" || !("entries" in data) || !data.entries) {
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return Object.values(data.entries) as BuildStoryType[];
|
|
274
|
+
} catch (error) {
|
|
275
|
+
this.error("Error getting stories:", error);
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// helpers
|
|
281
|
+
async listByTag(tagId: string): Promise<BuildType[]> {
|
|
282
|
+
const builds = await this.list({
|
|
283
|
+
filter: (item) => (item.tagIds ? item.tagIds.split(",").includes(tagId) : false),
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
return builds;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async deleteByTag(tagId: string, force: boolean): Promise<void> {
|
|
290
|
+
const builds = await this.listByTag(tagId);
|
|
291
|
+
this.log(
|
|
292
|
+
"Delete builds by tag: '%s' (%d, force: %s)...",
|
|
293
|
+
tagId,
|
|
294
|
+
builds.length,
|
|
295
|
+
force.valueOf(),
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
await Promise.allSettled(
|
|
299
|
+
builds.map(async (build): Promise<void> => {
|
|
300
|
+
const buildTagIds = build.tagIds?.split(",") ?? [];
|
|
301
|
+
if (!force && buildTagIds.length > 1) {
|
|
302
|
+
const newIds = buildTagIds.filter((id) => id !== tagId);
|
|
303
|
+
await this.update(build.id, { tagIds: newIds.join(",") });
|
|
304
|
+
} else {
|
|
305
|
+
await this.delete(build.id, false);
|
|
306
|
+
}
|
|
307
|
+
}),
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async #updateOrCreateTag(tagId: string, buildId: string): Promise<string> {
|
|
312
|
+
const tagsModel = new TagsModel(this.projectId);
|
|
313
|
+
// Either "my-tag" or "my-tag;branch" or "my-tag;branch;My tag"
|
|
314
|
+
const [id = tagId, tagType, tagValue] = tagId.split(";").map((part) => part.trim());
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
const existingTag = await tagsModel.get(id);
|
|
318
|
+
await tagsModel.update(id, {
|
|
319
|
+
buildsCount: existingTag.buildsCount + 1,
|
|
320
|
+
latestBuildId: buildId,
|
|
321
|
+
});
|
|
322
|
+
return id;
|
|
323
|
+
} catch {
|
|
324
|
+
try {
|
|
325
|
+
const type = (tagType as TagVariant) ?? TagsModel.guessType(id);
|
|
326
|
+
const value = tagValue ?? id;
|
|
327
|
+
this.log("A new tag '%s' (%s) is being created.", value, type);
|
|
328
|
+
const tag = await tagsModel.create({ latestBuildId: buildId, type, value }, true);
|
|
329
|
+
|
|
330
|
+
return tag.id;
|
|
331
|
+
} catch (error) {
|
|
332
|
+
this.error("Error creating tag:", error);
|
|
333
|
+
return id;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async #uploadZipFile(
|
|
339
|
+
buildId: string,
|
|
340
|
+
variant: BuildUploadVariant,
|
|
341
|
+
zipFile?: File,
|
|
342
|
+
): Promise<number | undefined> {
|
|
343
|
+
const { request } = getStore();
|
|
344
|
+
this.debug("(%s-%s) Uploading zip file", buildId, variant);
|
|
345
|
+
|
|
346
|
+
const content: string | Blob | ReadableStream | null = zipFile
|
|
347
|
+
? zipFile.stream()
|
|
348
|
+
: request.body;
|
|
349
|
+
|
|
350
|
+
if (!content) {
|
|
351
|
+
throw new Error(`No content found for zip file.`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
await this.storage.uploadFiles(
|
|
355
|
+
generateStorageContainerId(this.projectId),
|
|
356
|
+
[
|
|
357
|
+
{
|
|
358
|
+
content,
|
|
359
|
+
mimeType: mimes.zip,
|
|
360
|
+
path: `${buildId}/${variant}.zip`,
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
this.storageOptions,
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
if (!zipFile) {
|
|
367
|
+
const length = request.headers.get("Content-Length");
|
|
368
|
+
return length ? Number(length) : undefined;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return zipFile?.size;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// oxlint-disable sort-keys
|
|
2
|
+
|
|
3
|
+
import { z } from "@hono/zod-openapi";
|
|
4
|
+
import { buildUploadVariants } from "../utils/constants.ts";
|
|
5
|
+
import { BuildIdSchema, TagIdSchema } from "./~shared-schema.ts";
|
|
6
|
+
|
|
7
|
+
const buildContentAvailabilityOptions = ["none", "uploaded", "processing", "ready"] as const;
|
|
8
|
+
|
|
9
|
+
export type BuildType = z.infer<typeof BuildSchema>;
|
|
10
|
+
/** @private */
|
|
11
|
+
export const BuildSchema = z
|
|
12
|
+
.object({
|
|
13
|
+
authorEmail: z
|
|
14
|
+
.string()
|
|
15
|
+
.refine((val) => val.includes("@"), "Invalid email format")
|
|
16
|
+
.meta({ description: "Email of the author" }),
|
|
17
|
+
authorName: z.string(),
|
|
18
|
+
createdAt: z.iso.datetime().default(new Date().toISOString()),
|
|
19
|
+
id: BuildIdSchema,
|
|
20
|
+
tagIds: z.string().optional(),
|
|
21
|
+
message: z.optional(z.string()),
|
|
22
|
+
updatedAt: z.iso.datetime().default(new Date().toISOString()),
|
|
23
|
+
coverage: z.enum(buildContentAvailabilityOptions),
|
|
24
|
+
screenshots: z.enum(buildContentAvailabilityOptions),
|
|
25
|
+
storybook: z.enum(buildContentAvailabilityOptions),
|
|
26
|
+
testReport: z.enum(buildContentAvailabilityOptions),
|
|
27
|
+
})
|
|
28
|
+
.meta({ id: "Build", title: "StoryBooker Build" });
|
|
29
|
+
|
|
30
|
+
export type BuildCreateType = z.infer<typeof BuildCreateSchema>;
|
|
31
|
+
/** @private */
|
|
32
|
+
export const BuildCreateSchema = BuildSchema.omit({
|
|
33
|
+
createdAt: true,
|
|
34
|
+
coverage: true,
|
|
35
|
+
screenshots: true,
|
|
36
|
+
storybook: true,
|
|
37
|
+
testReport: true,
|
|
38
|
+
tagIds: true,
|
|
39
|
+
updatedAt: true,
|
|
40
|
+
}).extend({
|
|
41
|
+
tags: z.union([TagIdSchema.array(), TagIdSchema]).meta({
|
|
42
|
+
description: "Tag IDs associated with the build. Should be created beforehand.",
|
|
43
|
+
}),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export type BuildUpdateType = z.infer<typeof BuildUpdateSchema>;
|
|
47
|
+
export const BuildUpdateSchema = BuildSchema.omit({
|
|
48
|
+
createdAt: true,
|
|
49
|
+
id: true,
|
|
50
|
+
updatedAt: true,
|
|
51
|
+
}).partial();
|
|
52
|
+
|
|
53
|
+
export type BuildUploadVariant = (typeof buildUploadVariants)[number];
|
|
54
|
+
export const BuildUploadQueryParamsSchema = z.object({
|
|
55
|
+
variant: z.enum(buildUploadVariants).default("storybook"),
|
|
56
|
+
});
|
|
57
|
+
export const BuildUploadFormBodySchema = z.object({
|
|
58
|
+
file: z.file().openapi({ type: "object" }),
|
|
59
|
+
variant: z.enum(buildUploadVariants).default("storybook"),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export type BuildsListResultType = z.infer<typeof BuildsListResultSchema>;
|
|
63
|
+
export const BuildsListResultSchema = z.object({
|
|
64
|
+
builds: BuildSchema.array(),
|
|
65
|
+
});
|
|
66
|
+
export type BuildsGetResultType = z.infer<typeof BuildsGetResultSchema>;
|
|
67
|
+
export const BuildsGetResultSchema = z.object({
|
|
68
|
+
build: BuildSchema,
|
|
69
|
+
url: z.url(),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export type BuildStoryType = z.infer<typeof BuildStorySchema>;
|
|
73
|
+
export const BuildStorySchema = z.object({
|
|
74
|
+
id: z.string(),
|
|
75
|
+
title: z.string(),
|
|
76
|
+
name: z.string(),
|
|
77
|
+
importPath: z.string(),
|
|
78
|
+
tags: z.array(z.string()),
|
|
79
|
+
type: z.enum(["docs", "story"]),
|
|
80
|
+
componentPath: z.string().optional(),
|
|
81
|
+
storiesImports: z.array(z.string()).optional(),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
export { BuildIdSchema, buildUploadVariants };
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { HTTPException } from "hono/http-exception";
|
|
2
|
+
import type { StoryBookerPermissionAction } from "../adapters/_internal/auth.ts";
|
|
3
|
+
import {
|
|
4
|
+
generateDatabaseCollectionId,
|
|
5
|
+
generateStorageContainerId,
|
|
6
|
+
} from "../utils/adapter-utils.ts";
|
|
7
|
+
import { checkAuthorisation } from "../utils/auth.ts";
|
|
8
|
+
import {
|
|
9
|
+
ProjectSchema,
|
|
10
|
+
type ProjectCreateType,
|
|
11
|
+
type ProjectType,
|
|
12
|
+
type ProjectUpdateType,
|
|
13
|
+
} from "./projects-schema.ts";
|
|
14
|
+
import { TagsModel } from "./tags-model.ts";
|
|
15
|
+
import { Model, type BaseModel, type ListOptions } from "./~model.ts";
|
|
16
|
+
|
|
17
|
+
export class ProjectsModel extends Model<ProjectType> {
|
|
18
|
+
constructor() {
|
|
19
|
+
super(null, generateDatabaseCollectionId("Projects", ""));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async list(options: ListOptions<ProjectType> = {}): Promise<ProjectType[]> {
|
|
23
|
+
this.log("List projects...");
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const items = await this.database.listDocuments(this.collectionId, options, this.dbOptions);
|
|
27
|
+
|
|
28
|
+
return items;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
this.error("Error listing projects:", error);
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async create(data: ProjectCreateType): Promise<ProjectType> {
|
|
36
|
+
this.log("Creating project...");
|
|
37
|
+
|
|
38
|
+
const projectId = data.id;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
if (await this.has(projectId)) {
|
|
42
|
+
throw new HTTPException(409, {
|
|
43
|
+
message: `Project '${projectId}' already exists.`,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await this.storage.createContainer(
|
|
48
|
+
generateStorageContainerId(projectId),
|
|
49
|
+
this.storageOptions,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
this.debug("Creating project collection");
|
|
53
|
+
await this.database
|
|
54
|
+
.createCollection(this.collectionId, this.dbOptions)
|
|
55
|
+
.catch((error: unknown) => {
|
|
56
|
+
// ignore error if collection already exists since there can only be one projects collection
|
|
57
|
+
this.error("Error creating projects collection:", error);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
await this.database.createCollection(
|
|
61
|
+
generateDatabaseCollectionId(projectId, "Builds"),
|
|
62
|
+
this.dbOptions,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
await this.database.createCollection(
|
|
66
|
+
generateDatabaseCollectionId(projectId, "Tags"),
|
|
67
|
+
this.dbOptions,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
this.debug("Creating default branch (%s) tag", data.gitHubDefaultBranch);
|
|
71
|
+
await new TagsModel(projectId)
|
|
72
|
+
.create({
|
|
73
|
+
type: "branch",
|
|
74
|
+
value: data.gitHubDefaultBranch,
|
|
75
|
+
})
|
|
76
|
+
.catch((error: unknown) => {
|
|
77
|
+
// log error but continue since project creation should not fail because of tag creation
|
|
78
|
+
this.error("Error creating default branch tag:", error);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
this.debug("Creating project entry '%s' in collection", projectId);
|
|
82
|
+
const now = new Date().toISOString();
|
|
83
|
+
const project: ProjectType = {
|
|
84
|
+
...data,
|
|
85
|
+
createdAt: now,
|
|
86
|
+
updatedAt: now,
|
|
87
|
+
};
|
|
88
|
+
await this.database.createDocument(this.collectionId, project, this.dbOptions);
|
|
89
|
+
|
|
90
|
+
return project;
|
|
91
|
+
} catch (error) {
|
|
92
|
+
throw new HTTPException(500, {
|
|
93
|
+
cause: error,
|
|
94
|
+
message: `Failed to create project '${projectId}'.`,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async get(id: string): Promise<ProjectType> {
|
|
100
|
+
this.log("Get project '%s'...", id);
|
|
101
|
+
|
|
102
|
+
const item = await this.database.getDocument(this.collectionId, id, this.dbOptions);
|
|
103
|
+
|
|
104
|
+
return ProjectSchema.parse(item);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async has(id: string): Promise<boolean> {
|
|
108
|
+
this.log("Check project '%s'...", id);
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
return await this.database.hasDocument(this.collectionId, id, this.dbOptions);
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async update(id: string, data: ProjectUpdateType): Promise<void> {
|
|
118
|
+
this.log("Update project '%s'...", id);
|
|
119
|
+
|
|
120
|
+
await this.database.updateDocument(
|
|
121
|
+
this.collectionId,
|
|
122
|
+
id,
|
|
123
|
+
{ ...data, updatedAt: new Date().toISOString() },
|
|
124
|
+
this.dbOptions,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
if (data.gitHubDefaultBranch) {
|
|
128
|
+
try {
|
|
129
|
+
this.debug("Create default-branch tag '%s'...", data.gitHubDefaultBranch);
|
|
130
|
+
await new TagsModel(id).create({
|
|
131
|
+
type: "branch",
|
|
132
|
+
value: data.gitHubDefaultBranch,
|
|
133
|
+
});
|
|
134
|
+
} catch (error) {
|
|
135
|
+
this.error("Error creating default branch tag:", error);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async delete(id: string): Promise<void> {
|
|
141
|
+
this.log("Delete project '%s'...", id);
|
|
142
|
+
|
|
143
|
+
this.debug("Delete project entry '%s' in collection", id);
|
|
144
|
+
await this.database.deleteDocument(this.collectionId, id, this.dbOptions);
|
|
145
|
+
|
|
146
|
+
this.debug("Delete project-builds collection");
|
|
147
|
+
await this.database.deleteCollection(
|
|
148
|
+
generateDatabaseCollectionId(id, "Builds"),
|
|
149
|
+
this.dbOptions,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
this.debug("Delete project-tags collection");
|
|
153
|
+
await this.database.deleteCollection(generateDatabaseCollectionId(id, "Tags"), this.dbOptions);
|
|
154
|
+
|
|
155
|
+
this.debug("Delete project container");
|
|
156
|
+
await this.storage.deleteContainer(generateStorageContainerId(id), this.storageOptions);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
checkAuth(action: StoryBookerPermissionAction, id?: string): boolean {
|
|
160
|
+
return checkAuthorisation({
|
|
161
|
+
action,
|
|
162
|
+
projectId: id ?? (this.projectId || undefined),
|
|
163
|
+
resource: "project",
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
id: BaseModel<ProjectType>["id"] = (id: string) => {
|
|
168
|
+
return {
|
|
169
|
+
checkAuth: (action) => checkAuthorisation({ action, projectId: id, resource: "project" }),
|
|
170
|
+
delete: this.delete.bind(this, id),
|
|
171
|
+
get: this.get.bind(this, id),
|
|
172
|
+
has: this.has.bind(this, id),
|
|
173
|
+
id,
|
|
174
|
+
update: this.update.bind(this, id),
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { z } from "@hono/zod-openapi";
|
|
2
|
+
import { DEFAULT_GITHUB_BRANCH, DEFAULT_PURGE_AFTER_DAYS } from "../utils/constants.ts";
|
|
3
|
+
import { BuildIdSchema, ProjectIdSchema } from "./~shared-schema.ts";
|
|
4
|
+
|
|
5
|
+
export { ProjectIdSchema };
|
|
6
|
+
|
|
7
|
+
export type ProjectType = z.infer<typeof ProjectSchema>;
|
|
8
|
+
|
|
9
|
+
export const ProjectSchema = z
|
|
10
|
+
.object({
|
|
11
|
+
createdAt: z.iso.datetime().default(new Date().toISOString()),
|
|
12
|
+
|
|
13
|
+
gitHubDefaultBranch: z
|
|
14
|
+
.string()
|
|
15
|
+
.default(DEFAULT_GITHUB_BRANCH)
|
|
16
|
+
.meta({ description: "Default branch to use for GitHub repository" }),
|
|
17
|
+
gitHubPath: z.string().optional().meta({
|
|
18
|
+
description: "Path to the storybook project with respect to repository root.",
|
|
19
|
+
}),
|
|
20
|
+
gitHubRepository: z.string().check(
|
|
21
|
+
z.minLength(1, "Query-param 'gitHubRepo' is required."),
|
|
22
|
+
z.refine(
|
|
23
|
+
(val) => val.split("/").length === 2,
|
|
24
|
+
"Query-param 'gitHubRepo' should be in the format 'owner/repo'.",
|
|
25
|
+
),
|
|
26
|
+
),
|
|
27
|
+
|
|
28
|
+
id: ProjectIdSchema,
|
|
29
|
+
|
|
30
|
+
jiraDomain: z.union([z.url().optional(), z.literal("")]),
|
|
31
|
+
|
|
32
|
+
latestBuildId: z.union([BuildIdSchema.optional(), z.literal("")]),
|
|
33
|
+
|
|
34
|
+
name: z.string().meta({ description: "Name of the project." }),
|
|
35
|
+
|
|
36
|
+
purgeBuildsAfterDays: z.coerce.number().min(1).default(DEFAULT_PURGE_AFTER_DAYS).meta({
|
|
37
|
+
description: "Days after which the builds in the project should be purged.",
|
|
38
|
+
}),
|
|
39
|
+
|
|
40
|
+
updatedAt: z.iso.datetime().default(new Date().toISOString()),
|
|
41
|
+
})
|
|
42
|
+
.meta({ id: "Project", title: "StoryBooker project" });
|
|
43
|
+
|
|
44
|
+
export type ProjectCreateType = z.infer<typeof ProjectCreateSchema>;
|
|
45
|
+
export const ProjectCreateSchema = ProjectSchema.omit({
|
|
46
|
+
createdAt: true,
|
|
47
|
+
latestBuildId: true,
|
|
48
|
+
updatedAt: true,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export type ProjectUpdateType = z.infer<typeof ProjectUpdateSchema>;
|
|
52
|
+
export const ProjectUpdateSchema = ProjectSchema.omit({
|
|
53
|
+
createdAt: true,
|
|
54
|
+
gitHubDefaultBranch: true,
|
|
55
|
+
id: true,
|
|
56
|
+
updatedAt: true,
|
|
57
|
+
})
|
|
58
|
+
.extend({ gitHubDefaultBranch: z.string() })
|
|
59
|
+
.partial();
|
|
60
|
+
|
|
61
|
+
export type ProjectsListResultType = z.infer<typeof ProjectsListResultSchema>;
|
|
62
|
+
export const ProjectsListResultSchema = z.object({
|
|
63
|
+
projects: ProjectSchema.array(),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export type ProjectGetResultType = z.infer<typeof ProjectGetResultSchema>;
|
|
67
|
+
export const ProjectGetResultSchema = z.object({
|
|
68
|
+
project: ProjectSchema,
|
|
69
|
+
});
|