@storyshelf/core 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +9 -8
- package/dist/adapter/capture-runner.d.mts +4 -0
- package/dist/adapter/git-host/helpers.mjs +1 -1
- package/dist/adapter/git-host/helpers.mjs.map +1 -1
- package/dist/adapter/storage.d.mts +1 -0
- package/dist/adapters/webhook-events.mjs.map +1 -1
- package/dist/capture/adapter.d.mts +9 -1
- package/dist/capture/adapter.mjs +16 -1
- package/dist/capture/adapter.mjs.map +1 -1
- package/dist/capture/orchestrator.mjs +37 -10
- package/dist/capture/orchestrator.mjs.map +1 -1
- package/dist/capture/pipeline.d.mts +5 -3
- package/dist/capture/pipeline.mjs +18 -6
- package/dist/capture/pipeline.mjs.map +1 -1
- package/dist/capture/queue.mjs +1 -1
- package/dist/capture/queue.mjs.map +1 -1
- package/dist/capture/storybook.mjs +11 -3
- package/dist/capture/storybook.mjs.map +1 -1
- package/dist/config.mjs +2 -2
- package/dist/config.mjs.map +1 -1
- package/dist/ddl.d.mts +1 -1
- package/dist/ddl.mjs +2 -0
- package/dist/ddl.mjs.map +1 -1
- package/dist/diff/engine.mjs +2 -2
- package/dist/diff/engine.mjs.map +1 -1
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +4 -2
- package/dist/index.mjs.map +1 -1
- package/dist/logger.d.mts +3 -3
- package/dist/logger.mjs.map +1 -1
- package/dist/middleware/csrf.mjs +1 -1
- package/dist/middleware/csrf.mjs.map +1 -1
- package/dist/models/build.d.mts +16 -1
- package/dist/models/build.mjs +19 -1
- package/dist/models/build.mjs.map +1 -1
- package/dist/models/label.d.mts +6 -0
- package/dist/models/label.mjs +11 -0
- package/dist/models/label.mjs.map +1 -1
- package/dist/models/project.mjs +1 -1
- package/dist/models/project.mjs.map +1 -1
- package/dist/pages/build-detail.mjs +1 -1
- package/dist/pages/build-detail.mjs.map +1 -1
- package/dist/pages/build-diff.mjs.map +1 -1
- package/dist/pages/compute-jobs.mjs.map +1 -1
- package/dist/pages/label-detail.mjs +227 -0
- package/dist/pages/label-detail.mjs.map +1 -0
- package/dist/pages/project-builds.mjs.map +1 -1
- package/dist/pages/project-settings.mjs +32 -42
- package/dist/pages/project-settings.mjs.map +1 -1
- package/dist/pages/projects.mjs.map +1 -1
- package/dist/pages/settings-labels.mjs.map +1 -1
- package/dist/pages/settings-members.mjs.map +1 -1
- package/dist/pages/settings-tests.mjs +148 -0
- package/dist/pages/settings-tests.mjs.map +1 -0
- package/dist/pages/settings-tokens.mjs.map +1 -1
- package/dist/pages/settings-webhooks.mjs.map +1 -1
- package/dist/pages/storybook.mjs +59 -0
- package/dist/pages/storybook.mjs.map +1 -0
- package/dist/routers/builds.handlers.mjs +5 -3
- package/dist/routers/builds.handlers.mjs.map +1 -1
- package/dist/routers/builds.mjs +4 -2
- package/dist/routers/builds.mjs.map +1 -1
- package/dist/routers/helpers.mjs.map +1 -1
- package/dist/routers/labels.mjs +33 -1
- package/dist/routers/labels.mjs.map +1 -1
- package/dist/routers/schemas.mjs +15 -4
- package/dist/routers/schemas.mjs.map +1 -1
- package/dist/routers/settings.mjs +20 -0
- package/dist/routers/settings.mjs.map +1 -1
- package/dist/routers/storybook.mjs +112 -0
- package/dist/routers/storybook.mjs.map +1 -0
- package/dist/routers/ui.mjs +24 -2
- package/dist/routers/ui.mjs.map +1 -1
- package/dist/schema.d.mts +68 -0
- package/dist/schema.mjs +2 -0
- package/dist/schema.mjs.map +1 -1
- package/dist/types.d.mts +2 -1
- package/dist/types.mjs.map +1 -1
- package/dist/ui/components.mjs.map +1 -1
- package/dist/ui/document.mjs +9 -0
- package/dist/ui/document.mjs.map +1 -1
- package/dist/utils/ulid.mjs +3 -3
- package/dist/utils/ulid.mjs.map +1 -1
- package/package.json +76 -67
package/dist/ddl.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import __tsdown_shims_path from 'node:path';
|
|
2
2
|
import __tsdown_shims_url from 'node:url';
|
|
3
3
|
//#region src/ddl.d.ts
|
|
4
|
-
declare const DDL = "\nCREATE TABLE IF NOT EXISTS projects (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n slug TEXT NOT NULL UNIQUE,\n git_repository TEXT,\n git_default_branch TEXT NOT NULL DEFAULT 'main',\n pixel_threshold REAL NOT NULL DEFAULT 0.1,\n max_diff_ratio REAL NOT NULL DEFAULT 0.01,\n public_branch_regex TEXT,\n storybook_meta TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS project_status_configs (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n provider TEXT NOT NULL,\n config TEXT NOT NULL,\n token_encrypted TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS builds (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n git_sha TEXT NOT NULL,\n git_branch TEXT NOT NULL,\n is_default INTEGER NOT NULL DEFAULT 0,\n author_email TEXT,\n author_name TEXT,\n message TEXT,\n public INTEGER NOT NULL DEFAULT 0,\n status TEXT NOT NULL DEFAULT 'pending',\n snapshot_count INTEGER NOT NULL DEFAULT 0,\n changed_count INTEGER NOT NULL DEFAULT 0,\n approved_count INTEGER NOT NULL DEFAULT 0,\n rejected_count INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS builds_project_gitsha_idx ON builds (project_id, git_sha);\nCREATE INDEX IF NOT EXISTS builds_git_branch_idx ON builds (git_branch);\nCREATE TABLE IF NOT EXISTS snapshots (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE,\n story_id TEXT NOT NULL,\n story_name TEXT NOT NULL,\n story_title TEXT NOT NULL,\n story_import_path TEXT,\n viewport_name TEXT NOT NULL DEFAULT 'desktop',\n viewport_width INTEGER NOT NULL DEFAULT 1280,\n viewport_height INTEGER NOT NULL DEFAULT 720,\n screenshot_path TEXT NOT NULL,\n diff_path TEXT,\n diff_pixels INTEGER,\n diff_ratio REAL,\n diff_passed INTEGER,\n status TEXT NOT NULL DEFAULT 'pending',\n reviewed_by TEXT,\n reviewed_at TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS snapshots_build_story_viewport_idx ON snapshots (build_id, story_id, viewport_name);\nCREATE INDEX IF NOT EXISTS snapshots_build_id_idx ON snapshots (build_id);\nCREATE TABLE IF NOT EXISTS baselines (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n story_id TEXT NOT NULL,\n viewport_name TEXT NOT NULL DEFAULT 'desktop',\n branch TEXT NOT NULL,\n snapshot_id TEXT,\n screenshot_path TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS baselines_project_story_viewport_branch_idx ON baselines (project_id, story_id, viewport_name, branch);\nCREATE INDEX IF NOT EXISTS baselines_project_story_idx ON baselines (project_id, story_id);\nCREATE TABLE IF NOT EXISTS comments (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE,\n snapshot_id TEXT REFERENCES snapshots(id) ON DELETE CASCADE,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n body TEXT NOT NULL,\n parent_id TEXT,\n resolved INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS comments_build_id_idx ON comments (build_id);\nCREATE TABLE IF NOT EXISTS label_types (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n key TEXT NOT NULL,\n name TEXT NOT NULL,\n link_template TEXT,\n color TEXT,\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS label_types_project_key_idx ON label_types (project_id, key);\nCREATE TABLE IF NOT EXISTS build_labels (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE,\n type_key TEXT NOT NULL,\n value TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS build_labels_build_type_value_idx ON build_labels (build_id, type_key, value);\nCREATE TABLE IF NOT EXISTS tokens (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n name TEXT NOT NULL,\n hash TEXT NOT NULL UNIQUE,\n last_used_at TEXT,\n created_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS webhooks (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n url TEXT NOT NULL,\n secret TEXT NOT NULL,\n events TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY,\n email TEXT NOT NULL UNIQUE,\n name TEXT NOT NULL,\n avatar_url TEXT,\n role TEXT NOT NULL DEFAULT 'member',\n last_login_at TEXT,\n created_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS project_members (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n role TEXT NOT NULL DEFAULT 'viewer',\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS project_members_project_user_idx ON project_members (project_id, user_id);\n";
|
|
4
|
+
declare const DDL = "\nCREATE TABLE IF NOT EXISTS projects (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n slug TEXT NOT NULL UNIQUE,\n git_repository TEXT,\n git_default_branch TEXT NOT NULL DEFAULT 'main',\n pixel_threshold REAL NOT NULL DEFAULT 0.1,\n max_diff_ratio REAL NOT NULL DEFAULT 0.01,\n public_branch_regex TEXT,\n storybook_meta TEXT,\n execute_play INTEGER NOT NULL DEFAULT 0,\n play_timeout_ms INTEGER NOT NULL DEFAULT 10000,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS project_status_configs (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n provider TEXT NOT NULL,\n config TEXT NOT NULL,\n token_encrypted TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS builds (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n git_sha TEXT NOT NULL,\n git_branch TEXT NOT NULL,\n is_default INTEGER NOT NULL DEFAULT 0,\n author_email TEXT,\n author_name TEXT,\n message TEXT,\n public INTEGER NOT NULL DEFAULT 0,\n status TEXT NOT NULL DEFAULT 'pending',\n snapshot_count INTEGER NOT NULL DEFAULT 0,\n changed_count INTEGER NOT NULL DEFAULT 0,\n approved_count INTEGER NOT NULL DEFAULT 0,\n rejected_count INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS builds_project_gitsha_idx ON builds (project_id, git_sha);\nCREATE INDEX IF NOT EXISTS builds_git_branch_idx ON builds (git_branch);\nCREATE TABLE IF NOT EXISTS snapshots (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE,\n story_id TEXT NOT NULL,\n story_name TEXT NOT NULL,\n story_title TEXT NOT NULL,\n story_import_path TEXT,\n viewport_name TEXT NOT NULL DEFAULT 'desktop',\n viewport_width INTEGER NOT NULL DEFAULT 1280,\n viewport_height INTEGER NOT NULL DEFAULT 720,\n screenshot_path TEXT NOT NULL,\n diff_path TEXT,\n diff_pixels INTEGER,\n diff_ratio REAL,\n diff_passed INTEGER,\n status TEXT NOT NULL DEFAULT 'pending',\n reviewed_by TEXT,\n reviewed_at TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS snapshots_build_story_viewport_idx ON snapshots (build_id, story_id, viewport_name);\nCREATE INDEX IF NOT EXISTS snapshots_build_id_idx ON snapshots (build_id);\nCREATE TABLE IF NOT EXISTS baselines (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n story_id TEXT NOT NULL,\n viewport_name TEXT NOT NULL DEFAULT 'desktop',\n branch TEXT NOT NULL,\n snapshot_id TEXT,\n screenshot_path TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS baselines_project_story_viewport_branch_idx ON baselines (project_id, story_id, viewport_name, branch);\nCREATE INDEX IF NOT EXISTS baselines_project_story_idx ON baselines (project_id, story_id);\nCREATE TABLE IF NOT EXISTS comments (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE,\n snapshot_id TEXT REFERENCES snapshots(id) ON DELETE CASCADE,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n body TEXT NOT NULL,\n parent_id TEXT,\n resolved INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS comments_build_id_idx ON comments (build_id);\nCREATE TABLE IF NOT EXISTS label_types (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n key TEXT NOT NULL,\n name TEXT NOT NULL,\n link_template TEXT,\n color TEXT,\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS label_types_project_key_idx ON label_types (project_id, key);\nCREATE TABLE IF NOT EXISTS build_labels (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE,\n type_key TEXT NOT NULL,\n value TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS build_labels_build_type_value_idx ON build_labels (build_id, type_key, value);\nCREATE TABLE IF NOT EXISTS tokens (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n name TEXT NOT NULL,\n hash TEXT NOT NULL UNIQUE,\n last_used_at TEXT,\n created_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS webhooks (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n url TEXT NOT NULL,\n secret TEXT NOT NULL,\n events TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY,\n email TEXT NOT NULL UNIQUE,\n name TEXT NOT NULL,\n avatar_url TEXT,\n role TEXT NOT NULL DEFAULT 'member',\n last_login_at TEXT,\n created_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS project_members (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n role TEXT NOT NULL DEFAULT 'viewer',\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS project_members_project_user_idx ON project_members (project_id, user_id);\n";
|
|
5
5
|
//#endregion
|
|
6
6
|
export { DDL };
|
|
7
7
|
//# sourceMappingURL=ddl.d.mts.map
|
package/dist/ddl.mjs
CHANGED
|
@@ -13,6 +13,8 @@ CREATE TABLE IF NOT EXISTS projects (
|
|
|
13
13
|
max_diff_ratio REAL NOT NULL DEFAULT 0.01,
|
|
14
14
|
public_branch_regex TEXT,
|
|
15
15
|
storybook_meta TEXT,
|
|
16
|
+
execute_play INTEGER NOT NULL DEFAULT 0,
|
|
17
|
+
play_timeout_ms INTEGER NOT NULL DEFAULT 10000,
|
|
16
18
|
created_at TEXT NOT NULL,
|
|
17
19
|
updated_at TEXT NOT NULL
|
|
18
20
|
);
|
package/dist/ddl.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ddl.mjs","names":[],"sources":["../src/ddl.ts"],"sourcesContent":["export const DDL = `\nCREATE TABLE IF NOT EXISTS projects (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n slug TEXT NOT NULL UNIQUE,\n git_repository TEXT,\n git_default_branch TEXT NOT NULL DEFAULT 'main',\n pixel_threshold REAL NOT NULL DEFAULT 0.1,\n max_diff_ratio REAL NOT NULL DEFAULT 0.01,\n public_branch_regex TEXT,\n storybook_meta TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS project_status_configs (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n provider TEXT NOT NULL,\n config TEXT NOT NULL,\n token_encrypted TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS builds (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n git_sha TEXT NOT NULL,\n git_branch TEXT NOT NULL,\n is_default INTEGER NOT NULL DEFAULT 0,\n author_email TEXT,\n author_name TEXT,\n message TEXT,\n public INTEGER NOT NULL DEFAULT 0,\n status TEXT NOT NULL DEFAULT 'pending',\n snapshot_count INTEGER NOT NULL DEFAULT 0,\n changed_count INTEGER NOT NULL DEFAULT 0,\n approved_count INTEGER NOT NULL DEFAULT 0,\n rejected_count INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS builds_project_gitsha_idx ON builds (project_id, git_sha);\nCREATE INDEX IF NOT EXISTS builds_git_branch_idx ON builds (git_branch);\nCREATE TABLE IF NOT EXISTS snapshots (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE,\n story_id TEXT NOT NULL,\n story_name TEXT NOT NULL,\n story_title TEXT NOT NULL,\n story_import_path TEXT,\n viewport_name TEXT NOT NULL DEFAULT 'desktop',\n viewport_width INTEGER NOT NULL DEFAULT 1280,\n viewport_height INTEGER NOT NULL DEFAULT 720,\n screenshot_path TEXT NOT NULL,\n diff_path TEXT,\n diff_pixels INTEGER,\n diff_ratio REAL,\n diff_passed INTEGER,\n status TEXT NOT NULL DEFAULT 'pending',\n reviewed_by TEXT,\n reviewed_at TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS snapshots_build_story_viewport_idx ON snapshots (build_id, story_id, viewport_name);\nCREATE INDEX IF NOT EXISTS snapshots_build_id_idx ON snapshots (build_id);\nCREATE TABLE IF NOT EXISTS baselines (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n story_id TEXT NOT NULL,\n viewport_name TEXT NOT NULL DEFAULT 'desktop',\n branch TEXT NOT NULL,\n snapshot_id TEXT,\n screenshot_path TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS baselines_project_story_viewport_branch_idx ON baselines (project_id, story_id, viewport_name, branch);\nCREATE INDEX IF NOT EXISTS baselines_project_story_idx ON baselines (project_id, story_id);\nCREATE TABLE IF NOT EXISTS comments (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE,\n snapshot_id TEXT REFERENCES snapshots(id) ON DELETE CASCADE,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n body TEXT NOT NULL,\n parent_id TEXT,\n resolved INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS comments_build_id_idx ON comments (build_id);\nCREATE TABLE IF NOT EXISTS label_types (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n key TEXT NOT NULL,\n name TEXT NOT NULL,\n link_template TEXT,\n color TEXT,\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS label_types_project_key_idx ON label_types (project_id, key);\nCREATE TABLE IF NOT EXISTS build_labels (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE,\n type_key TEXT NOT NULL,\n value TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS build_labels_build_type_value_idx ON build_labels (build_id, type_key, value);\nCREATE TABLE IF NOT EXISTS tokens (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n name TEXT NOT NULL,\n hash TEXT NOT NULL UNIQUE,\n last_used_at TEXT,\n created_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS webhooks (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n url TEXT NOT NULL,\n secret TEXT NOT NULL,\n events TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY,\n email TEXT NOT NULL UNIQUE,\n name TEXT NOT NULL,\n avatar_url TEXT,\n role TEXT NOT NULL DEFAULT 'member',\n last_login_at TEXT,\n created_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS project_members (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n role TEXT NOT NULL DEFAULT 'viewer',\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS project_members_project_user_idx ON project_members (project_id, user_id);\n`;\n"],"mappings":";;;;AAAA,MAAa,MAAM"}
|
|
1
|
+
{"version":3,"file":"ddl.mjs","names":[],"sources":["../src/ddl.ts"],"sourcesContent":["export const DDL = `\nCREATE TABLE IF NOT EXISTS projects (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n slug TEXT NOT NULL UNIQUE,\n git_repository TEXT,\n git_default_branch TEXT NOT NULL DEFAULT 'main',\n pixel_threshold REAL NOT NULL DEFAULT 0.1,\n max_diff_ratio REAL NOT NULL DEFAULT 0.01,\n public_branch_regex TEXT,\n storybook_meta TEXT,\n execute_play INTEGER NOT NULL DEFAULT 0,\n play_timeout_ms INTEGER NOT NULL DEFAULT 10000,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS project_status_configs (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n provider TEXT NOT NULL,\n config TEXT NOT NULL,\n token_encrypted TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS builds (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n git_sha TEXT NOT NULL,\n git_branch TEXT NOT NULL,\n is_default INTEGER NOT NULL DEFAULT 0,\n author_email TEXT,\n author_name TEXT,\n message TEXT,\n public INTEGER NOT NULL DEFAULT 0,\n status TEXT NOT NULL DEFAULT 'pending',\n snapshot_count INTEGER NOT NULL DEFAULT 0,\n changed_count INTEGER NOT NULL DEFAULT 0,\n approved_count INTEGER NOT NULL DEFAULT 0,\n rejected_count INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS builds_project_gitsha_idx ON builds (project_id, git_sha);\nCREATE INDEX IF NOT EXISTS builds_git_branch_idx ON builds (git_branch);\nCREATE TABLE IF NOT EXISTS snapshots (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE,\n story_id TEXT NOT NULL,\n story_name TEXT NOT NULL,\n story_title TEXT NOT NULL,\n story_import_path TEXT,\n viewport_name TEXT NOT NULL DEFAULT 'desktop',\n viewport_width INTEGER NOT NULL DEFAULT 1280,\n viewport_height INTEGER NOT NULL DEFAULT 720,\n screenshot_path TEXT NOT NULL,\n diff_path TEXT,\n diff_pixels INTEGER,\n diff_ratio REAL,\n diff_passed INTEGER,\n status TEXT NOT NULL DEFAULT 'pending',\n reviewed_by TEXT,\n reviewed_at TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS snapshots_build_story_viewport_idx ON snapshots (build_id, story_id, viewport_name);\nCREATE INDEX IF NOT EXISTS snapshots_build_id_idx ON snapshots (build_id);\nCREATE TABLE IF NOT EXISTS baselines (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n story_id TEXT NOT NULL,\n viewport_name TEXT NOT NULL DEFAULT 'desktop',\n branch TEXT NOT NULL,\n snapshot_id TEXT,\n screenshot_path TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS baselines_project_story_viewport_branch_idx ON baselines (project_id, story_id, viewport_name, branch);\nCREATE INDEX IF NOT EXISTS baselines_project_story_idx ON baselines (project_id, story_id);\nCREATE TABLE IF NOT EXISTS comments (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE,\n snapshot_id TEXT REFERENCES snapshots(id) ON DELETE CASCADE,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n body TEXT NOT NULL,\n parent_id TEXT,\n resolved INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS comments_build_id_idx ON comments (build_id);\nCREATE TABLE IF NOT EXISTS label_types (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n key TEXT NOT NULL,\n name TEXT NOT NULL,\n link_template TEXT,\n color TEXT,\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS label_types_project_key_idx ON label_types (project_id, key);\nCREATE TABLE IF NOT EXISTS build_labels (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE,\n type_key TEXT NOT NULL,\n value TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS build_labels_build_type_value_idx ON build_labels (build_id, type_key, value);\nCREATE TABLE IF NOT EXISTS tokens (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n name TEXT NOT NULL,\n hash TEXT NOT NULL UNIQUE,\n last_used_at TEXT,\n created_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS webhooks (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n url TEXT NOT NULL,\n secret TEXT NOT NULL,\n events TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY,\n email TEXT NOT NULL UNIQUE,\n name TEXT NOT NULL,\n avatar_url TEXT,\n role TEXT NOT NULL DEFAULT 'member',\n last_login_at TEXT,\n created_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS project_members (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n role TEXT NOT NULL DEFAULT 'viewer',\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS project_members_project_user_idx ON project_members (project_id, user_id);\n`;\n"],"mappings":";;;;AAAA,MAAa,MAAM"}
|
package/dist/diff/engine.mjs
CHANGED
|
@@ -39,8 +39,8 @@ function diffImages(baseline, current, options) {
|
|
|
39
39
|
sizeChanged: true
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
|
-
const width = baselineDimensions
|
|
43
|
-
const height = baselineDimensions
|
|
42
|
+
const { width } = baselineDimensions;
|
|
43
|
+
const { height } = baselineDimensions;
|
|
44
44
|
const overlap = compareRegions(baselinePng.data, currentPng.data, width, height, options);
|
|
45
45
|
const diffRatio = overlap.diffPixels / (width * height);
|
|
46
46
|
return {
|
package/dist/diff/engine.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.mjs","names":[],"sources":["../../src/diff/engine.ts"],"sourcesContent":["import pixelmatch from \"pixelmatch\";\nimport { PNG } from \"pngjs\";\n\nimport type { DiffOptions, DiffResult } from \"./options.ts\";\n\nexport function diffImages(baseline: Buffer, current: Buffer, options: DiffOptions): DiffResult {\n const baselinePng = PNG.sync.read(baseline);\n const currentPng = PNG.sync.read(current);\n\n const baselineDimensions = { width: baselinePng.width, height: baselinePng.height };\n const currentDimensions = { width: currentPng.width, height: currentPng.height };\n const sizeChanged =\n baselineDimensions.width !== currentDimensions.width || baselineDimensions.height !== currentDimensions.height;\n\n if (sizeChanged) {\n if (options.failOnSizeChange) {\n return {\n passed: false,\n diffPixels: 0,\n diffRatio: 1,\n diffImage: null,\n baselineDimensions,\n currentDimensions,\n sizeChanged: true,\n };\n }\n\n // With `failOnSizeChange: false` a size mismatch is not a failure: diff only\n // The top-left overlapping region, normalised by its area. Pixelmatch needs\n // Equal-size inputs, so the larger image is cropped first.\n const width = Math.min(baselineDimensions.width, currentDimensions.width);\n const height = Math.min(baselineDimensions.height, currentDimensions.height);\n const overlap = compareRegions(\n cropRgba(baselinePng.data, baselineDimensions.width, width, height),\n cropRgba(currentPng.data, currentDimensions.width, width, height),\n width,\n height,\n options,\n );\n const diffRatio = overlap.diffPixels / (width * height);\n return {\n passed: diffRatio <= options.maxDiffRatio,\n diffPixels: overlap.diffPixels,\n diffRatio,\n diffImage: overlap.diffImage,\n baselineDimensions,\n currentDimensions,\n sizeChanged: true,\n };\n }\n\n const width = baselineDimensions
|
|
1
|
+
{"version":3,"file":"engine.mjs","names":[],"sources":["../../src/diff/engine.ts"],"sourcesContent":["import pixelmatch from \"pixelmatch\";\nimport { PNG } from \"pngjs\";\n\nimport type { DiffOptions, DiffResult } from \"./options.ts\";\n\nexport function diffImages(baseline: Buffer, current: Buffer, options: DiffOptions): DiffResult {\n const baselinePng = PNG.sync.read(baseline);\n const currentPng = PNG.sync.read(current);\n\n const baselineDimensions = { width: baselinePng.width, height: baselinePng.height };\n const currentDimensions = { width: currentPng.width, height: currentPng.height };\n const sizeChanged =\n baselineDimensions.width !== currentDimensions.width || baselineDimensions.height !== currentDimensions.height;\n\n if (sizeChanged) {\n if (options.failOnSizeChange) {\n return {\n passed: false,\n diffPixels: 0,\n diffRatio: 1,\n diffImage: null,\n baselineDimensions,\n currentDimensions,\n sizeChanged: true,\n };\n }\n\n // With `failOnSizeChange: false` a size mismatch is not a failure: diff only\n // The top-left overlapping region, normalised by its area. Pixelmatch needs\n // Equal-size inputs, so the larger image is cropped first.\n const width = Math.min(baselineDimensions.width, currentDimensions.width);\n const height = Math.min(baselineDimensions.height, currentDimensions.height);\n const overlap = compareRegions(\n cropRgba(baselinePng.data, baselineDimensions.width, width, height),\n cropRgba(currentPng.data, currentDimensions.width, width, height),\n width,\n height,\n options,\n );\n const diffRatio = overlap.diffPixels / (width * height);\n return {\n passed: diffRatio <= options.maxDiffRatio,\n diffPixels: overlap.diffPixels,\n diffRatio,\n diffImage: overlap.diffImage,\n baselineDimensions,\n currentDimensions,\n sizeChanged: true,\n };\n }\n\n const {width} = baselineDimensions;\n const {height} = baselineDimensions;\n const overlap = compareRegions(baselinePng.data, currentPng.data, width, height, options);\n const diffRatio = overlap.diffPixels / (width * height);\n return {\n passed: diffRatio <= options.maxDiffRatio,\n diffPixels: overlap.diffPixels,\n diffRatio,\n diffImage: overlap.diffImage,\n baselineDimensions,\n currentDimensions,\n sizeChanged: false,\n };\n}\n\nfunction cropRgba(source: Buffer, sourceWidth: number, width: number, height: number): Buffer {\n const cropped = Buffer.alloc(width * height * 4);\n const rowBytes = width * 4;\n const sourceRowBytes = sourceWidth * 4;\n for (let y = 0; y < height; y += 1) {\n source.copy(cropped, y * rowBytes, y * sourceRowBytes, y * sourceRowBytes + rowBytes);\n }\n return cropped;\n}\n\nfunction compareRegions(\n baselineData: Buffer,\n currentData: Buffer,\n width: number,\n height: number,\n options: DiffOptions,\n): { diffPixels: number; diffImage: Buffer } {\n const diffPng = new PNG({ width, height });\n const diffPixels = pixelmatch(baselineData, currentData, diffPng.data, width, height, {\n threshold: options.pixelThreshold,\n includeAA: options.includeAntialiasing,\n });\n return { diffPixels, diffImage: PNG.sync.write(diffPng) };\n}\n"],"mappings":";;;;;;AAKA,SAAgB,WAAW,UAAkB,SAAiB,SAAkC;CAC9F,MAAM,cAAc,IAAI,KAAK,KAAK,QAAQ;CAC1C,MAAM,aAAa,IAAI,KAAK,KAAK,OAAO;CAExC,MAAM,qBAAqB;EAAE,OAAO,YAAY;EAAO,QAAQ,YAAY;CAAO;CAClF,MAAM,oBAAoB;EAAE,OAAO,WAAW;EAAO,QAAQ,WAAW;CAAO;CAI/E,IAFE,mBAAmB,UAAU,kBAAkB,SAAS,mBAAmB,WAAW,kBAAkB,QAEzF;EACf,IAAI,QAAQ,kBACV,OAAO;GACL,QAAQ;GACR,YAAY;GACZ,WAAW;GACX,WAAW;GACX;GACA;GACA,aAAa;EACf;EAMF,MAAM,QAAQ,KAAK,IAAI,mBAAmB,OAAO,kBAAkB,KAAK;EACxE,MAAM,SAAS,KAAK,IAAI,mBAAmB,QAAQ,kBAAkB,MAAM;EAC3E,MAAM,UAAU,eACd,SAAS,YAAY,MAAM,mBAAmB,OAAO,OAAO,MAAM,GAClE,SAAS,WAAW,MAAM,kBAAkB,OAAO,OAAO,MAAM,GAChE,OACA,QACA,OACF;EACA,MAAM,YAAY,QAAQ,cAAc,QAAQ;EAChD,OAAO;GACL,QAAQ,aAAa,QAAQ;GAC7B,YAAY,QAAQ;GACpB;GACA,WAAW,QAAQ;GACnB;GACA;GACA,aAAa;EACf;CACF;CAEA,MAAM,EAAC,UAAS;CAChB,MAAM,EAAC,WAAU;CACjB,MAAM,UAAU,eAAe,YAAY,MAAM,WAAW,MAAM,OAAO,QAAQ,OAAO;CACxF,MAAM,YAAY,QAAQ,cAAc,QAAQ;CAChD,OAAO;EACL,QAAQ,aAAa,QAAQ;EAC7B,YAAY,QAAQ;EACpB;EACA,WAAW,QAAQ;EACnB;EACA;EACA,aAAa;CACf;AACF;AAEA,SAAS,SAAS,QAAgB,aAAqB,OAAe,QAAwB;CAC5F,MAAM,UAAU,OAAO,MAAM,QAAQ,SAAS,CAAC;CAC/C,MAAM,WAAW,QAAQ;CACzB,MAAM,iBAAiB,cAAc;CACrC,KAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,KAAK,GAC/B,OAAO,KAAK,SAAS,IAAI,UAAU,IAAI,gBAAgB,IAAI,iBAAiB,QAAQ;CAEtF,OAAO;AACT;AAEA,SAAS,eACP,cACA,aACA,OACA,QACA,SAC2C;CAC3C,MAAM,UAAU,IAAI,IAAI;EAAE;EAAO;CAAO,CAAC;CAKzC,OAAO;EAAE,YAJU,WAAW,cAAc,aAAa,QAAQ,MAAM,OAAO,QAAQ;GACpF,WAAW,QAAQ;GACnB,WAAW,QAAQ;EACrB,CACkB;EAAG,WAAW,IAAI,KAAK,MAAM,OAAO;CAAE;AAC1D"}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import __tsdown_shims_path from 'node:path';
|
|
2
2
|
import __tsdown_shims_url from 'node:url';
|
|
3
3
|
import { AdapterMetadata, GitAdapterMetadata } from "./adapters/metadata.mjs";
|
|
4
|
+
import { Logger, LoggerOptions, PinoTransport, createShelfLogger } from "./logger.mjs";
|
|
4
5
|
import { BUILD_STATUSES, BuildStatus, PERSISTENT_LABEL_KEY, PROJECT_ROLES, ProjectRole, RESERVED_LABEL_KEYS, SEEDED_LABEL_KEYS, SITE_ROLES, SNAPSHOT_STATUSES, SiteRole, SnapshotStatus, TERMINAL_BUILD_STATUSES } from "./types.mjs";
|
|
5
6
|
import { AuthAdapter, AuthCallback, AuthUser } from "./adapter/auth.mjs";
|
|
6
7
|
import { DEFAULT_VIEWPORTS, StoryEntry, StorySourceAdapter, Viewport } from "./capture/adapter.mjs";
|
|
@@ -11,7 +12,6 @@ import { CheckStatus, GitHostAdapter, GitHostProvider } from "./adapter/git-host
|
|
|
11
12
|
import { buildCommentMarkdown, commentMarker, describeStatus } from "./adapter/git-host/helpers.mjs";
|
|
12
13
|
import { StorageAdapter } from "./adapter/storage.mjs";
|
|
13
14
|
import { BrandTheme, ShelfConfig, ShelfOptions, UIConfig } from "./config.mjs";
|
|
14
|
-
import { LoggerOptions, PinoTransport, createShelfLogger } from "./logger.mjs";
|
|
15
15
|
import { DiffOptions, DiffResult } from "./diff/options.mjs";
|
|
16
16
|
import { diffImages } from "./diff/engine.mjs";
|
|
17
17
|
import { RenderedContent } from "./ui/document.mjs";
|
|
@@ -26,7 +26,7 @@ import { slugify, ulid } from "./utils/ulid.mjs";
|
|
|
26
26
|
import { baselinePath, diffPath, screenshotPath, storybookDir, storybookZipPath } from "./utils/paths.mjs";
|
|
27
27
|
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
28
28
|
//#region src/index.d.ts
|
|
29
|
-
|
|
29
|
+
interface ShelfContext {
|
|
30
30
|
requestId?: string;
|
|
31
31
|
db: DatabaseAdapter;
|
|
32
32
|
storage: StorageAdapter;
|
|
@@ -38,11 +38,11 @@ type ShelfContext = {
|
|
|
38
38
|
enqueueCapture?: (buildId: string, reqId?: string) => Promise<void>;
|
|
39
39
|
captureQueue: CaptureQueue | null;
|
|
40
40
|
gitHosts: GitHostProvider[];
|
|
41
|
-
}
|
|
41
|
+
}
|
|
42
42
|
type ShelfApp = OpenAPIHono<{
|
|
43
43
|
Variables: ShelfContext;
|
|
44
44
|
}>;
|
|
45
45
|
declare function createShelfRouter(options: ShelfOptions): ShelfApp;
|
|
46
46
|
//#endregion
|
|
47
|
-
export { type AdapterMetadata, type AuthAdapter, type AuthCallback, type AuthUser, BUILD_STATUSES, Baseline, type BrandTheme, Build, BuildLabel, BuildStatus, type CaptureContext, type CaptureJob, type CaptureJobOptions, type CaptureQueue, type CaptureRunner, type CheckStatus, Comment, DEFAULT_VIEWPORTS, type DatabaseAdapter, type DiffOptions, type DiffResult, type GitAdapterMetadata, type GitHostAdapter, type GitHostProvider, InMemoryCaptureQueue, type InMemoryCaptureQueueOptions, type JobStatus, LabelType, type ListOptions, type LoggerOptions, PERSISTENT_LABEL_KEY, PROJECT_ROLES, type PinoTransport, Project, ProjectMember, ProjectRole, ProjectStatusConfig, type QueueEntry, RESERVED_LABEL_KEYS, type RenderFailure, type RenderResult, type RenderedContent, type RenderedSnapshot, Retention, SEEDED_LABEL_KEYS, SITE_ROLES, SNAPSHOT_STATUSES, Schema, ShelfApp, type ShelfConfig, ShelfContext, type ShelfOptions, SiteRole, Snapshot, SnapshotStatus, type StorageAdapter, type StoryEntry, type StorySourceAdapter, StorybookAdapter, StorybookMeta, TERMINAL_BUILD_STATUSES, Token, type UIConfig, type UrlBuilder, User, type Viewport, Webhook, baselinePath, baselines, buildCommentMarkdown, buildLabels, builds, commentMarker, comments, createShelfLogger, createShelfRouter, createUrlBuilder, describeStatus, diffImages, diffPath, executeCaptureJob, labelTypes, persistCapture, projectMembers, projectStatusConfigs, projects, schema, screenshotPath, slugify, snapshots, storybookDir, storybookZipPath, tokens, ulid, users, webhooks };
|
|
47
|
+
export { type AdapterMetadata, type AuthAdapter, type AuthCallback, type AuthUser, BUILD_STATUSES, Baseline, type BrandTheme, Build, BuildLabel, BuildStatus, type CaptureContext, type CaptureJob, type CaptureJobOptions, type CaptureQueue, type CaptureRunner, type CheckStatus, Comment, DEFAULT_VIEWPORTS, type DatabaseAdapter, type DiffOptions, type DiffResult, type GitAdapterMetadata, type GitHostAdapter, type GitHostProvider, InMemoryCaptureQueue, type InMemoryCaptureQueueOptions, type JobStatus, LabelType, type ListOptions, type Logger, type LoggerOptions, PERSISTENT_LABEL_KEY, PROJECT_ROLES, type PinoTransport, Project, ProjectMember, ProjectRole, ProjectStatusConfig, type QueueEntry, RESERVED_LABEL_KEYS, type RenderFailure, type RenderResult, type RenderedContent, type RenderedSnapshot, Retention, SEEDED_LABEL_KEYS, SITE_ROLES, SNAPSHOT_STATUSES, Schema, ShelfApp, type ShelfConfig, ShelfContext, type ShelfOptions, SiteRole, Snapshot, SnapshotStatus, type StorageAdapter, type StoryEntry, type StorySourceAdapter, StorybookAdapter, StorybookMeta, TERMINAL_BUILD_STATUSES, Token, type UIConfig, type UrlBuilder, User, type Viewport, Webhook, baselinePath, baselines, buildCommentMarkdown, buildLabels, builds, commentMarker, comments, createShelfLogger, createShelfRouter, createUrlBuilder, describeStatus, diffImages, diffPath, executeCaptureJob, labelTypes, persistCapture, projectMembers, projectStatusConfigs, projects, schema, screenshotPath, slugify, snapshots, storybookDir, storybookZipPath, tokens, ulid, users, webhooks };
|
|
48
48
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import { registerMedia } from "./routers/media.mjs";
|
|
|
22
22
|
import { registerMembers } from "./routers/members.mjs";
|
|
23
23
|
import { registerProjects } from "./routers/projects.mjs";
|
|
24
24
|
import { registerStatusConfigs } from "./routers/status-configs.mjs";
|
|
25
|
+
import { registerStorybook } from "./routers/storybook.mjs";
|
|
25
26
|
import { registerTokens } from "./routers/tokens.mjs";
|
|
26
27
|
import { registerUiPages } from "./routers/ui.mjs";
|
|
27
28
|
import { registerWebhooks } from "./routers/webhooks.mjs";
|
|
@@ -290,8 +291,8 @@ function createShelfRouter(options) {
|
|
|
290
291
|
});
|
|
291
292
|
app.use("*", async (c, next) => {
|
|
292
293
|
const { user, authEnabled: isAuthEnabled } = getStore();
|
|
293
|
-
const path = c.req
|
|
294
|
-
if (!isAuthEnabled || user || path.startsWith("/api/") || path.startsWith("/auth/") || path.startsWith("/assets/")) return next();
|
|
294
|
+
const { path } = c.req;
|
|
295
|
+
if (!isAuthEnabled || user || path.startsWith("/api/") || path.startsWith("/auth/") || path.startsWith("/assets/") || path.startsWith("/projects/") && path.includes("/storybook")) return next();
|
|
295
296
|
if (c.req.header("HX-Request") === "true") {
|
|
296
297
|
c.header("HX-Redirect", "/auth/login");
|
|
297
298
|
return c.body(null, 204);
|
|
@@ -309,6 +310,7 @@ function createShelfRouter(options) {
|
|
|
309
310
|
registerAdmin(app);
|
|
310
311
|
if (options.auth) registerAuth(app, options.auth);
|
|
311
312
|
registerAssets(app);
|
|
313
|
+
registerStorybook(app);
|
|
312
314
|
registerUiPages(app);
|
|
313
315
|
app.doc("/api/v1/openapi.json", {
|
|
314
316
|
openapi: "3.0.0",
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.tsx"],"sourcesContent":["import { swaggerUI } from \"@hono/swagger-ui\";\nimport { OpenAPIHono } from \"@hono/zod-openapi\";\nimport { requestId } from \"hono/request-id\";\nimport type { AuthUser } from \"./adapters/auth.ts\";\nimport type { CaptureQueue } from \"./adapters/capture-queue.ts\";\nimport type { ShelfOptions } from \"./config.ts\";\nimport type { AdapterMetadata, GitAdapterMetadata } from \"./adapters/metadata.ts\";\nimport { validateConfig, validateUiConfig } from \"./config.ts\";\nimport { createShelfLogger } from \"./logger.ts\";\nimport { csrf, rateLimit } from \"./middleware/index.ts\";\nimport { BuildModel } from \"./models/build.ts\";\nimport { ProjectModel } from \"./models/project.ts\";\nimport { registerAdmin } from \"./routers/admin.ts\";\nimport { registerAssets } from \"./routers/assets.ts\";\nimport { registerAuth } from \"./routers/auth.ts\";\nimport { registerBuilds } from \"./routers/builds.ts\";\nimport { registerLabels } from \"./routers/labels.ts\";\nimport { registerMedia } from \"./routers/media.ts\";\nimport { registerMembers } from \"./routers/members.ts\";\nimport { registerProjects } from \"./routers/projects.ts\";\nimport { registerStatusConfigs } from \"./routers/status-configs.ts\";\nimport { registerTokens } from \"./routers/tokens.ts\";\nimport { registerUiPages } from \"./routers/ui.ts\";\nimport { registerWebhooks } from \"./routers/webhooks.ts\";\nimport { getStore, runWithStore } from \"./store.ts\";\nimport { executeCaptureJob, type CaptureJobOptions } from \"./capture/orchestrator.ts\";\nimport { InMemoryCaptureQueue } from \"./capture/queue.ts\";\nimport { postStatusesForBuild } from \"./capture/status-fanout.ts\";\n\nexport type ShelfContext = {\n requestId?: string;\n db: import(\"./adapters/database.ts\").DatabaseAdapter;\n storage: import(\"./adapters/storage.ts\").StorageAdapter;\n config: import(\"./config.ts\").ShelfConfig;\n ui: import(\"./config.ts\").UIConfig;\n logger: ReturnType<typeof createShelfLogger>;\n user: import(\"./adapters/auth.ts\").AuthUser | null;\n authEnabled: boolean;\n enqueueCapture?: (buildId: string, reqId?: string) => Promise<void>;\n captureQueue: import(\"./adapters/capture-queue.ts\").CaptureQueue | null;\n gitHosts: import(\"./adapters/git-host/index.ts\").GitHostProvider[];\n};\n\nexport type ShelfApp = OpenAPIHono<{ Variables: ShelfContext }>;\n\nfunction buildAdapterSnapshot(options: ShelfOptions): Record<string, AdapterMetadata | GitAdapterMetadata> {\n const snap: Record<string, AdapterMetadata | GitAdapterMetadata> = {};\n if (options.database.metadata) snap[\"database\"] = options.database.metadata;\n if (options.storage.metadata) snap[\"storage\"] = options.storage.metadata;\n if (options.captureRunner?.metadata) snap[\"captureRunner\"] = options.captureRunner.metadata;\n if (options.captureQueue?.metadata) snap[\"captureQueue\"] = options.captureQueue.metadata;\n if (options.auth?.metadata) snap[\"auth\"] = options.auth.metadata;\n for (const p of options.gitHosts ?? []) {\n snap[`git:${p.metadata.kind}`] = p.metadata;\n }\n return snap;\n}\n\nasync function hasApprovedBuildForSha(\n db: import(\"./adapters/database.ts\").DatabaseAdapter,\n projectId: string,\n sha: string,\n excludeBuildId: string,\n): Promise<boolean> {\n const builds = await new BuildModel(db).list(projectId);\n return builds.some((b) => b.gitSha === sha && b.id !== excludeBuildId && b.status === \"approved\");\n}\n\n// eslint-disable-next-line max-statements, complexity\nasync function isAlreadyMerged(opts: {\n providers: import(\"./adapters/git-host/index.ts\").GitHostProvider[];\n sha: string;\n branch: string;\n secret: string | undefined;\n db: import(\"./adapters/database.ts\").DatabaseAdapter;\n projectId: string;\n logger?: import(\"pino\").Logger;\n}): Promise<boolean> {\n if (opts.providers.length === 0) return false;\n const { StatusConfigModel } = await import(\"./models/status-config.ts\");\n const model = new StatusConfigModel(opts.db, opts.secret);\n const rows = await model.list(opts.projectId);\n for (const row of rows) {\n const provider = opts.providers.find((p) => p.metadata.kind === row.provider);\n if (!provider) continue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(row.config);\n provider.metadata.schema.parse(parsed);\n } catch {\n continue;\n }\n let token: string;\n try {\n token = model.decryptToken(row);\n } catch {\n continue;\n }\n const adapter = provider.create({ config: parsed, token, logger: opts.logger });\n if (!adapter.isMerged) continue;\n try {\n // eslint-disable-next-line no-await-in-loop -- short-circuit on first merged status\n const merged = await adapter.isMerged({ sha: opts.sha, branch: opts.branch });\n if (merged) return true;\n } catch {\n // ignore provider errors — do not skip on failure\n }\n }\n return false;\n}\n\nasync function resolveUser(\n c: { req: { raw: Request; header: (name: string) => string | undefined } },\n options: ShelfOptions,\n): Promise<AuthUser | null> {\n if (!options.auth) {\n return null;\n }\n if (c.req.header(\"authorization\")) {\n return null;\n }\n return await options.auth.check(c.req.raw);\n}\n\n\nexport function createShelfRouter(options: ShelfOptions): ShelfApp {\n const app = new OpenAPIHono<{ Variables: ShelfContext }>();\n // eslint-disable-next-line typescript/no-unnecessary-type-assertion -- ShelfConfig lacks index signature\n const rawConfig = options.config ? validateConfig(options.config as unknown as Record<string, unknown>) : {};\n // eslint-disable-next-line typescript/no-unnecessary-type-assertion -- UIConfig lacks index signature\n const ui = options.ui ? validateUiConfig(options.ui as unknown as Record<string, unknown>) : {};\n const logger = options.logger ?? createShelfLogger();\n const authEnabled = options.auth !== undefined;\n\n const gitHosts = options.gitHosts ?? [];\n\n // Adapter introspection — auto-populate config.adapters if not supplied\n const config: import(\"./config.ts\").ShelfConfig = rawConfig.adapters\n ? rawConfig\n : {\n ...rawConfig,\n adapters: buildAdapterSnapshot(options),\n };\n\n let queue: CaptureQueue | null = null;\n let enqueueCapture: ((buildId: string, reqId?: string) => Promise<void>) | undefined;\n if (options.captureRunner) {\n if (!config.scratchDir) {\n throw new Error(\"captureRunner is enabled but ShelfConfig.scratchDir is not set\");\n }\n const jobOptions: CaptureJobOptions = {\n db: options.database,\n storage: options.storage,\n runner: options.captureRunner,\n scratchDir: config.scratchDir,\n viewports: config.viewports,\n logger,\n };\n const captureQueue =\n options.captureQueue ??\n new InMemoryCaptureQueue({\n concurrency: config.captureConcurrency ?? 2,\n logger,\n runJob: async (job): Promise<void> => {\n const builds = new BuildModel(options.database);\n const build = await builds.get(job.buildId);\n if (!build) {\n await executeCaptureJob({ buildId: job.buildId, reqId: job.reqId }, jobOptions);\n return;\n }\n const project = await new ProjectModel(options.database).get(build.projectId);\n if (!project) {\n await executeCaptureJob({ buildId: job.buildId, reqId: job.reqId }, jobOptions);\n return;\n }\n const pendingUrl = `/projects/${project.slug}/builds/${job.buildId}`;\n await postStatusesForBuild({\n db: options.database,\n project,\n sha: build.gitSha,\n status: \"pending\",\n url: pendingUrl,\n providers: gitHosts,\n secret: config.secret,\n logger,\n }).catch((error: unknown) => {\n logger.error({ err: error }, \"failed to post pending status\");\n });\n // Skip capture if already merged (PR closed) or locally deduped\n if (!build.isDefault) {\n const merged = await isAlreadyMerged({\n providers: gitHosts,\n sha: build.gitSha,\n branch: build.gitBranch,\n secret: config.secret,\n db: options.database,\n projectId: project.id,\n logger,\n }).catch(() => false);\n if (merged) {\n logger.info({ buildId: build.id, sha: build.gitSha, branch: build.gitBranch }, \"skipping capture — already merged\");\n await builds.setStatus(build.id, \"approved\").catch(() => {}); // intentionally empty — fire-and-forget\n await postStatusesForBuild({\n db: options.database,\n project,\n sha: build.gitSha,\n status: \"success\",\n url: pendingUrl,\n providers: gitHosts,\n secret: config.secret,\n logger,\n }).catch(() => {}); // intentionally empty — fire-and-forget\n return;\n }\n }\n // Local dedupe: if another approved build for same sha exists, skip render\n const dup = await hasApprovedBuildForSha(options.database, project.id, build.gitSha, build.id);\n if (dup) {\n logger.info({ buildId: build.id, sha: build.gitSha }, \"skipping capture — duplicate sha already approved\");\n await builds.setStatus(build.id, \"approved\").catch(() => {}); // intentionally empty — fire-and-forget\n await postStatusesForBuild({\n db: options.database,\n project,\n sha: build.gitSha,\n status: \"success\",\n url: pendingUrl,\n providers: gitHosts,\n secret: config.secret,\n logger,\n }).catch(() => {}); // intentionally empty — fire-and-forget\n return;\n }\n try {\n await executeCaptureJob({ buildId: job.buildId, reqId: job.reqId }, jobOptions);\n const updated = await builds.get(job.buildId);\n if (updated) {\n const terminal =\n updated.status === \"approved\"\n ? \"success\"\n : updated.status === \"rejected\" || updated.status === \"failed\"\n ? \"failure\"\n : null;\n if (terminal) {\n await postStatusesForBuild({\n db: options.database,\n project,\n sha: updated.gitSha,\n status: terminal,\n url: pendingUrl,\n providers: gitHosts,\n secret: config.secret,\n logger,\n }).catch((error: unknown) => {\n logger.error({ err: error }, \"failed to post terminal status\");\n });\n }\n }\n } catch (error: unknown) {\n await postStatusesForBuild({\n db: options.database,\n project,\n sha: build.gitSha,\n status: \"failure\",\n url: pendingUrl,\n providers: gitHosts,\n secret: config.secret,\n logger,\n }).catch(() => {\n // ignore: status post failure already logged\n });\n throw error;\n }\n },\n });\n queue = captureQueue;\n enqueueCapture = async (buildId: string, reqId?: string): Promise<void> => {\n await captureQueue.enqueue({ buildId, reqId });\n };\n }\n\n app.use(\"*\", requestId());\n\n // Structured request logging. Uses a Hono-native middleware rather than\n // pino-http, which expects a Node server response (`res.on`) incompatible\n // with Hono's Web `Request`/`Response` model.\n app.use(\"*\", async (c, next) => {\n const started = performance.now();\n const id = c.get(\"requestId\");\n logger.info({ reqId: id, method: c.req.method, url: c.req.path }, \"request start\");\n await next();\n logger.info(\n {\n reqId: id,\n method: c.req.method,\n url: c.req.path,\n status: c.res.status,\n durationMs: Math.round(performance.now() - started),\n },\n \"request end\",\n );\n });\n\n app.use(\"/api/v1/*\", rateLimit({ windowMs: 60_000, max: 100 }));\n app.use(\"/api/v1/tokens/*\", rateLimit({ windowMs: 60_000, max: 10 }));\n app.use(\"/api/v1/webhooks/*\", rateLimit({ windowMs: 60_000, max: 20 }));\n app.use(\"/projects/:slug/settings/*\", csrf());\n\n app.use(\"*\", async (c, next) => {\n const user = await resolveUser(c, options);\n return runWithStore(\n {\n db: options.database,\n storage: options.storage,\n config,\n ui,\n logger,\n user,\n authEnabled,\n enqueueCapture,\n captureQueue: queue,\n gitHosts,\n },\n async () => {\n await next();\n },\n );\n });\n\n // Gate the server-rendered UI behind auth (ADR 0008): unauthenticated HTML\n // requests are redirected to the login page. API and auth routes are handled\n // by their own routers (401/403 vs the login flow).\n // eslint-disable-next-line require-await -- async is required to match Hono's middleware signature\n app.use(\"*\", async (c, next) => {\n const { user, authEnabled: isAuthEnabled } = getStore();\n const path = c.req.path;\n if (\n !isAuthEnabled ||\n user ||\n path.startsWith(\"/api/\") ||\n path.startsWith(\"/auth/\") ||\n path.startsWith(\"/assets/\")\n ) {\n return next();\n }\n if (c.req.header(\"HX-Request\") === \"true\") {\n c.header(\"HX-Redirect\", \"/auth/login\");\n return c.body(null, 204);\n }\n return c.redirect(\"/auth/login\", 302);\n });\n\n registerProjects(app);\n registerBuilds(app);\n registerLabels(app);\n registerMedia(app);\n registerMembers(app);\n registerTokens(app);\n registerWebhooks(app);\n registerStatusConfigs(app);\n registerAdmin(app);\n if (options.auth) {\n registerAuth(app, options.auth);\n }\n registerAssets(app);\n registerUiPages(app);\n\n app.doc(\"/api/v1/openapi.json\", {\n openapi: \"3.0.0\",\n info: {\n title: \"StoryShelf API\",\n description:\n \"REST API for the StoryShelf visual testing platform. JSON endpoints live under /api/v1; HTML pages are served at /.\",\n version: \"1.0.0\",\n },\n });\n app.get(\"/api/v1/docs\", swaggerUI({ url: \"/api/v1/openapi.json\" }));\n\n return app;\n}\n\nexport type { ShelfOptions, ShelfConfig, UIConfig, BrandTheme } from \"./config.ts\";\nexport type { DatabaseAdapter, ListOptions } from \"./adapters/database.ts\";\nexport type { StorageAdapter } from \"./adapters/storage.ts\";\nexport type {\n CaptureRunner,\n JobStatus,\n RenderedSnapshot,\n RenderResult,\n RenderFailure,\n} from \"./adapters/capture-runner.ts\";\nexport type { CaptureQueue, CaptureJob, QueueEntry } from \"./adapters/capture-queue.ts\";\nexport type { AuthAdapter, AuthUser, AuthCallback } from \"./adapters/auth.ts\";\nexport type { GitHostProvider, GitHostAdapter, CheckStatus } from \"./adapters/git-host/index.ts\";\nexport type { AdapterMetadata, GitAdapterMetadata } from \"./adapters/metadata.ts\";\nexport { describeStatus, buildCommentMarkdown, commentMarker } from \"./adapters/git-host/helpers.ts\";\nexport type { DiffOptions, DiffResult } from \"./diff/options.ts\";\nexport type { Viewport, StoryEntry, StorySourceAdapter } from \"./capture/adapter.ts\";\nexport { createShelfLogger, type LoggerOptions, type PinoTransport } from \"./logger.ts\";\nexport { diffImages } from \"./diff/engine.ts\";\nexport type { RenderedContent } from \"./ui/document.tsx\";\nexport { StorybookAdapter } from \"./capture/storybook.ts\";\nexport { persistCapture, type CaptureContext } from \"./capture/pipeline.ts\";\nexport { executeCaptureJob, type CaptureJobOptions } from \"./capture/orchestrator.ts\";\nexport { DEFAULT_VIEWPORTS } from \"./capture/adapter.ts\";\nexport { InMemoryCaptureQueue, type InMemoryCaptureQueueOptions } from \"./capture/queue.ts\";\nexport { Retention } from \"./retention/purge.ts\";\nexport { createUrlBuilder, type UrlBuilder } from \"./urls.ts\";\nexport { ulid, slugify } from \"./utils/ulid.ts\";\nexport {\n baselinePath,\n diffPath,\n screenshotPath,\n storybookDir,\n storybookZipPath,\n} from \"./utils/paths.ts\";\nexport * from \"./schema.ts\";\nexport * from \"./types.ts\";\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.tsx"],"sourcesContent":["import { swaggerUI } from \"@hono/swagger-ui\";\nimport { OpenAPIHono } from \"@hono/zod-openapi\";\nimport { requestId } from \"hono/request-id\";\nimport type { AuthUser } from \"./adapters/auth.ts\";\nimport type { CaptureQueue } from \"./adapters/capture-queue.ts\";\nimport type { ShelfOptions } from \"./config.ts\";\nimport type { AdapterMetadata, GitAdapterMetadata } from \"./adapters/metadata.ts\";\nimport { validateConfig, validateUiConfig } from \"./config.ts\";\nimport { createShelfLogger } from \"./logger.ts\";\nimport { csrf, rateLimit } from \"./middleware/index.ts\";\nimport { BuildModel } from \"./models/build.ts\";\nimport { ProjectModel } from \"./models/project.ts\";\nimport { registerAdmin } from \"./routers/admin.ts\";\nimport { registerAssets } from \"./routers/assets.ts\";\nimport { registerAuth } from \"./routers/auth.ts\";\nimport { registerBuilds } from \"./routers/builds.ts\";\nimport { registerLabels } from \"./routers/labels.ts\";\nimport { registerMedia } from \"./routers/media.ts\";\nimport { registerMembers } from \"./routers/members.ts\";\nimport { registerProjects } from \"./routers/projects.ts\";\nimport { registerStatusConfigs } from \"./routers/status-configs.ts\";\nimport { registerStorybook } from \"./routers/storybook.ts\";\nimport { registerTokens } from \"./routers/tokens.ts\";\nimport { registerUiPages } from \"./routers/ui.ts\";\nimport { registerWebhooks } from \"./routers/webhooks.ts\";\nimport { getStore, runWithStore } from \"./store.ts\";\nimport { executeCaptureJob, type CaptureJobOptions } from \"./capture/orchestrator.ts\";\nimport { InMemoryCaptureQueue } from \"./capture/queue.ts\";\nimport { postStatusesForBuild } from \"./capture/status-fanout.ts\";\n\nexport interface ShelfContext {\n requestId?: string;\n db: import(\"./adapters/database.ts\").DatabaseAdapter;\n storage: import(\"./adapters/storage.ts\").StorageAdapter;\n config: import(\"./config.ts\").ShelfConfig;\n ui: import(\"./config.ts\").UIConfig;\n logger: ReturnType<typeof createShelfLogger>;\n user: import(\"./adapters/auth.ts\").AuthUser | null;\n authEnabled: boolean;\n enqueueCapture?: (buildId: string, reqId?: string) => Promise<void>;\n captureQueue: import(\"./adapters/capture-queue.ts\").CaptureQueue | null;\n gitHosts: import(\"./adapters/git-host/index.ts\").GitHostProvider[];\n}\n\nexport type ShelfApp = OpenAPIHono<{ Variables: ShelfContext }>;\n\nfunction buildAdapterSnapshot(options: ShelfOptions): Record<string, AdapterMetadata | GitAdapterMetadata> {\n const snap: Record<string, AdapterMetadata | GitAdapterMetadata> = {};\n if (options.database.metadata) {snap[\"database\"] = options.database.metadata;}\n if (options.storage.metadata) {snap[\"storage\"] = options.storage.metadata;}\n if (options.captureRunner?.metadata) {snap[\"captureRunner\"] = options.captureRunner.metadata;}\n if (options.captureQueue?.metadata) {snap[\"captureQueue\"] = options.captureQueue.metadata;}\n if (options.auth?.metadata) {snap[\"auth\"] = options.auth.metadata;}\n for (const p of options.gitHosts ?? []) {\n snap[`git:${p.metadata.kind}`] = p.metadata;\n }\n return snap;\n}\n\nasync function hasApprovedBuildForSha(\n db: import(\"./adapters/database.ts\").DatabaseAdapter,\n projectId: string,\n sha: string,\n excludeBuildId: string,\n): Promise<boolean> {\n const builds = await new BuildModel(db).list(projectId);\n return builds.some((b) => b.gitSha === sha && b.id !== excludeBuildId && b.status === \"approved\");\n}\n\n// eslint-disable-next-line max-statements, complexity\nasync function isAlreadyMerged(opts: {\n providers: import(\"./adapters/git-host/index.ts\").GitHostProvider[];\n sha: string;\n branch: string;\n secret: string | undefined;\n db: import(\"./adapters/database.ts\").DatabaseAdapter;\n projectId: string;\n logger?: import(\"./logger.ts\").Logger;\n}): Promise<boolean> {\n if (opts.providers.length === 0) {return false;}\n const { StatusConfigModel } = await import(\"./models/status-config.ts\");\n const model = new StatusConfigModel(opts.db, opts.secret);\n const rows = await model.list(opts.projectId);\n for (const row of rows) {\n const provider = opts.providers.find((p) => p.metadata.kind === row.provider);\n if (!provider) {continue;}\n let parsed: unknown;\n try {\n parsed = JSON.parse(row.config);\n provider.metadata.schema.parse(parsed);\n } catch {\n continue;\n }\n let token: string;\n try {\n token = model.decryptToken(row);\n } catch {\n continue;\n }\n const adapter = provider.create({ config: parsed, token, logger: opts.logger });\n if (!adapter.isMerged) {continue;}\n try {\n // eslint-disable-next-line no-await-in-loop -- short-circuit on first merged status\n const merged = await adapter.isMerged({ sha: opts.sha, branch: opts.branch });\n if (merged) {return true;}\n } catch {\n // Ignore provider errors — do not skip on failure\n }\n }\n return false;\n}\n\nasync function resolveUser(\n c: { req: { raw: Request; header: (name: string) => string | undefined } },\n options: ShelfOptions,\n): Promise<AuthUser | null> {\n if (!options.auth) {\n return null;\n }\n if (c.req.header(\"authorization\")) {\n return null;\n }\n return await options.auth.check(c.req.raw);\n}\n\n\nexport function createShelfRouter(options: ShelfOptions): ShelfApp {\n const app = new OpenAPIHono<{ Variables: ShelfContext }>();\n // eslint-disable-next-line typescript/no-unnecessary-type-assertion -- ShelfConfig lacks index signature\n const rawConfig = options.config ? validateConfig(options.config as unknown as Record<string, unknown>) : {};\n // eslint-disable-next-line typescript/no-unnecessary-type-assertion -- UIConfig lacks index signature\n const ui = options.ui ? validateUiConfig(options.ui as unknown as Record<string, unknown>) : {};\n const logger = options.logger ?? createShelfLogger();\n const authEnabled = options.auth !== undefined;\n\n const gitHosts = options.gitHosts ?? [];\n\n // Adapter introspection — auto-populate config.adapters if not supplied\n const config: import(\"./config.ts\").ShelfConfig = rawConfig.adapters\n ? rawConfig\n : {\n ...rawConfig,\n adapters: buildAdapterSnapshot(options),\n };\n\n let queue: CaptureQueue | null = null;\n let enqueueCapture: ((buildId: string, reqId?: string) => Promise<void>) | undefined;\n if (options.captureRunner) {\n if (!config.scratchDir) {\n throw new Error(\"captureRunner is enabled but ShelfConfig.scratchDir is not set\");\n }\n const jobOptions: CaptureJobOptions = {\n db: options.database,\n storage: options.storage,\n runner: options.captureRunner,\n scratchDir: config.scratchDir,\n viewports: config.viewports,\n logger,\n };\n const captureQueue =\n options.captureQueue ??\n new InMemoryCaptureQueue({\n concurrency: config.captureConcurrency ?? 2,\n logger,\n runJob: async (job): Promise<void> => {\n const builds = new BuildModel(options.database);\n const build = await builds.get(job.buildId);\n if (!build) {\n await executeCaptureJob({ buildId: job.buildId, reqId: job.reqId }, jobOptions);\n return;\n }\n const project = await new ProjectModel(options.database).get(build.projectId);\n if (!project) {\n await executeCaptureJob({ buildId: job.buildId, reqId: job.reqId }, jobOptions);\n return;\n }\n const pendingUrl = `/projects/${project.slug}/builds/${job.buildId}`;\n await postStatusesForBuild({\n db: options.database,\n project,\n sha: build.gitSha,\n status: \"pending\",\n url: pendingUrl,\n providers: gitHosts,\n secret: config.secret,\n logger,\n }).catch((error: unknown) => {\n logger.error({ err: error }, \"failed to post pending status\");\n });\n // Skip capture if already merged (PR closed) or locally deduped\n if (!build.isDefault) {\n const merged = await isAlreadyMerged({\n providers: gitHosts,\n sha: build.gitSha,\n branch: build.gitBranch,\n secret: config.secret,\n db: options.database,\n projectId: project.id,\n logger,\n }).catch(() => false);\n if (merged) {\n logger.info({ buildId: build.id, sha: build.gitSha, branch: build.gitBranch }, \"skipping capture — already merged\");\n await builds.setStatus(build.id, \"approved\").catch(() => {}); // Intentionally empty — fire-and-forget\n await postStatusesForBuild({\n db: options.database,\n project,\n sha: build.gitSha,\n status: \"success\",\n url: pendingUrl,\n providers: gitHosts,\n secret: config.secret,\n logger,\n }).catch(() => {}); // Intentionally empty — fire-and-forget\n return;\n }\n }\n // Local dedupe: if another approved build for same sha exists, skip render\n const dup = await hasApprovedBuildForSha(options.database, project.id, build.gitSha, build.id);\n if (dup) {\n logger.info({ buildId: build.id, sha: build.gitSha }, \"skipping capture — duplicate sha already approved\");\n await builds.setStatus(build.id, \"approved\").catch(() => {}); // Intentionally empty — fire-and-forget\n await postStatusesForBuild({\n db: options.database,\n project,\n sha: build.gitSha,\n status: \"success\",\n url: pendingUrl,\n providers: gitHosts,\n secret: config.secret,\n logger,\n }).catch(() => {}); // Intentionally empty — fire-and-forget\n return;\n }\n try {\n await executeCaptureJob({ buildId: job.buildId, reqId: job.reqId }, jobOptions);\n const updated = await builds.get(job.buildId);\n if (updated) {\n const terminal =\n updated.status === \"approved\"\n ? \"success\"\n : (updated.status === \"rejected\" || updated.status === \"failed\"\n ? \"failure\"\n : null);\n if (terminal) {\n await postStatusesForBuild({\n db: options.database,\n project,\n sha: updated.gitSha,\n status: terminal,\n url: pendingUrl,\n providers: gitHosts,\n secret: config.secret,\n logger,\n }).catch((error: unknown) => {\n logger.error({ err: error }, \"failed to post terminal status\");\n });\n }\n }\n } catch (error: unknown) {\n await postStatusesForBuild({\n db: options.database,\n project,\n sha: build.gitSha,\n status: \"failure\",\n url: pendingUrl,\n providers: gitHosts,\n secret: config.secret,\n logger,\n }).catch(() => {\n // Ignore: status post failure already logged\n });\n throw error;\n }\n },\n });\n queue = captureQueue;\n enqueueCapture = async (buildId: string, reqId?: string): Promise<void> => {\n await captureQueue.enqueue({ buildId, reqId });\n };\n }\n\n app.use(\"*\", requestId());\n\n // Structured request logging. Uses a Hono-native middleware rather than\n // Pino-http, which expects a Node server response (`res.on`) incompatible\n // With Hono's Web `Request`/`Response` model.\n app.use(\"*\", async (c, next) => {\n const started = performance.now();\n const id = c.get(\"requestId\");\n logger.info({ reqId: id, method: c.req.method, url: c.req.path }, \"request start\");\n await next();\n logger.info(\n {\n reqId: id,\n method: c.req.method,\n url: c.req.path,\n status: c.res.status,\n durationMs: Math.round(performance.now() - started),\n },\n \"request end\",\n );\n });\n\n app.use(\"/api/v1/*\", rateLimit({ windowMs: 60_000, max: 100 }));\n app.use(\"/api/v1/tokens/*\", rateLimit({ windowMs: 60_000, max: 10 }));\n app.use(\"/api/v1/webhooks/*\", rateLimit({ windowMs: 60_000, max: 20 }));\n app.use(\"/projects/:slug/settings/*\", csrf());\n\n app.use(\"*\", async (c, next) => {\n const user = await resolveUser(c, options);\n return runWithStore(\n {\n db: options.database,\n storage: options.storage,\n config,\n ui,\n logger,\n user,\n authEnabled,\n enqueueCapture,\n captureQueue: queue,\n gitHosts,\n },\n async () => {\n await next();\n },\n );\n });\n\n // Gate the server-rendered UI behind auth (ADR 0008): unauthenticated HTML\n // Requests are redirected to the login page. API and auth routes are handled\n // By their own routers (401/403 vs the login flow).\n // eslint-disable-next-line require-await -- async is required to match Hono's middleware signature\n app.use(\"*\", async (c, next) => {\n const { user, authEnabled: isAuthEnabled } = getStore();\n const {path} = c.req;\n if (\n !isAuthEnabled ||\n user ||\n path.startsWith(\"/api/\") ||\n path.startsWith(\"/auth/\") ||\n path.startsWith(\"/assets/\") ||\n // Published Storybook routes enforce their own auth inside the handler\n // (public builds are viewable without a session, ADR 0011).\n path.startsWith(\"/projects/\") && path.includes(\"/storybook\")\n ) {\n return next();\n }\n if (c.req.header(\"HX-Request\") === \"true\") {\n c.header(\"HX-Redirect\", \"/auth/login\");\n return c.body(null, 204);\n }\n return c.redirect(\"/auth/login\", 302);\n });\n\n registerProjects(app);\n registerBuilds(app);\n registerLabels(app);\n registerMedia(app);\n registerMembers(app);\n registerTokens(app);\n registerWebhooks(app);\n registerStatusConfigs(app);\n registerAdmin(app);\n if (options.auth) {\n registerAuth(app, options.auth);\n }\n registerAssets(app);\n registerStorybook(app);\n registerUiPages(app);\n\n app.doc(\"/api/v1/openapi.json\", {\n openapi: \"3.0.0\",\n info: {\n title: \"StoryShelf API\",\n description:\n \"REST API for the StoryShelf visual testing platform. JSON endpoints live under /api/v1; HTML pages are served at /.\",\n version: \"1.0.0\",\n },\n });\n app.get(\"/api/v1/docs\", swaggerUI({ url: \"/api/v1/openapi.json\" }));\n\n return app;\n}\n\nexport type { ShelfOptions, ShelfConfig, UIConfig, BrandTheme } from \"./config.ts\";\nexport type { DatabaseAdapter, ListOptions } from \"./adapters/database.ts\";\nexport type { StorageAdapter } from \"./adapters/storage.ts\";\nexport type {\n CaptureRunner,\n JobStatus,\n RenderedSnapshot,\n RenderResult,\n RenderFailure,\n} from \"./adapters/capture-runner.ts\";\nexport type { CaptureQueue, CaptureJob, QueueEntry } from \"./adapters/capture-queue.ts\";\nexport type { AuthAdapter, AuthUser, AuthCallback } from \"./adapters/auth.ts\";\nexport type { GitHostProvider, GitHostAdapter, CheckStatus } from \"./adapters/git-host/index.ts\";\nexport type { AdapterMetadata, GitAdapterMetadata } from \"./adapters/metadata.ts\";\nexport { describeStatus, buildCommentMarkdown, commentMarker } from \"./adapters/git-host/helpers.ts\";\nexport type { DiffOptions, DiffResult } from \"./diff/options.ts\";\nexport type { Viewport, StoryEntry, StorySourceAdapter } from \"./capture/adapter.ts\";\nexport { createShelfLogger, type LoggerOptions, type Logger, type PinoTransport } from \"./logger.ts\";\nexport { diffImages } from \"./diff/engine.ts\";\nexport type { RenderedContent } from \"./ui/document.tsx\";\nexport { StorybookAdapter } from \"./capture/storybook.ts\";\nexport { persistCapture, type CaptureContext } from \"./capture/pipeline.ts\";\nexport { executeCaptureJob, type CaptureJobOptions } from \"./capture/orchestrator.ts\";\nexport { DEFAULT_VIEWPORTS } from \"./capture/adapter.ts\";\nexport { InMemoryCaptureQueue, type InMemoryCaptureQueueOptions } from \"./capture/queue.ts\";\nexport { Retention } from \"./retention/purge.ts\";\nexport { createUrlBuilder, type UrlBuilder } from \"./urls.ts\";\nexport { ulid, slugify } from \"./utils/ulid.ts\";\nexport {\n baselinePath,\n diffPath,\n screenshotPath,\n storybookDir,\n storybookZipPath,\n} from \"./utils/paths.ts\";\nexport * from \"./schema.ts\";\nexport * from \"./types.ts\";\n"],"mappings":"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"}
|
package/dist/logger.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import __tsdown_shims_path from 'node:path';
|
|
2
2
|
import __tsdown_shims_url from 'node:url';
|
|
3
|
-
import { Logger } from "pino";
|
|
3
|
+
import { Logger as Logger$1 } from "pino";
|
|
4
4
|
//#region src/logger.d.ts
|
|
5
5
|
/** A pino worker transport to attach to the logger output. */
|
|
6
6
|
interface PinoTransport {
|
|
@@ -33,7 +33,7 @@ interface LoggerOptions {
|
|
|
33
33
|
* Datadog, GCP, OTEL collector, etc.) are added as pino worker `transports` —
|
|
34
34
|
* they are sinks for this logger, not standalone loggers.
|
|
35
35
|
*/
|
|
36
|
-
declare function createShelfLogger(options?: LoggerOptions): Logger;
|
|
36
|
+
declare function createShelfLogger(options?: LoggerOptions): Logger$1;
|
|
37
37
|
//#endregion
|
|
38
|
-
export { LoggerOptions, PinoTransport, createShelfLogger };
|
|
38
|
+
export { type Logger$1 as Logger, LoggerOptions, PinoTransport, createShelfLogger };
|
|
39
39
|
//# sourceMappingURL=logger.d.mts.map
|
package/dist/logger.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.mjs","names":[],"sources":["../src/logger.ts"],"sourcesContent":["import pino
|
|
1
|
+
{"version":3,"file":"logger.mjs","names":[],"sources":["../src/logger.ts"],"sourcesContent":["import pino from \"pino\";\nimport type { Logger } from \"pino\";\n\nexport type { Logger };\n\n/** A pino worker transport to attach to the logger output. */\nexport interface PinoTransport {\n /** Package name or absolute path of the transport module. */\n target: string;\n /** Options passed to the transport worker. */\n options?: Record<string, unknown>;\n}\n\n/** Configuration for constructing the shelf logger. */\nexport interface LoggerOptions {\n /** Minimum level to emit. Defaults to `\"info\"`. */\n level?: string;\n /** Extra pino worker transports appended to the default stdout sink. */\n transports?: PinoTransport[];\n /** Deployment environment recorded in the `env` base field. Defaults to `NODE_ENV`. */\n env?: string;\n}\n\n/**\n * Create the structured JSON logger used across StoryShelf.\n *\n * Logging conventions:\n * - Always log structured objects, never interpolate into the message:\n * `logger.info({ projectId, buildId }, \"build started\")`.\n * - Attach errors as an `err` child field rather than stringifying into the message:\n * `logger.error({ err }, \"capture failed\")`.\n * - Derive scoped child loggers for background work:\n * `const captureLogger = logger.child({ buildId })`.\n *\n * The default sink is stdout. Hosted observability platforms (Sentry, PostHog,\n * Datadog, GCP, OTEL collector, etc.) are added as pino worker `transports` —\n * they are sinks for this logger, not standalone loggers.\n */\nexport function createShelfLogger(options: LoggerOptions = {}): Logger {\n const targets = [\n { target: \"pino/file\", options: { destination: 1 } },\n ...(options.transports ?? []),\n ];\n const transport = pino.transport({ targets });\n return pino(\n {\n level: options.level ?? \"info\",\n base: { env: options.env ?? process.env[\"NODE_ENV\"] },\n },\n transport,\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAsCA,SAAgB,kBAAkB,UAAyB,CAAC,GAAW;CACrE,MAAM,UAAU,CACd;EAAE,QAAQ;EAAa,SAAS,EAAE,aAAa,EAAE;CAAE,GACnD,GAAI,QAAQ,cAAc,CAAC,CAC7B;CACA,MAAM,YAAY,KAAK,UAAU,EAAE,QAAQ,CAAC;CAC5C,OAAO,KACL;EACE,OAAO,QAAQ,SAAS;EACxB,MAAM,EAAE,KAAK,QAAQ,OAAO,QAAQ,IAAI,YAAY;CACtD,GACA,SACF;AACF"}
|
package/dist/middleware/csrf.mjs
CHANGED
|
@@ -20,7 +20,7 @@ function verifyToken(token, sessionId) {
|
|
|
20
20
|
}
|
|
21
21
|
function csrf() {
|
|
22
22
|
return async (c, next) => {
|
|
23
|
-
const method = c.req
|
|
23
|
+
const { method } = c.req;
|
|
24
24
|
if (method === "GET" || method === "HEAD" || method === "OPTIONS") {
|
|
25
25
|
const token = generateToken(c.req.header("session-id") ?? "default");
|
|
26
26
|
c.header("X-CSRF-Token", token);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"csrf.mjs","names":[],"sources":["../../src/middleware/csrf.ts"],"sourcesContent":["import { createHash, randomBytes } from \"node:crypto\";\nimport type { Context, Next } from \"hono\";\n\nconst CSRF_SECRET = process.env[\"CSRF_SECRET\"] ?? randomBytes(32).toString(\"hex\");\n\nfunction generateToken(sessionId: string): string {\n const timestamp = Date.now().toString(36);\n const payload = `${sessionId}:${timestamp}`;\n const signature = createHash(\"sha256\").update(`${CSRF_SECRET}:${payload}`).digest(\"hex\").slice(0, 16);\n return `${payload}:${signature}`;\n}\n\nfunction verifyToken(token: string, sessionId: string): boolean {\n const parts = token.split(\":\");\n if (parts.length !== 3) {\n return false;\n }\n const [payloadTimestamp, signature] = parts.slice(1);\n if (!payloadTimestamp || !signature) {\n return false;\n }\n const expectedSignature = createHash(\"sha256\").update(`${CSRF_SECRET}:${sessionId}:${payloadTimestamp}`).digest(\"hex\").slice(0, 16);\n if (signature !== expectedSignature) {\n return false;\n }\n const timestamp = Number.parseInt(payloadTimestamp, 36);\n const age = Date.now() - timestamp;\n return age > 0 && age < 24 * 60 * 60 * 1000;\n}\n\nexport function csrf() {\n // oxlint-disable-next-line typescript/no-invalid-void-type -- Hono middleware may not return Response\n return async (c: Context, next: Next): Promise<Response | void> => {\n const method = c.req
|
|
1
|
+
{"version":3,"file":"csrf.mjs","names":[],"sources":["../../src/middleware/csrf.ts"],"sourcesContent":["import { createHash, randomBytes } from \"node:crypto\";\nimport type { Context, Next } from \"hono\";\n\nconst CSRF_SECRET = process.env[\"CSRF_SECRET\"] ?? randomBytes(32).toString(\"hex\");\n\nfunction generateToken(sessionId: string): string {\n const timestamp = Date.now().toString(36);\n const payload = `${sessionId}:${timestamp}`;\n const signature = createHash(\"sha256\").update(`${CSRF_SECRET}:${payload}`).digest(\"hex\").slice(0, 16);\n return `${payload}:${signature}`;\n}\n\nfunction verifyToken(token: string, sessionId: string): boolean {\n const parts = token.split(\":\");\n if (parts.length !== 3) {\n return false;\n }\n const [payloadTimestamp, signature] = parts.slice(1);\n if (!payloadTimestamp || !signature) {\n return false;\n }\n const expectedSignature = createHash(\"sha256\").update(`${CSRF_SECRET}:${sessionId}:${payloadTimestamp}`).digest(\"hex\").slice(0, 16);\n if (signature !== expectedSignature) {\n return false;\n }\n const timestamp = Number.parseInt(payloadTimestamp, 36);\n const age = Date.now() - timestamp;\n return age > 0 && age < 24 * 60 * 60 * 1000;\n}\n\nexport function csrf() {\n // oxlint-disable-next-line typescript/no-invalid-void-type -- Hono middleware may not return Response\n return async (c: Context, next: Next): Promise<Response | void> => {\n const {method} = c.req;\n if (method === \"GET\" || method === \"HEAD\" || method === \"OPTIONS\") {\n const sessionId = c.req.header(\"session-id\") ?? \"default\";\n const token = generateToken(sessionId);\n c.header(\"X-CSRF-Token\", token);\n await next();\n return;\n }\n const sessionId = c.req.header(\"session-id\") ?? \"default\";\n const token = c.req.header(\"x-csrf-token\") ?? c.req.query(\"csrf_token\");\n if (!token || !verifyToken(token, sessionId)) {\n return c.json({ error: \"Invalid CSRF token\" }, 403);\n }\n await next();\n };\n}\n\nexport function getCsrfToken(sessionId: string): string {\n return generateToken(sessionId);\n}\n\n"],"mappings":";;;;;AAGA,MAAM,cAAc,QAAQ,IAAI,kBAAkB,YAAY,EAAE,CAAC,CAAC,SAAS,KAAK;AAEhF,SAAS,cAAc,WAA2B;CAEhD,MAAM,UAAU,GAAG,UAAU,GADX,KAAK,IAAI,CAAC,CAAC,SAAS,EACE;CAExC,OAAO,GAAG,QAAQ,GADA,WAAW,QAAQ,CAAC,CAAC,OAAO,GAAG,YAAY,GAAG,SAAS,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,GAAG,EACrE;AAC/B;AAEA,SAAS,YAAY,OAAe,WAA4B;CAC9D,MAAM,QAAQ,MAAM,MAAM,GAAG;CAC7B,IAAI,MAAM,WAAW,GACnB,OAAO;CAET,MAAM,CAAC,kBAAkB,aAAa,MAAM,MAAM,CAAC;CACnD,IAAI,CAAC,oBAAoB,CAAC,WACxB,OAAO;CAGT,IAAI,cADsB,WAAW,QAAQ,CAAC,CAAC,OAAO,GAAG,YAAY,GAAG,UAAU,GAAG,kBAAkB,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,GAAG,EAC9F,GAChC,OAAO;CAET,MAAM,YAAY,OAAO,SAAS,kBAAkB,EAAE;CACtD,MAAM,MAAM,KAAK,IAAI,IAAI;CACzB,OAAO,MAAM,KAAK,MAAM;AAC1B;AAEA,SAAgB,OAAO;CAErB,OAAO,OAAO,GAAY,SAAyC;EACjE,MAAM,EAAC,WAAU,EAAE;EACnB,IAAI,WAAW,SAAS,WAAW,UAAU,WAAW,WAAW;GAEjE,MAAM,QAAQ,cADI,EAAE,IAAI,OAAO,YAAY,KAAK,SACX;GACrC,EAAE,OAAO,gBAAgB,KAAK;GAC9B,MAAM,KAAK;GACX;EACF;EACA,MAAM,YAAY,EAAE,IAAI,OAAO,YAAY,KAAK;EAChD,MAAM,QAAQ,EAAE,IAAI,OAAO,cAAc,KAAK,EAAE,IAAI,MAAM,YAAY;EACtE,IAAI,CAAC,SAAS,CAAC,YAAY,OAAO,SAAS,GACzC,OAAO,EAAE,KAAK,EAAE,OAAO,qBAAqB,GAAG,GAAG;EAEpD,MAAM,KAAK;CACb;AACF"}
|
package/dist/models/build.d.mts
CHANGED
|
@@ -4,6 +4,16 @@ import { BuildStatus } from "../types.mjs";
|
|
|
4
4
|
import { DatabaseAdapter } from "../adapter/database.mjs";
|
|
5
5
|
import { Build } from "../schema.mjs";
|
|
6
6
|
//#region src/models/build.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* Return whether a build is publicly viewable without a session.
|
|
9
|
+
*
|
|
10
|
+
* A build is public iff `builds.public` is set or its branch matches the
|
|
11
|
+
* project's `public_branch_regex` (ADR 0011). Supplying an empty/nonexistent
|
|
12
|
+
* regex makes every build require auth.
|
|
13
|
+
*/
|
|
14
|
+
declare function isPublicBuild(project: {
|
|
15
|
+
publicBranchRegex: string | null;
|
|
16
|
+
}, build: Pick<Build, "public" | "gitBranch">): boolean;
|
|
7
17
|
interface BuildCreateInput {
|
|
8
18
|
gitSha: string;
|
|
9
19
|
gitBranch: string;
|
|
@@ -25,11 +35,16 @@ declare class BuildModel {
|
|
|
25
35
|
create(projectId: string, input: BuildCreateInput): Promise<Build>;
|
|
26
36
|
get(id: string): Promise<Build | null>;
|
|
27
37
|
list(projectId: string, filter?: BuildListFilter): Promise<Build[]>;
|
|
38
|
+
/** Return the most recent build intended for publishing, if any. */
|
|
39
|
+
latestPublished(project: {
|
|
40
|
+
id: string;
|
|
41
|
+
publicBranchRegex: string | null;
|
|
42
|
+
}): Promise<Build | null>;
|
|
28
43
|
update(id: string, patch: Partial<Pick<Build, "status" | "public" | "message" | "authorEmail" | "authorName">>): Promise<Build>;
|
|
29
44
|
setStatus(id: string, status: BuildStatus): Promise<Build>;
|
|
30
45
|
updateCounts(id: string): Promise<Build>;
|
|
31
46
|
remove(id: string): Promise<void>;
|
|
32
47
|
}
|
|
33
48
|
//#endregion
|
|
34
|
-
export { type Build, BuildCreateInput, BuildListFilter, BuildModel };
|
|
49
|
+
export { type Build, BuildCreateInput, BuildListFilter, BuildModel, isPublicBuild };
|
|
35
50
|
//# sourceMappingURL=build.d.mts.map
|
package/dist/models/build.mjs
CHANGED
|
@@ -5,6 +5,18 @@ import { buildLabels, builds, snapshots } from "../schema.mjs";
|
|
|
5
5
|
import { ulid } from "../utils/ulid.mjs";
|
|
6
6
|
import { and, desc, eq, inArray } from "drizzle-orm";
|
|
7
7
|
//#region src/models/build.ts
|
|
8
|
+
/**
|
|
9
|
+
* Return whether a build is publicly viewable without a session.
|
|
10
|
+
*
|
|
11
|
+
* A build is public iff `builds.public` is set or its branch matches the
|
|
12
|
+
* project's `public_branch_regex` (ADR 0011). Supplying an empty/nonexistent
|
|
13
|
+
* regex makes every build require auth.
|
|
14
|
+
*/
|
|
15
|
+
function isPublicBuild(project, build) {
|
|
16
|
+
if (build.public) return true;
|
|
17
|
+
if (!project.publicBranchRegex) return false;
|
|
18
|
+
return new RegExp(project.publicBranchRegex, "u").test(build.gitBranch);
|
|
19
|
+
}
|
|
8
20
|
var BuildModel = class {
|
|
9
21
|
db;
|
|
10
22
|
constructor(db) {
|
|
@@ -44,6 +56,12 @@ var BuildModel = class {
|
|
|
44
56
|
orderBy: desc(builds.createdAt)
|
|
45
57
|
});
|
|
46
58
|
}
|
|
59
|
+
/** Return the most recent build intended for publishing, if any. */
|
|
60
|
+
async latestPublished(project) {
|
|
61
|
+
const rows = await this.list(project.id);
|
|
62
|
+
for (const build of rows) if (isPublicBuild(project, build)) return build;
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
47
65
|
async update(id, patch) {
|
|
48
66
|
return await this.db.update(builds, id, {
|
|
49
67
|
...patch,
|
|
@@ -72,6 +90,6 @@ var BuildModel = class {
|
|
|
72
90
|
}
|
|
73
91
|
};
|
|
74
92
|
//#endregion
|
|
75
|
-
export { BuildModel };
|
|
93
|
+
export { BuildModel, isPublicBuild };
|
|
76
94
|
|
|
77
95
|
//# sourceMappingURL=build.mjs.map
|
|
@@ -1 +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":"
|
|
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\n/**\n * Return whether a build is publicly viewable without a session.\n *\n * A build is public iff `builds.public` is set or its branch matches the\n * project's `public_branch_regex` (ADR 0011). Supplying an empty/nonexistent\n * regex makes every build require auth.\n */\nexport function isPublicBuild(project: { publicBranchRegex: string | null }, build: Pick<Build, \"public\" | \"gitBranch\">): boolean {\n if (build.public) {\n return true;\n }\n if (!project.publicBranchRegex) {\n return false;\n }\n return new RegExp(project.publicBranchRegex, \"u\").test(build.gitBranch);\n}\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 /** Return the most recent build intended for publishing, if any. */\n async latestPublished(project: { id: string; publicBranchRegex: string | null }): Promise<Build | null> {\n const rows = await this.list(project.id);\n for (const build of rows) {\n if (isPublicBuild(project, build)) {\n return build;\n }\n }\n return null;\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":"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"}
|
package/dist/models/label.d.mts
CHANGED
|
@@ -27,6 +27,12 @@ declare class LabelModel {
|
|
|
27
27
|
linkTemplate?: string;
|
|
28
28
|
color?: string;
|
|
29
29
|
}): Promise<LabelType>;
|
|
30
|
+
/** Update a custom label type's name, template or color. */
|
|
31
|
+
updateType(projectId: string, key: string, input: {
|
|
32
|
+
name?: string;
|
|
33
|
+
linkTemplate?: string | null;
|
|
34
|
+
color?: string | null;
|
|
35
|
+
}): Promise<LabelType | null>;
|
|
30
36
|
/** Remove a custom label type, rejecting reserved or persistent types. */
|
|
31
37
|
removeType(projectId: string, key: string): Promise<void>;
|
|
32
38
|
/** Attach a label value to a build. */
|
package/dist/models/label.mjs
CHANGED
|
@@ -68,6 +68,17 @@ var LabelModel = class {
|
|
|
68
68
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
69
69
|
});
|
|
70
70
|
}
|
|
71
|
+
/** Update a custom label type's name, template or color. */
|
|
72
|
+
async updateType(projectId, key, input) {
|
|
73
|
+
if (key === "persistent" || RESERVED_LABEL_KEYS.includes(key)) throw new Error(`Label type '${key}' cannot be updated.`);
|
|
74
|
+
const existing = await this.getType(projectId, key);
|
|
75
|
+
if (!existing) return null;
|
|
76
|
+
return await this.db.update(labelTypes, existing.id, {
|
|
77
|
+
name: input.name ?? existing.name,
|
|
78
|
+
linkTemplate: input.linkTemplate === void 0 ? existing.linkTemplate : input.linkTemplate,
|
|
79
|
+
color: input.color === void 0 ? existing.color : input.color
|
|
80
|
+
});
|
|
81
|
+
}
|
|
71
82
|
/** Remove a custom label type, rejecting reserved or persistent types. */
|
|
72
83
|
async removeType(projectId, key) {
|
|
73
84
|
if (key === "persistent" || RESERVED_LABEL_KEYS.includes(key)) throw new Error(`Label type '${key}' cannot be removed.`);
|
|
@@ -1 +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":"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"}
|
|
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 /** Update a custom label type's name, template or color. */\n async updateType(projectId: string, key: string, input: { name?: string; linkTemplate?: string | null; color?: string | null }): Promise<LabelType | null> {\n if (key === PERSISTENT_LABEL_KEY || RESERVED_LABEL_KEYS.includes(key as never)) {\n throw new Error(`Label type '${key}' cannot be updated.`);\n }\n const existing = await this.getType(projectId, key);\n if (!existing) {\n return null;\n }\n return await this.db.update(labelTypes, existing.id, {\n name: input.name ?? existing.name,\n linkTemplate: input.linkTemplate === undefined ? existing.linkTemplate : input.linkTemplate,\n color: input.color === undefined ? existing.color : input.color,\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":"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"}
|
package/dist/models/project.mjs
CHANGED
|
@@ -52,7 +52,7 @@ var ProjectModel = class {
|
|
|
52
52
|
/** Update mutable fields of a project. */
|
|
53
53
|
async update(id, patch) {
|
|
54
54
|
const normalized = { ...patch };
|
|
55
|
-
if (patch.storybookMeta !== void 0 && patch.storybookMeta !== null && typeof patch.storybookMeta !== "string") normalized
|
|
55
|
+
if (patch.storybookMeta !== void 0 && patch.storybookMeta !== null && typeof patch.storybookMeta !== "string") normalized.storybookMeta = JSON.stringify(patch.storybookMeta);
|
|
56
56
|
return await this.db.update(projects, id, {
|
|
57
57
|
...normalized,
|
|
58
58
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project.mjs","names":[],"sources":["../../src/models/project.ts"],"sourcesContent":["import { eq } from \"drizzle-orm\";\n\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport { projects, type Project } from \"../schema.ts\";\nimport { slugify, ulid } from \"../utils/ulid.ts\";\n\nexport interface ProjectCreateInput {\n name: string;\n gitRepository?: string;\n gitDefaultBranch?: string;\n storybookMeta?: unknown;\n}\n\n/** Data operations for project records. */\nexport class ProjectModel {\n /**\n * @param db - Database adapter.\n */\n constructor(private readonly db: DatabaseAdapter) {}\n\n /**\n * Create a project with a unique slug.\n *\n * @param input - Project creation input.\n * @returns The created project.\n */\n async create(input: ProjectCreateInput): Promise<Project> {\n const now = new Date().toISOString();\n const slug = await this.uniqueSlug(input.name);\n return this.db.insert(projects, {\n id: ulid(),\n name: input.name,\n slug,\n gitRepository: input.gitRepository,\n gitDefaultBranch: input.gitDefaultBranch ?? \"main\",\n storybookMeta: input.storybookMeta ? JSON.stringify(input.storybookMeta) : null,\n createdAt: now,\n updatedAt: now,\n });\n }\n\n /** Fetch a project by id, or null if not found. */\n async get(id: string): Promise<Project | null> {\n return await this.db.get(projects, id);\n }\n\n /** Fetch a project by its slug, or null if not found. */\n async getBySlug(slug: string): Promise<Project | null> {\n const rows = await this.db.list(projects, { where: eq(projects.slug, slug), limit: 1 });\n return rows[0] ?? null;\n }\n\n /** List all projects. */\n async list(): Promise<Project[]> {\n return await this.db.list(projects);\n }\n\n /** Update mutable fields of a project. */\n async update(\n id: string,\n patch: Partial<Omit<Project, \"storybookMeta\">> & {\n storybookMeta?: unknown;\n },\n ): Promise<Project> {\n const normalized: Record<string, unknown> = { ...patch }
|
|
1
|
+
{"version":3,"file":"project.mjs","names":[],"sources":["../../src/models/project.ts"],"sourcesContent":["import { eq } from \"drizzle-orm\";\n\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport { projects, type Project } from \"../schema.ts\";\nimport { slugify, ulid } from \"../utils/ulid.ts\";\n\nexport interface ProjectCreateInput {\n name: string;\n gitRepository?: string;\n gitDefaultBranch?: string;\n storybookMeta?: unknown;\n}\n\n/** Data operations for project records. */\nexport class ProjectModel {\n /**\n * @param db - Database adapter.\n */\n constructor(private readonly db: DatabaseAdapter) {}\n\n /**\n * Create a project with a unique slug.\n *\n * @param input - Project creation input.\n * @returns The created project.\n */\n async create(input: ProjectCreateInput): Promise<Project> {\n const now = new Date().toISOString();\n const slug = await this.uniqueSlug(input.name);\n return this.db.insert(projects, {\n id: ulid(),\n name: input.name,\n slug,\n gitRepository: input.gitRepository,\n gitDefaultBranch: input.gitDefaultBranch ?? \"main\",\n storybookMeta: input.storybookMeta ? JSON.stringify(input.storybookMeta) : null,\n createdAt: now,\n updatedAt: now,\n });\n }\n\n /** Fetch a project by id, or null if not found. */\n async get(id: string): Promise<Project | null> {\n return await this.db.get(projects, id);\n }\n\n /** Fetch a project by its slug, or null if not found. */\n async getBySlug(slug: string): Promise<Project | null> {\n const rows = await this.db.list(projects, { where: eq(projects.slug, slug), limit: 1 });\n return rows[0] ?? null;\n }\n\n /** List all projects. */\n async list(): Promise<Project[]> {\n return await this.db.list(projects);\n }\n\n /** Update mutable fields of a project. */\n async update(\n id: string,\n patch: Partial<Omit<Project, \"storybookMeta\">> & {\n storybookMeta?: unknown;\n },\n ): Promise<Project> {\n const normalized: Partial<Project> & Record<string, unknown> = { ...patch } as unknown as Partial<Project> & Record<string, unknown>;\n if (patch.storybookMeta !== undefined && patch.storybookMeta !== null && typeof patch.storybookMeta !== \"string\") {\n normalized.storybookMeta = JSON.stringify(patch.storybookMeta);\n }\n return await this.db.update(projects, id, {\n ...normalized,\n updatedAt: new Date().toISOString(),\n });\n }\n\n /** Delete a project by id. */\n async remove(id: string): Promise<void> {\n await this.db.remove(projects, id);\n }\n\n private async uniqueSlug(name: string): Promise<string> {\n const base = slugify(name) || \"project\";\n const existing = await this.db.list(projects, { where: eq(projects.slug, base) });\n if (existing.length === 0) {\n return base;\n }\n const taken = new Set(existing.map((p) => p.slug));\n let suffix = 2;\n while (taken.has(`${base}-${suffix}`)) {\n suffix += 1;\n }\n return `${base}-${suffix}`;\n }\n}\n\nexport type { Project };\n"],"mappings":";;;;;;;;AAcA,IAAa,eAAb,MAA0B;CAIK;;;;CAA7B,YAAY,IAAsC;EAArB,KAAA,KAAA;CAAsB;;;;;;;CAQnD,MAAM,OAAO,OAA6C;EACxD,MAAM,uBAAM,IAAI,KAAK,EAAA,CAAE,YAAY;EACnC,MAAM,OAAO,MAAM,KAAK,WAAW,MAAM,IAAI;EAC7C,OAAO,KAAK,GAAG,OAAO,UAAU;GAC9B,IAAI,KAAK;GACT,MAAM,MAAM;GACZ;GACA,eAAe,MAAM;GACrB,kBAAkB,MAAM,oBAAoB;GAC5C,eAAe,MAAM,gBAAgB,KAAK,UAAU,MAAM,aAAa,IAAI;GAC3E,WAAW;GACX,WAAW;EACb,CAAC;CACH;;CAGA,MAAM,IAAI,IAAqC;EAC7C,OAAO,MAAM,KAAK,GAAG,IAAI,UAAU,EAAE;CACvC;;CAGA,MAAM,UAAU,MAAuC;EAErD,QAAO,MADY,KAAK,GAAG,KAAK,UAAU;GAAE,OAAO,GAAG,SAAS,MAAM,IAAI;GAAG,OAAO;EAAE,CAAC,EAAA,CAC1E,MAAM;CACpB;;CAGA,MAAM,OAA2B;EAC/B,OAAO,MAAM,KAAK,GAAG,KAAK,QAAQ;CACpC;;CAGA,MAAM,OACJ,IACA,OAGkB;EAClB,MAAM,aAAyD,EAAE,GAAG,MAAM;EAC1E,IAAI,MAAM,kBAAkB,KAAA,KAAa,MAAM,kBAAkB,QAAQ,OAAO,MAAM,kBAAkB,UACtG,WAAW,gBAAgB,KAAK,UAAU,MAAM,aAAa;EAE/D,OAAO,MAAM,KAAK,GAAG,OAAO,UAAU,IAAI;GACxC,GAAG;GACH,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;EACpC,CAAC;CACH;;CAGA,MAAM,OAAO,IAA2B;EACtC,MAAM,KAAK,GAAG,OAAO,UAAU,EAAE;CACnC;CAEA,MAAc,WAAW,MAA+B;EACtD,MAAM,OAAO,QAAQ,IAAI,KAAK;EAC9B,MAAM,WAAW,MAAM,KAAK,GAAG,KAAK,UAAU,EAAE,OAAO,GAAG,SAAS,MAAM,IAAI,EAAE,CAAC;EAChF,IAAI,SAAS,WAAW,GACtB,OAAO;EAET,MAAM,QAAQ,IAAI,IAAI,SAAS,KAAK,MAAM,EAAE,IAAI,CAAC;EACjD,IAAI,SAAS;EACb,OAAO,MAAM,IAAI,GAAG,KAAK,GAAG,QAAQ,GAClC,UAAU;EAEZ,OAAO,GAAG,KAAK,GAAG;CACpB;AACF"}
|
|
@@ -11,7 +11,7 @@ import { Badge, statusTone } from "../ui/components.mjs";
|
|
|
11
11
|
import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
12
12
|
//#region src/pages/build-detail.tsx
|
|
13
13
|
async function renderBuildDetailPage(buildId) {
|
|
14
|
-
const db = getStore()
|
|
14
|
+
const { db } = getStore();
|
|
15
15
|
const build = await new BuildModel(db).get(buildId);
|
|
16
16
|
if (!build) return null;
|
|
17
17
|
const project = await new ProjectModel(db).get(build.projectId);
|