@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,89 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { baselines } from "../schema.mjs";
|
|
5
|
+
import { ulid } from "../utils/ulid.mjs";
|
|
6
|
+
import { emitWebhookEvent } from "../adapters/webhook-events.mjs";
|
|
7
|
+
import { baselinePath } from "../utils/paths.mjs";
|
|
8
|
+
import { and, eq } from "drizzle-orm";
|
|
9
|
+
//#region src/models/baseline.ts
|
|
10
|
+
var BaselineModel = class {
|
|
11
|
+
db;
|
|
12
|
+
storage;
|
|
13
|
+
constructor(db, storage) {
|
|
14
|
+
this.db = db;
|
|
15
|
+
this.storage = storage;
|
|
16
|
+
}
|
|
17
|
+
async getFor(projectId, storyId, viewport, branch) {
|
|
18
|
+
return (await this.db.list(baselines, {
|
|
19
|
+
where: and(eq(baselines.projectId, projectId), eq(baselines.storyId, storyId), eq(baselines.viewportName, viewport), eq(baselines.branch, branch)),
|
|
20
|
+
limit: 1
|
|
21
|
+
}))[0] ?? null;
|
|
22
|
+
}
|
|
23
|
+
async resolve(projectId, storyId, viewport, branch, defaultBranch) {
|
|
24
|
+
if (branch !== defaultBranch) {
|
|
25
|
+
const own = await this.getFor(projectId, storyId, viewport, branch);
|
|
26
|
+
if (own) return own;
|
|
27
|
+
}
|
|
28
|
+
return this.getFor(projectId, storyId, viewport, defaultBranch);
|
|
29
|
+
}
|
|
30
|
+
async read(baseline) {
|
|
31
|
+
return await this.storage.read(baseline.screenshotPath);
|
|
32
|
+
}
|
|
33
|
+
async upsert(projectId, storyId, viewport, branch, snapshotId, sourcePath) {
|
|
34
|
+
const screenshotPath = baselinePath(projectId, branch, storyId, viewport);
|
|
35
|
+
const source = await this.storage.read(sourcePath);
|
|
36
|
+
await this.storage.write(screenshotPath, source);
|
|
37
|
+
const existing = await this.getFor(projectId, storyId, viewport, branch);
|
|
38
|
+
let baseline;
|
|
39
|
+
if (existing) {
|
|
40
|
+
baseline = await this.db.update(baselines, existing.id, {
|
|
41
|
+
snapshotId,
|
|
42
|
+
screenshotPath,
|
|
43
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
44
|
+
});
|
|
45
|
+
await emitWebhookEvent(this.db, projectId, "baseline:updated", {
|
|
46
|
+
baselineId: baseline.id,
|
|
47
|
+
storyId,
|
|
48
|
+
viewport,
|
|
49
|
+
branch,
|
|
50
|
+
snapshotId
|
|
51
|
+
});
|
|
52
|
+
} else {
|
|
53
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
54
|
+
baseline = await this.db.insert(baselines, {
|
|
55
|
+
id: ulid(),
|
|
56
|
+
projectId,
|
|
57
|
+
storyId,
|
|
58
|
+
viewportName: viewport,
|
|
59
|
+
branch,
|
|
60
|
+
snapshotId,
|
|
61
|
+
screenshotPath,
|
|
62
|
+
createdAt: now,
|
|
63
|
+
updatedAt: now
|
|
64
|
+
});
|
|
65
|
+
await emitWebhookEvent(this.db, projectId, "baseline:created", {
|
|
66
|
+
baselineId: baseline.id,
|
|
67
|
+
storyId,
|
|
68
|
+
viewport,
|
|
69
|
+
branch,
|
|
70
|
+
snapshotId
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return baseline;
|
|
74
|
+
}
|
|
75
|
+
async list(projectId) {
|
|
76
|
+
return await this.db.list(baselines, { where: eq(baselines.projectId, projectId) });
|
|
77
|
+
}
|
|
78
|
+
async removeOrphans(projectId, validStoryIds) {
|
|
79
|
+
const toRemove = (await this.list(projectId)).filter((baseline) => !validStoryIds.has(baseline.storyId));
|
|
80
|
+
await Promise.all(toRemove.map(async (baseline) => {
|
|
81
|
+
await this.db.remove(baselines, baseline.id);
|
|
82
|
+
}));
|
|
83
|
+
return toRemove.length;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
//#endregion
|
|
87
|
+
export { BaselineModel };
|
|
88
|
+
|
|
89
|
+
//# sourceMappingURL=baseline.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"baseline.mjs","names":[],"sources":["../../src/models/baseline.ts"],"sourcesContent":["import { and, eq } from \"drizzle-orm\";\n\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport { emitWebhookEvent } from \"../adapters/webhook-events.ts\";\nimport { baselines, type Baseline } from \"../schema.ts\";\nimport { baselinePath } from \"../utils/paths.ts\";\nimport { ulid } from \"../utils/ulid.ts\";\n\nexport class BaselineModel {\n constructor(\n private readonly db: DatabaseAdapter,\n private readonly storage: StorageAdapter,\n ) {}\n\n async getFor(projectId: string, storyId: string, viewport: string, branch: string): Promise<Baseline | null> {\n const rows = await this.db.list(baselines, {\n where: and(eq(baselines.projectId, projectId), eq(baselines.storyId, storyId), eq(baselines.viewportName, viewport), eq(baselines.branch, branch)),\n limit: 1,\n });\n return rows[0] ?? null;\n }\n\n async resolve(projectId: string, storyId: string, viewport: string, branch: string, defaultBranch: string): Promise<Baseline | null> {\n if (branch !== defaultBranch) {\n const own = await this.getFor(projectId, storyId, viewport, branch);\n if (own) {\n return own;\n }\n }\n return this.getFor(projectId, storyId, viewport, defaultBranch);\n }\n\n async read(baseline: Baseline): Promise<Buffer> {\n return await this.storage.read(baseline.screenshotPath);\n }\n\n async upsert(\n projectId: string,\n storyId: string,\n viewport: string,\n branch: string,\n snapshotId: string,\n sourcePath: string,\n ): Promise<Baseline> {\n const screenshotPath = baselinePath(projectId, branch, storyId, viewport);\n const source = await this.storage.read(sourcePath);\n await this.storage.write(screenshotPath, source);\n\n const existing = await this.getFor(projectId, storyId, viewport, branch);\n let baseline: Baseline;\n if (existing) {\n baseline = await this.db.update(baselines, existing.id, {\n snapshotId,\n screenshotPath,\n updatedAt: new Date().toISOString(),\n });\n await emitWebhookEvent(this.db, projectId, \"baseline:updated\", {\n baselineId: baseline.id,\n storyId,\n viewport,\n branch,\n snapshotId,\n });\n } else {\n const now = new Date().toISOString();\n baseline = await this.db.insert(baselines, {\n id: ulid(),\n projectId,\n storyId,\n viewportName: viewport,\n branch,\n snapshotId,\n screenshotPath,\n createdAt: now,\n updatedAt: now,\n });\n await emitWebhookEvent(this.db, projectId, \"baseline:created\", {\n baselineId: baseline.id,\n storyId,\n viewport,\n branch,\n snapshotId,\n });\n }\n return baseline;\n }\n\n async list(projectId: string): Promise<Baseline[]> {\n return await this.db.list(baselines, { where: eq(baselines.projectId, projectId) });\n }\n\n async removeOrphans(projectId: string, validStoryIds: Set<string>): Promise<number> {\n const all = await this.list(projectId);\n const toRemove = all.filter((baseline) => !validStoryIds.has(baseline.storyId));\n await Promise.all(\n toRemove.map(async (baseline) => {\n await this.db.remove(baselines, baseline.id);\n }),\n );\n return toRemove.length;\n }\n}\n\nexport type { Baseline };\n"],"mappings":";;;;;;;;;AASA,IAAa,gBAAb,MAA2B;CAEN;CACA;CAFnB,YACE,IACA,SACA;EAFiB,KAAA,KAAA;EACA,KAAA,UAAA;CAChB;CAEH,MAAM,OAAO,WAAmB,SAAiB,UAAkB,QAA0C;EAK3G,QAAO,MAJY,KAAK,GAAG,KAAK,WAAW;GACzC,OAAO,IAAI,GAAG,UAAU,WAAW,SAAS,GAAG,GAAG,UAAU,SAAS,OAAO,GAAG,GAAG,UAAU,cAAc,QAAQ,GAAG,GAAG,UAAU,QAAQ,MAAM,CAAC;GACjJ,OAAO;EACT,CAAC,EAAA,CACW,MAAM;CACpB;CAEA,MAAM,QAAQ,WAAmB,SAAiB,UAAkB,QAAgB,eAAiD;EACnI,IAAI,WAAW,eAAe;GAC5B,MAAM,MAAM,MAAM,KAAK,OAAO,WAAW,SAAS,UAAU,MAAM;GAClE,IAAI,KACF,OAAO;EAEX;EACA,OAAO,KAAK,OAAO,WAAW,SAAS,UAAU,aAAa;CAChE;CAEA,MAAM,KAAK,UAAqC;EAC9C,OAAO,MAAM,KAAK,QAAQ,KAAK,SAAS,cAAc;CACxD;CAEA,MAAM,OACJ,WACA,SACA,UACA,QACA,YACA,YACmB;EACnB,MAAM,iBAAiB,aAAa,WAAW,QAAQ,SAAS,QAAQ;EACxE,MAAM,SAAS,MAAM,KAAK,QAAQ,KAAK,UAAU;EACjD,MAAM,KAAK,QAAQ,MAAM,gBAAgB,MAAM;EAE/C,MAAM,WAAW,MAAM,KAAK,OAAO,WAAW,SAAS,UAAU,MAAM;EACvE,IAAI;EACJ,IAAI,UAAU;GACZ,WAAW,MAAM,KAAK,GAAG,OAAO,WAAW,SAAS,IAAI;IACtD;IACA;IACA,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;GACpC,CAAC;GACD,MAAM,iBAAiB,KAAK,IAAI,WAAW,oBAAoB;IAC7D,YAAY,SAAS;IACrB;IACA;IACA;IACA;GACF,CAAC;EACH,OAAO;GACL,MAAM,uBAAM,IAAI,KAAK,EAAA,CAAE,YAAY;GACnC,WAAW,MAAM,KAAK,GAAG,OAAO,WAAW;IACzC,IAAI,KAAK;IACT;IACA;IACA,cAAc;IACd;IACA;IACA;IACA,WAAW;IACX,WAAW;GACb,CAAC;GACD,MAAM,iBAAiB,KAAK,IAAI,WAAW,oBAAoB;IAC7D,YAAY,SAAS;IACrB;IACA;IACA;IACA;GACF,CAAC;EACH;EACA,OAAO;CACT;CAEA,MAAM,KAAK,WAAwC;EACjD,OAAO,MAAM,KAAK,GAAG,KAAK,WAAW,EAAE,OAAO,GAAG,UAAU,WAAW,SAAS,EAAE,CAAC;CACpF;CAEA,MAAM,cAAc,WAAmB,eAA6C;EAElF,MAAM,YAAW,MADC,KAAK,KAAK,SAAS,EAAA,CAChB,QAAQ,aAAa,CAAC,cAAc,IAAI,SAAS,OAAO,CAAC;EAC9E,MAAM,QAAQ,IACZ,SAAS,IAAI,OAAO,aAAa;GAC/B,MAAM,KAAK,GAAG,OAAO,WAAW,SAAS,EAAE;EAC7C,CAAC,CACH;EACA,OAAO,SAAS;CAClB;AACF"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { BuildStatus } from "../types.mjs";
|
|
4
|
+
import { DatabaseAdapter } from "../adapter/database.mjs";
|
|
5
|
+
import { Build } from "../schema.mjs";
|
|
6
|
+
//#region src/models/build.d.ts
|
|
7
|
+
interface BuildCreateInput {
|
|
8
|
+
gitSha: string;
|
|
9
|
+
gitBranch: string;
|
|
10
|
+
isDefault?: boolean;
|
|
11
|
+
authorEmail?: string;
|
|
12
|
+
authorName?: string;
|
|
13
|
+
message?: string;
|
|
14
|
+
public?: boolean;
|
|
15
|
+
}
|
|
16
|
+
interface BuildListFilter {
|
|
17
|
+
status?: BuildStatus;
|
|
18
|
+
branch?: string;
|
|
19
|
+
labelKey?: string;
|
|
20
|
+
labelValue?: string;
|
|
21
|
+
}
|
|
22
|
+
declare class BuildModel {
|
|
23
|
+
private readonly db;
|
|
24
|
+
constructor(db: DatabaseAdapter);
|
|
25
|
+
create(projectId: string, input: BuildCreateInput): Promise<Build>;
|
|
26
|
+
get(id: string): Promise<Build | null>;
|
|
27
|
+
list(projectId: string, filter?: BuildListFilter): Promise<Build[]>;
|
|
28
|
+
update(id: string, patch: Partial<Pick<Build, "status" | "public" | "message" | "authorEmail" | "authorName">>): Promise<Build>;
|
|
29
|
+
setStatus(id: string, status: BuildStatus): Promise<Build>;
|
|
30
|
+
updateCounts(id: string): Promise<Build>;
|
|
31
|
+
remove(id: string): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { type Build, BuildCreateInput, BuildListFilter, BuildModel };
|
|
35
|
+
//# sourceMappingURL=build.d.mts.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { buildLabels, builds, snapshots } from "../schema.mjs";
|
|
5
|
+
import { ulid } from "../utils/ulid.mjs";
|
|
6
|
+
import { and, desc, eq, inArray } from "drizzle-orm";
|
|
7
|
+
//#region src/models/build.ts
|
|
8
|
+
var BuildModel = class {
|
|
9
|
+
db;
|
|
10
|
+
constructor(db) {
|
|
11
|
+
this.db = db;
|
|
12
|
+
}
|
|
13
|
+
async create(projectId, input) {
|
|
14
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
15
|
+
return await this.db.insert(builds, {
|
|
16
|
+
id: ulid(),
|
|
17
|
+
projectId,
|
|
18
|
+
gitSha: input.gitSha,
|
|
19
|
+
gitBranch: input.gitBranch,
|
|
20
|
+
isDefault: input.isDefault ?? false,
|
|
21
|
+
authorEmail: input.authorEmail,
|
|
22
|
+
authorName: input.authorName,
|
|
23
|
+
message: input.message,
|
|
24
|
+
public: input.public ?? false,
|
|
25
|
+
status: "pending",
|
|
26
|
+
createdAt: now,
|
|
27
|
+
updatedAt: now
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
async get(id) {
|
|
31
|
+
return await this.db.get(builds, id);
|
|
32
|
+
}
|
|
33
|
+
async list(projectId, filter = {}) {
|
|
34
|
+
const conditions = [eq(builds.projectId, projectId)];
|
|
35
|
+
if (filter.status) conditions.push(eq(builds.status, filter.status));
|
|
36
|
+
if (filter.branch) conditions.push(eq(builds.gitBranch, filter.branch));
|
|
37
|
+
if (filter.labelKey && filter.labelValue) {
|
|
38
|
+
const ids = (await this.db.list(buildLabels, { where: and(eq(buildLabels.projectId, projectId), eq(buildLabels.typeKey, filter.labelKey), eq(buildLabels.value, filter.labelValue)) })).map((l) => l.buildId);
|
|
39
|
+
if (ids.length === 0) return [];
|
|
40
|
+
conditions.push(inArray(builds.id, ids));
|
|
41
|
+
}
|
|
42
|
+
return this.db.list(builds, {
|
|
43
|
+
where: and(...conditions),
|
|
44
|
+
orderBy: desc(builds.createdAt)
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async update(id, patch) {
|
|
48
|
+
return await this.db.update(builds, id, {
|
|
49
|
+
...patch,
|
|
50
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async setStatus(id, status) {
|
|
54
|
+
return await this.update(id, { status });
|
|
55
|
+
}
|
|
56
|
+
async updateCounts(id) {
|
|
57
|
+
const rows = await this.db.list(snapshots, { where: eq(snapshots.buildId, id) });
|
|
58
|
+
const snapshotCount = rows.length;
|
|
59
|
+
const changedCount = rows.filter((s) => s.status === "changed" || s.status === "new").length;
|
|
60
|
+
const approvedCount = rows.filter((s) => s.status === "approved" || s.status === "unchanged").length;
|
|
61
|
+
const rejectedCount = rows.filter((s) => s.status === "rejected").length;
|
|
62
|
+
return this.db.update(builds, id, {
|
|
63
|
+
snapshotCount,
|
|
64
|
+
changedCount,
|
|
65
|
+
approvedCount,
|
|
66
|
+
rejectedCount,
|
|
67
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
async remove(id) {
|
|
71
|
+
await this.db.remove(builds, id);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
//#endregion
|
|
75
|
+
export { BuildModel };
|
|
76
|
+
|
|
77
|
+
//# sourceMappingURL=build.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build.mjs","names":[],"sources":["../../src/models/build.ts"],"sourcesContent":["import { and, desc, eq, inArray } from \"drizzle-orm\";\n\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport { buildLabels, builds, snapshots, type Build } from \"../schema.ts\";\nimport type { BuildStatus } from \"../types.ts\";\nimport { ulid } from \"../utils/ulid.ts\";\n\nexport interface BuildCreateInput {\n gitSha: string;\n gitBranch: string;\n isDefault?: boolean;\n authorEmail?: string;\n authorName?: string;\n message?: string;\n public?: boolean;\n}\n\nexport interface BuildListFilter {\n status?: BuildStatus;\n branch?: string;\n labelKey?: string;\n labelValue?: string;\n}\n\nexport class BuildModel {\n constructor(private readonly db: DatabaseAdapter) {}\n\n async create(projectId: string, input: BuildCreateInput): Promise<Build> {\n const now = new Date().toISOString();\n return await this.db.insert(builds, {\n id: ulid(),\n projectId,\n gitSha: input.gitSha,\n gitBranch: input.gitBranch,\n isDefault: input.isDefault ?? false,\n authorEmail: input.authorEmail,\n authorName: input.authorName,\n message: input.message,\n public: input.public ?? false,\n status: \"pending\",\n createdAt: now,\n updatedAt: now,\n });\n }\n\n async get(id: string): Promise<Build | null> {\n return await this.db.get(builds, id);\n }\n\n async list(projectId: string, filter: BuildListFilter = {}): Promise<Build[]> {\n const conditions = [eq(builds.projectId, projectId)];\n if (filter.status) {\n conditions.push(eq(builds.status, filter.status));\n }\n if (filter.branch) {\n conditions.push(eq(builds.gitBranch, filter.branch));\n }\n if (filter.labelKey && filter.labelValue) {\n const labels = await this.db.list(buildLabels, {\n where: and(eq(buildLabels.projectId, projectId), eq(buildLabels.typeKey, filter.labelKey), eq(buildLabels.value, filter.labelValue)),\n });\n const ids = labels.map((l) => l.buildId);\n if (ids.length === 0) {\n return [];\n }\n conditions.push(inArray(builds.id, ids));\n }\n return this.db.list(builds, { where: and(...conditions), orderBy: desc(builds.createdAt) });\n }\n\n async update(id: string, patch: Partial<Pick<Build, \"status\" | \"public\" | \"message\" | \"authorEmail\" | \"authorName\">>): Promise<Build> {\n return await this.db.update(builds, id, { ...patch, updatedAt: new Date().toISOString() });\n }\n\n async setStatus(id: string, status: BuildStatus): Promise<Build> {\n return await this.update(id, { status });\n }\n\n async updateCounts(id: string): Promise<Build> {\n const rows = await this.db.list(snapshots, { where: eq(snapshots.buildId, id) });\n const snapshotCount = rows.length;\n const changedCount = rows.filter((s) => s.status === \"changed\" || s.status === \"new\").length;\n const approvedCount = rows.filter((s) => s.status === \"approved\" || s.status === \"unchanged\").length;\n const rejectedCount = rows.filter((s) => s.status === \"rejected\").length;\n return this.db.update(builds, id, {\n snapshotCount,\n changedCount,\n approvedCount,\n rejectedCount,\n updatedAt: new Date().toISOString(),\n });\n }\n\n async remove(id: string): Promise<void> {\n await this.db.remove(builds, id);\n }\n}\n\nexport type { Build };\n"],"mappings":";;;;;;;AAwBA,IAAa,aAAb,MAAwB;CACO;CAA7B,YAAY,IAAsC;EAArB,KAAA,KAAA;CAAsB;CAEnD,MAAM,OAAO,WAAmB,OAAyC;EACvE,MAAM,uBAAM,IAAI,KAAK,EAAA,CAAE,YAAY;EACnC,OAAO,MAAM,KAAK,GAAG,OAAO,QAAQ;GAClC,IAAI,KAAK;GACT;GACA,QAAQ,MAAM;GACd,WAAW,MAAM;GACjB,WAAW,MAAM,aAAa;GAC9B,aAAa,MAAM;GACnB,YAAY,MAAM;GAClB,SAAS,MAAM;GACf,QAAQ,MAAM,UAAU;GACxB,QAAQ;GACR,WAAW;GACX,WAAW;EACb,CAAC;CACH;CAEA,MAAM,IAAI,IAAmC;EAC3C,OAAO,MAAM,KAAK,GAAG,IAAI,QAAQ,EAAE;CACrC;CAEA,MAAM,KAAK,WAAmB,SAA0B,CAAC,GAAqB;EAC5E,MAAM,aAAa,CAAC,GAAG,OAAO,WAAW,SAAS,CAAC;EACnD,IAAI,OAAO,QACT,WAAW,KAAK,GAAG,OAAO,QAAQ,OAAO,MAAM,CAAC;EAElD,IAAI,OAAO,QACT,WAAW,KAAK,GAAG,OAAO,WAAW,OAAO,MAAM,CAAC;EAErD,IAAI,OAAO,YAAY,OAAO,YAAY;GAIxC,MAAM,OAAM,MAHS,KAAK,GAAG,KAAK,aAAa,EAC7C,OAAO,IAAI,GAAG,YAAY,WAAW,SAAS,GAAG,GAAG,YAAY,SAAS,OAAO,QAAQ,GAAG,GAAG,YAAY,OAAO,OAAO,UAAU,CAAC,EACrI,CAAC,EAAA,CACkB,KAAK,MAAM,EAAE,OAAO;GACvC,IAAI,IAAI,WAAW,GACjB,OAAO,CAAC;GAEV,WAAW,KAAK,QAAQ,OAAO,IAAI,GAAG,CAAC;EACzC;EACA,OAAO,KAAK,GAAG,KAAK,QAAQ;GAAE,OAAO,IAAI,GAAG,UAAU;GAAG,SAAS,KAAK,OAAO,SAAS;EAAE,CAAC;CAC5F;CAEA,MAAM,OAAO,IAAY,OAA6G;EACpI,OAAO,MAAM,KAAK,GAAG,OAAO,QAAQ,IAAI;GAAE,GAAG;GAAO,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;EAAE,CAAC;CAC3F;CAEA,MAAM,UAAU,IAAY,QAAqC;EAC/D,OAAO,MAAM,KAAK,OAAO,IAAI,EAAE,OAAO,CAAC;CACzC;CAEA,MAAM,aAAa,IAA4B;EAC7C,MAAM,OAAO,MAAM,KAAK,GAAG,KAAK,WAAW,EAAE,OAAO,GAAG,UAAU,SAAS,EAAE,EAAE,CAAC;EAC/E,MAAM,gBAAgB,KAAK;EAC3B,MAAM,eAAe,KAAK,QAAQ,MAAM,EAAE,WAAW,aAAa,EAAE,WAAW,KAAK,CAAC,CAAC;EACtF,MAAM,gBAAgB,KAAK,QAAQ,MAAM,EAAE,WAAW,cAAc,EAAE,WAAW,WAAW,CAAC,CAAC;EAC9F,MAAM,gBAAgB,KAAK,QAAQ,MAAM,EAAE,WAAW,UAAU,CAAC,CAAC;EAClE,OAAO,KAAK,GAAG,OAAO,QAAQ,IAAI;GAChC;GACA;GACA;GACA;GACA,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;EACpC,CAAC;CACH;CAEA,MAAM,OAAO,IAA2B;EACtC,MAAM,KAAK,GAAG,OAAO,QAAQ,EAAE;CACjC;AACF"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { DatabaseAdapter } from "../adapter/database.mjs";
|
|
4
|
+
import { Comment } from "../schema.mjs";
|
|
5
|
+
//#region src/models/comment.d.ts
|
|
6
|
+
interface CommentCreateInput {
|
|
7
|
+
body: string;
|
|
8
|
+
snapshotId?: string;
|
|
9
|
+
parentId?: string;
|
|
10
|
+
}
|
|
11
|
+
declare class CommentModel {
|
|
12
|
+
private readonly db;
|
|
13
|
+
constructor(db: DatabaseAdapter);
|
|
14
|
+
listByBuild(buildId: string): Promise<Comment[]>;
|
|
15
|
+
create(projectId: string, buildId: string, userId: string, input: CommentCreateInput): Promise<Comment>;
|
|
16
|
+
resolve(id: string): Promise<Comment>;
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
export { type Comment, CommentCreateInput, CommentModel };
|
|
20
|
+
//# sourceMappingURL=comment.d.mts.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { comments, projects } from "../schema.mjs";
|
|
5
|
+
import { ulid } from "../utils/ulid.mjs";
|
|
6
|
+
import { eq } from "drizzle-orm";
|
|
7
|
+
//#region src/models/comment.ts
|
|
8
|
+
var CommentModel = class {
|
|
9
|
+
db;
|
|
10
|
+
constructor(db) {
|
|
11
|
+
this.db = db;
|
|
12
|
+
}
|
|
13
|
+
async listByBuild(buildId) {
|
|
14
|
+
return await this.db.list(comments, { where: eq(comments.buildId, buildId) });
|
|
15
|
+
}
|
|
16
|
+
async create(projectId, buildId, userId, input) {
|
|
17
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
18
|
+
if (!await this.db.get(projects, projectId)) throw new Error(`Project not found: ${projectId}`);
|
|
19
|
+
return await this.db.insert(comments, {
|
|
20
|
+
id: ulid(),
|
|
21
|
+
projectId,
|
|
22
|
+
buildId,
|
|
23
|
+
snapshotId: input.snapshotId,
|
|
24
|
+
userId,
|
|
25
|
+
body: input.body,
|
|
26
|
+
parentId: input.parentId,
|
|
27
|
+
resolved: false,
|
|
28
|
+
createdAt: now,
|
|
29
|
+
updatedAt: now
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
async resolve(id) {
|
|
33
|
+
return await this.db.update(comments, id, {
|
|
34
|
+
resolved: true,
|
|
35
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
//#endregion
|
|
40
|
+
export { CommentModel };
|
|
41
|
+
|
|
42
|
+
//# sourceMappingURL=comment.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comment.mjs","names":[],"sources":["../../src/models/comment.ts"],"sourcesContent":["import { eq } from \"drizzle-orm\";\n\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport { comments, projects, type Comment } from \"../schema.ts\";\nimport { ulid } from \"../utils/ulid.ts\";\n\nexport interface CommentCreateInput {\n body: string;\n snapshotId?: string;\n parentId?: string;\n}\n\nexport class CommentModel {\n constructor(private readonly db: DatabaseAdapter) {}\n\n async listByBuild(buildId: string): Promise<Comment[]> {\n return await this.db.list(comments, { where: eq(comments.buildId, buildId) });\n }\n\n async create(projectId: string, buildId: string, userId: string, input: CommentCreateInput): Promise<Comment> {\n const now = new Date().toISOString();\n const project = await this.db.get(projects, projectId);\n if (!project) {\n throw new Error(`Project not found: ${projectId}`);\n }\n return await this.db.insert(comments, {\n id: ulid(),\n projectId,\n buildId,\n snapshotId: input.snapshotId,\n userId,\n body: input.body,\n parentId: input.parentId,\n resolved: false,\n createdAt: now,\n updatedAt: now,\n });\n }\n\n async resolve(id: string): Promise<Comment> {\n return await this.db.update(comments, id, { resolved: true, updatedAt: new Date().toISOString() });\n }\n}\n\nexport type { Comment };\n"],"mappings":";;;;;;;AAYA,IAAa,eAAb,MAA0B;CACK;CAA7B,YAAY,IAAsC;EAArB,KAAA,KAAA;CAAsB;CAEnD,MAAM,YAAY,SAAqC;EACrD,OAAO,MAAM,KAAK,GAAG,KAAK,UAAU,EAAE,OAAO,GAAG,SAAS,SAAS,OAAO,EAAE,CAAC;CAC9E;CAEA,MAAM,OAAO,WAAmB,SAAiB,QAAgB,OAA6C;EAC5G,MAAM,uBAAM,IAAI,KAAK,EAAA,CAAE,YAAY;EAEnC,IAAI,CAAC,MADiB,KAAK,GAAG,IAAI,UAAU,SAAS,GAEnD,MAAM,IAAI,MAAM,sBAAsB,WAAW;EAEnD,OAAO,MAAM,KAAK,GAAG,OAAO,UAAU;GACpC,IAAI,KAAK;GACT;GACA;GACA,YAAY,MAAM;GAClB;GACA,MAAM,MAAM;GACZ,UAAU,MAAM;GAChB,UAAU;GACV,WAAW;GACX,WAAW;EACb,CAAC;CACH;CAEA,MAAM,QAAQ,IAA8B;EAC1C,OAAO,MAAM,KAAK,GAAG,OAAO,UAAU,IAAI;GAAE,UAAU;GAAM,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;EAAE,CAAC;CACnG;AACF"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { DatabaseAdapter } from "../adapter/database.mjs";
|
|
4
|
+
import { BuildLabel, LabelType } from "../schema.mjs";
|
|
5
|
+
//#region src/models/label.d.ts
|
|
6
|
+
/** Data operations for label types and build labels. */
|
|
7
|
+
declare class LabelModel {
|
|
8
|
+
private readonly db;
|
|
9
|
+
/**
|
|
10
|
+
* @param db - Database adapter.
|
|
11
|
+
*/
|
|
12
|
+
constructor(db: DatabaseAdapter);
|
|
13
|
+
/**
|
|
14
|
+
* Seed the default label types for a project if missing.
|
|
15
|
+
*
|
|
16
|
+
* @param projectId - Project ID.
|
|
17
|
+
*/
|
|
18
|
+
seedFor(projectId: string): Promise<void>;
|
|
19
|
+
/** List all label types for a project. */
|
|
20
|
+
listTypes(projectId: string): Promise<LabelType[]>;
|
|
21
|
+
/** Fetch a label type by key, or null if not found. */
|
|
22
|
+
getType(projectId: string, key: string): Promise<LabelType | null>;
|
|
23
|
+
/** Create a custom label type for a project. */
|
|
24
|
+
createType(projectId: string, input: {
|
|
25
|
+
key: string;
|
|
26
|
+
name: string;
|
|
27
|
+
linkTemplate?: string;
|
|
28
|
+
color?: string;
|
|
29
|
+
}): Promise<LabelType>;
|
|
30
|
+
/** Remove a custom label type, rejecting reserved or persistent types. */
|
|
31
|
+
removeType(projectId: string, key: string): Promise<void>;
|
|
32
|
+
/** Attach a label value to a build. */
|
|
33
|
+
attach(projectId: string, buildId: string, typeKey: string, value: string): Promise<BuildLabel>;
|
|
34
|
+
/** List all labels attached to a build. */
|
|
35
|
+
listForBuild(buildId: string): Promise<BuildLabel[]>;
|
|
36
|
+
/** Return the id of the latest build carrying a given label value, if any. */
|
|
37
|
+
latestBuildId(projectId: string, typeKey: string, value: string): Promise<string | null>;
|
|
38
|
+
/** Return whether a build is marked as persistent. */
|
|
39
|
+
hasPersistent(projectId: string, buildId: string): Promise<boolean>;
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
export { type BuildLabel, LabelModel, type LabelType };
|
|
43
|
+
//# sourceMappingURL=label.d.mts.map
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { buildLabels, builds, labelTypes } from "../schema.mjs";
|
|
5
|
+
import { ulid } from "../utils/ulid.mjs";
|
|
6
|
+
import { PERSISTENT_LABEL_KEY, RESERVED_LABEL_KEYS, SEEDED_LABEL_KEYS } from "../types.mjs";
|
|
7
|
+
import { and, desc, eq } from "drizzle-orm";
|
|
8
|
+
//#region src/models/label.ts
|
|
9
|
+
const SEEDED_NAMES = {
|
|
10
|
+
branch: "Branch",
|
|
11
|
+
persistent: "Persistent",
|
|
12
|
+
pr: "Pull request",
|
|
13
|
+
mr: "Merge request",
|
|
14
|
+
jira: "Jira issue",
|
|
15
|
+
linear: "Linear issue",
|
|
16
|
+
figma: "Figma file",
|
|
17
|
+
custom: "Custom"
|
|
18
|
+
};
|
|
19
|
+
/** Data operations for label types and build labels. */
|
|
20
|
+
var LabelModel = class {
|
|
21
|
+
db;
|
|
22
|
+
/**
|
|
23
|
+
* @param db - Database adapter.
|
|
24
|
+
*/
|
|
25
|
+
constructor(db) {
|
|
26
|
+
this.db = db;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Seed the default label types for a project if missing.
|
|
30
|
+
*
|
|
31
|
+
* @param projectId - Project ID.
|
|
32
|
+
*/
|
|
33
|
+
async seedFor(projectId) {
|
|
34
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
35
|
+
const existing = await this.listTypes(projectId);
|
|
36
|
+
const existingKeys = new Set(existing.map((t) => t.key));
|
|
37
|
+
const missing = SEEDED_LABEL_KEYS.filter((key) => !existingKeys.has(key));
|
|
38
|
+
await Promise.all(missing.map(async (key) => {
|
|
39
|
+
await this.db.insert(labelTypes, {
|
|
40
|
+
id: ulid(),
|
|
41
|
+
projectId,
|
|
42
|
+
key,
|
|
43
|
+
name: SEEDED_NAMES[key] ?? key,
|
|
44
|
+
createdAt: now
|
|
45
|
+
});
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
/** List all label types for a project. */
|
|
49
|
+
async listTypes(projectId) {
|
|
50
|
+
return await this.db.list(labelTypes, { where: eq(labelTypes.projectId, projectId) });
|
|
51
|
+
}
|
|
52
|
+
/** Fetch a label type by key, or null if not found. */
|
|
53
|
+
async getType(projectId, key) {
|
|
54
|
+
return (await this.db.list(labelTypes, {
|
|
55
|
+
where: and(eq(labelTypes.projectId, projectId), eq(labelTypes.key, key)),
|
|
56
|
+
limit: 1
|
|
57
|
+
}))[0] ?? null;
|
|
58
|
+
}
|
|
59
|
+
/** Create a custom label type for a project. */
|
|
60
|
+
async createType(projectId, input) {
|
|
61
|
+
return await this.db.insert(labelTypes, {
|
|
62
|
+
id: ulid(),
|
|
63
|
+
projectId,
|
|
64
|
+
key: input.key,
|
|
65
|
+
name: input.name,
|
|
66
|
+
linkTemplate: input.linkTemplate,
|
|
67
|
+
color: input.color,
|
|
68
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/** Remove a custom label type, rejecting reserved or persistent types. */
|
|
72
|
+
async removeType(projectId, key) {
|
|
73
|
+
if (key === "persistent" || RESERVED_LABEL_KEYS.includes(key)) throw new Error(`Label type '${key}' cannot be removed.`);
|
|
74
|
+
const existing = await this.getType(projectId, key);
|
|
75
|
+
if (existing) await this.db.remove(labelTypes, existing.id);
|
|
76
|
+
}
|
|
77
|
+
/** Attach a label value to a build. */
|
|
78
|
+
async attach(projectId, buildId, typeKey, value) {
|
|
79
|
+
return await this.db.insert(buildLabels, {
|
|
80
|
+
id: ulid(),
|
|
81
|
+
projectId,
|
|
82
|
+
buildId,
|
|
83
|
+
typeKey,
|
|
84
|
+
value,
|
|
85
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/** List all labels attached to a build. */
|
|
89
|
+
async listForBuild(buildId) {
|
|
90
|
+
return await this.db.list(buildLabels, { where: eq(buildLabels.buildId, buildId) });
|
|
91
|
+
}
|
|
92
|
+
/** Return the id of the latest build carrying a given label value, if any. */
|
|
93
|
+
async latestBuildId(projectId, typeKey, value) {
|
|
94
|
+
const labels = await this.db.list(buildLabels, { where: and(eq(buildLabels.projectId, projectId), eq(buildLabels.typeKey, typeKey), eq(buildLabels.value, value)) });
|
|
95
|
+
if (labels.length === 0) return null;
|
|
96
|
+
const idSet = new Set(labels.map((l) => l.buildId));
|
|
97
|
+
return (await this.db.list(builds, { orderBy: desc(builds.createdAt) })).find((b) => idSet.has(b.id))?.id ?? null;
|
|
98
|
+
}
|
|
99
|
+
/** Return whether a build is marked as persistent. */
|
|
100
|
+
async hasPersistent(projectId, buildId) {
|
|
101
|
+
return (await this.db.list(buildLabels, { where: and(eq(buildLabels.projectId, projectId), eq(buildLabels.buildId, buildId), eq(buildLabels.typeKey, PERSISTENT_LABEL_KEY)) })).length > 0;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
//#endregion
|
|
105
|
+
export { LabelModel };
|
|
106
|
+
|
|
107
|
+
//# sourceMappingURL=label.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"label.mjs","names":[],"sources":["../../src/models/label.ts"],"sourcesContent":["import { and, desc, eq } from \"drizzle-orm\";\n\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport { buildLabels, builds, labelTypes, type BuildLabel, type LabelType } from \"../schema.ts\";\nimport { PERSISTENT_LABEL_KEY, RESERVED_LABEL_KEYS, SEEDED_LABEL_KEYS } from \"../types.ts\";\nimport { ulid } from \"../utils/ulid.ts\";\n\nconst SEEDED_NAMES: Record<string, string> = {\n branch: \"Branch\",\n persistent: \"Persistent\",\n pr: \"Pull request\",\n mr: \"Merge request\",\n jira: \"Jira issue\",\n linear: \"Linear issue\",\n figma: \"Figma file\",\n custom: \"Custom\",\n};\n\n/** Data operations for label types and build labels. */\nexport class LabelModel {\n /**\n * @param db - Database adapter.\n */\n constructor(private readonly db: DatabaseAdapter) {}\n\n /**\n * Seed the default label types for a project if missing.\n *\n * @param projectId - Project ID.\n */\n async seedFor(projectId: string): Promise<void> {\n const now = new Date().toISOString();\n const existing = await this.listTypes(projectId);\n const existingKeys = new Set(existing.map((t) => t.key));\n const missing = SEEDED_LABEL_KEYS.filter((key) => !existingKeys.has(key));\n await Promise.all(\n missing.map(async (key) => {\n await this.db.insert(labelTypes, {\n id: ulid(),\n projectId,\n key,\n name: SEEDED_NAMES[key] ?? key,\n createdAt: now,\n });\n }),\n );\n }\n\n /** List all label types for a project. */\n async listTypes(projectId: string): Promise<LabelType[]> {\n return await this.db.list(labelTypes, { where: eq(labelTypes.projectId, projectId) });\n }\n\n /** Fetch a label type by key, or null if not found. */\n async getType(projectId: string, key: string): Promise<LabelType | null> {\n const rows = await this.db.list(labelTypes, {\n where: and(eq(labelTypes.projectId, projectId), eq(labelTypes.key, key)),\n limit: 1,\n });\n return rows[0] ?? null;\n }\n\n /** Create a custom label type for a project. */\n async createType(projectId: string, input: { key: string; name: string; linkTemplate?: string; color?: string }): Promise<LabelType> {\n return await this.db.insert(labelTypes, {\n id: ulid(),\n projectId,\n key: input.key,\n name: input.name,\n linkTemplate: input.linkTemplate,\n color: input.color,\n createdAt: new Date().toISOString(),\n });\n }\n\n /** Remove a custom label type, rejecting reserved or persistent types. */\n async removeType(projectId: string, key: string): Promise<void> {\n if (key === PERSISTENT_LABEL_KEY || RESERVED_LABEL_KEYS.includes(key as never)) {\n throw new Error(`Label type '${key}' cannot be removed.`);\n }\n const existing = await this.getType(projectId, key);\n if (existing) {\n await this.db.remove(labelTypes, existing.id);\n }\n }\n\n /** Attach a label value to a build. */\n async attach(projectId: string, buildId: string, typeKey: string, value: string): Promise<BuildLabel> {\n return await this.db.insert(buildLabels, {\n id: ulid(),\n projectId,\n buildId,\n typeKey,\n value,\n createdAt: new Date().toISOString(),\n });\n }\n\n /** List all labels attached to a build. */\n async listForBuild(buildId: string): Promise<BuildLabel[]> {\n return await this.db.list(buildLabels, { where: eq(buildLabels.buildId, buildId) });\n }\n\n /** Return the id of the latest build carrying a given label value, if any. */\n async latestBuildId(projectId: string, typeKey: string, value: string): Promise<string | null> {\n const labels = await this.db.list(buildLabels, {\n where: and(eq(buildLabels.projectId, projectId), eq(buildLabels.typeKey, typeKey), eq(buildLabels.value, value)),\n });\n if (labels.length === 0) {\n return null;\n }\n const idSet = new Set(labels.map((l) => l.buildId));\n const rows = await this.db.list(builds, { orderBy: desc(builds.createdAt) });\n return rows.find((b) => idSet.has(b.id))?.id ?? null;\n }\n\n /** Return whether a build is marked as persistent. */\n async hasPersistent(projectId: string, buildId: string): Promise<boolean> {\n const labels = await this.db.list(buildLabels, {\n where: and(eq(buildLabels.projectId, projectId), eq(buildLabels.buildId, buildId), eq(buildLabels.typeKey, PERSISTENT_LABEL_KEY)),\n });\n return labels.length > 0;\n }\n}\n\nexport type { BuildLabel, LabelType } from \"../schema.ts\";\n"],"mappings":";;;;;;;;AAOA,MAAM,eAAuC;CAC3C,QAAQ;CACR,YAAY;CACZ,IAAI;CACJ,IAAI;CACJ,MAAM;CACN,QAAQ;CACR,OAAO;CACP,QAAQ;AACV;;AAGA,IAAa,aAAb,MAAwB;CAIO;;;;CAA7B,YAAY,IAAsC;EAArB,KAAA,KAAA;CAAsB;;;;;;CAOnD,MAAM,QAAQ,WAAkC;EAC9C,MAAM,uBAAM,IAAI,KAAK,EAAA,CAAE,YAAY;EACnC,MAAM,WAAW,MAAM,KAAK,UAAU,SAAS;EAC/C,MAAM,eAAe,IAAI,IAAI,SAAS,KAAK,MAAM,EAAE,GAAG,CAAC;EACvD,MAAM,UAAU,kBAAkB,QAAQ,QAAQ,CAAC,aAAa,IAAI,GAAG,CAAC;EACxE,MAAM,QAAQ,IACZ,QAAQ,IAAI,OAAO,QAAQ;GACzB,MAAM,KAAK,GAAG,OAAO,YAAY;IAC/B,IAAI,KAAK;IACT;IACA;IACA,MAAM,aAAa,QAAQ;IAC3B,WAAW;GACb,CAAC;EACH,CAAC,CACH;CACF;;CAGA,MAAM,UAAU,WAAyC;EACvD,OAAO,MAAM,KAAK,GAAG,KAAK,YAAY,EAAE,OAAO,GAAG,WAAW,WAAW,SAAS,EAAE,CAAC;CACtF;;CAGA,MAAM,QAAQ,WAAmB,KAAwC;EAKvE,QAAO,MAJY,KAAK,GAAG,KAAK,YAAY;GAC1C,OAAO,IAAI,GAAG,WAAW,WAAW,SAAS,GAAG,GAAG,WAAW,KAAK,GAAG,CAAC;GACvE,OAAO;EACT,CAAC,EAAA,CACW,MAAM;CACpB;;CAGA,MAAM,WAAW,WAAmB,OAAiG;EACnI,OAAO,MAAM,KAAK,GAAG,OAAO,YAAY;GACtC,IAAI,KAAK;GACT;GACA,KAAK,MAAM;GACX,MAAM,MAAM;GACZ,cAAc,MAAM;GACpB,OAAO,MAAM;GACb,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;EACpC,CAAC;CACH;;CAGA,MAAM,WAAW,WAAmB,KAA4B;EAC9D,IAAI,QAAA,gBAAgC,oBAAoB,SAAS,GAAY,GAC3E,MAAM,IAAI,MAAM,eAAe,IAAI,qBAAqB;EAE1D,MAAM,WAAW,MAAM,KAAK,QAAQ,WAAW,GAAG;EAClD,IAAI,UACF,MAAM,KAAK,GAAG,OAAO,YAAY,SAAS,EAAE;CAEhD;;CAGA,MAAM,OAAO,WAAmB,SAAiB,SAAiB,OAAoC;EACpG,OAAO,MAAM,KAAK,GAAG,OAAO,aAAa;GACvC,IAAI,KAAK;GACT;GACA;GACA;GACA;GACA,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;EACpC,CAAC;CACH;;CAGA,MAAM,aAAa,SAAwC;EACzD,OAAO,MAAM,KAAK,GAAG,KAAK,aAAa,EAAE,OAAO,GAAG,YAAY,SAAS,OAAO,EAAE,CAAC;CACpF;;CAGA,MAAM,cAAc,WAAmB,SAAiB,OAAuC;EAC7F,MAAM,SAAS,MAAM,KAAK,GAAG,KAAK,aAAa,EAC7C,OAAO,IAAI,GAAG,YAAY,WAAW,SAAS,GAAG,GAAG,YAAY,SAAS,OAAO,GAAG,GAAG,YAAY,OAAO,KAAK,CAAC,EACjH,CAAC;EACD,IAAI,OAAO,WAAW,GACpB,OAAO;EAET,MAAM,QAAQ,IAAI,IAAI,OAAO,KAAK,MAAM,EAAE,OAAO,CAAC;EAElD,QAAO,MADY,KAAK,GAAG,KAAK,QAAQ,EAAE,SAAS,KAAK,OAAO,SAAS,EAAE,CAAC,EAAA,CAC/D,MAAM,MAAM,MAAM,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM;CAClD;;CAGA,MAAM,cAAc,WAAmB,SAAmC;EAIxE,QAAO,MAHc,KAAK,GAAG,KAAK,aAAa,EAC7C,OAAO,IAAI,GAAG,YAAY,WAAW,SAAS,GAAG,GAAG,YAAY,SAAS,OAAO,GAAG,GAAG,YAAY,SAAS,oBAAoB,CAAC,EAClI,CAAC,EAAA,CACa,SAAS;CACzB;AACF"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { ProjectRole, SiteRole } from "../types.mjs";
|
|
4
|
+
import { DatabaseAdapter } from "../adapter/database.mjs";
|
|
5
|
+
import { ProjectMember } from "../schema.mjs";
|
|
6
|
+
//#region src/models/member.d.ts
|
|
7
|
+
/** Data operations for project membership and roles. */
|
|
8
|
+
declare class MemberModel {
|
|
9
|
+
private readonly db;
|
|
10
|
+
/**
|
|
11
|
+
* @param db - Database adapter.
|
|
12
|
+
*/
|
|
13
|
+
constructor(db: DatabaseAdapter);
|
|
14
|
+
/** List all members of a project. */
|
|
15
|
+
list(projectId: string): Promise<ProjectMember[]>;
|
|
16
|
+
/** Fetch a project member by project and user id, or null if not found. */
|
|
17
|
+
get(projectId: string, userId: string): Promise<ProjectMember | null>;
|
|
18
|
+
/** Set a user's role in a project, creating the membership if needed. */
|
|
19
|
+
set(projectId: string, userId: string, role: ProjectRole): Promise<ProjectMember>;
|
|
20
|
+
/** Remove a user from a project if they are a member. */
|
|
21
|
+
remove(projectId: string, userId: string): Promise<void>;
|
|
22
|
+
/** Resolve a user's effective project role, honoring site-wide admins. */
|
|
23
|
+
effectiveRole(siteRole: SiteRole, projectId: string, userId: string): Promise<ProjectRole | null>;
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
export { MemberModel, type ProjectMember };
|
|
27
|
+
//# sourceMappingURL=member.d.mts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { projectMembers } from "../schema.mjs";
|
|
5
|
+
import { ulid } from "../utils/ulid.mjs";
|
|
6
|
+
import { and, eq } from "drizzle-orm";
|
|
7
|
+
//#region src/models/member.ts
|
|
8
|
+
/** Data operations for project membership and roles. */
|
|
9
|
+
var MemberModel = class {
|
|
10
|
+
db;
|
|
11
|
+
/**
|
|
12
|
+
* @param db - Database adapter.
|
|
13
|
+
*/
|
|
14
|
+
constructor(db) {
|
|
15
|
+
this.db = db;
|
|
16
|
+
}
|
|
17
|
+
/** List all members of a project. */
|
|
18
|
+
async list(projectId) {
|
|
19
|
+
return await this.db.list(projectMembers, { where: eq(projectMembers.projectId, projectId) });
|
|
20
|
+
}
|
|
21
|
+
/** Fetch a project member by project and user id, or null if not found. */
|
|
22
|
+
async get(projectId, userId) {
|
|
23
|
+
return (await this.db.list(projectMembers, {
|
|
24
|
+
where: and(eq(projectMembers.projectId, projectId), eq(projectMembers.userId, userId)),
|
|
25
|
+
limit: 1
|
|
26
|
+
}))[0] ?? null;
|
|
27
|
+
}
|
|
28
|
+
/** Set a user's role in a project, creating the membership if needed. */
|
|
29
|
+
async set(projectId, userId, role) {
|
|
30
|
+
const existing = await this.get(projectId, userId);
|
|
31
|
+
if (existing) return this.db.update(projectMembers, existing.id, { role });
|
|
32
|
+
return this.db.insert(projectMembers, {
|
|
33
|
+
id: ulid(),
|
|
34
|
+
projectId,
|
|
35
|
+
userId,
|
|
36
|
+
role,
|
|
37
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/** Remove a user from a project if they are a member. */
|
|
41
|
+
async remove(projectId, userId) {
|
|
42
|
+
const existing = await this.get(projectId, userId);
|
|
43
|
+
if (existing) await this.db.remove(projectMembers, existing.id);
|
|
44
|
+
}
|
|
45
|
+
/** Resolve a user's effective project role, honoring site-wide admins. */
|
|
46
|
+
async effectiveRole(siteRole, projectId, userId) {
|
|
47
|
+
if (siteRole === "admin") return "admin";
|
|
48
|
+
return (await this.get(projectId, userId))?.role ?? null;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
//#endregion
|
|
52
|
+
export { MemberModel };
|
|
53
|
+
|
|
54
|
+
//# sourceMappingURL=member.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"member.mjs","names":[],"sources":["../../src/models/member.ts"],"sourcesContent":["import { and, eq } from \"drizzle-orm\";\n\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport { projectMembers, type ProjectMember } from \"../schema.ts\";\nimport type { ProjectRole, SiteRole } from \"../types.ts\";\nimport { ulid } from \"../utils/ulid.ts\";\n\n/** Data operations for project membership and roles. */\nexport class MemberModel {\n /**\n * @param db - Database adapter.\n */\n constructor(private readonly db: DatabaseAdapter) {}\n\n /** List all members of a project. */\n async list(projectId: string): Promise<ProjectMember[]> {\n return await this.db.list(projectMembers, { where: eq(projectMembers.projectId, projectId) });\n }\n\n /** Fetch a project member by project and user id, or null if not found. */\n async get(projectId: string, userId: string): Promise<ProjectMember | null> {\n const rows = await this.db.list(projectMembers, {\n where: and(eq(projectMembers.projectId, projectId), eq(projectMembers.userId, userId)),\n limit: 1,\n });\n return rows[0] ?? null;\n }\n\n /** Set a user's role in a project, creating the membership if needed. */\n async set(projectId: string, userId: string, role: ProjectRole): Promise<ProjectMember> {\n const existing = await this.get(projectId, userId);\n if (existing) {\n return this.db.update(projectMembers, existing.id, { role });\n }\n return this.db.insert(projectMembers, {\n id: ulid(),\n projectId,\n userId,\n role,\n createdAt: new Date().toISOString(),\n });\n }\n\n /** Remove a user from a project if they are a member. */\n async remove(projectId: string, userId: string): Promise<void> {\n const existing = await this.get(projectId, userId);\n if (existing) {\n await this.db.remove(projectMembers, existing.id);\n }\n }\n\n /** Resolve a user's effective project role, honoring site-wide admins. */\n async effectiveRole(siteRole: SiteRole, projectId: string, userId: string): Promise<ProjectRole | null> {\n if (siteRole === \"admin\") {\n return \"admin\";\n }\n const member = await this.get(projectId, userId);\n return member?.role ?? null;\n }\n}\n\nexport type { ProjectMember };\n"],"mappings":";;;;;;;;AAQA,IAAa,cAAb,MAAyB;CAIM;;;;CAA7B,YAAY,IAAsC;EAArB,KAAA,KAAA;CAAsB;;CAGnD,MAAM,KAAK,WAA6C;EACtD,OAAO,MAAM,KAAK,GAAG,KAAK,gBAAgB,EAAE,OAAO,GAAG,eAAe,WAAW,SAAS,EAAE,CAAC;CAC9F;;CAGA,MAAM,IAAI,WAAmB,QAA+C;EAK1E,QAAO,MAJY,KAAK,GAAG,KAAK,gBAAgB;GAC9C,OAAO,IAAI,GAAG,eAAe,WAAW,SAAS,GAAG,GAAG,eAAe,QAAQ,MAAM,CAAC;GACrF,OAAO;EACT,CAAC,EAAA,CACW,MAAM;CACpB;;CAGA,MAAM,IAAI,WAAmB,QAAgB,MAA2C;EACtF,MAAM,WAAW,MAAM,KAAK,IAAI,WAAW,MAAM;EACjD,IAAI,UACF,OAAO,KAAK,GAAG,OAAO,gBAAgB,SAAS,IAAI,EAAE,KAAK,CAAC;EAE7D,OAAO,KAAK,GAAG,OAAO,gBAAgB;GACpC,IAAI,KAAK;GACT;GACA;GACA;GACA,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;EACpC,CAAC;CACH;;CAGA,MAAM,OAAO,WAAmB,QAA+B;EAC7D,MAAM,WAAW,MAAM,KAAK,IAAI,WAAW,MAAM;EACjD,IAAI,UACF,MAAM,KAAK,GAAG,OAAO,gBAAgB,SAAS,EAAE;CAEpD;;CAGA,MAAM,cAAc,UAAoB,WAAmB,QAA6C;EACtG,IAAI,aAAa,SACf,OAAO;EAGT,QAAO,MADc,KAAK,IAAI,WAAW,MAAM,EAAA,EAChC,QAAQ;CACzB;AACF"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { DatabaseAdapter } from "../adapter/database.mjs";
|
|
4
|
+
import { Project } from "../schema.mjs";
|
|
5
|
+
//#region src/models/project.d.ts
|
|
6
|
+
interface ProjectCreateInput {
|
|
7
|
+
name: string;
|
|
8
|
+
gitRepository?: string;
|
|
9
|
+
gitDefaultBranch?: string;
|
|
10
|
+
storybookMeta?: unknown;
|
|
11
|
+
}
|
|
12
|
+
/** Data operations for project records. */
|
|
13
|
+
declare class ProjectModel {
|
|
14
|
+
private readonly db;
|
|
15
|
+
/**
|
|
16
|
+
* @param db - Database adapter.
|
|
17
|
+
*/
|
|
18
|
+
constructor(db: DatabaseAdapter);
|
|
19
|
+
/**
|
|
20
|
+
* Create a project with a unique slug.
|
|
21
|
+
*
|
|
22
|
+
* @param input - Project creation input.
|
|
23
|
+
* @returns The created project.
|
|
24
|
+
*/
|
|
25
|
+
create(input: ProjectCreateInput): Promise<Project>;
|
|
26
|
+
/** Fetch a project by id, or null if not found. */
|
|
27
|
+
get(id: string): Promise<Project | null>;
|
|
28
|
+
/** Fetch a project by its slug, or null if not found. */
|
|
29
|
+
getBySlug(slug: string): Promise<Project | null>;
|
|
30
|
+
/** List all projects. */
|
|
31
|
+
list(): Promise<Project[]>;
|
|
32
|
+
/** Update mutable fields of a project. */
|
|
33
|
+
update(id: string, patch: Partial<Omit<Project, "storybookMeta">> & {
|
|
34
|
+
storybookMeta?: unknown;
|
|
35
|
+
}): Promise<Project>;
|
|
36
|
+
/** Delete a project by id. */
|
|
37
|
+
remove(id: string): Promise<void>;
|
|
38
|
+
private uniqueSlug;
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
export { type Project, ProjectCreateInput, ProjectModel };
|
|
42
|
+
//# sourceMappingURL=project.d.mts.map
|