@storyshelf/app 0.5.3
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/LICENSE +21 -0
- package/README.md +18 -0
- package/dist/app-types.d.mts +54 -0
- package/dist/asset-manifest.mjs +41 -0
- package/dist/asset-manifest.mjs.map +1 -0
- package/dist/asset-paths.mjs +23 -0
- package/dist/asset-paths.mjs.map +1 -0
- package/dist/assets/htmx.min.js +3304 -0
- package/dist/assets/mark.svg +1 -0
- package/dist/capture-setup.mjs +139 -0
- package/dist/capture-setup.mjs.map +1 -0
- package/dist/credential-keys.mjs +20 -0
- package/dist/credential-keys.mjs.map +1 -0
- package/dist/credentials.mjs +56 -0
- package/dist/credentials.mjs.map +1 -0
- package/dist/index.d.mts +18 -0
- package/dist/index.mjs +310 -0
- package/dist/index.mjs.map +1 -0
- package/dist/lifecycle.mjs +132 -0
- package/dist/lifecycle.mjs.map +1 -0
- package/dist/middleware/auth-gate.mjs +36 -0
- package/dist/middleware/auth-gate.mjs.map +1 -0
- package/dist/middleware/csrf.mjs +75 -0
- package/dist/middleware/csrf.mjs.map +1 -0
- package/dist/middleware/rate-limit.mjs +68 -0
- package/dist/middleware/rate-limit.mjs.map +1 -0
- package/dist/middleware/request-log.mjs +30 -0
- package/dist/middleware/request-log.mjs.map +1 -0
- package/dist/middleware/server-timing.mjs +39 -0
- package/dist/middleware/server-timing.mjs.map +1 -0
- package/dist/middleware/setup-gate.mjs +37 -0
- package/dist/middleware/setup-gate.mjs.map +1 -0
- package/dist/middleware/store-scope.mjs +33 -0
- package/dist/middleware/store-scope.mjs.map +1 -0
- package/dist/notify.d.mts +17 -0
- package/dist/notify.mjs +141 -0
- package/dist/notify.mjs.map +1 -0
- package/dist/pages/admin-system.mjs +243 -0
- package/dist/pages/admin-system.mjs.map +1 -0
- package/dist/pages/build-detail.mjs +392 -0
- package/dist/pages/build-detail.mjs.map +1 -0
- package/dist/pages/build-diff-actions.mjs +162 -0
- package/dist/pages/build-diff-actions.mjs.map +1 -0
- package/dist/pages/build-diff-comments.mjs +104 -0
- package/dist/pages/build-diff-comments.mjs.map +1 -0
- package/dist/pages/build-diff-header.mjs +117 -0
- package/dist/pages/build-diff-header.mjs.map +1 -0
- package/dist/pages/build-diff-model.mjs +48 -0
- package/dist/pages/build-diff-model.mjs.map +1 -0
- package/dist/pages/build-diff-nav.mjs +122 -0
- package/dist/pages/build-diff-nav.mjs.map +1 -0
- package/dist/pages/build-diff-shortcuts.mjs +78 -0
- package/dist/pages/build-diff-shortcuts.mjs.map +1 -0
- package/dist/pages/build-diff-stale.mjs +70 -0
- package/dist/pages/build-diff-stale.mjs.map +1 -0
- package/dist/pages/build-diff-viewer.mjs +70 -0
- package/dist/pages/build-diff-viewer.mjs.map +1 -0
- package/dist/pages/build-diff.mjs +110 -0
- package/dist/pages/build-diff.mjs.map +1 -0
- package/dist/pages/compute-jobs.mjs +165 -0
- package/dist/pages/compute-jobs.mjs.map +1 -0
- package/dist/pages/invite.mjs +91 -0
- package/dist/pages/invite.mjs.map +1 -0
- package/dist/pages/label-detail.mjs +154 -0
- package/dist/pages/label-detail.mjs.map +1 -0
- package/dist/pages/library.mjs +343 -0
- package/dist/pages/library.mjs.map +1 -0
- package/dist/pages/login.mjs +117 -0
- package/dist/pages/login.mjs.map +1 -0
- package/dist/pages/passkey-ceremony.mjs +97 -0
- package/dist/pages/passkey-ceremony.mjs.map +1 -0
- package/dist/pages/profile.mjs +364 -0
- package/dist/pages/profile.mjs.map +1 -0
- package/dist/pages/project-builds-table.mjs +151 -0
- package/dist/pages/project-builds-table.mjs.map +1 -0
- package/dist/pages/project-builds.mjs +143 -0
- package/dist/pages/project-builds.mjs.map +1 -0
- package/dist/pages/project-create.mjs +90 -0
- package/dist/pages/project-create.mjs.map +1 -0
- package/dist/pages/project-settings.mjs +95 -0
- package/dist/pages/project-settings.mjs.map +1 -0
- package/dist/pages/projects-card.mjs +194 -0
- package/dist/pages/projects-card.mjs.map +1 -0
- package/dist/pages/projects.mjs +129 -0
- package/dist/pages/projects.mjs.map +1 -0
- package/dist/pages/root.mjs +98 -0
- package/dist/pages/root.mjs.map +1 -0
- package/dist/pages/settings-general.mjs +138 -0
- package/dist/pages/settings-general.mjs.map +1 -0
- package/dist/pages/settings-labels.mjs +107 -0
- package/dist/pages/settings-labels.mjs.map +1 -0
- package/dist/pages/settings-members.mjs +192 -0
- package/dist/pages/settings-members.mjs.map +1 -0
- package/dist/pages/settings-notifications.mjs +240 -0
- package/dist/pages/settings-notifications.mjs.map +1 -0
- package/dist/pages/settings-status.mjs +137 -0
- package/dist/pages/settings-status.mjs.map +1 -0
- package/dist/pages/settings-tests.mjs +111 -0
- package/dist/pages/settings-tests.mjs.map +1 -0
- package/dist/pages/settings-tokens.mjs +90 -0
- package/dist/pages/settings-tokens.mjs.map +1 -0
- package/dist/pages/settings-webhooks.mjs +104 -0
- package/dist/pages/settings-webhooks.mjs.map +1 -0
- package/dist/pages/storybook.mjs +116 -0
- package/dist/pages/storybook.mjs.map +1 -0
- package/dist/retention-timer.mjs +65 -0
- package/dist/retention-timer.mjs.map +1 -0
- package/dist/routers/admin-pages.mjs +48 -0
- package/dist/routers/admin-pages.mjs.map +1 -0
- package/dist/routers/admin.mjs +113 -0
- package/dist/routers/admin.mjs.map +1 -0
- package/dist/routers/assets.mjs +64 -0
- package/dist/routers/assets.mjs.map +1 -0
- package/dist/routers/attempts.mjs +96 -0
- package/dist/routers/attempts.mjs.map +1 -0
- package/dist/routers/auth-sync.mjs +64 -0
- package/dist/routers/auth-sync.mjs.map +1 -0
- package/dist/routers/auth.mjs +236 -0
- package/dist/routers/auth.mjs.map +1 -0
- package/dist/routers/baseline-guard.mjs +83 -0
- package/dist/routers/baseline-guard.mjs.map +1 -0
- package/dist/routers/builds.handlers.mjs +217 -0
- package/dist/routers/builds.handlers.mjs.map +1 -0
- package/dist/routers/builds.mjs +381 -0
- package/dist/routers/builds.mjs.map +1 -0
- package/dist/routers/comments.mjs +109 -0
- package/dist/routers/comments.mjs.map +1 -0
- package/dist/routers/flash.mjs +35 -0
- package/dist/routers/flash.mjs.map +1 -0
- package/dist/routers/health-report.mjs +159 -0
- package/dist/routers/health-report.mjs.map +1 -0
- package/dist/routers/health.mjs +46 -0
- package/dist/routers/health.mjs.map +1 -0
- package/dist/routers/helpers.mjs +110 -0
- package/dist/routers/helpers.mjs.map +1 -0
- package/dist/routers/htmx.mjs +24 -0
- package/dist/routers/htmx.mjs.map +1 -0
- package/dist/routers/labels.mjs +118 -0
- package/dist/routers/labels.mjs.map +1 -0
- package/dist/routers/media.mjs +53 -0
- package/dist/routers/media.mjs.map +1 -0
- package/dist/routers/members.mjs +177 -0
- package/dist/routers/members.mjs.map +1 -0
- package/dist/routers/notification-channels.mjs +190 -0
- package/dist/routers/notification-channels.mjs.map +1 -0
- package/dist/routers/notification-subscriptions.mjs +120 -0
- package/dist/routers/notification-subscriptions.mjs.map +1 -0
- package/dist/routers/profile.mjs +276 -0
- package/dist/routers/profile.mjs.map +1 -0
- package/dist/routers/projects.mjs +133 -0
- package/dist/routers/projects.mjs.map +1 -0
- package/dist/routers/review-next.mjs +47 -0
- package/dist/routers/review-next.mjs.map +1 -0
- package/dist/routers/schemas.mjs +337 -0
- package/dist/routers/schemas.mjs.map +1 -0
- package/dist/routers/settings-general.mjs +104 -0
- package/dist/routers/settings-general.mjs.map +1 -0
- package/dist/routers/settings-labels.mjs +52 -0
- package/dist/routers/settings-labels.mjs.map +1 -0
- package/dist/routers/settings-members.mjs +58 -0
- package/dist/routers/settings-members.mjs.map +1 -0
- package/dist/routers/settings-notifications.mjs +108 -0
- package/dist/routers/settings-notifications.mjs.map +1 -0
- package/dist/routers/settings-status.mjs +77 -0
- package/dist/routers/settings-status.mjs.map +1 -0
- package/dist/routers/settings-tokens.mjs +40 -0
- package/dist/routers/settings-tokens.mjs.map +1 -0
- package/dist/routers/settings-webhooks.mjs +67 -0
- package/dist/routers/settings-webhooks.mjs.map +1 -0
- package/dist/routers/settings.handlers.mjs +149 -0
- package/dist/routers/settings.handlers.mjs.map +1 -0
- package/dist/routers/settings.mjs +25 -0
- package/dist/routers/settings.mjs.map +1 -0
- package/dist/routers/snapshots.mjs +240 -0
- package/dist/routers/snapshots.mjs.map +1 -0
- package/dist/routers/status-configs.mjs +108 -0
- package/dist/routers/status-configs.mjs.map +1 -0
- package/dist/routers/storybook.mjs +171 -0
- package/dist/routers/storybook.mjs.map +1 -0
- package/dist/routers/tokens.mjs +91 -0
- package/dist/routers/tokens.mjs.map +1 -0
- package/dist/routers/ui.mjs +166 -0
- package/dist/routers/ui.mjs.map +1 -0
- package/dist/routers/webhooks.mjs +97 -0
- package/dist/routers/webhooks.mjs.map +1 -0
- package/dist/routers/well-known.mjs +43 -0
- package/dist/routers/well-known.mjs.map +1 -0
- package/dist/runtime.mjs +52 -0
- package/dist/runtime.mjs.map +1 -0
- package/dist/store.mjs +20 -0
- package/dist/store.mjs.map +1 -0
- package/dist/ui/avatar.mjs +76 -0
- package/dist/ui/avatar.mjs.map +1 -0
- package/dist/ui/buttons.mjs +205 -0
- package/dist/ui/buttons.mjs.map +1 -0
- package/dist/ui/code-block.mjs +47 -0
- package/dist/ui/code-block.mjs.map +1 -0
- package/dist/ui/compare-stage.mjs +344 -0
- package/dist/ui/compare-stage.mjs.map +1 -0
- package/dist/ui/csrf-field.mjs +20 -0
- package/dist/ui/csrf-field.mjs.map +1 -0
- package/dist/ui/css.mjs +36 -0
- package/dist/ui/css.mjs.map +1 -0
- package/dist/ui/document.mjs +130 -0
- package/dist/ui/document.mjs.map +1 -0
- package/dist/ui/dropdown.mjs +181 -0
- package/dist/ui/dropdown.mjs.map +1 -0
- package/dist/ui/feedback.mjs +341 -0
- package/dist/ui/feedback.mjs.map +1 -0
- package/dist/ui/filter-input.mjs +58 -0
- package/dist/ui/filter-input.mjs.map +1 -0
- package/dist/ui/forms.mjs +291 -0
- package/dist/ui/forms.mjs.map +1 -0
- package/dist/ui/icons/icon.mjs +53 -0
- package/dist/ui/icons/icon.mjs.map +1 -0
- package/dist/ui/icons/paths.mjs +61 -0
- package/dist/ui/icons/paths.mjs.map +1 -0
- package/dist/ui/icons/sprite.mjs +26 -0
- package/dist/ui/icons/sprite.mjs.map +1 -0
- package/dist/ui/kbd.mjs +33 -0
- package/dist/ui/kbd.mjs.map +1 -0
- package/dist/ui/layout.mjs +173 -0
- package/dist/ui/layout.mjs.map +1 -0
- package/dist/ui/progress.mjs +44 -0
- package/dist/ui/progress.mjs.map +1 -0
- package/dist/ui/segmented.mjs +77 -0
- package/dist/ui/segmented.mjs.map +1 -0
- package/dist/ui/shell/account.mjs +161 -0
- package/dist/ui/shell/account.mjs.map +1 -0
- package/dist/ui/shell/bare.mjs +69 -0
- package/dist/ui/shell/bare.mjs.map +1 -0
- package/dist/ui/shell/boot-script.mjs +46 -0
- package/dist/ui/shell/boot-script.mjs.map +1 -0
- package/dist/ui/shell/client-script.mjs +259 -0
- package/dist/ui/shell/client-script.mjs.map +1 -0
- package/dist/ui/shell/nav.mjs +111 -0
- package/dist/ui/shell/nav.mjs.map +1 -0
- package/dist/ui/shell/review-script.mjs +195 -0
- package/dist/ui/shell/review-script.mjs.map +1 -0
- package/dist/ui/shell/sidebar.mjs +323 -0
- package/dist/ui/shell/sidebar.mjs.map +1 -0
- package/dist/ui/shell/topbar.mjs +268 -0
- package/dist/ui/shell/topbar.mjs.map +1 -0
- package/dist/ui/stacks.mjs +82 -0
- package/dist/ui/stacks.mjs.map +1 -0
- package/dist/ui/styles/base.mjs +41 -0
- package/dist/ui/styles/base.mjs.map +1 -0
- package/dist/ui/styles/behavior.mjs +43 -0
- package/dist/ui/styles/behavior.mjs.map +1 -0
- package/dist/ui/styles/review-layout.mjs +347 -0
- package/dist/ui/styles/review-layout.mjs.map +1 -0
- package/dist/ui/styles/review.mjs +86 -0
- package/dist/ui/styles/review.mjs.map +1 -0
- package/dist/ui/styles/tokens.mjs +132 -0
- package/dist/ui/styles/tokens.mjs.map +1 -0
- package/dist/ui/styles.mjs +19 -0
- package/dist/ui/styles.mjs.map +1 -0
- package/dist/ui/subnav.mjs +91 -0
- package/dist/ui/subnav.mjs.map +1 -0
- package/dist/ui/table.mjs +73 -0
- package/dist/ui/table.mjs.map +1 -0
- package/dist/ui/theme.mjs +64 -0
- package/dist/ui/theme.mjs.map +1 -0
- package/dist/ui/thumbnail.mjs +51 -0
- package/dist/ui/thumbnail.mjs.map +1 -0
- package/dist/ui/time.mjs +50 -0
- package/dist/ui/time.mjs.map +1 -0
- package/package.json +92 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { getStore } from "../store.mjs";
|
|
5
|
+
import { hxRefresh } from "./htmx.mjs";
|
|
6
|
+
import { flashHx } from "./flash.mjs";
|
|
7
|
+
import { resolveAuthorizedProject } from "./helpers.mjs";
|
|
8
|
+
import { badRequest, buildSchema, forbidden, notFound, unauthorized } from "./schemas.mjs";
|
|
9
|
+
import { APPROVER_ROLES, DEVELOPER_ROLES, VIEW_ROLES, buildCreateJsonSchema, buildForProject, buildListQuery, createBuildRecord, persistInlineStatics, storeUploadStream } from "./builds.handlers.mjs";
|
|
10
|
+
import { registerAttempts } from "./attempts.mjs";
|
|
11
|
+
import { registerComments } from "./comments.mjs";
|
|
12
|
+
import { registerSnapshots } from "./snapshots.mjs";
|
|
13
|
+
import { createRoute, z } from "@hono/zod-openapi";
|
|
14
|
+
import { BuildModel } from "@storyshelf/core/models";
|
|
15
|
+
import { storybookZipPath } from "@storyshelf/core/utils";
|
|
16
|
+
import { HTTPException } from "hono/http-exception";
|
|
17
|
+
import { DEFAULT_MAX_UPLOAD_BYTES } from "@storyshelf/core/config";
|
|
18
|
+
//#region src/routers/builds.ts
|
|
19
|
+
async function upsertContentRef(hash) {
|
|
20
|
+
try {
|
|
21
|
+
const contentRefs = getStore().db.tables.contentRefs;
|
|
22
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
23
|
+
const existing = await getStore().db.get(contentRefs, hash);
|
|
24
|
+
if (existing) await getStore().db.update(contentRefs, hash, {
|
|
25
|
+
refCount: existing.refCount + 1,
|
|
26
|
+
lastSeenAt: now
|
|
27
|
+
});
|
|
28
|
+
else await getStore().db.insert(contentRefs, {
|
|
29
|
+
hash,
|
|
30
|
+
refCount: 1,
|
|
31
|
+
lastSeenAt: now,
|
|
32
|
+
createdAt: now
|
|
33
|
+
});
|
|
34
|
+
} catch {}
|
|
35
|
+
}
|
|
36
|
+
/** Register the build list, upload, fetch, retry, and delete endpoints. */
|
|
37
|
+
function registerBuilds(app) {
|
|
38
|
+
app.openapi(listBuildsRoute, async (c) => {
|
|
39
|
+
const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...VIEW_ROLES);
|
|
40
|
+
const { status, branch, labelKey, labelValue } = c.req.valid("query");
|
|
41
|
+
const builds = new BuildModel(getStore().db).list(project.id, {
|
|
42
|
+
status,
|
|
43
|
+
branch: branch ?? void 0,
|
|
44
|
+
labelKey: labelKey ?? void 0,
|
|
45
|
+
labelValue: labelValue ?? void 0
|
|
46
|
+
});
|
|
47
|
+
return c.json(await builds);
|
|
48
|
+
});
|
|
49
|
+
app.openapi(createBuildRoute, async (c) => {
|
|
50
|
+
const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...DEVELOPER_ROLES);
|
|
51
|
+
const parsed = buildCreateJsonSchema.safeParse(await c.req.json());
|
|
52
|
+
if (!parsed.success) throw new HTTPException(400, { message: parsed.error.message });
|
|
53
|
+
const build = await createBuildRecord(project, parsed.data);
|
|
54
|
+
const uploadUrl = `/api/v1/projects/${project.slug}/builds/${build.id}/zip`;
|
|
55
|
+
return c.json({
|
|
56
|
+
build,
|
|
57
|
+
uploadUrl,
|
|
58
|
+
baselineSha: build.baselineSha
|
|
59
|
+
}, 202);
|
|
60
|
+
});
|
|
61
|
+
app.openapi(affectedRoute, async (c) => {
|
|
62
|
+
const { slug, buildId } = c.req.valid("param");
|
|
63
|
+
const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);
|
|
64
|
+
const build = await buildForProject(project.id, buildId);
|
|
65
|
+
const { baselineSha, changedFiles, affectedImportPaths } = c.req.valid("json");
|
|
66
|
+
const updated = await new BuildModel(getStore().db).setAffected(build.id, {
|
|
67
|
+
baselineSha,
|
|
68
|
+
changedFiles,
|
|
69
|
+
affectedImportPaths
|
|
70
|
+
});
|
|
71
|
+
return c.json(updated);
|
|
72
|
+
});
|
|
73
|
+
app.openapi(uploadZipRoute, async (c) => {
|
|
74
|
+
const { slug, buildId } = c.req.valid("param");
|
|
75
|
+
const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);
|
|
76
|
+
const build = await buildForProject(project.id, buildId);
|
|
77
|
+
const maxBytes = getStore().config.maxUploadBytes ?? DEFAULT_MAX_UPLOAD_BYTES;
|
|
78
|
+
await storeUploadStream(getStore().storage, storybookZipPath(project.id, build.id), c.req.raw.body, maxBytes);
|
|
79
|
+
const reqId = c.get("requestId");
|
|
80
|
+
await getStore().enqueueCapture?.(build.id, reqId);
|
|
81
|
+
const { storage, config, logger } = getStore();
|
|
82
|
+
await persistInlineStatics(storage, config.scratchDir, config.maxInlineUnzipSize, c.req.header("content-length"), project.id, build.id, logger);
|
|
83
|
+
return c.json(build, 202);
|
|
84
|
+
});
|
|
85
|
+
app.openapi(dedupRoute, async (c) => {
|
|
86
|
+
const { slug, buildId } = c.req.valid("param");
|
|
87
|
+
const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);
|
|
88
|
+
await buildForProject(project.id, buildId);
|
|
89
|
+
const { hashes } = c.req.valid("json");
|
|
90
|
+
const needed = [];
|
|
91
|
+
for (const hash of hashes) if (!await getStore().storage.exists(`content/${hash}`)) needed.push(hash);
|
|
92
|
+
return c.json({ needed });
|
|
93
|
+
});
|
|
94
|
+
app.openapi(contentUploadRoute, async (c) => {
|
|
95
|
+
const { slug, buildId } = c.req.valid("param");
|
|
96
|
+
const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);
|
|
97
|
+
await buildForProject(project.id, buildId);
|
|
98
|
+
if ((c.req.header("content-type") ?? "").includes("multipart/")) {
|
|
99
|
+
const form = await c.req.formData();
|
|
100
|
+
for (const [key, value] of form.entries()) if (value instanceof File) {
|
|
101
|
+
const hash = key;
|
|
102
|
+
const buffer = Buffer.from(await value.arrayBuffer());
|
|
103
|
+
await getStore().storage.write(`content/${hash}`, buffer);
|
|
104
|
+
await upsertContentRef(hash);
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
const hash = c.req.header("x-content-hash") ?? "";
|
|
108
|
+
if (!hash) throw new HTTPException(400, { message: "Missing X-Content-Hash" });
|
|
109
|
+
const buffer = Buffer.from(await c.req.arrayBuffer());
|
|
110
|
+
await getStore().storage.write(`content/${hash}`, buffer);
|
|
111
|
+
}
|
|
112
|
+
return c.json({ ok: true });
|
|
113
|
+
});
|
|
114
|
+
app.openapi(manifestRoute, async (c) => {
|
|
115
|
+
const { slug, buildId } = c.req.valid("param");
|
|
116
|
+
const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);
|
|
117
|
+
const build = await buildForProject(project.id, buildId);
|
|
118
|
+
const { files } = c.req.valid("json");
|
|
119
|
+
const manifest = {};
|
|
120
|
+
for (const f of files) manifest[f.rel] = f.hash;
|
|
121
|
+
await getStore().storage.write(`${project.id}/builds/${build.id}/storybook/manifest.json`, Buffer.from(JSON.stringify(manifest)));
|
|
122
|
+
for (const f of files) try {
|
|
123
|
+
const contentRefs = getStore().db.tables.contentRefs;
|
|
124
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
125
|
+
if (!await getStore().db.get(contentRefs, f.hash)) await getStore().db.insert(contentRefs, {
|
|
126
|
+
hash: f.hash,
|
|
127
|
+
refCount: 1,
|
|
128
|
+
lastSeenAt: now,
|
|
129
|
+
createdAt: now
|
|
130
|
+
});
|
|
131
|
+
else await getStore().db.update(contentRefs, f.hash, { lastSeenAt: now });
|
|
132
|
+
} catch {}
|
|
133
|
+
const reqId = c.get("requestId");
|
|
134
|
+
await getStore().enqueueCapture?.(build.id, reqId);
|
|
135
|
+
return c.json({ ok: true });
|
|
136
|
+
});
|
|
137
|
+
app.openapi(getBuildRoute, async (c) => {
|
|
138
|
+
const { slug, buildId } = c.req.valid("param");
|
|
139
|
+
const project = await resolveAuthorizedProject(c, slug, ...VIEW_ROLES);
|
|
140
|
+
const build = await buildForProject(project.id, buildId);
|
|
141
|
+
return c.json(build);
|
|
142
|
+
});
|
|
143
|
+
app.openapi(retryBuildRoute, async (c) => {
|
|
144
|
+
const { slug, buildId } = c.req.valid("param");
|
|
145
|
+
const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);
|
|
146
|
+
const build = await buildForProject(project.id, buildId);
|
|
147
|
+
const updated = await new BuildModel(getStore().db).setStatus(build.id, "pending");
|
|
148
|
+
await getStore().enqueueCapture?.(build.id, c.get("requestId"));
|
|
149
|
+
hxRefresh(c);
|
|
150
|
+
flashHx(c, "Capture queued");
|
|
151
|
+
return c.json(updated, 202);
|
|
152
|
+
});
|
|
153
|
+
app.openapi(deleteBuildRoute, async (c) => {
|
|
154
|
+
const { slug, buildId } = c.req.valid("param");
|
|
155
|
+
const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);
|
|
156
|
+
const build = await buildForProject(project.id, buildId);
|
|
157
|
+
await new BuildModel(getStore().db).remove(build.id);
|
|
158
|
+
return c.body(null, 204);
|
|
159
|
+
});
|
|
160
|
+
registerSnapshots(app);
|
|
161
|
+
registerComments(app);
|
|
162
|
+
registerAttempts(app);
|
|
163
|
+
}
|
|
164
|
+
const listBuildsRoute = createRoute({
|
|
165
|
+
method: "get",
|
|
166
|
+
tags: ["Builds"],
|
|
167
|
+
summary: "List builds",
|
|
168
|
+
path: "/api/v1/projects/{slug}/builds",
|
|
169
|
+
request: {
|
|
170
|
+
params: z.object({ slug: z.string() }),
|
|
171
|
+
query: buildListQuery
|
|
172
|
+
},
|
|
173
|
+
responses: {
|
|
174
|
+
200: {
|
|
175
|
+
content: { "application/json": { schema: buildSchema.array() } },
|
|
176
|
+
description: "List builds for a project"
|
|
177
|
+
},
|
|
178
|
+
...notFound,
|
|
179
|
+
...unauthorized
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
const buildCreatedSchema = z.object({
|
|
183
|
+
build: buildSchema,
|
|
184
|
+
uploadUrl: z.string(),
|
|
185
|
+
baselineSha: z.string().nullable()
|
|
186
|
+
}).openapi("BuildCreated");
|
|
187
|
+
const createBuildRoute = createRoute({
|
|
188
|
+
method: "post",
|
|
189
|
+
tags: ["Builds"],
|
|
190
|
+
summary: "Create a build",
|
|
191
|
+
path: "/api/v1/projects/{slug}/builds",
|
|
192
|
+
request: {
|
|
193
|
+
params: z.object({ slug: z.string() }),
|
|
194
|
+
body: { content: { "application/json": { schema: buildCreateJsonSchema } } }
|
|
195
|
+
},
|
|
196
|
+
responses: {
|
|
197
|
+
202: {
|
|
198
|
+
content: { "application/json": { schema: buildCreatedSchema } },
|
|
199
|
+
description: "Build created. PUT the Storybook zip to uploadUrl to queue capture."
|
|
200
|
+
},
|
|
201
|
+
...badRequest,
|
|
202
|
+
...forbidden,
|
|
203
|
+
...notFound
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
const uploadZipRoute = createRoute({
|
|
207
|
+
method: "put",
|
|
208
|
+
tags: ["Builds"],
|
|
209
|
+
summary: "Upload the Storybook zip",
|
|
210
|
+
path: "/api/v1/projects/{slug}/builds/{buildId}/zip",
|
|
211
|
+
request: {
|
|
212
|
+
params: z.object({
|
|
213
|
+
slug: z.string(),
|
|
214
|
+
buildId: z.string()
|
|
215
|
+
}),
|
|
216
|
+
body: { content: { "application/zip": { schema: z.string().openapi({
|
|
217
|
+
type: "string",
|
|
218
|
+
format: "binary"
|
|
219
|
+
}) } } }
|
|
220
|
+
},
|
|
221
|
+
responses: {
|
|
222
|
+
202: {
|
|
223
|
+
content: { "application/json": { schema: buildSchema } },
|
|
224
|
+
description: "Zip stored and capture queued"
|
|
225
|
+
},
|
|
226
|
+
413: {
|
|
227
|
+
content: { "application/json": { schema: z.object({ message: z.string() }).openapi("Error") } },
|
|
228
|
+
description: "Upload exceeds the configured size limit"
|
|
229
|
+
},
|
|
230
|
+
...badRequest,
|
|
231
|
+
...forbidden,
|
|
232
|
+
...notFound
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
const dedupHashesSchema = z.object({ hashes: z.array(z.string()) });
|
|
236
|
+
const dedupNeededSchema = z.object({ needed: z.array(z.string()) });
|
|
237
|
+
const dedupRoute = createRoute({
|
|
238
|
+
method: "post",
|
|
239
|
+
tags: ["Builds"],
|
|
240
|
+
summary: "Check which files need uploading",
|
|
241
|
+
path: "/api/v1/projects/{slug}/builds/{buildId}/dedup",
|
|
242
|
+
request: {
|
|
243
|
+
params: z.object({
|
|
244
|
+
slug: z.string(),
|
|
245
|
+
buildId: z.string()
|
|
246
|
+
}),
|
|
247
|
+
body: { content: { "application/json": { schema: dedupHashesSchema } } }
|
|
248
|
+
},
|
|
249
|
+
responses: {
|
|
250
|
+
200: {
|
|
251
|
+
content: { "application/json": { schema: dedupNeededSchema } },
|
|
252
|
+
description: "Hashes needed for upload"
|
|
253
|
+
},
|
|
254
|
+
...notFound
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
const contentUploadRoute = createRoute({
|
|
258
|
+
method: "post",
|
|
259
|
+
tags: ["Builds"],
|
|
260
|
+
summary: "Upload build content",
|
|
261
|
+
path: "/api/v1/projects/{slug}/builds/{buildId}/content",
|
|
262
|
+
request: {
|
|
263
|
+
params: z.object({
|
|
264
|
+
slug: z.string(),
|
|
265
|
+
buildId: z.string()
|
|
266
|
+
}),
|
|
267
|
+
body: { content: { "multipart/form-data": { schema: z.any() } } }
|
|
268
|
+
},
|
|
269
|
+
responses: {
|
|
270
|
+
200: {
|
|
271
|
+
content: { "application/json": { schema: z.object({ ok: z.boolean() }) } },
|
|
272
|
+
description: "Content uploaded"
|
|
273
|
+
},
|
|
274
|
+
...notFound
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
const manifestFileSchema = z.object({
|
|
278
|
+
rel: z.string(),
|
|
279
|
+
hash: z.string(),
|
|
280
|
+
size: z.number()
|
|
281
|
+
});
|
|
282
|
+
const manifestRoute = createRoute({
|
|
283
|
+
method: "post",
|
|
284
|
+
tags: ["Builds"],
|
|
285
|
+
summary: "Store the build manifest",
|
|
286
|
+
path: "/api/v1/projects/{slug}/builds/{buildId}/manifest",
|
|
287
|
+
request: {
|
|
288
|
+
params: z.object({
|
|
289
|
+
slug: z.string(),
|
|
290
|
+
buildId: z.string()
|
|
291
|
+
}),
|
|
292
|
+
body: { content: { "application/json": { schema: z.object({ files: z.array(manifestFileSchema) }) } } }
|
|
293
|
+
},
|
|
294
|
+
responses: {
|
|
295
|
+
200: {
|
|
296
|
+
content: { "application/json": { schema: z.object({ ok: z.boolean() }) } },
|
|
297
|
+
description: "Manifest stored"
|
|
298
|
+
},
|
|
299
|
+
...notFound
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
const affectedPayloadSchema = z.object({
|
|
303
|
+
baselineSha: z.string().nullable(),
|
|
304
|
+
changedFiles: z.array(z.string()).max(2e4),
|
|
305
|
+
affectedImportPaths: z.array(z.string()).max(2e4).nullable()
|
|
306
|
+
});
|
|
307
|
+
const affectedRoute = createRoute({
|
|
308
|
+
method: "post",
|
|
309
|
+
tags: ["Builds"],
|
|
310
|
+
summary: "Record the affected stories",
|
|
311
|
+
path: "/api/v1/projects/{slug}/builds/{buildId}/affected",
|
|
312
|
+
request: {
|
|
313
|
+
params: z.object({
|
|
314
|
+
slug: z.string(),
|
|
315
|
+
buildId: z.string()
|
|
316
|
+
}),
|
|
317
|
+
body: { content: { "application/json": { schema: affectedPayloadSchema } } }
|
|
318
|
+
},
|
|
319
|
+
responses: {
|
|
320
|
+
200: {
|
|
321
|
+
content: { "application/json": { schema: buildSchema } },
|
|
322
|
+
description: "Affected set recorded"
|
|
323
|
+
},
|
|
324
|
+
...badRequest,
|
|
325
|
+
...forbidden,
|
|
326
|
+
...notFound
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
const getBuildRoute = createRoute({
|
|
330
|
+
method: "get",
|
|
331
|
+
tags: ["Builds"],
|
|
332
|
+
summary: "Get a build",
|
|
333
|
+
path: "/api/v1/projects/{slug}/builds/{buildId}",
|
|
334
|
+
request: { params: z.object({
|
|
335
|
+
slug: z.string(),
|
|
336
|
+
buildId: z.string()
|
|
337
|
+
}) },
|
|
338
|
+
responses: {
|
|
339
|
+
200: {
|
|
340
|
+
content: { "application/json": { schema: buildSchema } },
|
|
341
|
+
description: "Fetch a build"
|
|
342
|
+
},
|
|
343
|
+
...notFound,
|
|
344
|
+
...unauthorized
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
const retryBuildRoute = createRoute({
|
|
348
|
+
method: "post",
|
|
349
|
+
tags: ["Builds"],
|
|
350
|
+
summary: "Retry capture",
|
|
351
|
+
path: "/api/v1/projects/{slug}/builds/{buildId}/retry",
|
|
352
|
+
request: { params: z.object({
|
|
353
|
+
slug: z.string(),
|
|
354
|
+
buildId: z.string()
|
|
355
|
+
}) },
|
|
356
|
+
responses: {
|
|
357
|
+
202: {
|
|
358
|
+
content: { "application/json": { schema: buildSchema } },
|
|
359
|
+
description: "Build reset to pending and capture re-queued (new attempt)"
|
|
360
|
+
},
|
|
361
|
+
...notFound
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
const deleteBuildRoute = createRoute({
|
|
365
|
+
method: "delete",
|
|
366
|
+
tags: ["Builds"],
|
|
367
|
+
summary: "Delete a build",
|
|
368
|
+
path: "/api/v1/projects/{slug}/builds/{buildId}",
|
|
369
|
+
request: { params: z.object({
|
|
370
|
+
slug: z.string(),
|
|
371
|
+
buildId: z.string()
|
|
372
|
+
}) },
|
|
373
|
+
responses: {
|
|
374
|
+
204: { description: "Build deleted" },
|
|
375
|
+
...notFound
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
//#endregion
|
|
379
|
+
export { registerBuilds };
|
|
380
|
+
|
|
381
|
+
//# sourceMappingURL=builds.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builds.mjs","names":["notFoundResponse","forbiddenResponse"],"sources":["../../src/routers/builds.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport { DEFAULT_MAX_UPLOAD_BYTES } from \"@storyshelf/core/config\";\nimport { BuildModel } from \"@storyshelf/core/models\";\nimport { storybookZipPath } from \"@storyshelf/core/utils\";\nimport { HTTPException } from \"hono/http-exception\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { registerAttempts } from \"./attempts.ts\";\nimport {\n VIEW_ROLES,\n DEVELOPER_ROLES,\n APPROVER_ROLES,\n buildCreateJsonSchema,\n buildListQuery,\n buildForProject,\n createBuildRecord,\n persistInlineStatics,\n storeUploadStream,\n} from \"./builds.handlers.ts\";\n\nasync function upsertContentRef(hash: string): Promise<void> {\n try {\n const contentRefs = getStore().db.tables.contentRefs;\n const now = new Date().toISOString();\n const existing = await getStore().db.get(contentRefs, hash);\n if (existing) {\n await getStore().db.update(contentRefs, hash, {\n refCount: (existing as { refCount: number }).refCount + 1,\n lastSeenAt: now,\n });\n } else {\n await getStore().db.insert(contentRefs, {\n hash,\n refCount: 1,\n lastSeenAt: now,\n createdAt: now,\n });\n }\n } catch {\n // ignore — content_refs may not exist on old DB\n }\n}\nimport { registerComments } from \"./comments.ts\";\nimport { flashHx } from \"./flash.ts\";\nimport { resolveAuthorizedProject } from \"./helpers.ts\";\nimport { hxRefresh } from \"./htmx.ts\";\nimport {\n badRequest,\n buildSchema,\n forbidden as forbiddenResponse,\n notFound as notFoundResponse,\n unauthorized,\n} from \"./schemas.ts\";\nimport { registerSnapshots } from \"./snapshots.ts\";\n/** Register the build list, upload, fetch, retry, and delete endpoints. */\nexport function registerBuilds(app: ShelfRouter): void {\n app.openapi(listBuildsRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...VIEW_ROLES);\n const { status, branch, labelKey, labelValue } = c.req.valid(\"query\");\n const builds = new BuildModel(getStore().db).list(project.id, {\n status,\n branch: branch ?? undefined,\n labelKey: labelKey ?? undefined,\n labelValue: labelValue ?? undefined,\n });\n return c.json(await builds);\n });\n\n app.openapi(createBuildRoute, async (c) => {\n const project = await resolveAuthorizedProject(\n c,\n c.req.valid(\"param\").slug,\n ...DEVELOPER_ROLES,\n );\n const parsed = buildCreateJsonSchema.safeParse(await c.req.json());\n if (!parsed.success) {\n throw new HTTPException(400, { message: parsed.error.message });\n }\n const build = await createBuildRecord(project, parsed.data);\n const uploadUrl = `/api/v1/projects/${project.slug}/builds/${build.id}/zip`;\n return c.json({ build, uploadUrl, baselineSha: build.baselineSha }, 202);\n });\n\n app.openapi(affectedRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const { baselineSha, changedFiles, affectedImportPaths } = c.req.valid(\"json\");\n const updated = await new BuildModel(getStore().db).setAffected(build.id, {\n baselineSha,\n changedFiles,\n affectedImportPaths,\n });\n return c.json(updated);\n });\n\n app.openapi(uploadZipRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const maxBytes = getStore().config.maxUploadBytes ?? DEFAULT_MAX_UPLOAD_BYTES;\n await storeUploadStream(\n getStore().storage,\n storybookZipPath(project.id, build.id),\n c.req.raw.body,\n maxBytes,\n );\n\n const reqId = c.get(\"requestId\");\n await getStore().enqueueCapture?.(build.id, reqId);\n const { storage, config, logger } = getStore();\n await persistInlineStatics(\n storage,\n config.scratchDir,\n config.maxInlineUnzipSize,\n c.req.header(\"content-length\"),\n project.id,\n build.id,\n logger,\n );\n return c.json(build, 202);\n });\n\n app.openapi(dedupRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n await buildForProject(project.id, buildId);\n const { hashes } = c.req.valid(\"json\");\n const needed: string[] = [];\n for (const hash of hashes) {\n if (!(await getStore().storage.exists(`content/${hash}`))) {\n needed.push(hash);\n }\n }\n return c.json({ needed });\n });\n\n // oxlint-disable eslint(max-depth) -- dedup upload handler is intentionally nested for batch handling\n app.openapi(contentUploadRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n await buildForProject(project.id, buildId);\n const contentType = c.req.header(\"content-type\") ?? \"\";\n if (contentType.includes(\"multipart/\")) {\n const form = await c.req.formData();\n for (const [key, value] of form.entries()) {\n if (value instanceof File) {\n const hash = key;\n const buffer = Buffer.from(await value.arrayBuffer());\n await getStore().storage.write(`content/${hash}`, buffer);\n await upsertContentRef(hash);\n }\n }\n } else {\n // Single content upload via PUT with hash header\n const hash = c.req.header(\"x-content-hash\") ?? \"\";\n if (!hash) throw new HTTPException(400, { message: \"Missing X-Content-Hash\" });\n const buffer = Buffer.from(await c.req.arrayBuffer());\n await getStore().storage.write(`content/${hash}`, buffer);\n }\n return c.json({ ok: true });\n });\n\n app.openapi(manifestRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const { files } = c.req.valid(\"json\");\n const manifest: Record<string, string> = {};\n for (const f of files) {\n manifest[f.rel] = f.hash;\n }\n await getStore().storage.write(\n `${project.id}/builds/${build.id}/storybook/manifest.json`,\n Buffer.from(JSON.stringify(manifest)),\n );\n // Upsert content_refs for each hash\n for (const f of files) {\n try {\n const contentRefs = getStore().db.tables.contentRefs;\n const now = new Date().toISOString();\n const existing = await getStore().db.get(contentRefs, f.hash);\n // oxlint-disable-next-line unicorn/no-if-else -- upsert is clearer as if/else\n if (!existing) {\n await getStore().db.insert(contentRefs, {\n hash: f.hash,\n refCount: 1,\n lastSeenAt: now,\n createdAt: now,\n });\n } else {\n await getStore().db.update(contentRefs, f.hash, {\n lastSeenAt: now,\n });\n }\n } catch {\n // ignore — content_refs may not exist\n }\n }\n const reqId = c.get(\"requestId\");\n await getStore().enqueueCapture?.(build.id, reqId);\n return c.json({ ok: true });\n });\n\n app.openapi(getBuildRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...VIEW_ROLES);\n const build = await buildForProject(project.id, buildId);\n return c.json(build);\n });\n\n app.openapi(retryBuildRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const updated = await new BuildModel(getStore().db).setStatus(build.id, \"pending\");\n await getStore().enqueueCapture?.(build.id, c.get(\"requestId\"));\n hxRefresh(c);\n flashHx(c, \"Capture queued\");\n return c.json(updated, 202);\n });\n\n app.openapi(deleteBuildRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);\n const build = await buildForProject(project.id, buildId);\n await new BuildModel(getStore().db).remove(build.id);\n return c.body(null, 204);\n });\n\n registerSnapshots(app);\n registerComments(app);\n registerAttempts(app);\n}\n\nconst listBuildsRoute = createRoute({\n method: \"get\",\n tags: [\"Builds\"],\n summary: \"List builds\",\n path: \"/api/v1/projects/{slug}/builds\",\n request: { params: z.object({ slug: z.string() }), query: buildListQuery },\n responses: {\n 200: {\n content: { \"application/json\": { schema: buildSchema.array() } },\n description: \"List builds for a project\",\n },\n ...notFoundResponse,\n ...unauthorized,\n },\n});\n\nconst buildCreatedSchema = z\n .object({ build: buildSchema, uploadUrl: z.string(), baselineSha: z.string().nullable() })\n .openapi(\"BuildCreated\");\n\nconst createBuildRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Create a build\",\n path: \"/api/v1/projects/{slug}/builds\",\n request: {\n params: z.object({ slug: z.string() }),\n body: {\n content: {\n \"application/json\": { schema: buildCreateJsonSchema },\n },\n },\n },\n responses: {\n 202: {\n content: {\n \"application/json\": {\n schema: buildCreatedSchema,\n },\n },\n description: \"Build created. PUT the Storybook zip to uploadUrl to queue capture.\",\n },\n ...badRequest,\n ...forbiddenResponse,\n ...notFoundResponse,\n },\n});\n\nconst uploadZipRoute = createRoute({\n method: \"put\",\n tags: [\"Builds\"],\n summary: \"Upload the Storybook zip\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/zip\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: {\n content: {\n \"application/zip\": {\n schema: z.string().openapi({ type: \"string\", format: \"binary\" }),\n },\n },\n },\n },\n responses: {\n 202: {\n content: { \"application/json\": { schema: buildSchema } },\n description: \"Zip stored and capture queued\",\n },\n 413: {\n content: {\n \"application/json\": {\n schema: z.object({ message: z.string() }).openapi(\"Error\"),\n },\n },\n description: \"Upload exceeds the configured size limit\",\n },\n ...badRequest,\n ...forbiddenResponse,\n ...notFoundResponse,\n },\n});\n\nconst dedupHashesSchema = z.object({ hashes: z.array(z.string()) });\nconst dedupNeededSchema = z.object({ needed: z.array(z.string()) });\nconst dedupRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Check which files need uploading\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/dedup\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: { content: { \"application/json\": { schema: dedupHashesSchema } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: dedupNeededSchema } },\n description: \"Hashes needed for upload\",\n },\n ...notFoundResponse,\n },\n});\n\nconst contentUploadRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Upload build content\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/content\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: { content: { \"multipart/form-data\": { schema: z.any() } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: z.object({ ok: z.boolean() }) } },\n description: \"Content uploaded\",\n },\n ...notFoundResponse,\n },\n});\n\nconst manifestFileSchema = z.object({ rel: z.string(), hash: z.string(), size: z.number() });\nconst manifestRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Store the build manifest\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/manifest\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: {\n content: {\n \"application/json\": {\n schema: z.object({ files: z.array(manifestFileSchema) }),\n },\n },\n },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: z.object({ ok: z.boolean() }) } },\n description: \"Manifest stored\",\n },\n ...notFoundResponse,\n },\n});\n\nconst affectedPayloadSchema = z.object({\n baselineSha: z.string().nullable(),\n changedFiles: z.array(z.string()).max(20_000),\n affectedImportPaths: z.array(z.string()).max(20_000).nullable(),\n});\nconst affectedRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Record the affected stories\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/affected\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: { content: { \"application/json\": { schema: affectedPayloadSchema } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: buildSchema } },\n description: \"Affected set recorded\",\n },\n ...badRequest,\n ...forbiddenResponse,\n ...notFoundResponse,\n },\n});\n\nconst getBuildRoute = createRoute({\n method: \"get\",\n tags: [\"Builds\"],\n summary: \"Get a build\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 200: { content: { \"application/json\": { schema: buildSchema } }, description: \"Fetch a build\" },\n ...notFoundResponse,\n ...unauthorized,\n },\n});\n\nconst retryBuildRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Retry capture\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/retry\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 202: {\n content: { \"application/json\": { schema: buildSchema } },\n description: \"Build reset to pending and capture re-queued (new attempt)\",\n },\n ...notFoundResponse,\n },\n});\n\nconst deleteBuildRoute = createRoute({\n method: \"delete\",\n tags: [\"Builds\"],\n summary: \"Delete a build\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 204: { description: \"Build deleted\" },\n ...notFoundResponse,\n },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;AAoBA,eAAe,iBAAiB,MAA6B;CAC3D,IAAI;EACF,MAAM,cAAc,SAAS,CAAC,CAAC,GAAG,OAAO;EACzC,MAAM,uBAAM,IAAI,KAAK,EAAA,CAAE,YAAY;EACnC,MAAM,WAAW,MAAM,SAAS,CAAC,CAAC,GAAG,IAAI,aAAa,IAAI;EAC1D,IAAI,UACF,MAAM,SAAS,CAAC,CAAC,GAAG,OAAO,aAAa,MAAM;GAC5C,UAAW,SAAkC,WAAW;GACxD,YAAY;EACd,CAAC;OAED,MAAM,SAAS,CAAC,CAAC,GAAG,OAAO,aAAa;GACtC;GACA,UAAU;GACV,YAAY;GACZ,WAAW;EACb,CAAC;CAEL,QAAQ,CAER;AACF;;AAcA,SAAgB,eAAe,KAAwB;CACrD,IAAI,QAAQ,iBAAiB,OAAO,MAAM;EACxC,MAAM,UAAU,MAAM,yBAAyB,GAAG,EAAE,IAAI,MAAM,OAAO,CAAC,CAAC,MAAM,GAAG,UAAU;EAC1F,MAAM,EAAE,QAAQ,QAAQ,UAAU,eAAe,EAAE,IAAI,MAAM,OAAO;EACpE,MAAM,SAAS,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,QAAQ,IAAI;GAC5D;GACA,QAAQ,UAAU,KAAA;GAClB,UAAU,YAAY,KAAA;GACtB,YAAY,cAAc,KAAA;EAC5B,CAAC;EACD,OAAO,EAAE,KAAK,MAAM,MAAM;CAC5B,CAAC;CAED,IAAI,QAAQ,kBAAkB,OAAO,MAAM;EACzC,MAAM,UAAU,MAAM,yBACpB,GACA,EAAE,IAAI,MAAM,OAAO,CAAC,CAAC,MACrB,GAAG,eACL;EACA,MAAM,SAAS,sBAAsB,UAAU,MAAM,EAAE,IAAI,KAAK,CAAC;EACjE,IAAI,CAAC,OAAO,SACV,MAAM,IAAI,cAAc,KAAK,EAAE,SAAS,OAAO,MAAM,QAAQ,CAAC;EAEhE,MAAM,QAAQ,MAAM,kBAAkB,SAAS,OAAO,IAAI;EAC1D,MAAM,YAAY,oBAAoB,QAAQ,KAAK,UAAU,MAAM,GAAG;EACtE,OAAO,EAAE,KAAK;GAAE;GAAO;GAAW,aAAa,MAAM;EAAY,GAAG,GAAG;CACzE,CAAC;CAED,IAAI,QAAQ,eAAe,OAAO,MAAM;EACtC,MAAM,EAAE,MAAM,YAAY,EAAE,IAAI,MAAM,OAAO;EAC7C,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,eAAe;EAC1E,MAAM,QAAQ,MAAM,gBAAgB,QAAQ,IAAI,OAAO;EACvD,MAAM,EAAE,aAAa,cAAc,wBAAwB,EAAE,IAAI,MAAM,MAAM;EAC7E,MAAM,UAAU,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,YAAY,MAAM,IAAI;GACxE;GACA;GACA;EACF,CAAC;EACD,OAAO,EAAE,KAAK,OAAO;CACvB,CAAC;CAED,IAAI,QAAQ,gBAAgB,OAAO,MAAM;EACvC,MAAM,EAAE,MAAM,YAAY,EAAE,IAAI,MAAM,OAAO;EAC7C,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,eAAe;EAC1E,MAAM,QAAQ,MAAM,gBAAgB,QAAQ,IAAI,OAAO;EACvD,MAAM,WAAW,SAAS,CAAC,CAAC,OAAO,kBAAkB;EACrD,MAAM,kBACJ,SAAS,CAAC,CAAC,SACX,iBAAiB,QAAQ,IAAI,MAAM,EAAE,GACrC,EAAE,IAAI,IAAI,MACV,QACF;EAEA,MAAM,QAAQ,EAAE,IAAI,WAAW;EAC/B,MAAM,SAAS,CAAC,CAAC,iBAAiB,MAAM,IAAI,KAAK;EACjD,MAAM,EAAE,SAAS,QAAQ,WAAW,SAAS;EAC7C,MAAM,qBACJ,SACA,OAAO,YACP,OAAO,oBACP,EAAE,IAAI,OAAO,gBAAgB,GAC7B,QAAQ,IACR,MAAM,IACN,MACF;EACA,OAAO,EAAE,KAAK,OAAO,GAAG;CAC1B,CAAC;CAED,IAAI,QAAQ,YAAY,OAAO,MAAM;EACnC,MAAM,EAAE,MAAM,YAAY,EAAE,IAAI,MAAM,OAAO;EAC7C,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,eAAe;EAC1E,MAAM,gBAAgB,QAAQ,IAAI,OAAO;EACzC,MAAM,EAAE,WAAW,EAAE,IAAI,MAAM,MAAM;EACrC,MAAM,SAAmB,CAAC;EAC1B,KAAK,MAAM,QAAQ,QACjB,IAAI,CAAE,MAAM,SAAS,CAAC,CAAC,QAAQ,OAAO,WAAW,MAAM,GACrD,OAAO,KAAK,IAAI;EAGpB,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC;CAC1B,CAAC;CAGD,IAAI,QAAQ,oBAAoB,OAAO,MAAM;EAC3C,MAAM,EAAE,MAAM,YAAY,EAAE,IAAI,MAAM,OAAO;EAC7C,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,eAAe;EAC1E,MAAM,gBAAgB,QAAQ,IAAI,OAAO;EAEzC,KADoB,EAAE,IAAI,OAAO,cAAc,KAAK,GAAA,CACpC,SAAS,YAAY,GAAG;GACtC,MAAM,OAAO,MAAM,EAAE,IAAI,SAAS;GAClC,KAAK,MAAM,CAAC,KAAK,UAAU,KAAK,QAAQ,GACtC,IAAI,iBAAiB,MAAM;IACzB,MAAM,OAAO;IACb,MAAM,SAAS,OAAO,KAAK,MAAM,MAAM,YAAY,CAAC;IACpD,MAAM,SAAS,CAAC,CAAC,QAAQ,MAAM,WAAW,QAAQ,MAAM;IACxD,MAAM,iBAAiB,IAAI;GAC7B;EAEJ,OAAO;GAEL,MAAM,OAAO,EAAE,IAAI,OAAO,gBAAgB,KAAK;GAC/C,IAAI,CAAC,MAAM,MAAM,IAAI,cAAc,KAAK,EAAE,SAAS,yBAAyB,CAAC;GAC7E,MAAM,SAAS,OAAO,KAAK,MAAM,EAAE,IAAI,YAAY,CAAC;GACpD,MAAM,SAAS,CAAC,CAAC,QAAQ,MAAM,WAAW,QAAQ,MAAM;EAC1D;EACA,OAAO,EAAE,KAAK,EAAE,IAAI,KAAK,CAAC;CAC5B,CAAC;CAED,IAAI,QAAQ,eAAe,OAAO,MAAM;EACtC,MAAM,EAAE,MAAM,YAAY,EAAE,IAAI,MAAM,OAAO;EAC7C,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,eAAe;EAC1E,MAAM,QAAQ,MAAM,gBAAgB,QAAQ,IAAI,OAAO;EACvD,MAAM,EAAE,UAAU,EAAE,IAAI,MAAM,MAAM;EACpC,MAAM,WAAmC,CAAC;EAC1C,KAAK,MAAM,KAAK,OACd,SAAS,EAAE,OAAO,EAAE;EAEtB,MAAM,SAAS,CAAC,CAAC,QAAQ,MACvB,GAAG,QAAQ,GAAG,UAAU,MAAM,GAAG,2BACjC,OAAO,KAAK,KAAK,UAAU,QAAQ,CAAC,CACtC;EAEA,KAAK,MAAM,KAAK,OACd,IAAI;GACF,MAAM,cAAc,SAAS,CAAC,CAAC,GAAG,OAAO;GACzC,MAAM,uBAAM,IAAI,KAAK,EAAA,CAAE,YAAY;GAGnC,IAAI,CAAC,MAFkB,SAAS,CAAC,CAAC,GAAG,IAAI,aAAa,EAAE,IAAI,GAG1D,MAAM,SAAS,CAAC,CAAC,GAAG,OAAO,aAAa;IACtC,MAAM,EAAE;IACR,UAAU;IACV,YAAY;IACZ,WAAW;GACb,CAAC;QAED,MAAM,SAAS,CAAC,CAAC,GAAG,OAAO,aAAa,EAAE,MAAM,EAC9C,YAAY,IACd,CAAC;EAEL,QAAQ,CAER;EAEF,MAAM,QAAQ,EAAE,IAAI,WAAW;EAC/B,MAAM,SAAS,CAAC,CAAC,iBAAiB,MAAM,IAAI,KAAK;EACjD,OAAO,EAAE,KAAK,EAAE,IAAI,KAAK,CAAC;CAC5B,CAAC;CAED,IAAI,QAAQ,eAAe,OAAO,MAAM;EACtC,MAAM,EAAE,MAAM,YAAY,EAAE,IAAI,MAAM,OAAO;EAC7C,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,UAAU;EACrE,MAAM,QAAQ,MAAM,gBAAgB,QAAQ,IAAI,OAAO;EACvD,OAAO,EAAE,KAAK,KAAK;CACrB,CAAC;CAED,IAAI,QAAQ,iBAAiB,OAAO,MAAM;EACxC,MAAM,EAAE,MAAM,YAAY,EAAE,IAAI,MAAM,OAAO;EAC7C,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,eAAe;EAC1E,MAAM,QAAQ,MAAM,gBAAgB,QAAQ,IAAI,OAAO;EACvD,MAAM,UAAU,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,UAAU,MAAM,IAAI,SAAS;EACjF,MAAM,SAAS,CAAC,CAAC,iBAAiB,MAAM,IAAI,EAAE,IAAI,WAAW,CAAC;EAC9D,UAAU,CAAC;EACX,QAAQ,GAAG,gBAAgB;EAC3B,OAAO,EAAE,KAAK,SAAS,GAAG;CAC5B,CAAC;CAED,IAAI,QAAQ,kBAAkB,OAAO,MAAM;EACzC,MAAM,EAAE,MAAM,YAAY,EAAE,IAAI,MAAM,OAAO;EAC7C,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,cAAc;EACzE,MAAM,QAAQ,MAAM,gBAAgB,QAAQ,IAAI,OAAO;EACvD,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,MAAM,EAAE;EACnD,OAAO,EAAE,KAAK,MAAM,GAAG;CACzB,CAAC;CAED,kBAAkB,GAAG;CACrB,iBAAiB,GAAG;CACpB,iBAAiB,GAAG;AACtB;AAEA,MAAM,kBAAkB,YAAY;CAClC,QAAQ;CACR,MAAM,CAAC,QAAQ;CACf,SAAS;CACT,MAAM;CACN,SAAS;EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;EAAG,OAAO;CAAe;CACzE,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,YAAY,MAAM,EAAE,EAAE;GAC/D,aAAa;EACf;EACA,GAAGA;EACH,GAAG;CACL;AACF,CAAC;AAED,MAAM,qBAAqB,EACxB,OAAO;CAAE,OAAO;CAAa,WAAW,EAAE,OAAO;CAAG,aAAa,EAAE,OAAO,CAAC,CAAC,SAAS;AAAE,CAAC,CAAC,CACzF,QAAQ,cAAc;AAEzB,MAAM,mBAAmB,YAAY;CACnC,QAAQ;CACR,MAAM,CAAC,QAAQ;CACf,SAAS;CACT,MAAM;CACN,SAAS;EACP,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;EACrC,MAAM,EACJ,SAAS,EACP,oBAAoB,EAAE,QAAQ,sBAAsB,EACtD,EACF;CACF;CACA,WAAW;EACT,KAAK;GACH,SAAS,EACP,oBAAoB,EAClB,QAAQ,mBACV,EACF;GACA,aAAa;EACf;EACA,GAAG;EACH,GAAGC;EACH,GAAGD;CACL;AACF,CAAC;AAED,MAAM,iBAAiB,YAAY;CACjC,QAAQ;CACR,MAAM,CAAC,QAAQ;CACf,SAAS;CACT,MAAM;CACN,SAAS;EACP,QAAQ,EAAE,OAAO;GAAE,MAAM,EAAE,OAAO;GAAG,SAAS,EAAE,OAAO;EAAE,CAAC;EAC1D,MAAM,EACJ,SAAS,EACP,mBAAmB,EACjB,QAAQ,EAAE,OAAO,CAAC,CAAC,QAAQ;GAAE,MAAM;GAAU,QAAQ;EAAS,CAAC,EACjE,EACF,EACF;CACF;CACA,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,YAAY,EAAE;GACvD,aAAa;EACf;EACA,KAAK;GACH,SAAS,EACP,oBAAoB,EAClB,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,QAAQ,OAAO,EAC3D,EACF;GACA,aAAa;EACf;EACA,GAAG;EACH,GAAGC;EACH,GAAGD;CACL;AACF,CAAC;AAED,MAAM,oBAAoB,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;AAClE,MAAM,oBAAoB,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;AAClE,MAAM,aAAa,YAAY;CAC7B,QAAQ;CACR,MAAM,CAAC,QAAQ;CACf,SAAS;CACT,MAAM;CACN,SAAS;EACP,QAAQ,EAAE,OAAO;GAAE,MAAM,EAAE,OAAO;GAAG,SAAS,EAAE,OAAO;EAAE,CAAC;EAC1D,MAAM,EAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,kBAAkB,EAAE,EAAE;CACzE;CACA,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,kBAAkB,EAAE;GAC7D,aAAa;EACf;EACA,GAAGA;CACL;AACF,CAAC;AAED,MAAM,qBAAqB,YAAY;CACrC,QAAQ;CACR,MAAM,CAAC,QAAQ;CACf,SAAS;CACT,MAAM;CACN,SAAS;EACP,QAAQ,EAAE,OAAO;GAAE,MAAM,EAAE,OAAO;GAAG,SAAS,EAAE,OAAO;EAAE,CAAC;EAC1D,MAAM,EAAE,SAAS,EAAE,uBAAuB,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,EAAE;CAClE;CACA,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,EAAE,EAAE;GACzE,aAAa;EACf;EACA,GAAGA;CACL;AACF,CAAC;AAED,MAAM,qBAAqB,EAAE,OAAO;CAAE,KAAK,EAAE,OAAO;CAAG,MAAM,EAAE,OAAO;CAAG,MAAM,EAAE,OAAO;AAAE,CAAC;AAC3F,MAAM,gBAAgB,YAAY;CAChC,QAAQ;CACR,MAAM,CAAC,QAAQ;CACf,SAAS;CACT,MAAM;CACN,SAAS;EACP,QAAQ,EAAE,OAAO;GAAE,MAAM,EAAE,OAAO;GAAG,SAAS,EAAE,OAAO;EAAE,CAAC;EAC1D,MAAM,EACJ,SAAS,EACP,oBAAoB,EAClB,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,EAAE,CAAC,EACzD,EACF,EACF;CACF;CACA,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,EAAE,EAAE;GACzE,aAAa;EACf;EACA,GAAGA;CACL;AACF,CAAC;AAED,MAAM,wBAAwB,EAAE,OAAO;CACrC,aAAa,EAAE,OAAO,CAAC,CAAC,SAAS;CACjC,cAAc,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,GAAM;CAC5C,qBAAqB,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,GAAM,CAAC,CAAC,SAAS;AAChE,CAAC;AACD,MAAM,gBAAgB,YAAY;CAChC,QAAQ;CACR,MAAM,CAAC,QAAQ;CACf,SAAS;CACT,MAAM;CACN,SAAS;EACP,QAAQ,EAAE,OAAO;GAAE,MAAM,EAAE,OAAO;GAAG,SAAS,EAAE,OAAO;EAAE,CAAC;EAC1D,MAAM,EAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,sBAAsB,EAAE,EAAE;CAC7E;CACA,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,YAAY,EAAE;GACvD,aAAa;EACf;EACA,GAAG;EACH,GAAGC;EACH,GAAGD;CACL;AACF,CAAC;AAED,MAAM,gBAAgB,YAAY;CAChC,QAAQ;CACR,MAAM,CAAC,QAAQ;CACf,SAAS;CACT,MAAM;CACN,SAAS,EAAE,QAAQ,EAAE,OAAO;EAAE,MAAM,EAAE,OAAO;EAAG,SAAS,EAAE,OAAO;CAAE,CAAC,EAAE;CACvE,WAAW;EACT,KAAK;GAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,YAAY,EAAE;GAAG,aAAa;EAAgB;EAC9F,GAAGA;EACH,GAAG;CACL;AACF,CAAC;AAED,MAAM,kBAAkB,YAAY;CAClC,QAAQ;CACR,MAAM,CAAC,QAAQ;CACf,SAAS;CACT,MAAM;CACN,SAAS,EAAE,QAAQ,EAAE,OAAO;EAAE,MAAM,EAAE,OAAO;EAAG,SAAS,EAAE,OAAO;CAAE,CAAC,EAAE;CACvE,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,YAAY,EAAE;GACvD,aAAa;EACf;EACA,GAAGA;CACL;AACF,CAAC;AAED,MAAM,mBAAmB,YAAY;CACnC,QAAQ;CACR,MAAM,CAAC,QAAQ;CACf,SAAS;CACT,MAAM;CACN,SAAS,EAAE,QAAQ,EAAE,OAAO;EAAE,MAAM,EAAE,OAAO;EAAG,SAAS,EAAE,OAAO;CAAE,CAAC,EAAE;CACvE,WAAW;EACT,KAAK,EAAE,aAAa,gBAAgB;EACpC,GAAGA;CACL;AACF,CAAC"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { getStore } from "../store.mjs";
|
|
5
|
+
import { notifyProject } from "../notify.mjs";
|
|
6
|
+
import { hxRefresh } from "./htmx.mjs";
|
|
7
|
+
import { flashHx } from "./flash.mjs";
|
|
8
|
+
import { notFound, resolveAuthorizedProject } from "./helpers.mjs";
|
|
9
|
+
import { commentCreateSchema, commentSchema, notFound as notFound$1, unauthorized } from "./schemas.mjs";
|
|
10
|
+
import { DEVELOPER_ROLES, VIEW_ROLES, buildForProject } from "./builds.handlers.mjs";
|
|
11
|
+
import { createRoute, z } from "@hono/zod-openapi";
|
|
12
|
+
import { CommentModel } from "@storyshelf/core/models";
|
|
13
|
+
//#region src/routers/comments.ts
|
|
14
|
+
const listCommentsRoute = createRoute({
|
|
15
|
+
method: "get",
|
|
16
|
+
tags: ["Review"],
|
|
17
|
+
summary: "List comments",
|
|
18
|
+
path: "/api/v1/projects/{slug}/builds/{buildId}/comments",
|
|
19
|
+
request: { params: z.object({
|
|
20
|
+
slug: z.string(),
|
|
21
|
+
buildId: z.string()
|
|
22
|
+
}) },
|
|
23
|
+
responses: {
|
|
24
|
+
200: {
|
|
25
|
+
content: { "application/json": { schema: commentSchema.array() } },
|
|
26
|
+
description: "List comments on a build"
|
|
27
|
+
},
|
|
28
|
+
...notFound$1,
|
|
29
|
+
...unauthorized
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
const createCommentRoute = createRoute({
|
|
33
|
+
method: "post",
|
|
34
|
+
tags: ["Review"],
|
|
35
|
+
summary: "Add a comment",
|
|
36
|
+
path: "/api/v1/projects/{slug}/builds/{buildId}/comments",
|
|
37
|
+
request: {
|
|
38
|
+
params: z.object({
|
|
39
|
+
slug: z.string(),
|
|
40
|
+
buildId: z.string()
|
|
41
|
+
}),
|
|
42
|
+
body: { content: { "application/json": { schema: commentCreateSchema } } }
|
|
43
|
+
},
|
|
44
|
+
responses: {
|
|
45
|
+
201: {
|
|
46
|
+
content: { "application/json": { schema: commentSchema } },
|
|
47
|
+
description: "Comment created"
|
|
48
|
+
},
|
|
49
|
+
...notFound$1
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
const resolveCommentRoute = createRoute({
|
|
53
|
+
method: "post",
|
|
54
|
+
tags: ["Review"],
|
|
55
|
+
summary: "Resolve a comment",
|
|
56
|
+
path: "/api/v1/projects/{slug}/builds/{buildId}/comments/{commentId}/resolve",
|
|
57
|
+
request: { params: z.object({
|
|
58
|
+
slug: z.string(),
|
|
59
|
+
buildId: z.string(),
|
|
60
|
+
commentId: z.string()
|
|
61
|
+
}) },
|
|
62
|
+
responses: {
|
|
63
|
+
200: {
|
|
64
|
+
content: { "application/json": { schema: commentSchema } },
|
|
65
|
+
description: "Comment resolved"
|
|
66
|
+
},
|
|
67
|
+
...notFound$1
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
/** Register the build comment list, create, and resolve endpoints. */
|
|
71
|
+
function registerComments(app) {
|
|
72
|
+
app.openapi(listCommentsRoute, async (c) => {
|
|
73
|
+
const { slug, buildId } = c.req.valid("param");
|
|
74
|
+
const project = await resolveAuthorizedProject(c, slug, ...VIEW_ROLES);
|
|
75
|
+
const build = await buildForProject(project.id, buildId);
|
|
76
|
+
return c.json(await new CommentModel(getStore().db).listByBuild(build.id));
|
|
77
|
+
});
|
|
78
|
+
app.openapi(createCommentRoute, async (c) => {
|
|
79
|
+
const { slug, buildId } = c.req.valid("param");
|
|
80
|
+
const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);
|
|
81
|
+
const build = await buildForProject(project.id, buildId);
|
|
82
|
+
const body = c.req.valid("json");
|
|
83
|
+
const userId = getStore().user?.id ?? null;
|
|
84
|
+
const comment = await new CommentModel(getStore().db).create(project.id, build.id, userId, body);
|
|
85
|
+
await notifyProject(project, "comment:created", {
|
|
86
|
+
buildId: build.id,
|
|
87
|
+
commentId: comment.id,
|
|
88
|
+
snapshotId: comment.snapshotId,
|
|
89
|
+
body: comment.body.slice(0, 500)
|
|
90
|
+
}, `/builds/${build.id}`);
|
|
91
|
+
hxRefresh(c);
|
|
92
|
+
flashHx(c, "Comment added");
|
|
93
|
+
return c.json(comment, 201);
|
|
94
|
+
});
|
|
95
|
+
app.openapi(resolveCommentRoute, async (c) => {
|
|
96
|
+
const { slug, buildId, commentId } = c.req.valid("param");
|
|
97
|
+
const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);
|
|
98
|
+
const build = await buildForProject(project.id, buildId);
|
|
99
|
+
const comment = await new CommentModel(getStore().db).resolve(commentId);
|
|
100
|
+
if (comment.buildId !== build.id) notFound("Comment not found");
|
|
101
|
+
hxRefresh(c);
|
|
102
|
+
flashHx(c, "Comment resolved");
|
|
103
|
+
return c.json(comment);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
//#endregion
|
|
107
|
+
export { registerComments };
|
|
108
|
+
|
|
109
|
+
//# sourceMappingURL=comments.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comments.mjs","names":["notFound"],"sources":["../../src/routers/comments.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport { CommentModel } from \"@storyshelf/core/models\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { notifyProject } from \"../notify.ts\";\nimport { getStore } from \"../store.ts\";\nimport { VIEW_ROLES, DEVELOPER_ROLES, buildForProject } from \"./builds.handlers.ts\";\nimport { flashHx } from \"./flash.ts\";\nimport { resolveAuthorizedProject, notFound as throwNotFound } from \"./helpers.ts\";\nimport { hxRefresh } from \"./htmx.ts\";\nimport { commentSchema, commentCreateSchema, notFound, unauthorized } from \"./schemas.ts\";\nconst listCommentsRoute = createRoute({\n method: \"get\",\n tags: [\"Review\"],\n summary: \"List comments\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/comments\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: commentSchema.array() } },\n description: \"List comments on a build\",\n },\n ...notFound,\n ...unauthorized,\n },\n});\n\nconst createCommentRoute = createRoute({\n method: \"post\",\n tags: [\"Review\"],\n summary: \"Add a comment\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/comments\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: { content: { \"application/json\": { schema: commentCreateSchema } } },\n },\n responses: {\n 201: {\n content: { \"application/json\": { schema: commentSchema } },\n description: \"Comment created\",\n },\n ...notFound,\n },\n});\n\nconst resolveCommentRoute = createRoute({\n method: \"post\",\n tags: [\"Review\"],\n summary: \"Resolve a comment\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/comments/{commentId}/resolve\",\n request: { params: z.object({ slug: z.string(), buildId: z.string(), commentId: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: commentSchema } },\n description: \"Comment resolved\",\n },\n ...notFound,\n },\n});\n\n/** Register the build comment list, create, and resolve endpoints. */\nexport function registerComments(app: ShelfRouter): void {\n app.openapi(listCommentsRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...VIEW_ROLES);\n const build = await buildForProject(project.id, buildId);\n return c.json(await new CommentModel(getStore().db).listByBuild(build.id));\n });\n\n app.openapi(createCommentRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const body = c.req.valid(\"json\");\n const userId = getStore().user?.id ?? null;\n const comment = await new CommentModel(getStore().db).create(\n project.id,\n build.id,\n userId,\n body,\n );\n await notifyProject(\n project,\n \"comment:created\",\n {\n buildId: build.id,\n commentId: comment.id,\n snapshotId: comment.snapshotId,\n body: comment.body.slice(0, 500),\n },\n `/builds/${build.id}`,\n );\n hxRefresh(c);\n flashHx(c, \"Comment added\");\n return c.json(comment, 201);\n });\n\n app.openapi(resolveCommentRoute, async (c) => {\n const { slug, buildId, commentId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const comment = await new CommentModel(getStore().db).resolve(commentId);\n if (comment.buildId !== build.id) {\n throwNotFound(\"Comment not found\");\n }\n hxRefresh(c);\n flashHx(c, \"Comment resolved\");\n return c.json(comment);\n });\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { isHxRequest } from "./htmx.mjs";
|
|
5
|
+
import { setCookie } from "hono/cookie";
|
|
6
|
+
//#region src/routers/flash.ts
|
|
7
|
+
/** Cookie the client script reads, shows as a toast, and clears. */
|
|
8
|
+
const FLASH_COOKIE = "storyshelf_flash";
|
|
9
|
+
const MAX_MESSAGE_LENGTH = 140;
|
|
10
|
+
/**
|
|
11
|
+
* Queue a one-shot toast for the next page the browser renders. The server
|
|
12
|
+
* decides this (only after the action actually succeeded), so it works for
|
|
13
|
+
* HTMX redirects, in-place swaps, and plain form posts alike. The cookie is
|
|
14
|
+
* short-lived and readable by the page script, which shows it with
|
|
15
|
+
* `textContent` and removes it.
|
|
16
|
+
*/
|
|
17
|
+
function flash(c, message, tone = "success") {
|
|
18
|
+
const text = message.length > MAX_MESSAGE_LENGTH ? `${message.slice(0, 139)}…` : message;
|
|
19
|
+
setCookie(c, FLASH_COOKIE, JSON.stringify({
|
|
20
|
+
message: text,
|
|
21
|
+
tone
|
|
22
|
+
}), {
|
|
23
|
+
path: "/",
|
|
24
|
+
maxAge: 60,
|
|
25
|
+
sameSite: "Lax"
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/** Like {@link flash}, but only for HTMX callers (keeps JSON API responses cookie-free). */
|
|
29
|
+
function flashHx(c, message, tone = "success") {
|
|
30
|
+
if (isHxRequest(c)) flash(c, message, tone);
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
export { FLASH_COOKIE, flash, flashHx };
|
|
34
|
+
|
|
35
|
+
//# sourceMappingURL=flash.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flash.mjs","names":[],"sources":["../../src/routers/flash.ts"],"sourcesContent":["import type { Context } from \"hono\";\nimport { setCookie } from \"hono/cookie\";\nimport { isHxRequest } from \"./htmx.ts\";\n\n/** Visual tone of a flash message. */\nexport type FlashTone = \"success\" | \"danger\" | \"warning\" | \"info\";\n\n/** Cookie the client script reads, shows as a toast, and clears. */\nexport const FLASH_COOKIE = \"storyshelf_flash\";\n\nconst MAX_MESSAGE_LENGTH = 140;\n\n/**\n * Queue a one-shot toast for the next page the browser renders. The server\n * decides this (only after the action actually succeeded), so it works for\n * HTMX redirects, in-place swaps, and plain form posts alike. The cookie is\n * short-lived and readable by the page script, which shows it with\n * `textContent` and removes it.\n */\nexport function flash(c: Context, message: string, tone: FlashTone = \"success\"): void {\n const text =\n message.length > MAX_MESSAGE_LENGTH ? `${message.slice(0, MAX_MESSAGE_LENGTH - 1)}…` : message;\n setCookie(c, FLASH_COOKIE, JSON.stringify({ message: text, tone }), {\n path: \"/\",\n maxAge: 60,\n sameSite: \"Lax\",\n });\n}\n\n/** Like {@link flash}, but only for HTMX callers (keeps JSON API responses cookie-free). */\nexport function flashHx(c: Context, message: string, tone: FlashTone = \"success\"): void {\n if (isHxRequest(c)) {\n flash(c, message, tone);\n }\n}\n"],"mappings":";;;;;;;AAQA,MAAa,eAAe;AAE5B,MAAM,qBAAqB;;;;;;;;AAS3B,SAAgB,MAAM,GAAY,SAAiB,OAAkB,WAAiB;CACpF,MAAM,OACJ,QAAQ,SAAS,qBAAqB,GAAG,QAAQ,MAAM,GAAG,GAAsB,EAAE,KAAK;CACzF,UAAU,GAAG,cAAc,KAAK,UAAU;EAAE,SAAS;EAAM;CAAK,CAAC,GAAG;EAClE,MAAM;EACN,QAAQ;EACR,UAAU;CACZ,CAAC;AACH;;AAGA,SAAgB,QAAQ,GAAY,SAAiB,OAAkB,WAAiB;CACtF,IAAI,YAAY,CAAC,GACf,MAAM,GAAG,SAAS,IAAI;AAE1B"}
|