@storyshelf/core 0.1.0 → 0.1.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 +9 -8
- package/dist/adapter/auth.mjs.map +1 -1
- package/dist/adapter/capture-runner.d.mts +4 -0
- package/dist/adapter/git-host/helpers.d.mts +1 -0
- package/dist/adapter/git-host/helpers.mjs +2 -1
- package/dist/adapter/git-host/helpers.mjs.map +1 -1
- package/dist/adapter/storage.d.mts +1 -0
- package/dist/adapters/capture-queue.d.mts +2 -0
- package/dist/adapters/webhook-events.mjs +8 -0
- package/dist/adapters/webhook-events.mjs.map +1 -1
- package/dist/capture/adapter.d.mts +11 -1
- package/dist/capture/adapter.mjs +19 -1
- package/dist/capture/adapter.mjs.map +1 -1
- package/dist/capture/orchestrator.d.mts +7 -0
- package/dist/capture/orchestrator.mjs +43 -10
- package/dist/capture/orchestrator.mjs.map +1 -1
- package/dist/capture/pipeline.d.mts +5 -3
- package/dist/capture/pipeline.mjs +18 -6
- package/dist/capture/pipeline.mjs.map +1 -1
- package/dist/capture/queue.d.mts +2 -0
- package/dist/capture/queue.mjs +2 -1
- package/dist/capture/queue.mjs.map +1 -1
- package/dist/capture/status-fanout.mjs +1 -0
- package/dist/capture/status-fanout.mjs.map +1 -1
- package/dist/capture/storybook.d.mts +1 -0
- package/dist/capture/storybook.mjs +12 -3
- package/dist/capture/storybook.mjs.map +1 -1
- package/dist/config.d.mts +27 -20
- package/dist/config.mjs +16 -2
- package/dist/config.mjs.map +1 -1
- package/dist/ddl.d.mts +2 -1
- package/dist/ddl.mjs +3 -0
- package/dist/ddl.mjs.map +1 -1
- package/dist/diff/engine.d.mts +8 -0
- package/dist/diff/engine.mjs +10 -2
- package/dist/diff/engine.mjs.map +1 -1
- package/dist/index.d.mts +7 -4
- package/dist/index.mjs +10 -3
- package/dist/index.mjs.map +1 -1
- package/dist/logger.d.mts +3 -3
- package/dist/logger.mjs.map +1 -1
- package/dist/middleware/csrf.mjs +2 -1
- package/dist/middleware/csrf.mjs.map +1 -1
- package/dist/middleware/rate-limit.mjs +1 -0
- package/dist/middleware/rate-limit.mjs.map +1 -1
- package/dist/models/baseline.d.mts +1 -0
- package/dist/models/baseline.mjs +3 -1
- package/dist/models/baseline.mjs.map +1 -1
- package/dist/models/build.d.mts +19 -1
- package/dist/models/build.mjs +22 -2
- package/dist/models/build.mjs.map +1 -1
- package/dist/models/comment.d.mts +2 -0
- package/dist/models/comment.mjs +3 -1
- package/dist/models/comment.mjs.map +1 -1
- package/dist/models/label.d.mts +6 -0
- package/dist/models/label.mjs +13 -1
- package/dist/models/label.mjs.map +1 -1
- package/dist/models/member.mjs +2 -1
- package/dist/models/member.mjs.map +1 -1
- package/dist/models/project.d.mts +1 -0
- package/dist/models/project.mjs +3 -2
- package/dist/models/project.mjs.map +1 -1
- package/dist/models/snapshot.d.mts +1 -0
- package/dist/models/snapshot.mjs +2 -1
- package/dist/models/snapshot.mjs.map +1 -1
- package/dist/models/status-config.mjs +2 -1
- package/dist/models/status-config.mjs.map +1 -1
- package/dist/models/token.mjs +2 -1
- package/dist/models/token.mjs.map +1 -1
- package/dist/models/webhook.d.mts +1 -0
- package/dist/models/webhook.mjs +2 -1
- package/dist/models/webhook.mjs.map +1 -1
- package/dist/pages/build-detail.mjs +2 -1
- package/dist/pages/build-detail.mjs.map +1 -1
- package/dist/pages/build-diff.mjs +1 -0
- package/dist/pages/build-diff.mjs.map +1 -1
- package/dist/pages/compute-jobs.mjs +2 -0
- package/dist/pages/compute-jobs.mjs.map +1 -1
- package/dist/pages/label-detail.mjs +227 -0
- package/dist/pages/label-detail.mjs.map +1 -0
- package/dist/pages/login.mjs +1 -0
- package/dist/pages/login.mjs.map +1 -1
- package/dist/pages/project-builds.mjs +1 -0
- package/dist/pages/project-builds.mjs.map +1 -1
- package/dist/pages/project-create.mjs +1 -0
- package/dist/pages/project-create.mjs.map +1 -1
- package/dist/pages/project-settings.mjs +33 -42
- package/dist/pages/project-settings.mjs.map +1 -1
- package/dist/pages/projects.mjs +1 -0
- package/dist/pages/projects.mjs.map +1 -1
- package/dist/pages/root.mjs +1 -0
- package/dist/pages/root.mjs.map +1 -1
- package/dist/pages/settings-general.mjs +1 -0
- package/dist/pages/settings-general.mjs.map +1 -1
- package/dist/pages/settings-labels.mjs +1 -0
- package/dist/pages/settings-labels.mjs.map +1 -1
- package/dist/pages/settings-members.mjs +1 -0
- package/dist/pages/settings-members.mjs.map +1 -1
- package/dist/pages/settings-status.mjs +1 -0
- package/dist/pages/settings-status.mjs.map +1 -1
- package/dist/pages/settings-tests.mjs +149 -0
- package/dist/pages/settings-tests.mjs.map +1 -0
- package/dist/pages/settings-tokens.mjs +1 -0
- package/dist/pages/settings-tokens.mjs.map +1 -1
- package/dist/pages/settings-webhooks.mjs +1 -0
- package/dist/pages/settings-webhooks.mjs.map +1 -1
- package/dist/pages/storybook.mjs +59 -0
- package/dist/pages/storybook.mjs.map +1 -0
- package/dist/retention/purge.d.mts +3 -0
- package/dist/retention/purge.mjs +2 -1
- package/dist/retention/purge.mjs.map +1 -1
- package/dist/routers/admin.mjs +1 -0
- package/dist/routers/admin.mjs.map +1 -1
- package/dist/routers/assets.mjs +1 -0
- package/dist/routers/assets.mjs.map +1 -1
- package/dist/routers/auth.mjs +1 -0
- package/dist/routers/auth.mjs.map +1 -1
- package/dist/routers/builds.handlers.mjs +15 -3
- package/dist/routers/builds.handlers.mjs.map +1 -1
- package/dist/routers/builds.mjs +5 -2
- package/dist/routers/builds.mjs.map +1 -1
- package/dist/routers/comments.mjs +1 -0
- package/dist/routers/comments.mjs.map +1 -1
- package/dist/routers/helpers.mjs +7 -1
- package/dist/routers/helpers.mjs.map +1 -1
- package/dist/routers/htmx.mjs +2 -0
- package/dist/routers/htmx.mjs.map +1 -1
- package/dist/routers/labels.mjs +34 -1
- package/dist/routers/labels.mjs.map +1 -1
- package/dist/routers/media.mjs +1 -0
- package/dist/routers/media.mjs.map +1 -1
- package/dist/routers/members.mjs +1 -0
- package/dist/routers/members.mjs.map +1 -1
- package/dist/routers/projects.mjs +1 -0
- package/dist/routers/projects.mjs.map +1 -1
- package/dist/routers/schemas.mjs +40 -4
- package/dist/routers/schemas.mjs.map +1 -1
- package/dist/routers/settings.mjs +22 -0
- package/dist/routers/settings.mjs.map +1 -1
- package/dist/routers/snapshots.mjs +1 -0
- package/dist/routers/snapshots.mjs.map +1 -1
- package/dist/routers/status-configs.mjs +1 -0
- package/dist/routers/status-configs.mjs.map +1 -1
- package/dist/routers/storybook.mjs +113 -0
- package/dist/routers/storybook.mjs.map +1 -0
- package/dist/routers/tokens.mjs +1 -0
- package/dist/routers/tokens.mjs.map +1 -1
- package/dist/routers/ui.mjs +25 -2
- package/dist/routers/ui.mjs.map +1 -1
- package/dist/routers/webhooks.mjs +1 -0
- package/dist/routers/webhooks.mjs.map +1 -1
- package/dist/schema-tables.mjs +166 -0
- package/dist/schema-tables.mjs.map +1 -0
- package/dist/schema.d.mts +194 -4352
- package/dist/schema.mjs +26 -136
- package/dist/schema.mjs.map +1 -1
- package/dist/store.mjs +2 -0
- package/dist/store.mjs.map +1 -1
- package/dist/types.d.mts +14 -1
- package/dist/types.mjs +8 -0
- package/dist/types.mjs.map +1 -1
- package/dist/ui/components.mjs +5 -0
- package/dist/ui/components.mjs.map +1 -1
- package/dist/ui/document.d.mts +1 -0
- package/dist/ui/document.mjs +10 -0
- package/dist/ui/document.mjs.map +1 -1
- package/dist/ui/styles.mjs +1 -0
- package/dist/ui/styles.mjs.map +1 -1
- package/dist/ui/theme.mjs +2 -0
- package/dist/ui/theme.mjs.map +1 -1
- package/dist/urls.d.mts +8 -0
- package/dist/urls.mjs +7 -0
- package/dist/urls.mjs.map +1 -1
- package/dist/utils/hash.mjs +3 -0
- package/dist/utils/hash.mjs.map +1 -1
- package/dist/utils/paths.d.mts +5 -0
- package/dist/utils/paths.mjs +5 -0
- package/dist/utils/paths.mjs.map +1 -1
- package/dist/utils/ulid.d.mts +2 -0
- package/dist/utils/ulid.mjs +5 -3
- package/dist/utils/ulid.mjs.map +1 -1
- package/package.json +76 -67
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { BuildModel, isPublicBuild } from "../models/build.mjs";
|
|
5
|
+
import { ProjectModel } from "../models/project.mjs";
|
|
6
|
+
import { LabelModel } from "../models/label.mjs";
|
|
7
|
+
import { getStore } from "../store.mjs";
|
|
8
|
+
import { currentProjectRole, notFound } from "./helpers.mjs";
|
|
9
|
+
import { storybookDir } from "../utils/paths.mjs";
|
|
10
|
+
import { renderStorybookPage } from "../pages/storybook.mjs";
|
|
11
|
+
import { posix } from "node:path";
|
|
12
|
+
//#region src/routers/storybook.ts
|
|
13
|
+
const VIEW_ROLES = /* @__PURE__ */ new Set([
|
|
14
|
+
"viewer",
|
|
15
|
+
"developer",
|
|
16
|
+
"approver",
|
|
17
|
+
"admin"
|
|
18
|
+
]);
|
|
19
|
+
const MIME = {
|
|
20
|
+
".html": "text/html; charset=utf-8",
|
|
21
|
+
".htm": "text/html; charset=utf-8",
|
|
22
|
+
".js": "application/javascript",
|
|
23
|
+
".mjs": "application/javascript",
|
|
24
|
+
".json": "application/json",
|
|
25
|
+
".css": "text/css; charset=utf-8",
|
|
26
|
+
".png": "image/png",
|
|
27
|
+
".jpg": "image/jpeg",
|
|
28
|
+
".jpeg": "image/jpeg",
|
|
29
|
+
".gif": "image/gif",
|
|
30
|
+
".svg": "image/svg+xml",
|
|
31
|
+
".webp": "image/webp",
|
|
32
|
+
".ico": "image/x-icon",
|
|
33
|
+
".woff": "font/woff",
|
|
34
|
+
".woff2": "font/woff2",
|
|
35
|
+
".ttf": "font/ttf",
|
|
36
|
+
".map": "application/json",
|
|
37
|
+
".wasm": "application/wasm"
|
|
38
|
+
};
|
|
39
|
+
function contentTypeFor(path) {
|
|
40
|
+
const ext = posix.extname(path).toLowerCase();
|
|
41
|
+
return MIME[ext] ?? "application/octet-stream";
|
|
42
|
+
}
|
|
43
|
+
function isSafeSegment(segment) {
|
|
44
|
+
return segment !== ".." && !segment.startsWith("/") && !segment.includes("\\") && !segment.includes("..");
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Decide whether the current request may view `build`'s published Storybook.
|
|
48
|
+
*
|
|
49
|
+
* Public builds (ADR 0011) are viewable without auth; every other build requires
|
|
50
|
+
* a logged-in session with at least `viewer` membership on the project.
|
|
51
|
+
*/
|
|
52
|
+
async function canViewBuild(build, project) {
|
|
53
|
+
if (isPublicBuild(project, build)) return true;
|
|
54
|
+
const { authEnabled } = getStore();
|
|
55
|
+
if (!authEnabled) return true;
|
|
56
|
+
const role = await currentProjectRole(project.id);
|
|
57
|
+
return Boolean(role && VIEW_ROLES.has(role));
|
|
58
|
+
}
|
|
59
|
+
/** Register the published Storybook resolver and static-asset routes. */
|
|
60
|
+
function registerStorybook(app) {
|
|
61
|
+
app.get("/projects/:slug/storybook", async (c) => {
|
|
62
|
+
const slug = c.req.param("slug");
|
|
63
|
+
const project = await new ProjectModel(getStore().db).getBySlug(slug);
|
|
64
|
+
if (!project) notFound("Project not found");
|
|
65
|
+
const build = await new BuildModel(getStore().db).latestPublished(project);
|
|
66
|
+
if (!build) notFound("No published Storybook for this project");
|
|
67
|
+
if (!await canViewBuild(build, project)) return c.redirect("/auth/login", 302);
|
|
68
|
+
return c.redirect(`/projects/${slug}/storybook/build/${build.id}/`, 302);
|
|
69
|
+
});
|
|
70
|
+
app.get("/projects/:slug/storybook/:key/:value", async (c) => {
|
|
71
|
+
const slug = c.req.param("slug");
|
|
72
|
+
const key = c.req.param("key");
|
|
73
|
+
if (key === "build") return c.notFound();
|
|
74
|
+
const value = c.req.param("value");
|
|
75
|
+
const project = await new ProjectModel(getStore().db).getBySlug(slug);
|
|
76
|
+
if (!project) notFound("Project not found");
|
|
77
|
+
const buildId = await new LabelModel(getStore().db).latestBuildId(project.id, key, value);
|
|
78
|
+
if (!buildId) notFound("No build carries that label");
|
|
79
|
+
const build = await new BuildModel(getStore().db).get(buildId);
|
|
80
|
+
if (!build || !await canViewBuild(build, project)) return c.redirect("/auth/login", 302);
|
|
81
|
+
return c.redirect(`/projects/${slug}/storybook/build/${buildId}/`, 302);
|
|
82
|
+
});
|
|
83
|
+
app.get("/projects/:slug/storybook/build/:buildId/*", async (c) => {
|
|
84
|
+
const slug = c.req.param("slug");
|
|
85
|
+
const buildId = c.req.param("buildId");
|
|
86
|
+
const base = `/projects/${slug}/storybook/build/${buildId}/`;
|
|
87
|
+
if (!c.req.path.startsWith(base)) return c.notFound();
|
|
88
|
+
let rest;
|
|
89
|
+
try {
|
|
90
|
+
rest = decodeURIComponent(c.req.path.slice(base.length));
|
|
91
|
+
} catch {
|
|
92
|
+
return c.notFound();
|
|
93
|
+
}
|
|
94
|
+
const project = await new ProjectModel(getStore().db).getBySlug(slug);
|
|
95
|
+
if (!project) notFound("Project not found");
|
|
96
|
+
const build = await new BuildModel(getStore().db).get(buildId);
|
|
97
|
+
if (!build || build.projectId !== project.id) notFound("Build not found");
|
|
98
|
+
if (!await canViewBuild(build, project)) return c.redirect("/auth/login", 302);
|
|
99
|
+
if (rest === "") return c.html(renderStorybookPage(project, build, slug), 200);
|
|
100
|
+
if (rest.split("/").some((segment) => !isSafeSegment(segment))) return c.notFound();
|
|
101
|
+
const path = posix.join(storybookDir(project.id, build.id), rest);
|
|
102
|
+
if (!await getStore().storage.exists(path)) return c.notFound();
|
|
103
|
+
const buffer = await getStore().storage.read(path);
|
|
104
|
+
return c.body(new Uint8Array(buffer), 200, {
|
|
105
|
+
"content-type": contentTypeFor(rest),
|
|
106
|
+
"cache-control": "public, max-age=3600"
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
//#endregion
|
|
111
|
+
export { registerStorybook };
|
|
112
|
+
|
|
113
|
+
//# sourceMappingURL=storybook.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storybook.mjs","names":[],"sources":["../../src/routers/storybook.ts"],"sourcesContent":["import { posix } from \"node:path\";\n\nimport type { ShelfApp } from \"../index.tsx\";\n\nimport { isPublicBuild, BuildModel } from \"../models/build.ts\";\nimport { LabelModel } from \"../models/label.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport { renderStorybookPage } from \"../pages/storybook.tsx\";\nimport { getStore } from \"../store.ts\";\nimport type { Build, Project } from \"../schema.ts\";\nimport { storybookDir } from \"../utils/paths.ts\";\nimport { currentProjectRole, notFound } from \"./helpers.ts\";\n\nconst VIEW_ROLES: ReadonlySet<string> = new Set([\"viewer\", \"developer\", \"approver\", \"admin\"]);\n\nconst MIME: Record<string, string> = {\n \".html\": \"text/html; charset=utf-8\",\n \".htm\": \"text/html; charset=utf-8\",\n \".js\": \"application/javascript\",\n \".mjs\": \"application/javascript\",\n \".json\": \"application/json\",\n \".css\": \"text/css; charset=utf-8\",\n \".png\": \"image/png\",\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".gif\": \"image/gif\",\n \".svg\": \"image/svg+xml\",\n \".webp\": \"image/webp\",\n \".ico\": \"image/x-icon\",\n \".woff\": \"font/woff\",\n \".woff2\": \"font/woff2\",\n \".ttf\": \"font/ttf\",\n \".map\": \"application/json\",\n \".wasm\": \"application/wasm\",\n};\n\nfunction contentTypeFor(path: string): string {\n const ext = posix.extname(path).toLowerCase();\n return MIME[ext] ?? \"application/octet-stream\";\n}\n\nfunction isSafeSegment(segment: string): boolean {\n return segment !== \"..\" && !segment.startsWith(\"/\") && !segment.includes(\"\\\\\") && !segment.includes(\"..\");\n}\n\n/**\n * Decide whether the current request may view `build`'s published Storybook.\n *\n * Public builds (ADR 0011) are viewable without auth; every other build requires\n * a logged-in session with at least `viewer` membership on the project.\n */\nasync function canViewBuild(build: Pick<Build, \"public\" | \"gitBranch\">, project: Project): Promise<boolean> {\n if (isPublicBuild(project, build)) {\n return true;\n }\n const { authEnabled } = getStore();\n if (!authEnabled) {\n return true;\n }\n const role = await currentProjectRole(project.id);\n return Boolean(role && VIEW_ROLES.has(role));\n}\n\n/** Register the published Storybook resolver and static-asset routes. */\nexport function registerStorybook(app: ShelfApp): void {\n // Resolver: latest published build on the default branch.\n app.get(\"/projects/:slug/storybook\", async (c) => {\n const slug = c.req.param(\"slug\");\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n notFound(\"Project not found\");\n }\n const build = await new BuildModel(getStore().db).latestPublished(project);\n if (!build) {\n notFound(\"No published Storybook for this project\");\n }\n if (!(await canViewBuild(build, project))) {\n return c.redirect(\"/auth/login\", 302);\n }\n return c.redirect(`/projects/${slug}/storybook/build/${build.id}/`, 302);\n });\n\n // Resolver: latest build bearing a label (`build` is a reserved label key).\n app.get(\"/projects/:slug/storybook/:key/:value\", async (c) => {\n const slug = c.req.param(\"slug\");\n const key = c.req.param(\"key\");\n if (key === \"build\") {\n return c.notFound();\n }\n const value = c.req.param(\"value\");\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n notFound(\"Project not found\");\n }\n const buildId = await new LabelModel(getStore().db).latestBuildId(project.id, key, value);\n if (!buildId) {\n notFound(\"No build carries that label\");\n }\n const build = await new BuildModel(getStore().db).get(buildId);\n if (!build || !(await canViewBuild(build, project))) {\n return c.redirect(\"/auth/login\", 302);\n }\n return c.redirect(`/projects/${slug}/storybook/build/${buildId}/`, 302);\n });\n\n // Canonical build route: serves the static Storybook and the landing page.\n app.get(\"/projects/:slug/storybook/build/:buildId/*\", async (c) => {\n const slug = c.req.param(\"slug\");\n const buildId = c.req.param(\"buildId\");\n // Hono's bare `/*` wildcard is not exposed through `param()`, so derive the\n // remainder from the raw request path (un-encoded) and decode it ourselves.\n const base = `/projects/${slug}/storybook/build/${buildId}/`;\n if (!c.req.path.startsWith(base)) {\n return c.notFound();\n }\n let rest: string;\n try {\n rest = decodeURIComponent(c.req.path.slice(base.length));\n } catch {\n return c.notFound();\n }\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n notFound(\"Project not found\");\n }\n const build = await new BuildModel(getStore().db).get(buildId);\n if (!build || build.projectId !== project.id) {\n notFound(\"Build not found\");\n }\n if (!(await canViewBuild(build, project))) {\n return c.redirect(\"/auth/login\", 302);\n }\n\n if (rest === \"\") {\n return c.html(renderStorybookPage(project, build, slug), 200);\n }\n\n const segments = rest.split(\"/\");\n if (segments.some((segment) => !isSafeSegment(segment))) {\n return c.notFound();\n }\n const path = posix.join(storybookDir(project.id, build.id), rest);\n if (!(await getStore().storage.exists(path))) {\n return c.notFound();\n }\n const buffer = await getStore().storage.read(path);\n return c.body(new Uint8Array(buffer), 200, {\n \"content-type\": contentTypeFor(rest),\n \"cache-control\": \"public, max-age=3600\",\n });\n });\n}\n"],"mappings":"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"}
|
package/dist/routers/tokens.mjs
CHANGED
|
@@ -49,6 +49,7 @@ const deleteTokenRoute = createRoute({
|
|
|
49
49
|
...notFound$1
|
|
50
50
|
}
|
|
51
51
|
});
|
|
52
|
+
/** Register the project API token list, create, and revoke endpoints. */
|
|
52
53
|
function registerTokens(app) {
|
|
53
54
|
app.openapi(listTokensRoute, async (c) => {
|
|
54
55
|
const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...ADMIN_ROLES);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tokens.mjs","names":["notFoundResponse"],"sources":["../../src/routers/tokens.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport type { ShelfApp } from \"../index.tsx\";\n\nimport { TokenModel } from \"../models/token.ts\";\nimport { getStore } from \"../store.ts\";\nimport type { ProjectRole } from \"../types.ts\";\nimport { randomToken } from \"../utils/hash.ts\";\nimport { resolveAuthorizedProject, notFound } from \"./helpers.ts\";\nimport {\n notFound as notFoundResponse,\n tokenCreatedSchema,\n tokenCreateSchema,\n tokenPublicSchema,\n unauthorized,\n} from \"./schemas.ts\";\n\nconst ADMIN_ROLES: readonly ProjectRole[] = [\"admin\"];\n\nconst listTokensRoute = createRoute({\n method: \"get\",\n path: \"/api/v1/projects/{slug}/tokens\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: { content: { \"application/json\": { schema: tokenPublicSchema.array() } }, description: \"List API tokens\" },\n ...notFoundResponse,\n ...unauthorized,\n },\n});\n\nconst createTokenRoute = createRoute({\n method: \"post\",\n path: \"/api/v1/projects/{slug}/tokens\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: tokenCreateSchema } } },\n },\n responses: {\n 201: { content: { \"application/json\": { schema: tokenCreatedSchema } }, description: \"Created token\" },\n ...notFoundResponse,\n },\n});\n\nconst deleteTokenRoute = createRoute({\n method: \"delete\",\n path: \"/api/v1/projects/{slug}/tokens/{tokenId}\",\n request: { params: z.object({ slug: z.string(), tokenId: z.string() }) },\n responses: {\n 204: { description: \"Deleted token\" },\n ...notFoundResponse,\n },\n});\n\nexport function registerTokens(app: ShelfApp): void {\n app.openapi(listTokensRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const tokens = await new TokenModel(getStore().db).list(project.id);\n return c.json(tokens.map(({ hash: _hash, ...rest }) => rest));\n });\n\n app.openapi(createTokenRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n const token = randomToken(\"shelf_\");\n await new TokenModel(getStore().db).create(project.id, body.name, token.hash);\n return c.json({ ...body, token: token.value }, 201);\n });\n\n app.openapi(deleteTokenRoute, async (c) => {\n const { slug, tokenId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n const found = await new TokenModel(getStore().db).get(project.id, tokenId);\n if (!found) {\n notFound(\"Token not found\");\n }\n await new TokenModel(getStore().db).remove(found.id);\n return c.body(null, 204);\n });\n}"],"mappings":";;;;;;;;;;AAgBA,MAAM,cAAsC,CAAC,OAAO;AAEpD,MAAM,kBAAkB,YAAY;CAClC,QAAQ;CACR,MAAM;CACN,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE;CAClD,WAAW;EACT,KAAK;GAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,kBAAkB,MAAM,EAAE,EAAE;GAAG,aAAa;EAAkB;EAC9G,GAAGA;EACH,GAAG;CACL;AACF,CAAC;AAED,MAAM,mBAAmB,YAAY;CACnC,QAAQ;CACR,MAAM;CACN,SAAS;EACP,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;EACrC,MAAM,EAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,kBAAkB,EAAE,EAAE;CACzE;CACA,WAAW;EACT,KAAK;GAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,mBAAmB,EAAE;GAAG,aAAa;EAAgB;EACrG,GAAGA;CACL;AACF,CAAC;AAED,MAAM,mBAAmB,YAAY;CACnC,QAAQ;CACR,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
|
|
1
|
+
{"version":3,"file":"tokens.mjs","names":["notFoundResponse"],"sources":["../../src/routers/tokens.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport type { ShelfApp } from \"../index.tsx\";\n\nimport { TokenModel } from \"../models/token.ts\";\nimport { getStore } from \"../store.ts\";\nimport type { ProjectRole } from \"../types.ts\";\nimport { randomToken } from \"../utils/hash.ts\";\nimport { resolveAuthorizedProject, notFound } from \"./helpers.ts\";\nimport {\n notFound as notFoundResponse,\n tokenCreatedSchema,\n tokenCreateSchema,\n tokenPublicSchema,\n unauthorized,\n} from \"./schemas.ts\";\n\nconst ADMIN_ROLES: readonly ProjectRole[] = [\"admin\"];\n\nconst listTokensRoute = createRoute({\n method: \"get\",\n path: \"/api/v1/projects/{slug}/tokens\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: { content: { \"application/json\": { schema: tokenPublicSchema.array() } }, description: \"List API tokens\" },\n ...notFoundResponse,\n ...unauthorized,\n },\n});\n\nconst createTokenRoute = createRoute({\n method: \"post\",\n path: \"/api/v1/projects/{slug}/tokens\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: tokenCreateSchema } } },\n },\n responses: {\n 201: { content: { \"application/json\": { schema: tokenCreatedSchema } }, description: \"Created token\" },\n ...notFoundResponse,\n },\n});\n\nconst deleteTokenRoute = createRoute({\n method: \"delete\",\n path: \"/api/v1/projects/{slug}/tokens/{tokenId}\",\n request: { params: z.object({ slug: z.string(), tokenId: z.string() }) },\n responses: {\n 204: { description: \"Deleted token\" },\n ...notFoundResponse,\n },\n});\n\n/** Register the project API token list, create, and revoke endpoints. */\nexport function registerTokens(app: ShelfApp): void {\n app.openapi(listTokensRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const tokens = await new TokenModel(getStore().db).list(project.id);\n return c.json(tokens.map(({ hash: _hash, ...rest }) => rest));\n });\n\n app.openapi(createTokenRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n const token = randomToken(\"shelf_\");\n await new TokenModel(getStore().db).create(project.id, body.name, token.hash);\n return c.json({ ...body, token: token.value }, 201);\n });\n\n app.openapi(deleteTokenRoute, async (c) => {\n const { slug, tokenId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n const found = await new TokenModel(getStore().db).get(project.id, tokenId);\n if (!found) {\n notFound(\"Token not found\");\n }\n await new TokenModel(getStore().db).remove(found.id);\n return c.body(null, 204);\n });\n}"],"mappings":";;;;;;;;;;AAgBA,MAAM,cAAsC,CAAC,OAAO;AAEpD,MAAM,kBAAkB,YAAY;CAClC,QAAQ;CACR,MAAM;CACN,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE;CAClD,WAAW;EACT,KAAK;GAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,kBAAkB,MAAM,EAAE,EAAE;GAAG,aAAa;EAAkB;EAC9G,GAAGA;EACH,GAAG;CACL;AACF,CAAC;AAED,MAAM,mBAAmB,YAAY;CACnC,QAAQ;CACR,MAAM;CACN,SAAS;EACP,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;EACrC,MAAM,EAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,kBAAkB,EAAE,EAAE;CACzE;CACA,WAAW;EACT,KAAK;GAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,mBAAmB,EAAE;GAAG,aAAa;EAAgB;EACrG,GAAGA;CACL;AACF,CAAC;AAED,MAAM,mBAAmB,YAAY;CACnC,QAAQ;CACR,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;;AAGD,SAAgB,eAAe,KAAqB;CAClD,IAAI,QAAQ,iBAAiB,OAAO,MAAM;EACxC,MAAM,UAAU,MAAM,yBAAyB,GAAG,EAAE,IAAI,MAAM,OAAO,CAAC,CAAC,MAAM,GAAG,WAAW;EAC3F,MAAM,SAAS,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,QAAQ,EAAE;EAClE,OAAO,EAAE,KAAK,OAAO,KAAK,EAAE,MAAM,OAAO,GAAG,WAAW,IAAI,CAAC;CAC9D,CAAC;CAED,IAAI,QAAQ,kBAAkB,OAAO,MAAM;EACzC,MAAM,UAAU,MAAM,yBAAyB,GAAG,EAAE,IAAI,MAAM,OAAO,CAAC,CAAC,MAAM,GAAG,WAAW;EAC3F,MAAM,OAAO,EAAE,IAAI,MAAM,MAAM;EAC/B,MAAM,QAAQ,YAAY,QAAQ;EAClC,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,QAAQ,IAAI,KAAK,MAAM,MAAM,IAAI;EAC5E,OAAO,EAAE,KAAK;GAAE,GAAG;GAAM,OAAO,MAAM;EAAM,GAAG,GAAG;CACpD,CAAC;CAED,IAAI,QAAQ,kBAAkB,OAAO,MAAM;EACzC,MAAM,EAAE,MAAM,YAAY,EAAE,IAAI,MAAM,OAAO;EAC7C,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,WAAW;EACtE,MAAM,QAAQ,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,QAAQ,IAAI,OAAO;EACzE,IAAI,CAAC,OACH,SAAS,iBAAiB;EAE5B,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,MAAM,EAAE;EACnD,OAAO,EAAE,KAAK,MAAM,GAAG;CACzB,CAAC;AACH"}
|
package/dist/routers/ui.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { CommentModel } from "../models/comment.mjs";
|
|
|
13
13
|
import { renderBuildDetailPage } from "../pages/build-detail.mjs";
|
|
14
14
|
import { renderBuildDiffPage } from "../pages/build-diff.mjs";
|
|
15
15
|
import { renderActiveQueue, renderComputeJobsPage } from "../pages/compute-jobs.mjs";
|
|
16
|
+
import { renderLabelDetailPage, renderLabelsPage } from "../pages/label-detail.mjs";
|
|
16
17
|
import { renderProjectCreatePage } from "../pages/project-create.mjs";
|
|
17
18
|
import { renderProjectBuildsPage } from "../pages/project-builds.mjs";
|
|
18
19
|
import { renderProjectsPage } from "../pages/projects.mjs";
|
|
@@ -22,6 +23,13 @@ import { registerSettingsPages } from "./settings.mjs";
|
|
|
22
23
|
function asString(value) {
|
|
23
24
|
return typeof value === "string" ? value.trim() : void 0;
|
|
24
25
|
}
|
|
26
|
+
function decodeRest(value) {
|
|
27
|
+
try {
|
|
28
|
+
return decodeURIComponent(value);
|
|
29
|
+
} catch {
|
|
30
|
+
return "";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
25
33
|
async function getQueueView() {
|
|
26
34
|
const queue = getStore().captureQueue;
|
|
27
35
|
if (!queue) return [];
|
|
@@ -45,10 +53,11 @@ async function canManageJobs(projectId) {
|
|
|
45
53
|
const role = await currentProjectRole(projectId);
|
|
46
54
|
return Boolean(role && roles.includes(role));
|
|
47
55
|
}
|
|
56
|
+
/** Register the server-rendered HTML pages (projects, builds, diffs, labels, settings). */
|
|
48
57
|
function registerUiPages(app) {
|
|
49
|
-
app.get("/", (c) => c.html(renderRootPage()));
|
|
58
|
+
app.get("/", async (c) => c.html(await renderRootPage()));
|
|
50
59
|
app.get("/projects", async (c) => c.html(await renderProjectsPage()));
|
|
51
|
-
app.get("/projects/new", (c) => c.html(renderProjectCreatePage()));
|
|
60
|
+
app.get("/projects/new", async (c) => c.html(await renderProjectCreatePage()));
|
|
52
61
|
app.post("/projects/new", async (c) => {
|
|
53
62
|
const form = await c.req.formData();
|
|
54
63
|
const name = asString(form.get("name"));
|
|
@@ -134,6 +143,20 @@ function registerUiPages(app) {
|
|
|
134
143
|
hasBaseline
|
|
135
144
|
}));
|
|
136
145
|
});
|
|
146
|
+
app.get("/projects/:slug/labels", async (c) => {
|
|
147
|
+
const html = await renderLabelsPage(c.req.param("slug"));
|
|
148
|
+
if (!html) return c.notFound();
|
|
149
|
+
return c.html(html);
|
|
150
|
+
});
|
|
151
|
+
app.get("/projects/:slug/labels/:key/*", async (c) => {
|
|
152
|
+
const slug = c.req.param("slug");
|
|
153
|
+
const key = c.req.param("key");
|
|
154
|
+
const base = `/projects/${slug}/labels/${key}/`;
|
|
155
|
+
const value = c.req.path.startsWith(base) ? decodeRest(c.req.path.slice(base.length)) : "";
|
|
156
|
+
const html = await renderLabelDetailPage(slug, key, value);
|
|
157
|
+
if (!html) return c.notFound();
|
|
158
|
+
return c.html(html);
|
|
159
|
+
});
|
|
137
160
|
registerSettingsPages(app);
|
|
138
161
|
}
|
|
139
162
|
//#endregion
|
package/dist/routers/ui.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui.mjs","names":[],"sources":["../../src/routers/ui.ts"],"sourcesContent":["import type { ShelfApp } from \"../index.tsx\";\n\nimport { BaselineModel } from \"../models/baseline.ts\";\nimport { BuildModel } from \"../models/build.ts\";\nimport { CommentModel } from \"../models/comment.ts\";\nimport { LabelModel } from \"../models/label.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport { SnapshotModel } from \"../models/snapshot.ts\";\nimport { renderBuildDetailPage } from \"../pages/build-detail.tsx\";\nimport { renderBuildDiffPage } from \"../pages/build-diff.tsx\";\nimport { renderComputeJobsPage, renderActiveQueue } from \"../pages/compute-jobs.tsx\";\nimport { renderProjectCreatePage } from \"../pages/project-create.tsx\";\nimport { renderProjectBuildsPage } from \"../pages/project-builds.tsx\";\nimport { renderProjectsPage } from \"../pages/projects.tsx\";\nimport { renderRootPage } from \"../pages/root.tsx\";\nimport { getStore } from \"../store.ts\";\nimport type { ProjectRole } from \"../types.ts\";\nimport { currentProjectRole } from \"./helpers.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\nimport { registerSettingsPages } from \"./settings.ts\";\n\nfunction asString(value: FormDataEntryValue | null): string | undefined {\n return typeof value === \"string\" ? value.trim() : undefined;\n}\n\ninterface QueueView {\n buildId: string;\n status: string;\n queuedAt: string;\n startedAt?: string;\n finishedAt?: string;\n error?: string;\n}\n\nasync function getQueueView(): Promise<QueueView[]> {\n const queue = getStore().captureQueue;\n if (!queue) {\n return [];\n }\n const active = await queue.active();\n return active.map((entry) => ({\n buildId: entry.buildId,\n status: entry.status,\n queuedAt: entry.queuedAt,\n startedAt: entry.startedAt,\n finishedAt: entry.finishedAt,\n error: entry.error,\n }));\n}\n\nasync function canManageJobs(projectId: string): Promise<boolean> {\n const { authEnabled } = getStore();\n if (!authEnabled) {\n return true;\n }\n const roles: readonly ProjectRole[] = [\"developer\", \"approver\", \"admin\"];\n const role = await currentProjectRole(projectId);\n return Boolean(role && roles.includes(role));\n}\n\nexport function registerUiPages(app: ShelfApp): void {\n // eslint-disable-next-line promise-function-async -- renderRootPage returns RenderedContent (string | Promise<string>)\n app.get(\"/\", (c) => c.html(renderRootPage()));\n app.get(\"/projects\", async (c) => c.html(await renderProjectsPage()));\n // eslint-disable-next-line promise-function-async -- renderProjectCreatePage returns RenderedContent (string | Promise<string>)\n app.get(\"/projects/new\", (c) => c.html(renderProjectCreatePage()));\n\n app.post(\"/projects/new\", async (c) => {\n const form = await c.req.formData();\n const name = asString(form.get(\"name\"));\n const gitRepository = asString(form.get(\"gitRepository\"));\n const gitDefaultBranch = asString(form.get(\"gitDefaultBranch\"));\n if (!name) {\n return c.html(renderProjectCreatePage({ values: { name, gitRepository, gitDefaultBranch }, errors: { name: \"Name is required\" } }), 400);\n }\n try {\n const project = await new ProjectModel(getStore().db).create({ name, gitRepository, gitDefaultBranch });\n await new LabelModel(getStore().db).seedFor(project.id);\n return hxRedirect(c, `/projects/${project.slug}/builds`);\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Failed to create project\";\n return c.html(renderProjectCreatePage({ values: { name, gitRepository, gitDefaultBranch }, globalError: message }), 400);\n }\n });\n\n app.get(\"/projects/:slug/builds\", async (c) => {\n const html = await renderProjectBuildsPage(c.req.param(\"slug\"), {\n status: c.req.query(\"status\"),\n branch: c.req.query(\"branch\"),\n });\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n app.get(\"/projects/:slug/builds/:buildId\", async (c) => {\n const html = await renderBuildDetailPage(c.req.param(\"buildId\"));\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n app.get(\"/projects/:slug/jobs\", async (c) => {\n const slug = c.req.param(\"slug\");\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n return c.notFound();\n }\n const queueView = await getQueueView();\n const canRetry = await canManageJobs(project.id);\n if (c.req.query(\"partial\") === \"queue\") {\n return c.html(renderActiveQueue(slug, queueView));\n }\n const html = await renderComputeJobsPage(slug, queueView, canRetry);\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n app.get(\"/projects/:slug/builds/:buildId/diff\", async (c) => {\n const slug = c.req.param(\"slug\");\n const buildId = c.req.param(\"buildId\");\n const snapshotId = c.req.query(\"snapshot\");\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n return c.notFound();\n }\n const build = await new BuildModel(getStore().db).get(buildId);\n if (!build || build.projectId !== project.id) {\n return c.notFound();\n }\n const snapshots = await new SnapshotModel(getStore().db).listByBuild(build.id);\n const comments = await new CommentModel(getStore().db).listByBuild(build.id);\n const baselines = new BaselineModel(getStore().db, getStore().storage);\n const hasBaselineEntries = await Promise.all(\n snapshots.map(async (snapshot) => {\n const baseline = await baselines.resolve(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, project.gitDefaultBranch);\n return [snapshot.id, Boolean(baseline)] as const;\n }),\n );\n const hasBaseline = Object.fromEntries(hasBaselineEntries);\n const { user, authEnabled } = getStore();\n const canReview = !authEnabled || Boolean(user);\n return c.html(\n renderBuildDiffPage({\n project,\n build,\n snapshots,\n comments,\n selectedId: snapshotId,\n canReview,\n hasBaseline,\n }),\n );\n });\n\n registerSettingsPages(app);\n}"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAqBA,SAAS,SAAS,OAAsD;CACtE,OAAO,OAAO,UAAU,WAAW,MAAM,KAAK,IAAI,KAAA;AACpD;AAWA,eAAe,eAAqC;CAClD,MAAM,QAAQ,SAAS,CAAC,CAAC;CACzB,IAAI,CAAC,OACH,OAAO,CAAC;CAGV,QAAO,MADc,MAAM,OAAO,EAAA,CACpB,KAAK,WAAW;EAC5B,SAAS,MAAM;EACf,QAAQ,MAAM;EACd,UAAU,MAAM;EAChB,WAAW,MAAM;EACjB,YAAY,MAAM;EAClB,OAAO,MAAM;CACf,EAAE;AACJ;AAEA,eAAe,cAAc,WAAqC;CAChE,MAAM,EAAE,gBAAgB,SAAS;CACjC,IAAI,CAAC,aACH,OAAO;CAET,MAAM,QAAgC;EAAC;EAAa;EAAY;CAAO;CACvE,MAAM,OAAO,MAAM,mBAAmB,SAAS;CAC/C,OAAO,QAAQ,QAAQ,MAAM,SAAS,IAAI,CAAC;AAC7C;AAEA,SAAgB,gBAAgB,KAAqB;CAEnD,IAAI,IAAI,MAAM,MAAM,EAAE,KAAK,eAAe,CAAC,CAAC;CAC5C,IAAI,IAAI,aAAa,OAAO,MAAM,EAAE,KAAK,MAAM,mBAAmB,CAAC,CAAC;CAEpE,IAAI,IAAI,kBAAkB,MAAM,EAAE,KAAK,wBAAwB,CAAC,CAAC;CAEjE,IAAI,KAAK,iBAAiB,OAAO,MAAM;EACrC,MAAM,OAAO,MAAM,EAAE,IAAI,SAAS;EAClC,MAAM,OAAO,SAAS,KAAK,IAAI,MAAM,CAAC;EACtC,MAAM,gBAAgB,SAAS,KAAK,IAAI,eAAe,CAAC;EACxD,MAAM,mBAAmB,SAAS,KAAK,IAAI,kBAAkB,CAAC;EAC9D,IAAI,CAAC,MACH,OAAO,EAAE,KAAK,wBAAwB;GAAE,QAAQ;IAAE;IAAM;IAAe;GAAiB;GAAG,QAAQ,EAAE,MAAM,mBAAmB;EAAE,CAAC,GAAG,GAAG;EAEzI,IAAI;GACF,MAAM,UAAU,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO;IAAE;IAAM;IAAe;GAAiB,CAAC;GACtG,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,QAAQ,QAAQ,EAAE;GACtD,OAAO,WAAW,GAAG,aAAa,QAAQ,KAAK,QAAQ;EACzD,SAAS,OAAO;GACd,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;GACzD,OAAO,EAAE,KAAK,wBAAwB;IAAE,QAAQ;KAAE;KAAM;KAAe;IAAiB;IAAG,aAAa;GAAQ,CAAC,GAAG,GAAG;EACzH;CACF,CAAC;CAED,IAAI,IAAI,0BAA0B,OAAO,MAAM;EAC7C,MAAM,OAAO,MAAM,wBAAwB,EAAE,IAAI,MAAM,MAAM,GAAG;GAC9D,QAAQ,EAAE,IAAI,MAAM,QAAQ;GAC5B,QAAQ,EAAE,IAAI,MAAM,QAAQ;EAC9B,CAAC;EACD,IAAI,CAAC,MACH,OAAO,EAAE,SAAS;EAEpB,OAAO,EAAE,KAAK,IAAI;CACpB,CAAC;CAED,IAAI,IAAI,mCAAmC,OAAO,MAAM;EACtD,MAAM,OAAO,MAAM,sBAAsB,EAAE,IAAI,MAAM,SAAS,CAAC;EAC/D,IAAI,CAAC,MACH,OAAO,EAAE,SAAS;EAEpB,OAAO,EAAE,KAAK,IAAI;CACpB,CAAC;CAED,IAAI,IAAI,wBAAwB,OAAO,MAAM;EAC3C,MAAM,OAAO,EAAE,IAAI,MAAM,MAAM;EAC/B,MAAM,UAAU,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,UAAU,IAAI;EACpE,IAAI,CAAC,SACH,OAAO,EAAE,SAAS;EAEpB,MAAM,YAAY,MAAM,aAAa;EACrC,MAAM,WAAW,MAAM,cAAc,QAAQ,EAAE;EAC/C,IAAI,EAAE,IAAI,MAAM,SAAS,MAAM,SAC7B,OAAO,EAAE,KAAK,kBAAkB,MAAM,SAAS,CAAC;EAElD,MAAM,OAAO,MAAM,sBAAsB,MAAM,WAAW,QAAQ;EAClE,IAAI,CAAC,MACH,OAAO,EAAE,SAAS;EAEpB,OAAO,EAAE,KAAK,IAAI;CACpB,CAAC;CAED,IAAI,IAAI,wCAAwC,OAAO,MAAM;EAC3D,MAAM,OAAO,EAAE,IAAI,MAAM,MAAM;EAC/B,MAAM,UAAU,EAAE,IAAI,MAAM,SAAS;EACrC,MAAM,aAAa,EAAE,IAAI,MAAM,UAAU;EACzC,MAAM,UAAU,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,UAAU,IAAI;EACpE,IAAI,CAAC,SACH,OAAO,EAAE,SAAS;EAEpB,MAAM,QAAQ,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,OAAO;EAC7D,IAAI,CAAC,SAAS,MAAM,cAAc,QAAQ,IACxC,OAAO,EAAE,SAAS;EAEpB,MAAM,YAAY,MAAM,IAAI,cAAc,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,YAAY,MAAM,EAAE;EAC7E,MAAM,WAAW,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,YAAY,MAAM,EAAE;EAC3E,MAAM,YAAY,IAAI,cAAc,SAAS,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,OAAO;EACrE,MAAM,qBAAqB,MAAM,QAAQ,IACvC,UAAU,IAAI,OAAO,aAAa;GAChC,MAAM,WAAW,MAAM,UAAU,QAAQ,QAAQ,IAAI,SAAS,SAAS,SAAS,cAAc,MAAM,WAAW,QAAQ,gBAAgB;GACvI,OAAO,CAAC,SAAS,IAAI,QAAQ,QAAQ,CAAC;EACxC,CAAC,CACH;EACA,MAAM,cAAc,OAAO,YAAY,kBAAkB;EACzD,MAAM,EAAE,MAAM,gBAAgB,SAAS;EACvC,MAAM,YAAY,CAAC,eAAe,QAAQ,IAAI;EAC9C,OAAO,EAAE,KACP,oBAAoB;GAClB;GACA;GACA;GACA;GACA,YAAY;GACZ;GACA;EACF,CAAC,CACH;CACF,CAAC;CAED,sBAAsB,GAAG;AAC3B"}
|
|
1
|
+
{"version":3,"file":"ui.mjs","names":[],"sources":["../../src/routers/ui.ts"],"sourcesContent":["import type { ShelfApp } from \"../index.tsx\";\n\nimport { BaselineModel } from \"../models/baseline.ts\";\nimport { BuildModel } from \"../models/build.ts\";\nimport { CommentModel } from \"../models/comment.ts\";\nimport { LabelModel } from \"../models/label.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport { SnapshotModel } from \"../models/snapshot.ts\";\nimport { renderBuildDetailPage } from \"../pages/build-detail.tsx\";\nimport { renderBuildDiffPage } from \"../pages/build-diff.tsx\";\nimport { renderComputeJobsPage, renderActiveQueue } from \"../pages/compute-jobs.tsx\";\nimport { renderLabelDetailPage, renderLabelsPage } from \"../pages/label-detail.tsx\";\nimport { renderProjectCreatePage } from \"../pages/project-create.tsx\";\nimport { renderProjectBuildsPage } from \"../pages/project-builds.tsx\";\nimport { renderProjectsPage } from \"../pages/projects.tsx\";\nimport { renderRootPage } from \"../pages/root.tsx\";\nimport { getStore } from \"../store.ts\";\nimport type { ProjectRole } from \"../types.ts\";\nimport { currentProjectRole } from \"./helpers.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\nimport { registerSettingsPages } from \"./settings.ts\";\n\nfunction asString(value: FormDataEntryValue | null): string | undefined {\n return typeof value === \"string\" ? value.trim() : undefined;\n}\n\nfunction decodeRest(value: string): string {\n try {\n return decodeURIComponent(value);\n } catch {\n return \"\";\n }\n}\n\ninterface QueueView {\n buildId: string;\n status: string;\n queuedAt: string;\n startedAt?: string;\n finishedAt?: string;\n error?: string;\n}\n\nasync function getQueueView(): Promise<QueueView[]> {\n const queue = getStore().captureQueue;\n if (!queue) {\n return [];\n }\n const active = await queue.active();\n return active.map((entry) => ({\n buildId: entry.buildId,\n status: entry.status,\n queuedAt: entry.queuedAt,\n startedAt: entry.startedAt,\n finishedAt: entry.finishedAt,\n error: entry.error,\n }));\n}\n\nasync function canManageJobs(projectId: string): Promise<boolean> {\n const { authEnabled } = getStore();\n if (!authEnabled) {\n return true;\n }\n const roles: readonly ProjectRole[] = [\"developer\", \"approver\", \"admin\"];\n const role = await currentProjectRole(projectId);\n return Boolean(role && roles.includes(role));\n}\n\n/** Register the server-rendered HTML pages (projects, builds, diffs, labels, settings). */\nexport function registerUiPages(app: ShelfApp): void {\n // eslint-disable-next-line promise-function-async -- renderRootPage returns RenderedContent (string | Promise<string>)\n app.get(\"/\", async (c) => c.html(await renderRootPage()));\n app.get(\"/projects\", async (c) => c.html(await renderProjectsPage()));\n // eslint-disable-next-line promise-function-async -- renderProjectCreatePage returns RenderedContent (string | Promise<string>)\n app.get(\"/projects/new\", async (c) => c.html(await renderProjectCreatePage()));\n\n app.post(\"/projects/new\", async (c) => {\n const form = await c.req.formData();\n const name = asString(form.get(\"name\"));\n const gitRepository = asString(form.get(\"gitRepository\"));\n const gitDefaultBranch = asString(form.get(\"gitDefaultBranch\"));\n if (!name) {\n return c.html(renderProjectCreatePage({ values: { name, gitRepository, gitDefaultBranch }, errors: { name: \"Name is required\" } }), 400);\n }\n try {\n const project = await new ProjectModel(getStore().db).create({ name, gitRepository, gitDefaultBranch });\n await new LabelModel(getStore().db).seedFor(project.id);\n return hxRedirect(c, `/projects/${project.slug}/builds`);\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Failed to create project\";\n return c.html(renderProjectCreatePage({ values: { name, gitRepository, gitDefaultBranch }, globalError: message }), 400);\n }\n });\n\n app.get(\"/projects/:slug/builds\", async (c) => {\n const html = await renderProjectBuildsPage(c.req.param(\"slug\"), {\n status: c.req.query(\"status\"),\n branch: c.req.query(\"branch\"),\n });\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n app.get(\"/projects/:slug/builds/:buildId\", async (c) => {\n const html = await renderBuildDetailPage(c.req.param(\"buildId\"));\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n app.get(\"/projects/:slug/jobs\", async (c) => {\n const slug = c.req.param(\"slug\");\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n return c.notFound();\n }\n const queueView = await getQueueView();\n const canRetry = await canManageJobs(project.id);\n if (c.req.query(\"partial\") === \"queue\") {\n return c.html(renderActiveQueue(slug, queueView));\n }\n const html = await renderComputeJobsPage(slug, queueView, canRetry);\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n app.get(\"/projects/:slug/builds/:buildId/diff\", async (c) => {\n const slug = c.req.param(\"slug\");\n const buildId = c.req.param(\"buildId\");\n const snapshotId = c.req.query(\"snapshot\");\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n return c.notFound();\n }\n const build = await new BuildModel(getStore().db).get(buildId);\n if (!build || build.projectId !== project.id) {\n return c.notFound();\n }\n const snapshots = await new SnapshotModel(getStore().db).listByBuild(build.id);\n const comments = await new CommentModel(getStore().db).listByBuild(build.id);\n const baselines = new BaselineModel(getStore().db, getStore().storage);\n const hasBaselineEntries = await Promise.all(\n snapshots.map(async (snapshot) => {\n const baseline = await baselines.resolve(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, project.gitDefaultBranch);\n return [snapshot.id, Boolean(baseline)] as const;\n }),\n );\n const hasBaseline = Object.fromEntries(hasBaselineEntries);\n const { user, authEnabled } = getStore();\n const canReview = !authEnabled || Boolean(user);\n return c.html(\n renderBuildDiffPage({\n project,\n build,\n snapshots,\n comments,\n selectedId: snapshotId,\n canReview,\n hasBaseline,\n }),\n );\n });\n\n app.get(\"/projects/:slug/labels\", async (c) => {\n const html = await renderLabelsPage(c.req.param(\"slug\"));\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n app.get(\"/projects/:slug/labels/:key/*\", async (c) => {\n const slug = c.req.param(\"slug\");\n const key = c.req.param(\"key\");\n // Hono's bare `/*` wildcard is not exposed through `param()`, so derive the\n // label value from the raw request path and decode it ourselves.\n const base = `/projects/${slug}/labels/${key}/`;\n const value = c.req.path.startsWith(base) ? decodeRest(c.req.path.slice(base.length)) : \"\";\n const html = await renderLabelDetailPage(slug, key, value);\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n registerSettingsPages(app);\n}"],"mappings":"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"}
|
|
@@ -49,6 +49,7 @@ const deleteWebhookRoute = createRoute({
|
|
|
49
49
|
...notFound$1
|
|
50
50
|
}
|
|
51
51
|
});
|
|
52
|
+
/** Register the project webhook list, create, and delete endpoints. */
|
|
52
53
|
function registerWebhooks(app) {
|
|
53
54
|
app.openapi(listWebhooksRoute, async (c) => {
|
|
54
55
|
const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...ADMIN_ROLES);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhooks.mjs","names":["notFoundResponse"],"sources":["../../src/routers/webhooks.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport type { ShelfApp } from \"../index.tsx\";\n\nimport { WebhookModel } from \"../models/webhook.ts\";\nimport { getStore } from \"../store.ts\";\nimport type { ProjectRole } from \"../types.ts\";\nimport { randomToken } from \"../utils/hash.ts\";\nimport { resolveAuthorizedProject, notFound } from \"./helpers.ts\";\nimport {\n notFound as notFoundResponse,\n unauthorized,\n webhookCreatedSchema,\n webhookCreateSchema,\n webhookPublicSchema,\n} from \"./schemas.ts\";\n\nconst ADMIN_ROLES: readonly ProjectRole[] = [\"admin\"];\n\nconst listWebhooksRoute = createRoute({\n method: \"get\",\n path: \"/api/v1/projects/{slug}/webhooks\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: { content: { \"application/json\": { schema: webhookPublicSchema.array() } }, description: \"List webhooks\" },\n ...notFoundResponse,\n ...unauthorized,\n },\n});\n\nconst createWebhookRoute = createRoute({\n method: \"post\",\n path: \"/api/v1/projects/{slug}/webhooks\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: webhookCreateSchema } } },\n },\n responses: {\n 201: { content: { \"application/json\": { schema: webhookCreatedSchema } }, description: \"Created webhook\" },\n ...notFoundResponse,\n },\n});\n\nconst deleteWebhookRoute = createRoute({\n method: \"delete\",\n path: \"/api/v1/projects/{slug}/webhooks/{webhookId}\",\n request: { params: z.object({ slug: z.string(), webhookId: z.string() }) },\n responses: {\n 204: { description: \"Deleted webhook\" },\n ...notFoundResponse,\n },\n});\n\nexport function registerWebhooks(app: ShelfApp): void {\n app.openapi(listWebhooksRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const webhooks = await new WebhookModel(getStore().db).list(project.id);\n return c.json(webhooks.map((webhook) => ({ id: webhook.id, url: webhook.url, events: WebhookModel.eventsOf(webhook) })));\n });\n\n app.openapi(createWebhookRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n const secret = randomToken(\"whsec_\").value;\n const webhook = await new WebhookModel(getStore().db).create(project.id, { ...body, secret });\n return c.json({ id: webhook.id, url: webhook.url, events: body.events ?? [], secret }, 201);\n });\n\n app.openapi(deleteWebhookRoute, async (c) => {\n const { slug, webhookId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n const webhook = await new WebhookModel(getStore().db).get(project.id, webhookId);\n if (!webhook) {\n notFound(\"Webhook not found\");\n }\n await new WebhookModel(getStore().db).remove(project.id, webhook.id);\n return c.body(null, 204);\n });\n}"],"mappings":";;;;;;;;;;AAgBA,MAAM,cAAsC,CAAC,OAAO;AAEpD,MAAM,oBAAoB,YAAY;CACpC,QAAQ;CACR,MAAM;CACN,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE;CAClD,WAAW;EACT,KAAK;GAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,oBAAoB,MAAM,EAAE,EAAE;GAAG,aAAa;EAAgB;EAC9G,GAAGA;EACH,GAAG;CACL;AACF,CAAC;AAED,MAAM,qBAAqB,YAAY;CACrC,QAAQ;CACR,MAAM;CACN,SAAS;EACP,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;EACrC,MAAM,EAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,oBAAoB,EAAE,EAAE;CAC3E;CACA,WAAW;EACT,KAAK;GAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,qBAAqB,EAAE;GAAG,aAAa;EAAkB;EACzG,GAAGA;CACL;AACF,CAAC;AAED,MAAM,qBAAqB,YAAY;CACrC,QAAQ;CACR,MAAM;CACN,SAAS,EAAE,QAAQ,EAAE,OAAO;EAAE,MAAM,EAAE,OAAO;EAAG,WAAW,EAAE,OAAO;CAAE,CAAC,EAAE;CACzE,WAAW;EACT,KAAK,EAAE,aAAa,kBAAkB;EACtC,GAAGA;CACL;AACF,CAAC
|
|
1
|
+
{"version":3,"file":"webhooks.mjs","names":["notFoundResponse"],"sources":["../../src/routers/webhooks.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport type { ShelfApp } from \"../index.tsx\";\n\nimport { WebhookModel } from \"../models/webhook.ts\";\nimport { getStore } from \"../store.ts\";\nimport type { ProjectRole } from \"../types.ts\";\nimport { randomToken } from \"../utils/hash.ts\";\nimport { resolveAuthorizedProject, notFound } from \"./helpers.ts\";\nimport {\n notFound as notFoundResponse,\n unauthorized,\n webhookCreatedSchema,\n webhookCreateSchema,\n webhookPublicSchema,\n} from \"./schemas.ts\";\n\nconst ADMIN_ROLES: readonly ProjectRole[] = [\"admin\"];\n\nconst listWebhooksRoute = createRoute({\n method: \"get\",\n path: \"/api/v1/projects/{slug}/webhooks\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: { content: { \"application/json\": { schema: webhookPublicSchema.array() } }, description: \"List webhooks\" },\n ...notFoundResponse,\n ...unauthorized,\n },\n});\n\nconst createWebhookRoute = createRoute({\n method: \"post\",\n path: \"/api/v1/projects/{slug}/webhooks\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: webhookCreateSchema } } },\n },\n responses: {\n 201: { content: { \"application/json\": { schema: webhookCreatedSchema } }, description: \"Created webhook\" },\n ...notFoundResponse,\n },\n});\n\nconst deleteWebhookRoute = createRoute({\n method: \"delete\",\n path: \"/api/v1/projects/{slug}/webhooks/{webhookId}\",\n request: { params: z.object({ slug: z.string(), webhookId: z.string() }) },\n responses: {\n 204: { description: \"Deleted webhook\" },\n ...notFoundResponse,\n },\n});\n\n/** Register the project webhook list, create, and delete endpoints. */\nexport function registerWebhooks(app: ShelfApp): void {\n app.openapi(listWebhooksRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const webhooks = await new WebhookModel(getStore().db).list(project.id);\n return c.json(webhooks.map((webhook) => ({ id: webhook.id, url: webhook.url, events: WebhookModel.eventsOf(webhook) })));\n });\n\n app.openapi(createWebhookRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n const secret = randomToken(\"whsec_\").value;\n const webhook = await new WebhookModel(getStore().db).create(project.id, { ...body, secret });\n return c.json({ id: webhook.id, url: webhook.url, events: body.events ?? [], secret }, 201);\n });\n\n app.openapi(deleteWebhookRoute, async (c) => {\n const { slug, webhookId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n const webhook = await new WebhookModel(getStore().db).get(project.id, webhookId);\n if (!webhook) {\n notFound(\"Webhook not found\");\n }\n await new WebhookModel(getStore().db).remove(project.id, webhook.id);\n return c.body(null, 204);\n });\n}"],"mappings":";;;;;;;;;;AAgBA,MAAM,cAAsC,CAAC,OAAO;AAEpD,MAAM,oBAAoB,YAAY;CACpC,QAAQ;CACR,MAAM;CACN,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE;CAClD,WAAW;EACT,KAAK;GAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,oBAAoB,MAAM,EAAE,EAAE;GAAG,aAAa;EAAgB;EAC9G,GAAGA;EACH,GAAG;CACL;AACF,CAAC;AAED,MAAM,qBAAqB,YAAY;CACrC,QAAQ;CACR,MAAM;CACN,SAAS;EACP,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;EACrC,MAAM,EAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,oBAAoB,EAAE,EAAE;CAC3E;CACA,WAAW;EACT,KAAK;GAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,qBAAqB,EAAE;GAAG,aAAa;EAAkB;EACzG,GAAGA;CACL;AACF,CAAC;AAED,MAAM,qBAAqB,YAAY;CACrC,QAAQ;CACR,MAAM;CACN,SAAS,EAAE,QAAQ,EAAE,OAAO;EAAE,MAAM,EAAE,OAAO;EAAG,WAAW,EAAE,OAAO;CAAE,CAAC,EAAE;CACzE,WAAW;EACT,KAAK,EAAE,aAAa,kBAAkB;EACtC,GAAGA;CACL;AACF,CAAC;;AAGD,SAAgB,iBAAiB,KAAqB;CACpD,IAAI,QAAQ,mBAAmB,OAAO,MAAM;EAC1C,MAAM,UAAU,MAAM,yBAAyB,GAAG,EAAE,IAAI,MAAM,OAAO,CAAC,CAAC,MAAM,GAAG,WAAW;EAC3F,MAAM,WAAW,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,QAAQ,EAAE;EACtE,OAAO,EAAE,KAAK,SAAS,KAAK,aAAa;GAAE,IAAI,QAAQ;GAAI,KAAK,QAAQ;GAAK,QAAQ,aAAa,SAAS,OAAO;EAAE,EAAE,CAAC;CACzH,CAAC;CAED,IAAI,QAAQ,oBAAoB,OAAO,MAAM;EAC3C,MAAM,UAAU,MAAM,yBAAyB,GAAG,EAAE,IAAI,MAAM,OAAO,CAAC,CAAC,MAAM,GAAG,WAAW;EAC3F,MAAM,OAAO,EAAE,IAAI,MAAM,MAAM;EAC/B,MAAM,SAAS,YAAY,QAAQ,CAAC,CAAC;EACrC,MAAM,UAAU,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,QAAQ,IAAI;GAAE,GAAG;GAAM;EAAO,CAAC;EAC5F,OAAO,EAAE,KAAK;GAAE,IAAI,QAAQ;GAAI,KAAK,QAAQ;GAAK,QAAQ,KAAK,UAAU,CAAC;GAAG;EAAO,GAAG,GAAG;CAC5F,CAAC;CAED,IAAI,QAAQ,oBAAoB,OAAO,MAAM;EAC3C,MAAM,EAAE,MAAM,cAAc,EAAE,IAAI,MAAM,OAAO;EAC/C,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,WAAW;EACtE,MAAM,UAAU,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,QAAQ,IAAI,SAAS;EAC/E,IAAI,CAAC,SACH,SAAS,mBAAmB;EAE9B,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,QAAQ,IAAI,QAAQ,EAAE;EACnE,OAAO,EAAE,KAAK,MAAM,GAAG;CACzB,CAAC;AACH"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { index, integer, real, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
|
|
5
|
+
//#region src/schema-tables.ts
|
|
6
|
+
/**
|
|
7
|
+
* Narrow Drizzle table definitions for StoryShelf.
|
|
8
|
+
*
|
|
9
|
+
* Kept in this internal module (not a package entrypoint) so models and
|
|
10
|
+
* query code keep exact column types. The `schema` entrypoint re-exports
|
|
11
|
+
* explicitly annotated aliases; row types are hand-written in `schema.ts`
|
|
12
|
+
* and pinned to these definitions by `schema-types.test.ts`.
|
|
13
|
+
*/
|
|
14
|
+
/** Narrow `projects` table definition. */
|
|
15
|
+
const projects = sqliteTable("projects", {
|
|
16
|
+
id: text("id").primaryKey(),
|
|
17
|
+
name: text("name").notNull(),
|
|
18
|
+
slug: text("slug").notNull(),
|
|
19
|
+
gitRepository: text("git_repository"),
|
|
20
|
+
gitDefaultBranch: text("git_default_branch").notNull().default("main"),
|
|
21
|
+
pixelThreshold: real("pixel_threshold").notNull().default(.1),
|
|
22
|
+
maxDiffRatio: real("max_diff_ratio").notNull().default(.01),
|
|
23
|
+
publicBranchRegex: text("public_branch_regex"),
|
|
24
|
+
storybookMeta: text("storybook_meta").$type().default(null),
|
|
25
|
+
executePlay: integer("execute_play", { mode: "boolean" }).notNull().default(false),
|
|
26
|
+
playTimeoutMs: integer("play_timeout_ms").notNull().default(1e4),
|
|
27
|
+
createdAt: text("created_at").notNull(),
|
|
28
|
+
updatedAt: text("updated_at").notNull()
|
|
29
|
+
});
|
|
30
|
+
/** Narrow `project_status_configs` table definition. */
|
|
31
|
+
const projectStatusConfigs = sqliteTable("project_status_configs", {
|
|
32
|
+
id: text("id").primaryKey(),
|
|
33
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
34
|
+
provider: text("provider").notNull(),
|
|
35
|
+
config: text("config").notNull(),
|
|
36
|
+
tokenEncrypted: text("token_encrypted").notNull(),
|
|
37
|
+
createdAt: text("created_at").notNull(),
|
|
38
|
+
updatedAt: text("updated_at").notNull()
|
|
39
|
+
});
|
|
40
|
+
/** Narrow `builds` table definition. */
|
|
41
|
+
const builds = sqliteTable("builds", {
|
|
42
|
+
id: text("id").primaryKey(),
|
|
43
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
44
|
+
gitSha: text("git_sha").notNull(),
|
|
45
|
+
gitBranch: text("git_branch").notNull(),
|
|
46
|
+
isDefault: integer("is_default", { mode: "boolean" }).notNull().default(false),
|
|
47
|
+
authorEmail: text("author_email"),
|
|
48
|
+
authorName: text("author_name"),
|
|
49
|
+
message: text("message"),
|
|
50
|
+
public: integer("public", { mode: "boolean" }).notNull().default(false),
|
|
51
|
+
status: text("status").$type().notNull().default("pending"),
|
|
52
|
+
snapshotCount: integer("snapshot_count").notNull().default(0),
|
|
53
|
+
changedCount: integer("changed_count").notNull().default(0),
|
|
54
|
+
approvedCount: integer("approved_count").notNull().default(0),
|
|
55
|
+
rejectedCount: integer("rejected_count").notNull().default(0),
|
|
56
|
+
createdAt: text("created_at").notNull(),
|
|
57
|
+
updatedAt: text("updated_at").notNull()
|
|
58
|
+
}, (t) => [uniqueIndex("builds_project_gitsha_idx").on(t.projectId, t.gitSha), index("builds_git_branch_idx").on(t.gitBranch)]);
|
|
59
|
+
/** Narrow `snapshots` table definition. */
|
|
60
|
+
const snapshots = sqliteTable("snapshots", {
|
|
61
|
+
id: text("id").primaryKey(),
|
|
62
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
63
|
+
buildId: text("build_id").notNull().references(() => builds.id, { onDelete: "cascade" }),
|
|
64
|
+
storyId: text("story_id").notNull(),
|
|
65
|
+
storyName: text("story_name").notNull(),
|
|
66
|
+
storyTitle: text("story_title").notNull(),
|
|
67
|
+
storyImportPath: text("story_import_path"),
|
|
68
|
+
viewportName: text("viewport_name").notNull().default("desktop"),
|
|
69
|
+
viewportWidth: integer("viewport_width").notNull().default(1280),
|
|
70
|
+
viewportHeight: integer("viewport_height").notNull().default(720),
|
|
71
|
+
screenshotPath: text("screenshot_path").notNull(),
|
|
72
|
+
diffPath: text("diff_path"),
|
|
73
|
+
diffPixels: integer("diff_pixels"),
|
|
74
|
+
diffRatio: real("diff_ratio"),
|
|
75
|
+
diffPassed: integer("diff_passed", { mode: "boolean" }),
|
|
76
|
+
status: text("status").$type().notNull().default("pending"),
|
|
77
|
+
reviewedBy: text("reviewed_by"),
|
|
78
|
+
reviewedAt: text("reviewed_at"),
|
|
79
|
+
createdAt: text("created_at").notNull(),
|
|
80
|
+
updatedAt: text("updated_at").notNull()
|
|
81
|
+
}, (t) => [uniqueIndex("snapshots_build_story_viewport_idx").on(t.buildId, t.storyId, t.viewportName), index("snapshots_build_id_idx").on(t.buildId)]);
|
|
82
|
+
/** Narrow `baselines` table definition. */
|
|
83
|
+
const baselines = sqliteTable("baselines", {
|
|
84
|
+
id: text("id").primaryKey(),
|
|
85
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
86
|
+
storyId: text("story_id").notNull(),
|
|
87
|
+
viewportName: text("viewport_name").notNull().default("desktop"),
|
|
88
|
+
branch: text("branch").notNull(),
|
|
89
|
+
snapshotId: text("snapshot_id"),
|
|
90
|
+
screenshotPath: text("screenshot_path").notNull(),
|
|
91
|
+
createdAt: text("created_at").notNull(),
|
|
92
|
+
updatedAt: text("updated_at").notNull()
|
|
93
|
+
}, (t) => [uniqueIndex("baselines_project_story_viewport_branch_idx").on(t.projectId, t.storyId, t.viewportName, t.branch), index("baselines_project_story_idx").on(t.projectId, t.storyId)]);
|
|
94
|
+
/** Narrow `comments` table definition. */
|
|
95
|
+
const comments = sqliteTable("comments", {
|
|
96
|
+
id: text("id").primaryKey(),
|
|
97
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
98
|
+
buildId: text("build_id").notNull().references(() => builds.id, { onDelete: "cascade" }),
|
|
99
|
+
snapshotId: text("snapshot_id").references(() => snapshots.id, { onDelete: "cascade" }),
|
|
100
|
+
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
101
|
+
body: text("body").notNull(),
|
|
102
|
+
parentId: text("parent_id"),
|
|
103
|
+
resolved: integer("resolved", { mode: "boolean" }).notNull().default(false),
|
|
104
|
+
createdAt: text("created_at").notNull(),
|
|
105
|
+
updatedAt: text("updated_at").notNull()
|
|
106
|
+
}, (t) => [index("comments_build_id_idx").on(t.buildId)]);
|
|
107
|
+
/** Narrow `label_types` table definition. */
|
|
108
|
+
const labelTypes = sqliteTable("label_types", {
|
|
109
|
+
id: text("id").primaryKey(),
|
|
110
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
111
|
+
key: text("key").notNull(),
|
|
112
|
+
name: text("name").notNull(),
|
|
113
|
+
linkTemplate: text("link_template"),
|
|
114
|
+
color: text("color"),
|
|
115
|
+
createdAt: text("created_at").notNull()
|
|
116
|
+
}, (t) => [uniqueIndex("label_types_project_key_idx").on(t.projectId, t.key)]);
|
|
117
|
+
/** Narrow `build_labels` table definition. */
|
|
118
|
+
const buildLabels = sqliteTable("build_labels", {
|
|
119
|
+
id: text("id").primaryKey(),
|
|
120
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
121
|
+
buildId: text("build_id").notNull().references(() => builds.id, { onDelete: "cascade" }),
|
|
122
|
+
typeKey: text("type_key").notNull(),
|
|
123
|
+
value: text("value").notNull(),
|
|
124
|
+
createdAt: text("created_at").notNull()
|
|
125
|
+
}, (t) => [uniqueIndex("build_labels_build_type_value_idx").on(t.buildId, t.typeKey, t.value)]);
|
|
126
|
+
/** Narrow `tokens` table definition. */
|
|
127
|
+
const tokens = sqliteTable("tokens", {
|
|
128
|
+
id: text("id").primaryKey(),
|
|
129
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
130
|
+
name: text("name").notNull(),
|
|
131
|
+
hash: text("hash").notNull(),
|
|
132
|
+
lastUsedAt: text("last_used_at"),
|
|
133
|
+
createdAt: text("created_at").notNull()
|
|
134
|
+
});
|
|
135
|
+
/** Narrow `webhooks` table definition. */
|
|
136
|
+
const webhooks = sqliteTable("webhooks", {
|
|
137
|
+
id: text("id").primaryKey(),
|
|
138
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
139
|
+
url: text("url").notNull(),
|
|
140
|
+
secret: text("secret").notNull(),
|
|
141
|
+
events: text("events"),
|
|
142
|
+
createdAt: text("created_at").notNull(),
|
|
143
|
+
updatedAt: text("updated_at").notNull()
|
|
144
|
+
});
|
|
145
|
+
/** Narrow `users` table definition. */
|
|
146
|
+
const users = sqliteTable("users", {
|
|
147
|
+
id: text("id").primaryKey(),
|
|
148
|
+
email: text("email").notNull(),
|
|
149
|
+
name: text("name").notNull(),
|
|
150
|
+
avatarUrl: text("avatar_url"),
|
|
151
|
+
role: text("role").$type().notNull().default("member"),
|
|
152
|
+
lastLoginAt: text("last_login_at"),
|
|
153
|
+
createdAt: text("created_at").notNull()
|
|
154
|
+
});
|
|
155
|
+
/** Narrow `project_members` table definition. */
|
|
156
|
+
const projectMembers = sqliteTable("project_members", {
|
|
157
|
+
id: text("id").primaryKey(),
|
|
158
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
159
|
+
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
160
|
+
role: text("role").$type().notNull().default("viewer"),
|
|
161
|
+
createdAt: text("created_at").notNull()
|
|
162
|
+
}, (t) => [uniqueIndex("project_members_project_user_idx").on(t.projectId, t.userId)]);
|
|
163
|
+
//#endregion
|
|
164
|
+
export { baselines, buildLabels, builds, comments, labelTypes, projectMembers, projectStatusConfigs, projects, snapshots, tokens, users, webhooks };
|
|
165
|
+
|
|
166
|
+
//# sourceMappingURL=schema-tables.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-tables.mjs","names":[],"sources":["../src/schema-tables.ts"],"sourcesContent":["import { index, integer, real, sqliteTable, text, uniqueIndex } from \"drizzle-orm/sqlite-core\";\n\nimport type { BuildStatus, ProjectRole, SiteRole, SnapshotStatus } from \"./types.ts\";\n\n/**\n * Narrow Drizzle table definitions for StoryShelf.\n *\n * Kept in this internal module (not a package entrypoint) so models and\n * query code keep exact column types. The `schema` entrypoint re-exports\n * explicitly annotated aliases; row types are hand-written in `schema.ts`\n * and pinned to these definitions by `schema-types.test.ts`.\n */\n\n/** Narrow `projects` table definition. */\nexport const projects = sqliteTable(\"projects\", {\n id: text(\"id\").primaryKey(),\n name: text(\"name\").notNull(),\n slug: text(\"slug\").notNull(),\n gitRepository: text(\"git_repository\"),\n gitDefaultBranch: text(\"git_default_branch\").notNull().default(\"main\"),\n pixelThreshold: real(\"pixel_threshold\").notNull().default(0.1),\n maxDiffRatio: real(\"max_diff_ratio\").notNull().default(0.01),\n publicBranchRegex: text(\"public_branch_regex\"),\n storybookMeta: text(\"storybook_meta\").$type<string | null | undefined>().default(null),\n executePlay: integer(\"execute_play\", { mode: \"boolean\" }).notNull().default(false),\n playTimeoutMs: integer(\"play_timeout_ms\").notNull().default(10_000),\n createdAt: text(\"created_at\").notNull(),\n updatedAt: text(\"updated_at\").notNull(),\n});\n\n/** Narrow `project_status_configs` table definition. */\nexport const projectStatusConfigs = sqliteTable(\"project_status_configs\", {\n id: text(\"id\").primaryKey(),\n projectId: text(\"project_id\")\n .notNull()\n .references(() => projects.id, { onDelete: \"cascade\" }),\n provider: text(\"provider\").notNull(),\n config: text(\"config\").notNull(),\n tokenEncrypted: text(\"token_encrypted\").notNull(),\n createdAt: text(\"created_at\").notNull(),\n updatedAt: text(\"updated_at\").notNull(),\n});\n\n/** Narrow `builds` table definition. */\nexport const builds = sqliteTable(\n \"builds\",\n {\n id: text(\"id\").primaryKey(),\n projectId: text(\"project_id\")\n .notNull()\n .references(() => projects.id, { onDelete: \"cascade\" }),\n gitSha: text(\"git_sha\").notNull(),\n gitBranch: text(\"git_branch\").notNull(),\n isDefault: integer(\"is_default\", { mode: \"boolean\" }).notNull().default(false),\n authorEmail: text(\"author_email\"),\n authorName: text(\"author_name\"),\n message: text(\"message\"),\n public: integer(\"public\", { mode: \"boolean\" }).notNull().default(false),\n status: text(\"status\").$type<BuildStatus>().notNull().default(\"pending\"),\n snapshotCount: integer(\"snapshot_count\").notNull().default(0),\n changedCount: integer(\"changed_count\").notNull().default(0),\n approvedCount: integer(\"approved_count\").notNull().default(0),\n rejectedCount: integer(\"rejected_count\").notNull().default(0),\n createdAt: text(\"created_at\").notNull(),\n updatedAt: text(\"updated_at\").notNull(),\n },\n (t) => [\n uniqueIndex(\"builds_project_gitsha_idx\").on(t.projectId, t.gitSha),\n index(\"builds_git_branch_idx\").on(t.gitBranch),\n ],\n);\n\n/** Narrow `snapshots` table definition. */\nexport const snapshots = sqliteTable(\n \"snapshots\",\n {\n id: text(\"id\").primaryKey(),\n projectId: text(\"project_id\")\n .notNull()\n .references(() => projects.id, { onDelete: \"cascade\" }),\n buildId: text(\"build_id\")\n .notNull()\n .references(() => builds.id, { onDelete: \"cascade\" }),\n storyId: text(\"story_id\").notNull(),\n storyName: text(\"story_name\").notNull(),\n storyTitle: text(\"story_title\").notNull(),\n storyImportPath: text(\"story_import_path\"),\n viewportName: text(\"viewport_name\").notNull().default(\"desktop\"),\n viewportWidth: integer(\"viewport_width\").notNull().default(1280),\n viewportHeight: integer(\"viewport_height\").notNull().default(720),\n screenshotPath: text(\"screenshot_path\").notNull(),\n diffPath: text(\"diff_path\"),\n diffPixels: integer(\"diff_pixels\"),\n diffRatio: real(\"diff_ratio\"),\n diffPassed: integer(\"diff_passed\", { mode: \"boolean\" }),\n status: text(\"status\").$type<SnapshotStatus>().notNull().default(\"pending\"),\n reviewedBy: text(\"reviewed_by\"),\n reviewedAt: text(\"reviewed_at\"),\n createdAt: text(\"created_at\").notNull(),\n updatedAt: text(\"updated_at\").notNull(),\n },\n (t) => [\n uniqueIndex(\"snapshots_build_story_viewport_idx\").on(t.buildId, t.storyId, t.viewportName),\n index(\"snapshots_build_id_idx\").on(t.buildId),\n ],\n);\n\n/** Narrow `baselines` table definition. */\nexport const baselines = sqliteTable(\n \"baselines\",\n {\n id: text(\"id\").primaryKey(),\n projectId: text(\"project_id\")\n .notNull()\n .references(() => projects.id, { onDelete: \"cascade\" }),\n storyId: text(\"story_id\").notNull(),\n viewportName: text(\"viewport_name\").notNull().default(\"desktop\"),\n branch: text(\"branch\").notNull(),\n snapshotId: text(\"snapshot_id\"),\n screenshotPath: text(\"screenshot_path\").notNull(),\n createdAt: text(\"created_at\").notNull(),\n updatedAt: text(\"updated_at\").notNull(),\n },\n (t) => [\n uniqueIndex(\"baselines_project_story_viewport_branch_idx\").on(t.projectId, t.storyId, t.viewportName, t.branch),\n index(\"baselines_project_story_idx\").on(t.projectId, t.storyId),\n ],\n);\n\n/** Narrow `comments` table definition. */\nexport const comments = sqliteTable(\n \"comments\",\n {\n id: text(\"id\").primaryKey(),\n projectId: text(\"project_id\")\n .notNull()\n .references(() => projects.id, { onDelete: \"cascade\" }),\n buildId: text(\"build_id\")\n .notNull()\n .references(() => builds.id, { onDelete: \"cascade\" }),\n snapshotId: text(\"snapshot_id\").references(() => snapshots.id, { onDelete: \"cascade\" }),\n userId: text(\"user_id\")\n .notNull()\n .references(() => users.id, { onDelete: \"cascade\" }),\n body: text(\"body\").notNull(),\n parentId: text(\"parent_id\"),\n resolved: integer(\"resolved\", { mode: \"boolean\" }).notNull().default(false),\n createdAt: text(\"created_at\").notNull(),\n updatedAt: text(\"updated_at\").notNull(),\n },\n (t) => [index(\"comments_build_id_idx\").on(t.buildId)],\n);\n\n/** Narrow `label_types` table definition. */\nexport const labelTypes = sqliteTable(\n \"label_types\",\n {\n id: text(\"id\").primaryKey(),\n projectId: text(\"project_id\")\n .notNull()\n .references(() => projects.id, { onDelete: \"cascade\" }),\n key: text(\"key\").notNull(),\n name: text(\"name\").notNull(),\n linkTemplate: text(\"link_template\"),\n color: text(\"color\"),\n createdAt: text(\"created_at\").notNull(),\n },\n (t) => [uniqueIndex(\"label_types_project_key_idx\").on(t.projectId, t.key)],\n);\n\n/** Narrow `build_labels` table definition. */\nexport const buildLabels = sqliteTable(\n \"build_labels\",\n {\n id: text(\"id\").primaryKey(),\n projectId: text(\"project_id\")\n .notNull()\n .references(() => projects.id, { onDelete: \"cascade\" }),\n buildId: text(\"build_id\")\n .notNull()\n .references(() => builds.id, { onDelete: \"cascade\" }),\n typeKey: text(\"type_key\").notNull(),\n value: text(\"value\").notNull(),\n createdAt: text(\"created_at\").notNull(),\n },\n (t) => [uniqueIndex(\"build_labels_build_type_value_idx\").on(t.buildId, t.typeKey, t.value)],\n);\n\n/** Narrow `tokens` table definition. */\nexport const tokens = sqliteTable(\"tokens\", {\n id: text(\"id\").primaryKey(),\n projectId: text(\"project_id\")\n .notNull()\n .references(() => projects.id, { onDelete: \"cascade\" }),\n name: text(\"name\").notNull(),\n hash: text(\"hash\").notNull(),\n lastUsedAt: text(\"last_used_at\"),\n createdAt: text(\"created_at\").notNull(),\n});\n\n/** Narrow `webhooks` table definition. */\nexport const webhooks = sqliteTable(\"webhooks\", {\n id: text(\"id\").primaryKey(),\n projectId: text(\"project_id\")\n .notNull()\n .references(() => projects.id, { onDelete: \"cascade\" }),\n url: text(\"url\").notNull(),\n secret: text(\"secret\").notNull(),\n events: text(\"events\"),\n createdAt: text(\"created_at\").notNull(),\n updatedAt: text(\"updated_at\").notNull(),\n});\n\n/** Narrow `users` table definition. */\nexport const users = sqliteTable(\"users\", {\n id: text(\"id\").primaryKey(),\n email: text(\"email\").notNull(),\n name: text(\"name\").notNull(),\n avatarUrl: text(\"avatar_url\"),\n role: text(\"role\").$type<SiteRole>().notNull().default(\"member\"),\n lastLoginAt: text(\"last_login_at\"),\n createdAt: text(\"created_at\").notNull(),\n});\n\n/** Narrow `project_members` table definition. */\nexport const projectMembers = sqliteTable(\n \"project_members\",\n {\n id: text(\"id\").primaryKey(),\n projectId: text(\"project_id\")\n .notNull()\n .references(() => projects.id, { onDelete: \"cascade\" }),\n userId: text(\"user_id\")\n .notNull()\n .references(() => users.id, { onDelete: \"cascade\" }),\n role: text(\"role\").$type<ProjectRole>().notNull().default(\"viewer\"),\n createdAt: text(\"created_at\").notNull(),\n },\n (t) => [uniqueIndex(\"project_members_project_user_idx\").on(t.projectId, t.userId)],\n);\n"],"mappings":"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"}
|