@storyshelf/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -0
- package/dist/adapter/auth.d.mts +50 -0
- package/dist/adapter/auth.mjs +10 -0
- package/dist/adapter/auth.mjs.map +1 -0
- package/dist/adapter/capture-runner.d.mts +70 -0
- package/dist/adapter/capture-runner.mjs +4 -0
- package/dist/adapter/database.d.mts +43 -0
- package/dist/adapter/database.mjs +4 -0
- package/dist/adapter/git-host/helpers.d.mts +12 -0
- package/dist/adapter/git-host/helpers.mjs +29 -0
- package/dist/adapter/git-host/helpers.mjs.map +1 -0
- package/dist/adapter/git-host.d.mts +46 -0
- package/dist/adapter/git-host.mjs +4 -0
- package/dist/adapter/storage.d.mts +22 -0
- package/dist/adapter/storage.mjs +4 -0
- package/dist/adapters/capture-queue.d.mts +49 -0
- package/dist/adapters/metadata.d.mts +24 -0
- package/dist/adapters/webhook-events.mjs +39 -0
- package/dist/adapters/webhook-events.mjs.map +1 -0
- package/dist/capture/adapter.d.mts +29 -0
- package/dist/capture/adapter.mjs +13 -0
- package/dist/capture/adapter.mjs.map +1 -0
- package/dist/capture/orchestrator.d.mts +23 -0
- package/dist/capture/orchestrator.mjs +103 -0
- package/dist/capture/orchestrator.mjs.map +1 -0
- package/dist/capture/pipeline.d.mts +41 -0
- package/dist/capture/pipeline.mjs +119 -0
- package/dist/capture/pipeline.mjs.map +1 -0
- package/dist/capture/queue.d.mts +35 -0
- package/dist/capture/queue.mjs +96 -0
- package/dist/capture/queue.mjs.map +1 -0
- package/dist/capture/status-fanout.mjs +66 -0
- package/dist/capture/status-fanout.mjs.map +1 -0
- package/dist/capture/storybook.d.mts +14 -0
- package/dist/capture/storybook.mjs +40 -0
- package/dist/capture/storybook.mjs.map +1 -0
- package/dist/config.d.mts +68 -0
- package/dist/config.mjs +69 -0
- package/dist/config.mjs.map +1 -0
- package/dist/ddl.d.mts +7 -0
- package/dist/ddl.mjs +155 -0
- package/dist/ddl.mjs.map +1 -0
- package/dist/diff/engine.d.mts +8 -0
- package/dist/diff/engine.mjs +79 -0
- package/dist/diff/engine.mjs.map +1 -0
- package/dist/diff/options.d.mts +40 -0
- package/dist/diff/options.mjs +15 -0
- package/dist/diff/options.mjs.map +1 -0
- package/dist/index.d.mts +48 -0
- package/dist/index.mjs +327 -0
- package/dist/index.mjs.map +1 -0
- package/dist/logger.d.mts +39 -0
- package/dist/logger.mjs +35 -0
- package/dist/logger.mjs.map +1 -0
- package/dist/middleware/csrf.mjs +39 -0
- package/dist/middleware/csrf.mjs.map +1 -0
- package/dist/middleware/rate-limit.mjs +38 -0
- package/dist/middleware/rate-limit.mjs.map +1 -0
- package/dist/models/baseline.d.mts +20 -0
- package/dist/models/baseline.mjs +89 -0
- package/dist/models/baseline.mjs.map +1 -0
- package/dist/models/build.d.mts +35 -0
- package/dist/models/build.mjs +77 -0
- package/dist/models/build.mjs.map +1 -0
- package/dist/models/comment.d.mts +20 -0
- package/dist/models/comment.mjs +42 -0
- package/dist/models/comment.mjs.map +1 -0
- package/dist/models/label.d.mts +43 -0
- package/dist/models/label.mjs +107 -0
- package/dist/models/label.mjs.map +1 -0
- package/dist/models/member.d.mts +27 -0
- package/dist/models/member.mjs +54 -0
- package/dist/models/member.mjs.map +1 -0
- package/dist/models/project.d.mts +42 -0
- package/dist/models/project.mjs +78 -0
- package/dist/models/project.mjs.map +1 -0
- package/dist/models/snapshot.d.mts +46 -0
- package/dist/models/snapshot.mjs +75 -0
- package/dist/models/snapshot.mjs.map +1 -0
- package/dist/models/status-config.mjs +61 -0
- package/dist/models/status-config.mjs.map +1 -0
- package/dist/models/token.d.mts +33 -0
- package/dist/models/token.mjs +60 -0
- package/dist/models/token.mjs.map +1 -0
- package/dist/models/webhook.d.mts +37 -0
- package/dist/models/webhook.mjs +67 -0
- package/dist/models/webhook.mjs.map +1 -0
- package/dist/pages/build-detail.mjs +333 -0
- package/dist/pages/build-detail.mjs.map +1 -0
- package/dist/pages/build-diff.mjs +437 -0
- package/dist/pages/build-diff.mjs.map +1 -0
- package/dist/pages/compute-jobs.mjs +196 -0
- package/dist/pages/compute-jobs.mjs.map +1 -0
- package/dist/pages/login.mjs +77 -0
- package/dist/pages/login.mjs.map +1 -0
- package/dist/pages/project-builds.mjs +228 -0
- package/dist/pages/project-builds.mjs.map +1 -0
- package/dist/pages/project-create.mjs +168 -0
- package/dist/pages/project-create.mjs.map +1 -0
- package/dist/pages/project-settings.mjs +111 -0
- package/dist/pages/project-settings.mjs.map +1 -0
- package/dist/pages/projects.mjs +178 -0
- package/dist/pages/projects.mjs.map +1 -0
- package/dist/pages/root.mjs +90 -0
- package/dist/pages/root.mjs.map +1 -0
- package/dist/pages/settings-general.mjs +191 -0
- package/dist/pages/settings-general.mjs.map +1 -0
- package/dist/pages/settings-labels.mjs +149 -0
- package/dist/pages/settings-labels.mjs.map +1 -0
- package/dist/pages/settings-members.mjs +124 -0
- package/dist/pages/settings-members.mjs.map +1 -0
- package/dist/pages/settings-status.mjs +150 -0
- package/dist/pages/settings-status.mjs.map +1 -0
- package/dist/pages/settings-tokens.mjs +95 -0
- package/dist/pages/settings-tokens.mjs.map +1 -0
- package/dist/pages/settings-webhooks.mjs +149 -0
- package/dist/pages/settings-webhooks.mjs.map +1 -0
- package/dist/retention/purge.d.mts +28 -0
- package/dist/retention/purge.mjs +66 -0
- package/dist/retention/purge.mjs.map +1 -0
- package/dist/routers/admin.mjs +41 -0
- package/dist/routers/admin.mjs.map +1 -0
- package/dist/routers/assets.mjs +24 -0
- package/dist/routers/assets.mjs.map +1 -0
- package/dist/routers/auth.mjs +77 -0
- package/dist/routers/auth.mjs.map +1 -0
- package/dist/routers/builds.handlers.mjs +87 -0
- package/dist/routers/builds.handlers.mjs.map +1 -0
- package/dist/routers/builds.mjs +162 -0
- package/dist/routers/builds.mjs.map +1 -0
- package/dist/routers/comments.mjs +89 -0
- package/dist/routers/comments.mjs.map +1 -0
- package/dist/routers/helpers.mjs +76 -0
- package/dist/routers/helpers.mjs.map +1 -0
- package/dist/routers/htmx.mjs +18 -0
- package/dist/routers/htmx.mjs.map +1 -0
- package/dist/routers/labels.mjs +77 -0
- package/dist/routers/labels.mjs.map +1 -0
- package/dist/routers/media.mjs +54 -0
- package/dist/routers/media.mjs.map +1 -0
- package/dist/routers/members.mjs +101 -0
- package/dist/routers/members.mjs.map +1 -0
- package/dist/routers/projects.mjs +121 -0
- package/dist/routers/projects.mjs.map +1 -0
- package/dist/routers/schemas.mjs +188 -0
- package/dist/routers/schemas.mjs.map +1 -0
- package/dist/routers/settings.mjs +235 -0
- package/dist/routers/settings.mjs.map +1 -0
- package/dist/routers/snapshots.mjs +143 -0
- package/dist/routers/snapshots.mjs.map +1 -0
- package/dist/routers/status-configs.mjs +100 -0
- package/dist/routers/status-configs.mjs.map +1 -0
- package/dist/routers/tokens.mjs +80 -0
- package/dist/routers/tokens.mjs.map +1 -0
- package/dist/routers/ui.mjs +142 -0
- package/dist/routers/ui.mjs.map +1 -0
- package/dist/routers/webhooks.mjs +89 -0
- package/dist/routers/webhooks.mjs.map +1 -0
- package/dist/schema.d.mts +4372 -0
- package/dist/schema.mjs +158 -0
- package/dist/schema.mjs.map +1 -0
- package/dist/store.mjs +18 -0
- package/dist/store.mjs.map +1 -0
- package/dist/types.d.mts +18 -0
- package/dist/types.mjs +49 -0
- package/dist/types.mjs.map +1 -0
- package/dist/ui/components.mjs +127 -0
- package/dist/ui/components.mjs.map +1 -0
- package/dist/ui/document.d.mts +8 -0
- package/dist/ui/document.mjs +338 -0
- package/dist/ui/document.mjs.map +1 -0
- package/dist/ui/styles.mjs +173 -0
- package/dist/ui/styles.mjs.map +1 -0
- package/dist/ui/theme.mjs +42 -0
- package/dist/ui/theme.mjs.map +1 -0
- package/dist/urls.d.mts +19 -0
- package/dist/urls.mjs +23 -0
- package/dist/urls.mjs.map +1 -0
- package/dist/utils/encrypt.mjs +49 -0
- package/dist/utils/encrypt.mjs.map +1 -0
- package/dist/utils/hash.mjs +22 -0
- package/dist/utils/hash.mjs.map +1 -0
- package/dist/utils/paths.d.mts +11 -0
- package/dist/utils/paths.mjs +23 -0
- package/dist/utils/paths.mjs.map +1 -0
- package/dist/utils/ulid.d.mts +8 -0
- package/dist/utils/ulid.mjs +41 -0
- package/dist/utils/ulid.mjs.map +1 -0
- package/package.json +162 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { BuildModel } from "../models/build.mjs";
|
|
5
|
+
import { ProjectModel } from "../models/project.mjs";
|
|
6
|
+
import { BUILD_STATUSES } from "../types.mjs";
|
|
7
|
+
import { getStore } from "../store.mjs";
|
|
8
|
+
import { notFound } from "./helpers.mjs";
|
|
9
|
+
import { emitWebhookEvent } from "../adapters/webhook-events.mjs";
|
|
10
|
+
import { BaselineModel } from "../models/baseline.mjs";
|
|
11
|
+
import { SnapshotModel } from "../models/snapshot.mjs";
|
|
12
|
+
import { z } from "@hono/zod-openapi";
|
|
13
|
+
//#region src/routers/builds.handlers.ts
|
|
14
|
+
const VIEW_ROLES = [
|
|
15
|
+
"viewer",
|
|
16
|
+
"developer",
|
|
17
|
+
"approver",
|
|
18
|
+
"admin"
|
|
19
|
+
];
|
|
20
|
+
const DEVELOPER_ROLES = [
|
|
21
|
+
"developer",
|
|
22
|
+
"approver",
|
|
23
|
+
"admin"
|
|
24
|
+
];
|
|
25
|
+
const APPROVER_ROLES = ["approver", "admin"];
|
|
26
|
+
const buildUploadSchema = z.object({
|
|
27
|
+
gitSha: z.string(),
|
|
28
|
+
gitBranch: z.string(),
|
|
29
|
+
authorEmail: z.string().optional(),
|
|
30
|
+
authorName: z.string().optional(),
|
|
31
|
+
message: z.string().optional(),
|
|
32
|
+
zip: z.instanceof(File).openapi({
|
|
33
|
+
type: "string",
|
|
34
|
+
format: "binary"
|
|
35
|
+
}).optional()
|
|
36
|
+
}).openapi("BuildUpload");
|
|
37
|
+
const buildListQuery = z.object({
|
|
38
|
+
status: z.enum(BUILD_STATUSES).optional(),
|
|
39
|
+
branch: z.string().optional()
|
|
40
|
+
});
|
|
41
|
+
async function buildForProject(projectId, buildId) {
|
|
42
|
+
const build = await new BuildModel(getStore().db).get(buildId);
|
|
43
|
+
if (!build || build.projectId !== projectId) notFound("Build not found");
|
|
44
|
+
return build;
|
|
45
|
+
}
|
|
46
|
+
async function snapshotForBuild(build, snapshotId) {
|
|
47
|
+
const snapshot = await new SnapshotModel(getStore().db).get(snapshotId);
|
|
48
|
+
if (!snapshot || snapshot.buildId !== build.id) notFound("Snapshot not found");
|
|
49
|
+
return snapshot;
|
|
50
|
+
}
|
|
51
|
+
async function refreshBuild(buildId) {
|
|
52
|
+
const db = getStore().db;
|
|
53
|
+
await new BuildModel(db).updateCounts(buildId);
|
|
54
|
+
const snapshots = await new SnapshotModel(db).listByBuild(buildId);
|
|
55
|
+
const unresolved = snapshots.some((s) => s.status === "new" || s.status === "changed");
|
|
56
|
+
let status;
|
|
57
|
+
if (unresolved) status = "reviewing";
|
|
58
|
+
else status = snapshots.some((s) => s.status === "rejected") ? "rejected" : "approved";
|
|
59
|
+
const build = await new BuildModel(db).get(buildId);
|
|
60
|
+
if (build) {
|
|
61
|
+
await new BuildModel(db).setStatus(buildId, status);
|
|
62
|
+
await emitWebhookEvent(db, build.projectId, `build:${status}`, {
|
|
63
|
+
buildId,
|
|
64
|
+
status,
|
|
65
|
+
snapshotCount: snapshots.length
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function approveSnapshot(snapshotId, userId) {
|
|
70
|
+
const db = getStore().db;
|
|
71
|
+
const snapshots = new SnapshotModel(db);
|
|
72
|
+
const snapshot = await snapshots.get(snapshotId);
|
|
73
|
+
if (!snapshot) notFound("Snapshot not found");
|
|
74
|
+
const project = await new ProjectModel(db).get(snapshot.projectId);
|
|
75
|
+
const build = await new BuildModel(db).get(snapshot.buildId);
|
|
76
|
+
if (!project || !build) notFound("Project or build not found");
|
|
77
|
+
await snapshots.review(snapshotId, "approved", userId);
|
|
78
|
+
await new BaselineModel(db, getStore().storage).upsert(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, snapshot.id, snapshot.screenshotPath);
|
|
79
|
+
await refreshBuild(build.id);
|
|
80
|
+
}
|
|
81
|
+
function asString(value) {
|
|
82
|
+
return typeof value === "string" ? value : void 0;
|
|
83
|
+
}
|
|
84
|
+
//#endregion
|
|
85
|
+
export { APPROVER_ROLES, DEVELOPER_ROLES, VIEW_ROLES, approveSnapshot, asString, buildForProject, buildListQuery, buildUploadSchema, refreshBuild, snapshotForBuild };
|
|
86
|
+
|
|
87
|
+
//# sourceMappingURL=builds.handlers.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builds.handlers.mjs","names":[],"sources":["../../src/routers/builds.handlers.ts"],"sourcesContent":["import { z } from \"@hono/zod-openapi\";\n\nimport { BaselineModel } from \"../models/baseline.ts\";\nimport { BuildModel } from \"../models/build.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport { SnapshotModel } from \"../models/snapshot.ts\";\nimport { emitWebhookEvent } from \"../adapters/webhook-events.ts\";\nimport { getStore } from \"../store.ts\";\nimport { type ProjectRole, BUILD_STATUSES } from \"../types.ts\";\nimport { notFound } from \"./helpers.ts\";\n\nexport const VIEW_ROLES: readonly ProjectRole[] = [\"viewer\", \"developer\", \"approver\", \"admin\"];\nexport const DEVELOPER_ROLES: readonly ProjectRole[] = [\"developer\", \"approver\", \"admin\"];\nexport const APPROVER_ROLES: readonly ProjectRole[] = [\"approver\", \"admin\"];\n\nexport const buildUploadSchema = z.object({\n gitSha: z.string(),\n gitBranch: z.string(),\n authorEmail: z.string().optional(),\n authorName: z.string().optional(),\n message: z.string().optional(),\n zip: z.instanceof(File).openapi({ type: \"string\", format: \"binary\" }).optional(),\n}).openapi(\"BuildUpload\");\n\nexport const buildListQuery = z.object({\n status: z.enum(BUILD_STATUSES).optional(),\n branch: z.string().optional(),\n});\n\nexport async function buildForProject(projectId: string, buildId: string): Promise<import(\"../models/build.ts\").Build> {\n const build = await new BuildModel(getStore().db).get(buildId);\n if (!build || build.projectId !== projectId) {\n notFound(\"Build not found\");\n }\n return build;\n}\n\nexport async function snapshotForBuild(build: { id: string }, snapshotId: string): Promise<import(\"../models/snapshot.ts\").Snapshot> {\n const snapshot = await new SnapshotModel(getStore().db).get(snapshotId);\n if (!snapshot || snapshot.buildId !== build.id) {\n notFound(\"Snapshot not found\");\n }\n return snapshot;\n}\n\nexport async function refreshBuild(buildId: string): Promise<void> {\n const db = getStore().db;\n await new BuildModel(db).updateCounts(buildId);\n const snapshots = await new SnapshotModel(db).listByBuild(buildId);\n const unresolved = snapshots.some((s) => s.status === \"new\" || s.status === \"changed\");\n let status: \"reviewing\" | \"rejected\" | \"approved\";\n if (unresolved) {\n status = \"reviewing\";\n } else {\n const rejected = snapshots.some((s) => s.status === \"rejected\");\n status = rejected ? \"rejected\" : \"approved\";\n }\n const build = await new BuildModel(db).get(buildId);\n if (build) {\n await new BuildModel(db).setStatus(buildId, status);\n await emitWebhookEvent(db, build.projectId, `build:${status}`, {\n buildId,\n status,\n snapshotCount: snapshots.length,\n });\n }\n}\n\nexport async function approveSnapshot(snapshotId: string, userId: string): Promise<void> {\n const db = getStore().db;\n const snapshots = new SnapshotModel(db);\n const snapshot = await snapshots.get(snapshotId);\n if (!snapshot) {\n notFound(\"Snapshot not found\");\n }\n const project = await new ProjectModel(db).get(snapshot.projectId);\n const build = await new BuildModel(db).get(snapshot.buildId);\n if (!project || !build) {\n notFound(\"Project or build not found\");\n }\n await snapshots.review(snapshotId, \"approved\", userId);\n const baselines = new BaselineModel(db, getStore().storage);\n await baselines.upsert(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, snapshot.id, snapshot.screenshotPath);\n await refreshBuild(build.id);\n}\n\nfunction asString(value: FormDataEntryValue | null): string | undefined {\n return typeof value === \"string\" ? value : undefined;\n}\n\nexport { asString };"],"mappings":";;;;;;;;;;;;;AAWA,MAAa,aAAqC;CAAC;CAAU;CAAa;CAAY;AAAO;AAC7F,MAAa,kBAA0C;CAAC;CAAa;CAAY;AAAO;AACxF,MAAa,iBAAyC,CAAC,YAAY,OAAO;AAE1E,MAAa,oBAAoB,EAAE,OAAO;CACxC,QAAQ,EAAE,OAAO;CACjB,WAAW,EAAE,OAAO;CACpB,aAAa,EAAE,OAAO,CAAC,CAAC,SAAS;CACjC,YAAY,EAAE,OAAO,CAAC,CAAC,SAAS;CAChC,SAAS,EAAE,OAAO,CAAC,CAAC,SAAS;CAC7B,KAAK,EAAE,WAAW,IAAI,CAAC,CAAC,QAAQ;EAAE,MAAM;EAAU,QAAQ;CAAS,CAAC,CAAC,CAAC,SAAS;AACjF,CAAC,CAAC,CAAC,QAAQ,aAAa;AAExB,MAAa,iBAAiB,EAAE,OAAO;CACrC,QAAQ,EAAE,KAAK,cAAc,CAAC,CAAC,SAAS;CACxC,QAAQ,EAAE,OAAO,CAAC,CAAC,SAAS;AAC9B,CAAC;AAED,eAAsB,gBAAgB,WAAmB,SAA8D;CACrH,MAAM,QAAQ,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,OAAO;CAC7D,IAAI,CAAC,SAAS,MAAM,cAAc,WAChC,SAAS,iBAAiB;CAE5B,OAAO;AACT;AAEA,eAAsB,iBAAiB,OAAuB,YAAuE;CACnI,MAAM,WAAW,MAAM,IAAI,cAAc,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,UAAU;CACtE,IAAI,CAAC,YAAY,SAAS,YAAY,MAAM,IAC1C,SAAS,oBAAoB;CAE/B,OAAO;AACT;AAEA,eAAsB,aAAa,SAAgC;CACjE,MAAM,KAAK,SAAS,CAAC,CAAC;CACtB,MAAM,IAAI,WAAW,EAAE,CAAC,CAAC,aAAa,OAAO;CAC7C,MAAM,YAAY,MAAM,IAAI,cAAc,EAAE,CAAC,CAAC,YAAY,OAAO;CACjE,MAAM,aAAa,UAAU,MAAM,MAAM,EAAE,WAAW,SAAS,EAAE,WAAW,SAAS;CACrF,IAAI;CACJ,IAAI,YACF,SAAS;MAGT,SADiB,UAAU,MAAM,MAAM,EAAE,WAAW,UACpC,IAAI,aAAa;CAEnC,MAAM,QAAQ,MAAM,IAAI,WAAW,EAAE,CAAC,CAAC,IAAI,OAAO;CAClD,IAAI,OAAO;EACT,MAAM,IAAI,WAAW,EAAE,CAAC,CAAC,UAAU,SAAS,MAAM;EAClD,MAAM,iBAAiB,IAAI,MAAM,WAAW,SAAS,UAAU;GAC7D;GACA;GACA,eAAe,UAAU;EAC3B,CAAC;CACH;AACF;AAEA,eAAsB,gBAAgB,YAAoB,QAA+B;CACvF,MAAM,KAAK,SAAS,CAAC,CAAC;CACtB,MAAM,YAAY,IAAI,cAAc,EAAE;CACtC,MAAM,WAAW,MAAM,UAAU,IAAI,UAAU;CAC/C,IAAI,CAAC,UACH,SAAS,oBAAoB;CAE/B,MAAM,UAAU,MAAM,IAAI,aAAa,EAAE,CAAC,CAAC,IAAI,SAAS,SAAS;CACjE,MAAM,QAAQ,MAAM,IAAI,WAAW,EAAE,CAAC,CAAC,IAAI,SAAS,OAAO;CAC3D,IAAI,CAAC,WAAW,CAAC,OACf,SAAS,4BAA4B;CAEvC,MAAM,UAAU,OAAO,YAAY,YAAY,MAAM;CAErD,MAAM,IADgB,cAAc,IAAI,SAAS,CAAC,CAAC,OACrC,CAAC,CAAC,OAAO,QAAQ,IAAI,SAAS,SAAS,SAAS,cAAc,MAAM,WAAW,SAAS,IAAI,SAAS,cAAc;CACjI,MAAM,aAAa,MAAM,EAAE;AAC7B;AAEA,SAAS,SAAS,OAAsD;CACtE,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAA;AAC7C"}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { BuildModel } from "../models/build.mjs";
|
|
5
|
+
import { getStore } from "../store.mjs";
|
|
6
|
+
import { resolveAuthorizedProject } from "./helpers.mjs";
|
|
7
|
+
import { badRequest, buildSchema, forbidden, notFound, unauthorized } from "./schemas.mjs";
|
|
8
|
+
import { emitWebhookEvent } from "../adapters/webhook-events.mjs";
|
|
9
|
+
import { storybookZipPath } from "../utils/paths.mjs";
|
|
10
|
+
import { APPROVER_ROLES, DEVELOPER_ROLES, VIEW_ROLES, asString, buildForProject, buildListQuery, buildUploadSchema } from "./builds.handlers.mjs";
|
|
11
|
+
import { registerSnapshots } from "./snapshots.mjs";
|
|
12
|
+
import { registerComments } from "./comments.mjs";
|
|
13
|
+
import { createRoute, z } from "@hono/zod-openapi";
|
|
14
|
+
import { HTTPException } from "hono/http-exception";
|
|
15
|
+
//#region src/routers/builds.ts
|
|
16
|
+
const listBuildsRoute = createRoute({
|
|
17
|
+
method: "get",
|
|
18
|
+
path: "/api/v1/projects/{slug}/builds",
|
|
19
|
+
request: {
|
|
20
|
+
params: z.object({ slug: z.string() }),
|
|
21
|
+
query: buildListQuery
|
|
22
|
+
},
|
|
23
|
+
responses: {
|
|
24
|
+
200: {
|
|
25
|
+
content: { "application/json": { schema: buildSchema.array() } },
|
|
26
|
+
description: "List builds for a project"
|
|
27
|
+
},
|
|
28
|
+
...notFound,
|
|
29
|
+
...unauthorized
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
const createBuildRoute = createRoute({
|
|
33
|
+
method: "post",
|
|
34
|
+
path: "/api/v1/projects/{slug}/builds",
|
|
35
|
+
request: {
|
|
36
|
+
params: z.object({ slug: z.string() }),
|
|
37
|
+
body: { content: { "multipart/form-data": { schema: buildUploadSchema } } }
|
|
38
|
+
},
|
|
39
|
+
responses: {
|
|
40
|
+
202: {
|
|
41
|
+
content: { "application/json": { schema: buildSchema } },
|
|
42
|
+
description: "Build created and capture queued"
|
|
43
|
+
},
|
|
44
|
+
...badRequest,
|
|
45
|
+
...forbidden,
|
|
46
|
+
...notFound
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
const getBuildRoute = createRoute({
|
|
50
|
+
method: "get",
|
|
51
|
+
path: "/api/v1/projects/{slug}/builds/{buildId}",
|
|
52
|
+
request: { params: z.object({
|
|
53
|
+
slug: z.string(),
|
|
54
|
+
buildId: z.string()
|
|
55
|
+
}) },
|
|
56
|
+
responses: {
|
|
57
|
+
200: {
|
|
58
|
+
content: { "application/json": { schema: buildSchema } },
|
|
59
|
+
description: "Fetch a build"
|
|
60
|
+
},
|
|
61
|
+
...notFound,
|
|
62
|
+
...unauthorized
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
const retryBuildRoute = createRoute({
|
|
66
|
+
method: "post",
|
|
67
|
+
path: "/api/v1/projects/{slug}/builds/{buildId}/retry",
|
|
68
|
+
request: { params: z.object({
|
|
69
|
+
slug: z.string(),
|
|
70
|
+
buildId: z.string()
|
|
71
|
+
}) },
|
|
72
|
+
responses: {
|
|
73
|
+
202: {
|
|
74
|
+
content: { "application/json": { schema: buildSchema } },
|
|
75
|
+
description: "Build reset to pending"
|
|
76
|
+
},
|
|
77
|
+
...notFound
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
const deleteBuildRoute = createRoute({
|
|
81
|
+
method: "delete",
|
|
82
|
+
path: "/api/v1/projects/{slug}/builds/{buildId}",
|
|
83
|
+
request: { params: z.object({
|
|
84
|
+
slug: z.string(),
|
|
85
|
+
buildId: z.string()
|
|
86
|
+
}) },
|
|
87
|
+
responses: {
|
|
88
|
+
204: { description: "Build deleted" },
|
|
89
|
+
...notFound
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
function registerBuilds(app) {
|
|
93
|
+
app.openapi(listBuildsRoute, async (c) => {
|
|
94
|
+
const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...VIEW_ROLES);
|
|
95
|
+
const { status, branch } = c.req.valid("query");
|
|
96
|
+
const builds = new BuildModel(getStore().db).list(project.id, {
|
|
97
|
+
status,
|
|
98
|
+
branch: branch ?? void 0
|
|
99
|
+
});
|
|
100
|
+
return c.json(await builds);
|
|
101
|
+
});
|
|
102
|
+
app.openapi(createBuildRoute, async (c) => {
|
|
103
|
+
const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...DEVELOPER_ROLES);
|
|
104
|
+
const form = await c.req.formData();
|
|
105
|
+
const gitSha = asString(form.get("gitSha")) ?? "";
|
|
106
|
+
const gitBranch = asString(form.get("gitBranch")) ?? "";
|
|
107
|
+
if (!gitSha || !gitBranch) throw new HTTPException(400, { message: "gitSha and gitBranch are required" });
|
|
108
|
+
const authorEmail = asString(form.get("authorEmail"));
|
|
109
|
+
const authorName = asString(form.get("authorName"));
|
|
110
|
+
const message = asString(form.get("message"));
|
|
111
|
+
const build = await new BuildModel(getStore().db).create(project.id, {
|
|
112
|
+
gitSha,
|
|
113
|
+
gitBranch,
|
|
114
|
+
isDefault: gitBranch === project.gitDefaultBranch,
|
|
115
|
+
authorEmail,
|
|
116
|
+
authorName,
|
|
117
|
+
message
|
|
118
|
+
});
|
|
119
|
+
await emitWebhookEvent(getStore().db, project.id, "build:created", {
|
|
120
|
+
buildId: build.id,
|
|
121
|
+
gitSha,
|
|
122
|
+
gitBranch,
|
|
123
|
+
authorEmail,
|
|
124
|
+
authorName,
|
|
125
|
+
message
|
|
126
|
+
});
|
|
127
|
+
const zip = form.get("zip");
|
|
128
|
+
if (zip && typeof zip !== "string") {
|
|
129
|
+
const buffer = Buffer.from(await zip.arrayBuffer());
|
|
130
|
+
await getStore().storage.write(storybookZipPath(project.id, build.id), buffer);
|
|
131
|
+
}
|
|
132
|
+
const reqId = c.get("requestId");
|
|
133
|
+
await getStore().enqueueCapture?.(build.id, reqId);
|
|
134
|
+
return c.json(build, 202);
|
|
135
|
+
});
|
|
136
|
+
app.openapi(getBuildRoute, async (c) => {
|
|
137
|
+
const { slug, buildId } = c.req.valid("param");
|
|
138
|
+
const project = await resolveAuthorizedProject(c, slug, ...VIEW_ROLES);
|
|
139
|
+
const build = await buildForProject(project.id, buildId);
|
|
140
|
+
return c.json(build);
|
|
141
|
+
});
|
|
142
|
+
app.openapi(retryBuildRoute, async (c) => {
|
|
143
|
+
const { slug, buildId } = c.req.valid("param");
|
|
144
|
+
const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);
|
|
145
|
+
const build = await buildForProject(project.id, buildId);
|
|
146
|
+
const updated = await new BuildModel(getStore().db).setStatus(build.id, "pending");
|
|
147
|
+
return c.json(updated, 202);
|
|
148
|
+
});
|
|
149
|
+
app.openapi(deleteBuildRoute, async (c) => {
|
|
150
|
+
const { slug, buildId } = c.req.valid("param");
|
|
151
|
+
const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);
|
|
152
|
+
const build = await buildForProject(project.id, buildId);
|
|
153
|
+
await new BuildModel(getStore().db).remove(build.id);
|
|
154
|
+
return c.body(null, 204);
|
|
155
|
+
});
|
|
156
|
+
registerSnapshots(app);
|
|
157
|
+
registerComments(app);
|
|
158
|
+
}
|
|
159
|
+
//#endregion
|
|
160
|
+
export { registerBuilds };
|
|
161
|
+
|
|
162
|
+
//# 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 type { ShelfApp } from \"../index.tsx\";\nimport { HTTPException } from \"hono/http-exception\";\n\nimport { BuildModel } from \"../models/build.ts\";\nimport { emitWebhookEvent } from \"../adapters/webhook-events.ts\";\nimport { getStore } from \"../store.ts\";\nimport { storybookZipPath } from \"../utils/paths.ts\";\nimport { resolveAuthorizedProject } from \"./helpers.ts\";\nimport { badRequest, buildSchema, forbidden as forbiddenResponse, notFound as notFoundResponse, unauthorized } from \"./schemas.ts\";\nimport { VIEW_ROLES, DEVELOPER_ROLES, APPROVER_ROLES, buildUploadSchema, buildListQuery, buildForProject, asString } from \"./builds.handlers.ts\";\nimport { registerSnapshots } from \"./snapshots.ts\";\nimport { registerComments } from \"./comments.ts\";\n\nconst listBuildsRoute = createRoute({\n method: \"get\",\n path: \"/api/v1/projects/{slug}/builds\",\n request: { params: z.object({ slug: z.string() }), query: buildListQuery },\n responses: {\n 200: { content: { \"application/json\": { schema: buildSchema.array() } }, description: \"List builds for a project\" },\n ...notFoundResponse,\n ...unauthorized,\n },\n});\n\nconst createBuildRoute = createRoute({\n method: \"post\",\n path: \"/api/v1/projects/{slug}/builds\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"multipart/form-data\": { schema: buildUploadSchema } } },\n },\n responses: {\n 202: { content: { \"application/json\": { schema: buildSchema } }, description: \"Build created and capture queued\" },\n ...badRequest,\n ...forbiddenResponse,\n ...notFoundResponse,\n },\n});\n\nconst getBuildRoute = createRoute({\n method: \"get\",\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 path: \"/api/v1/projects/{slug}/builds/{buildId}/retry\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 202: { content: { \"application/json\": { schema: buildSchema } }, description: \"Build reset to pending\" },\n ...notFoundResponse,\n },\n});\n\nconst deleteBuildRoute = createRoute({\n method: \"delete\",\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\nexport function registerBuilds(app: ShelfApp): void {\n app.openapi(listBuildsRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...VIEW_ROLES);\n const { status, branch } = c.req.valid(\"query\");\n const builds = new BuildModel(getStore().db).list(project.id, { status, branch: branch ?? undefined });\n return c.json(await builds);\n });\n\n app.openapi(createBuildRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...DEVELOPER_ROLES);\n const form = await c.req.formData();\n\n const gitSha = asString(form.get(\"gitSha\")) ?? \"\";\n const gitBranch = asString(form.get(\"gitBranch\")) ?? \"\";\n if (!gitSha || !gitBranch) {\n throw new HTTPException(400, { message: \"gitSha and gitBranch are required\" });\n }\n const authorEmail = asString(form.get(\"authorEmail\"));\n const authorName = asString(form.get(\"authorName\"));\n const message = asString(form.get(\"message\"));\n\n const build = await new BuildModel(getStore().db).create(project.id, {\n gitSha,\n gitBranch,\n isDefault: gitBranch === project.gitDefaultBranch,\n authorEmail,\n authorName,\n message,\n });\n\n await emitWebhookEvent(getStore().db, project.id, \"build:created\", {\n buildId: build.id,\n gitSha,\n gitBranch,\n authorEmail,\n authorName,\n message,\n });\n\n const zip = form.get(\"zip\");\n if (zip && typeof zip !== \"string\") {\n const buffer = Buffer.from(await zip.arrayBuffer());\n await getStore().storage.write(storybookZipPath(project.id, build.id), buffer);\n }\n\n const reqId = c.get(\"requestId\");\n await getStore().enqueueCapture?.(build.id, reqId);\n return c.json(build, 202);\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 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}"],"mappings":"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"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { getStore } from "../store.mjs";
|
|
5
|
+
import { notFound, resolveAuthorizedProject } from "./helpers.mjs";
|
|
6
|
+
import { commentCreateSchema, commentSchema, notFound as notFound$1, unauthorized } from "./schemas.mjs";
|
|
7
|
+
import { DEVELOPER_ROLES, VIEW_ROLES, buildForProject } from "./builds.handlers.mjs";
|
|
8
|
+
import { CommentModel } from "../models/comment.mjs";
|
|
9
|
+
import { createRoute, z } from "@hono/zod-openapi";
|
|
10
|
+
//#region src/routers/comments.ts
|
|
11
|
+
const listCommentsRoute = createRoute({
|
|
12
|
+
method: "get",
|
|
13
|
+
path: "/api/v1/projects/{slug}/builds/{buildId}/comments",
|
|
14
|
+
request: { params: z.object({
|
|
15
|
+
slug: z.string(),
|
|
16
|
+
buildId: z.string()
|
|
17
|
+
}) },
|
|
18
|
+
responses: {
|
|
19
|
+
200: {
|
|
20
|
+
content: { "application/json": { schema: commentSchema.array() } },
|
|
21
|
+
description: "List comments on a build"
|
|
22
|
+
},
|
|
23
|
+
...notFound$1,
|
|
24
|
+
...unauthorized
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
const createCommentRoute = createRoute({
|
|
28
|
+
method: "post",
|
|
29
|
+
path: "/api/v1/projects/{slug}/builds/{buildId}/comments",
|
|
30
|
+
request: {
|
|
31
|
+
params: z.object({
|
|
32
|
+
slug: z.string(),
|
|
33
|
+
buildId: z.string()
|
|
34
|
+
}),
|
|
35
|
+
body: { content: { "application/json": { schema: commentCreateSchema } } }
|
|
36
|
+
},
|
|
37
|
+
responses: {
|
|
38
|
+
201: {
|
|
39
|
+
content: { "application/json": { schema: commentSchema } },
|
|
40
|
+
description: "Comment created"
|
|
41
|
+
},
|
|
42
|
+
...notFound$1
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
const resolveCommentRoute = createRoute({
|
|
46
|
+
method: "post",
|
|
47
|
+
path: "/api/v1/projects/{slug}/builds/{buildId}/comments/{commentId}/resolve",
|
|
48
|
+
request: { params: z.object({
|
|
49
|
+
slug: z.string(),
|
|
50
|
+
buildId: z.string(),
|
|
51
|
+
commentId: z.string()
|
|
52
|
+
}) },
|
|
53
|
+
responses: {
|
|
54
|
+
200: {
|
|
55
|
+
content: { "application/json": { schema: commentSchema } },
|
|
56
|
+
description: "Comment resolved"
|
|
57
|
+
},
|
|
58
|
+
...notFound$1
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
function registerComments(app) {
|
|
62
|
+
app.openapi(listCommentsRoute, async (c) => {
|
|
63
|
+
const { slug, buildId } = c.req.valid("param");
|
|
64
|
+
const project = await resolveAuthorizedProject(c, slug, ...VIEW_ROLES);
|
|
65
|
+
const build = await buildForProject(project.id, buildId);
|
|
66
|
+
return c.json(await new CommentModel(getStore().db).listByBuild(build.id));
|
|
67
|
+
});
|
|
68
|
+
app.openapi(createCommentRoute, async (c) => {
|
|
69
|
+
const { slug, buildId } = c.req.valid("param");
|
|
70
|
+
const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);
|
|
71
|
+
const build = await buildForProject(project.id, buildId);
|
|
72
|
+
const body = c.req.valid("json");
|
|
73
|
+
const userId = getStore().user?.id ?? "anonymous";
|
|
74
|
+
const comment = await new CommentModel(getStore().db).create(project.id, build.id, userId, body);
|
|
75
|
+
return c.json(comment, 201);
|
|
76
|
+
});
|
|
77
|
+
app.openapi(resolveCommentRoute, async (c) => {
|
|
78
|
+
const { slug, buildId, commentId } = c.req.valid("param");
|
|
79
|
+
const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);
|
|
80
|
+
const build = await buildForProject(project.id, buildId);
|
|
81
|
+
const comment = await new CommentModel(getStore().db).resolve(commentId);
|
|
82
|
+
if (comment.buildId !== build.id) notFound("Comment not found");
|
|
83
|
+
return c.json(comment);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
export { registerComments };
|
|
88
|
+
|
|
89
|
+
//# 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 type { ShelfApp } from \"../index.tsx\";\n\nimport { CommentModel } from \"../models/comment.ts\";\nimport { getStore } from \"../store.ts\";\nimport { resolveAuthorizedProject, notFound as throwNotFound } from \"./helpers.ts\";\nimport { commentSchema, commentCreateSchema, notFound, unauthorized } from \"./schemas.ts\";\nimport { VIEW_ROLES, DEVELOPER_ROLES, buildForProject } from \"./builds.handlers.ts\";\n\nconst listCommentsRoute = createRoute({\n method: \"get\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/comments\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 200: { content: { \"application/json\": { schema: commentSchema.array() } }, description: \"List comments on a build\" },\n ...notFound,\n ...unauthorized,\n },\n});\n\nconst createCommentRoute = createRoute({\n method: \"post\",\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: { content: { \"application/json\": { schema: commentSchema } }, description: \"Comment created\" },\n ...notFound,\n },\n});\n\nconst resolveCommentRoute = createRoute({\n method: \"post\",\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: { content: { \"application/json\": { schema: commentSchema } }, description: \"Comment resolved\" },\n ...notFound,\n },\n});\n\nexport function registerComments(app: ShelfApp): 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 ?? \"anonymous\";\n const comment = await new CommentModel(getStore().db).create(project.id, build.id, userId, body);\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 return c.json(comment);\n });\n}"],"mappings":";;;;;;;;;;AASA,MAAM,oBAAoB,YAAY;CACpC,QAAQ;CACR,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,cAAc,MAAM,EAAE,EAAE;GAAG,aAAa;EAA2B;EACnH,GAAGA;EACH,GAAG;CACL;AACF,CAAC;AAED,MAAM,qBAAqB,YAAY;CACrC,QAAQ;CACR,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,oBAAoB,EAAE,EAAE;CAC3E;CACA,WAAW;EACT,KAAK;GAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,cAAc,EAAE;GAAG,aAAa;EAAkB;EAClG,GAAGA;CACL;AACF,CAAC;AAED,MAAM,sBAAsB,YAAY;CACtC,QAAQ;CACR,MAAM;CACN,SAAS,EAAE,QAAQ,EAAE,OAAO;EAAE,MAAM,EAAE,OAAO;EAAG,SAAS,EAAE,OAAO;EAAG,WAAW,EAAE,OAAO;CAAE,CAAC,EAAE;CAC9F,WAAW;EACT,KAAK;GAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,cAAc,EAAE;GAAG,aAAa;EAAmB;EACnG,GAAGA;CACL;AACF,CAAC;AAED,SAAgB,iBAAiB,KAAqB;CACpD,IAAI,QAAQ,mBAAmB,OAAO,MAAM;EAC1C,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,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,YAAY,MAAM,EAAE,CAAC;CAC3E,CAAC;CAED,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,QAAQ,MAAM,gBAAgB,QAAQ,IAAI,OAAO;EACvD,MAAM,OAAO,EAAE,IAAI,MAAM,MAAM;EAC/B,MAAM,SAAS,SAAS,CAAC,CAAC,MAAM,MAAM;EACtC,MAAM,UAAU,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,QAAQ,IAAI,MAAM,IAAI,QAAQ,IAAI;EAC/F,OAAO,EAAE,KAAK,SAAS,GAAG;CAC5B,CAAC;CAED,IAAI,QAAQ,qBAAqB,OAAO,MAAM;EAC5C,MAAM,EAAE,MAAM,SAAS,cAAc,EAAE,IAAI,MAAM,OAAO;EACxD,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,eAAe;EAC1E,MAAM,QAAQ,MAAM,gBAAgB,QAAQ,IAAI,OAAO;EACvD,MAAM,UAAU,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,QAAQ,SAAS;EACvE,IAAI,QAAQ,YAAY,MAAM,IAC5B,SAAc,mBAAmB;EAEnC,OAAO,EAAE,KAAK,OAAO;CACvB,CAAC;AACH"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { projects } from "../schema.mjs";
|
|
5
|
+
import { ProjectModel } from "../models/project.mjs";
|
|
6
|
+
import { getStore } from "../store.mjs";
|
|
7
|
+
import { MemberModel } from "../models/member.mjs";
|
|
8
|
+
import { TokenModel } from "../models/token.mjs";
|
|
9
|
+
import { sha256 } from "../utils/hash.mjs";
|
|
10
|
+
import { eq } from "drizzle-orm";
|
|
11
|
+
import { HTTPException } from "hono/http-exception";
|
|
12
|
+
//#region src/routers/helpers.ts
|
|
13
|
+
function unauthorized() {
|
|
14
|
+
throw new HTTPException(401, { message: "Unauthorized" });
|
|
15
|
+
}
|
|
16
|
+
function forbidden() {
|
|
17
|
+
throw new HTTPException(403, { message: "Forbidden" });
|
|
18
|
+
}
|
|
19
|
+
function notFound(message = "Not found") {
|
|
20
|
+
throw new HTTPException(404, { message });
|
|
21
|
+
}
|
|
22
|
+
async function currentProjectRole(projectId) {
|
|
23
|
+
const { db, user } = getStore();
|
|
24
|
+
if (!user) return null;
|
|
25
|
+
return await new MemberModel(db).effectiveRole(user.role, projectId, user.id);
|
|
26
|
+
}
|
|
27
|
+
async function findProjectBySlug(slug) {
|
|
28
|
+
const { db } = getStore();
|
|
29
|
+
return (await db.list(projects, {
|
|
30
|
+
where: eq(projects.slug, slug),
|
|
31
|
+
limit: 1
|
|
32
|
+
}))[0] ?? null;
|
|
33
|
+
}
|
|
34
|
+
async function resolveProjectByToken(c, slug) {
|
|
35
|
+
const authHeader = c.req.header("authorization");
|
|
36
|
+
if (authHeader?.startsWith("Bearer ")) {
|
|
37
|
+
const token = authHeader.slice(7);
|
|
38
|
+
const found = await new TokenModel(getStore().db).findByHash(sha256(token));
|
|
39
|
+
if (!found) unauthorized();
|
|
40
|
+
const project = await new ProjectModel(getStore().db).get(found.projectId);
|
|
41
|
+
if (!project || project.slug !== slug) forbidden();
|
|
42
|
+
return project;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Enforce the caller's project role for a given project.
|
|
48
|
+
*
|
|
49
|
+
* Authorization model (ADR 0008):
|
|
50
|
+
* - No auth adapter configured -> all operations permitted (development mode).
|
|
51
|
+
* - Bearer token (CLI) -> grants access to its own project regardless of role.
|
|
52
|
+
* - Session user -> must have an effective role in `minRoles`.
|
|
53
|
+
*/
|
|
54
|
+
async function assertRole(projectId, ...minRoles) {
|
|
55
|
+
if (!getStore().authEnabled) return;
|
|
56
|
+
const role = await currentProjectRole(projectId);
|
|
57
|
+
if (!role || !minRoles.includes(role)) forbidden();
|
|
58
|
+
}
|
|
59
|
+
async function resolveAuthorizedProject(c, slug, ...minRoles) {
|
|
60
|
+
const project = await resolveProjectByToken(c, slug);
|
|
61
|
+
if (project) return project;
|
|
62
|
+
const found = await findProjectBySlug(slug);
|
|
63
|
+
if (!found) notFound("Project not found");
|
|
64
|
+
await assertRole(found.id, ...minRoles);
|
|
65
|
+
return found;
|
|
66
|
+
}
|
|
67
|
+
/** Require a site-level admin (or permit when auth is disabled). */
|
|
68
|
+
function requireSiteAdmin() {
|
|
69
|
+
if (!getStore().authEnabled) return;
|
|
70
|
+
const { user } = getStore();
|
|
71
|
+
if (user?.role !== "admin") forbidden();
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
export { assertRole, currentProjectRole, findProjectBySlug, forbidden, notFound, requireSiteAdmin, resolveAuthorizedProject, unauthorized };
|
|
75
|
+
|
|
76
|
+
//# sourceMappingURL=helpers.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.mjs","names":[],"sources":["../../src/routers/helpers.ts"],"sourcesContent":["import { eq } from \"drizzle-orm\";\nimport type { Context, Next } from \"hono\";\nimport { HTTPException } from \"hono/http-exception\";\nimport type { ContentfulStatusCode } from \"hono/utils/http-status\";\nimport type { ZodType } from \"zod\";\n\nimport { MemberModel } from \"../models/member.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport { TokenModel } from \"../models/token.ts\";\nimport { projects, type Project } from \"../schema.ts\";\nimport { getStore } from \"../store.ts\";\nimport type { ProjectRole } from \"../types.ts\";\nimport { sha256 } from \"../utils/hash.ts\";\n\nexport function json(c: Context, data: unknown, status: ContentfulStatusCode = 200): Response {\n return c.json(data, status);\n}\n\nexport async function validJson<T>(c: Context, schema: ZodType<T>): Promise<T> {\n const result = schema.safeParse(await c.req.json());\n if (!result.success) {\n throw new HTTPException(400, { message: result.error.message });\n }\n return result.data;\n}\n\nexport function unauthorized(): never {\n throw new HTTPException(401, { message: \"Unauthorized\" });\n}\n\nexport function forbidden(): never {\n throw new HTTPException(403, { message: \"Forbidden\" });\n}\n\nexport function notFound(message = \"Not found\"): never {\n throw new HTTPException(404, { message });\n}\n\nexport async function currentProjectRole(projectId: string): Promise<ProjectRole | null> {\n const { db, user } = getStore();\n if (!user) {\n return null;\n }\n return await new MemberModel(db).effectiveRole(user.role, projectId, user.id);\n}\n\nexport function requireRole(...roles: ProjectRole[]) {\n return async (c: Context, next: Next): Promise<void> => {\n const projectId = c.req.param(\"projectId\") ?? c.req.param(\"slug\");\n if (!projectId) {\n forbidden();\n }\n const role = await currentProjectRole(projectId);\n if (!role || !roles.includes(role)) {\n forbidden();\n }\n await next();\n };\n}\n\nexport async function findProjectBySlug(slug: string): Promise<Project | null> {\n const { db } = getStore();\n const rows = await db.list(projects, { where: eq(projects.slug, slug), limit: 1 });\n return rows[0] ?? null;\n}\n\nasync function resolveProjectByToken(c: Context, slug: string): Promise<Project | null> {\n const authHeader = c.req.header(\"authorization\");\n if (authHeader?.startsWith(\"Bearer \")) {\n const token = authHeader.slice(\"Bearer \".length);\n const found = await new TokenModel(getStore().db).findByHash(sha256(token));\n if (!found) {\n unauthorized();\n }\n const project = await new ProjectModel(getStore().db).get(found.projectId);\n if (!project || project.slug !== slug) {\n forbidden();\n }\n return project;\n }\n return null;\n}\n\nexport async function resolveProject(c: Context, slug: string): Promise<Project> {\n const project = await resolveProjectByToken(c, slug);\n if (project) return project;\n const found = await findProjectBySlug(slug);\n if (!found) {\n notFound(\"Project not found\");\n }\n return found;\n}\n\n/**\n * Enforce the caller's project role for a given project.\n *\n * Authorization model (ADR 0008):\n * - No auth adapter configured -> all operations permitted (development mode).\n * - Bearer token (CLI) -> grants access to its own project regardless of role.\n * - Session user -> must have an effective role in `minRoles`.\n */\nexport async function assertRole(projectId: string, ...minRoles: ProjectRole[]): Promise<void> {\n if (!getStore().authEnabled) {\n return;\n }\n const role = await currentProjectRole(projectId);\n if (!role || !minRoles.includes(role)) {\n forbidden();\n }\n}\n\nexport async function resolveAuthorizedProject(c: Context, slug: string, ...minRoles: ProjectRole[]): Promise<Project> {\n const project = await resolveProjectByToken(c, slug);\n if (project) return project;\n const found = await findProjectBySlug(slug);\n if (!found) {\n notFound(\"Project not found\");\n }\n await assertRole(found.id, ...minRoles);\n return found;\n}\n\n/** Require a site-level admin (or permit when auth is disabled). */\nexport function requireSiteAdmin(): void {\n if (!getStore().authEnabled) {\n return;\n }\n const { user } = getStore();\n if (user?.role !== \"admin\") {\n forbidden();\n }\n}\n\n/** Require the current session user to hold one of the given project roles. */\nexport async function requireProjectRole(projectId: string, ...minRoles: ProjectRole[]): Promise<void> {\n await assertRole(projectId, ...minRoles);\n}\n"],"mappings":";;;;;;;;;;;;AA0BA,SAAgB,eAAsB;CACpC,MAAM,IAAI,cAAc,KAAK,EAAE,SAAS,eAAe,CAAC;AAC1D;AAEA,SAAgB,YAAmB;CACjC,MAAM,IAAI,cAAc,KAAK,EAAE,SAAS,YAAY,CAAC;AACvD;AAEA,SAAgB,SAAS,UAAU,aAAoB;CACrD,MAAM,IAAI,cAAc,KAAK,EAAE,QAAQ,CAAC;AAC1C;AAEA,eAAsB,mBAAmB,WAAgD;CACvF,MAAM,EAAE,IAAI,SAAS,SAAS;CAC9B,IAAI,CAAC,MACH,OAAO;CAET,OAAO,MAAM,IAAI,YAAY,EAAE,CAAC,CAAC,cAAc,KAAK,MAAM,WAAW,KAAK,EAAE;AAC9E;AAgBA,eAAsB,kBAAkB,MAAuC;CAC7E,MAAM,EAAE,OAAO,SAAS;CAExB,QAAO,MADY,GAAG,KAAK,UAAU;EAAE,OAAO,GAAG,SAAS,MAAM,IAAI;EAAG,OAAO;CAAE,CAAC,EAAA,CACrE,MAAM;AACpB;AAEA,eAAe,sBAAsB,GAAY,MAAuC;CACtF,MAAM,aAAa,EAAE,IAAI,OAAO,eAAe;CAC/C,IAAI,YAAY,WAAW,SAAS,GAAG;EACrC,MAAM,QAAQ,WAAW,MAAM,CAAgB;EAC/C,MAAM,QAAQ,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,WAAW,OAAO,KAAK,CAAC;EAC1E,IAAI,CAAC,OACH,aAAa;EAEf,MAAM,UAAU,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,SAAS;EACzE,IAAI,CAAC,WAAW,QAAQ,SAAS,MAC/B,UAAU;EAEZ,OAAO;CACT;CACA,OAAO;AACT;;;;;;;;;AAoBA,eAAsB,WAAW,WAAmB,GAAG,UAAwC;CAC7F,IAAI,CAAC,SAAS,CAAC,CAAC,aACd;CAEF,MAAM,OAAO,MAAM,mBAAmB,SAAS;CAC/C,IAAI,CAAC,QAAQ,CAAC,SAAS,SAAS,IAAI,GAClC,UAAU;AAEd;AAEA,eAAsB,yBAAyB,GAAY,MAAc,GAAG,UAA2C;CACrH,MAAM,UAAU,MAAM,sBAAsB,GAAG,IAAI;CACnD,IAAI,SAAS,OAAO;CACpB,MAAM,QAAQ,MAAM,kBAAkB,IAAI;CAC1C,IAAI,CAAC,OACH,SAAS,mBAAmB;CAE9B,MAAM,WAAW,MAAM,IAAI,GAAG,QAAQ;CACtC,OAAO;AACT;;AAGA,SAAgB,mBAAyB;CACvC,IAAI,CAAC,SAAS,CAAC,CAAC,aACd;CAEF,MAAM,EAAE,SAAS,SAAS;CAC1B,IAAI,MAAM,SAAS,SACjB,UAAU;AAEd"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
//#region src/routers/htmx.ts
|
|
5
|
+
function isHxRequest(c) {
|
|
6
|
+
return c.req.header("HX-Request") === "true";
|
|
7
|
+
}
|
|
8
|
+
function hxRedirect(c, url) {
|
|
9
|
+
if (isHxRequest(c)) {
|
|
10
|
+
c.header("HX-Redirect", url);
|
|
11
|
+
return c.body(null, 204);
|
|
12
|
+
}
|
|
13
|
+
return c.redirect(url, 302);
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
export { hxRedirect, isHxRequest };
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=htmx.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"htmx.mjs","names":[],"sources":["../../src/routers/htmx.ts"],"sourcesContent":["import type { Context } from \"hono\";\n\nexport function isHxRequest(c: Context): boolean {\n return c.req.header(\"HX-Request\") === \"true\";\n}\n\nexport function hxRedirect(c: Context, url: string): Response {\n if (isHxRequest(c)) {\n c.header(\"HX-Redirect\", url);\n return c.body(null, 204);\n }\n return c.redirect(url, 302);\n}"],"mappings":";;;;AAEA,SAAgB,YAAY,GAAqB;CAC/C,OAAO,EAAE,IAAI,OAAO,YAAY,MAAM;AACxC;AAEA,SAAgB,WAAW,GAAY,KAAuB;CAC5D,IAAI,YAAY,CAAC,GAAG;EAClB,EAAE,OAAO,eAAe,GAAG;EAC3B,OAAO,EAAE,KAAK,MAAM,GAAG;CACzB;CACA,OAAO,EAAE,SAAS,KAAK,GAAG;AAC5B"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { LabelModel } from "../models/label.mjs";
|
|
5
|
+
import { getStore } from "../store.mjs";
|
|
6
|
+
import { resolveAuthorizedProject } from "./helpers.mjs";
|
|
7
|
+
import { labelTypeCreateSchema, labelTypeSchema, notFound, unauthorized } from "./schemas.mjs";
|
|
8
|
+
import { createRoute, z } from "@hono/zod-openapi";
|
|
9
|
+
//#region src/routers/labels.ts
|
|
10
|
+
const VIEW_ROLES = [
|
|
11
|
+
"viewer",
|
|
12
|
+
"developer",
|
|
13
|
+
"approver",
|
|
14
|
+
"admin"
|
|
15
|
+
];
|
|
16
|
+
const ADMIN_ROLES = ["admin"];
|
|
17
|
+
const listLabelsRoute = createRoute({
|
|
18
|
+
method: "get",
|
|
19
|
+
path: "/api/v1/projects/{slug}/label-types",
|
|
20
|
+
request: { params: z.object({ slug: z.string() }) },
|
|
21
|
+
responses: {
|
|
22
|
+
200: {
|
|
23
|
+
content: { "application/json": { schema: labelTypeSchema.array() } },
|
|
24
|
+
description: "List label types"
|
|
25
|
+
},
|
|
26
|
+
...notFound,
|
|
27
|
+
...unauthorized
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
const createLabelRoute = createRoute({
|
|
31
|
+
method: "post",
|
|
32
|
+
path: "/api/v1/projects/{slug}/label-types",
|
|
33
|
+
request: {
|
|
34
|
+
params: z.object({ slug: z.string() }),
|
|
35
|
+
body: { content: { "application/json": { schema: labelTypeCreateSchema } } }
|
|
36
|
+
},
|
|
37
|
+
responses: {
|
|
38
|
+
201: {
|
|
39
|
+
content: { "application/json": { schema: labelTypeSchema } },
|
|
40
|
+
description: "Created label type"
|
|
41
|
+
},
|
|
42
|
+
...notFound
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
const deleteLabelRoute = createRoute({
|
|
46
|
+
method: "delete",
|
|
47
|
+
path: "/api/v1/projects/{slug}/label-types/{key}",
|
|
48
|
+
request: { params: z.object({
|
|
49
|
+
slug: z.string(),
|
|
50
|
+
key: z.string()
|
|
51
|
+
}) },
|
|
52
|
+
responses: {
|
|
53
|
+
204: { description: "Deleted label type" },
|
|
54
|
+
...notFound
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
function registerLabels(app) {
|
|
58
|
+
app.openapi(listLabelsRoute, async (c) => {
|
|
59
|
+
const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...VIEW_ROLES);
|
|
60
|
+
return c.json(await new LabelModel(getStore().db).listTypes(project.id));
|
|
61
|
+
});
|
|
62
|
+
app.openapi(createLabelRoute, async (c) => {
|
|
63
|
+
const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...ADMIN_ROLES);
|
|
64
|
+
const body = c.req.valid("json");
|
|
65
|
+
return c.json(await new LabelModel(getStore().db).createType(project.id, body), 201);
|
|
66
|
+
});
|
|
67
|
+
app.openapi(deleteLabelRoute, async (c) => {
|
|
68
|
+
const { slug, key } = c.req.valid("param");
|
|
69
|
+
const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);
|
|
70
|
+
await new LabelModel(getStore().db).removeType(project.id, key);
|
|
71
|
+
return c.body(null, 204);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
export { registerLabels };
|
|
76
|
+
|
|
77
|
+
//# sourceMappingURL=labels.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"labels.mjs","names":[],"sources":["../../src/routers/labels.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport type { ShelfApp } from \"../index.tsx\";\n\nimport { LabelModel } from \"../models/label.ts\";\nimport { getStore } from \"../store.ts\";\nimport type { ProjectRole } from \"../types.ts\";\nimport { resolveAuthorizedProject } from \"./helpers.ts\";\nimport { labelTypeCreateSchema, labelTypeSchema, notFound, unauthorized } from \"./schemas.ts\";\n\nconst VIEW_ROLES: readonly ProjectRole[] = [\"viewer\", \"developer\", \"approver\", \"admin\"];\nconst ADMIN_ROLES: readonly ProjectRole[] = [\"admin\"];\n\nconst listLabelsRoute = createRoute({\n method: \"get\",\n path: \"/api/v1/projects/{slug}/label-types\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: { content: { \"application/json\": { schema: labelTypeSchema.array() } }, description: \"List label types\" },\n ...notFound,\n ...unauthorized,\n },\n});\n\nconst createLabelRoute = createRoute({\n method: \"post\",\n path: \"/api/v1/projects/{slug}/label-types\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: labelTypeCreateSchema } } },\n },\n responses: {\n 201: { content: { \"application/json\": { schema: labelTypeSchema } }, description: \"Created label type\" },\n ...notFound,\n },\n});\n\nconst deleteLabelRoute = createRoute({\n method: \"delete\",\n path: \"/api/v1/projects/{slug}/label-types/{key}\",\n request: { params: z.object({ slug: z.string(), key: z.string() }) },\n responses: {\n 204: { description: \"Deleted label type\" },\n ...notFound,\n },\n});\n\nexport function registerLabels(app: ShelfApp): void {\n app.openapi(listLabelsRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...VIEW_ROLES);\n return c.json(await new LabelModel(getStore().db).listTypes(project.id));\n });\n\n app.openapi(createLabelRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n return c.json(await new LabelModel(getStore().db).createType(project.id, body), 201);\n });\n\n app.openapi(deleteLabelRoute, async (c) => {\n const { slug, key } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n await new LabelModel(getStore().db).removeType(project.id, key);\n return c.body(null, 204);\n });\n}"],"mappings":";;;;;;;;;AASA,MAAM,aAAqC;CAAC;CAAU;CAAa;CAAY;AAAO;AACtF,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,gBAAgB,MAAM,EAAE,EAAE;GAAG,aAAa;EAAmB;EAC7G,GAAG;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,sBAAsB,EAAE,EAAE;CAC7E;CACA,WAAW;EACT,KAAK;GAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,gBAAgB,EAAE;GAAG,aAAa;EAAqB;EACvG,GAAG;CACL;AACF,CAAC;AAED,MAAM,mBAAmB,YAAY;CACnC,QAAQ;CACR,MAAM;CACN,SAAS,EAAE,QAAQ,EAAE,OAAO;EAAE,MAAM,EAAE,OAAO;EAAG,KAAK,EAAE,OAAO;CAAE,CAAC,EAAE;CACnE,WAAW;EACT,KAAK,EAAE,aAAa,qBAAqB;EACzC,GAAG;CACL;AACF,CAAC;AAED,SAAgB,eAAe,KAAqB;CAClD,IAAI,QAAQ,iBAAiB,OAAO,MAAM;EACxC,MAAM,UAAU,MAAM,yBAAyB,GAAG,EAAE,IAAI,MAAM,OAAO,CAAC,CAAC,MAAM,GAAG,UAAU;EAC1F,OAAO,EAAE,KAAK,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,UAAU,QAAQ,EAAE,CAAC;CACzE,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,OAAO,EAAE,KAAK,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,WAAW,QAAQ,IAAI,IAAI,GAAG,GAAG;CACrF,CAAC;CAED,IAAI,QAAQ,kBAAkB,OAAO,MAAM;EACzC,MAAM,EAAE,MAAM,QAAQ,EAAE,IAAI,MAAM,OAAO;EACzC,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,WAAW;EACtE,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,WAAW,QAAQ,IAAI,GAAG;EAC9D,OAAO,EAAE,KAAK,MAAM,GAAG;CACzB,CAAC;AACH"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { BuildModel } from "../models/build.mjs";
|
|
5
|
+
import { getStore } from "../store.mjs";
|
|
6
|
+
import { notFound, resolveAuthorizedProject } from "./helpers.mjs";
|
|
7
|
+
import { BaselineModel } from "../models/baseline.mjs";
|
|
8
|
+
import { SnapshotModel } from "../models/snapshot.mjs";
|
|
9
|
+
//#region src/routers/media.ts
|
|
10
|
+
const VIEW_ROLES = [
|
|
11
|
+
"viewer",
|
|
12
|
+
"developer",
|
|
13
|
+
"approver",
|
|
14
|
+
"admin"
|
|
15
|
+
];
|
|
16
|
+
const PNG = "image/png";
|
|
17
|
+
function imageResponse(buffer) {
|
|
18
|
+
return new Response(new Uint8Array(buffer), { headers: {
|
|
19
|
+
"content-type": PNG,
|
|
20
|
+
"cache-control": "private, max-age=3600"
|
|
21
|
+
} });
|
|
22
|
+
}
|
|
23
|
+
function registerMedia(app) {
|
|
24
|
+
app.get("/api/v1/projects/:slug/builds/:buildId/snapshots/:snapshotId/image", async (c) => {
|
|
25
|
+
const project = await resolveAuthorizedProject(c, c.req.param("slug"), ...VIEW_ROLES);
|
|
26
|
+
const snapshot = await findSnapshot(c.req.param("buildId"), c.req.param("snapshotId"), project.id);
|
|
27
|
+
return imageResponse(await getStore().storage.read(snapshot.screenshotPath));
|
|
28
|
+
});
|
|
29
|
+
app.get("/api/v1/projects/:slug/builds/:buildId/snapshots/:snapshotId/diff", async (c) => {
|
|
30
|
+
const project = await resolveAuthorizedProject(c, c.req.param("slug"), ...VIEW_ROLES);
|
|
31
|
+
const snapshot = await findSnapshot(c.req.param("buildId"), c.req.param("snapshotId"), project.id);
|
|
32
|
+
if (!snapshot.diffPath) notFound("No diff for this snapshot");
|
|
33
|
+
return imageResponse(await getStore().storage.read(snapshot.diffPath));
|
|
34
|
+
});
|
|
35
|
+
app.get("/api/v1/projects/:slug/builds/:buildId/snapshots/:snapshotId/baseline", async (c) => {
|
|
36
|
+
const project = await resolveAuthorizedProject(c, c.req.param("slug"), ...VIEW_ROLES);
|
|
37
|
+
const snapshot = await findSnapshot(c.req.param("buildId"), c.req.param("snapshotId"), project.id);
|
|
38
|
+
const build = await new BuildModel(getStore().db).get(snapshot.buildId);
|
|
39
|
+
if (!build) notFound("Build not found");
|
|
40
|
+
const baselines = new BaselineModel(getStore().db, getStore().storage);
|
|
41
|
+
const baseline = await baselines.resolve(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, project.gitDefaultBranch);
|
|
42
|
+
if (!baseline) notFound("No baseline for this snapshot");
|
|
43
|
+
return imageResponse(await baselines.read(baseline));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async function findSnapshot(buildId, snapshotId, projectId) {
|
|
47
|
+
const snapshot = await new SnapshotModel(getStore().db).get(snapshotId);
|
|
48
|
+
if (!snapshot || snapshot.buildId !== buildId || snapshot.projectId !== projectId) notFound("Snapshot not found");
|
|
49
|
+
return snapshot;
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
export { registerMedia };
|
|
53
|
+
|
|
54
|
+
//# sourceMappingURL=media.mjs.map
|