@storyshelf/core 0.1.3 → 0.3.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/README.md +50 -46
- package/dist/adapter/auth.d.mts +4 -4
- package/dist/adapter/auth.mjs.map +1 -1
- package/dist/{adapters → adapter}/capture-queue.d.mts +4 -4
- package/dist/adapter/capture-queue.mjs +4 -0
- package/dist/adapter/capture-runner.d.mts +7 -7
- package/dist/adapter/database.d.mts +20 -9
- package/dist/adapter/database.mjs +60 -1
- package/dist/adapter/database.mjs.map +1 -0
- package/dist/adapter/git-host/comments.d.mts +41 -0
- package/dist/adapter/git-host/comments.mjs +54 -0
- package/dist/adapter/git-host/comments.mjs.map +1 -0
- package/dist/adapter/git-host.d.mts +3 -7
- package/dist/adapter/init.d.mts +58 -0
- package/dist/adapter/init.mjs +93 -0
- package/dist/adapter/init.mjs.map +1 -0
- package/dist/adapter/metadata.d.mts +83 -0
- package/dist/adapter/metadata.mjs +4 -0
- package/dist/adapter/storage.d.mts +15 -4
- package/dist/adapters/webhook-events.mjs +12 -4
- package/dist/adapters/webhook-events.mjs.map +1 -1
- package/dist/capture/adapter.d.mts +5 -1
- package/dist/capture/dispatch.mjs +150 -0
- package/dist/capture/dispatch.mjs.map +1 -0
- package/dist/capture/orchestrator.d.mts +2 -0
- package/dist/capture/orchestrator.mjs +5 -44
- package/dist/capture/orchestrator.mjs.map +1 -1
- package/dist/capture/pipeline.d.mts +4 -1
- package/dist/capture/pipeline.mjs +6 -6
- package/dist/capture/pipeline.mjs.map +1 -1
- package/dist/capture/queue.d.mts +2 -1
- package/dist/capture/queue.mjs +2 -1
- package/dist/capture/queue.mjs.map +1 -1
- package/dist/capture/skip-checks.mjs +76 -0
- package/dist/capture/skip-checks.mjs.map +1 -0
- package/dist/capture/statics.mjs +124 -0
- package/dist/capture/statics.mjs.map +1 -0
- package/dist/capture/status-fanout.mjs.map +1 -1
- package/dist/capture/storybook.mjs.map +1 -1
- package/dist/capture.d.mts +8 -0
- package/dist/capture.mjs +9 -0
- package/dist/config.d.mts +10 -2
- package/dist/config.mjs +14 -2
- package/dist/config.mjs.map +1 -1
- package/dist/ddl.d.mts +1 -1
- package/dist/ddl.mjs +1 -1
- package/dist/ddl.mjs.map +1 -1
- package/dist/diff/engine.mjs.map +1 -1
- package/dist/diff/options.d.mts +3 -1
- package/dist/diff.d.mts +5 -0
- package/dist/diff.mjs +6 -0
- package/dist/index.d.mts +32 -25
- package/dist/index.mjs +207 -253
- package/dist/index.mjs.map +1 -1
- package/dist/logger.d.mts +3 -3
- package/dist/middleware/auth-gate.mjs +36 -0
- package/dist/middleware/auth-gate.mjs.map +1 -0
- package/dist/middleware/csrf.mjs.map +1 -1
- package/dist/middleware/request-log.mjs +28 -0
- package/dist/middleware/request-log.mjs.map +1 -0
- package/dist/middleware/store-scope.mjs +29 -0
- package/dist/middleware/store-scope.mjs.map +1 -0
- package/dist/models/baseline.mjs +7 -5
- package/dist/models/baseline.mjs.map +1 -1
- package/dist/models/build.mjs +3 -1
- package/dist/models/build.mjs.map +1 -1
- package/dist/models/comment.mjs +2 -1
- package/dist/models/comment.mjs.map +1 -1
- package/dist/models/label.mjs +2 -1
- package/dist/models/label.mjs.map +1 -1
- package/dist/models/member.mjs +1 -1
- package/dist/models/member.mjs.map +1 -1
- package/dist/models/project.mjs +1 -1
- package/dist/models/project.mjs.map +1 -1
- package/dist/models/snapshot.mjs +1 -1
- package/dist/models/snapshot.mjs.map +1 -1
- package/dist/models/status-config.mjs +1 -1
- package/dist/models/status-config.mjs.map +1 -1
- package/dist/models/token.mjs +1 -1
- package/dist/models/token.mjs.map +1 -1
- package/dist/models/webhook.mjs +11 -3
- package/dist/models/webhook.mjs.map +1 -1
- package/dist/pages/build-detail.mjs +9 -4
- package/dist/pages/build-detail.mjs.map +1 -1
- package/dist/pages/build-diff-comments.mjs +113 -0
- package/dist/pages/build-diff-comments.mjs.map +1 -0
- package/dist/pages/build-diff-header.mjs +163 -0
- package/dist/pages/build-diff-header.mjs.map +1 -0
- package/dist/pages/build-diff-nav.mjs +66 -0
- package/dist/pages/build-diff-nav.mjs.map +1 -0
- package/dist/pages/build-diff-viewer.mjs +197 -0
- package/dist/pages/build-diff-viewer.mjs.map +1 -0
- package/dist/pages/build-diff.mjs +65 -413
- package/dist/pages/build-diff.mjs.map +1 -1
- package/dist/pages/compute-jobs.mjs +1 -1
- package/dist/pages/compute-jobs.mjs.map +1 -1
- package/dist/pages/label-detail.mjs +4 -3
- package/dist/pages/label-detail.mjs.map +1 -1
- package/dist/pages/login.mjs.map +1 -1
- package/dist/pages/project-builds.mjs +5 -3
- package/dist/pages/project-builds.mjs.map +1 -1
- package/dist/pages/project-create.mjs.map +1 -1
- package/dist/pages/project-settings.mjs.map +1 -1
- package/dist/pages/projects.mjs +11 -6
- package/dist/pages/projects.mjs.map +1 -1
- package/dist/pages/root.mjs +6 -3
- package/dist/pages/root.mjs.map +1 -1
- package/dist/pages/settings-general.mjs.map +1 -1
- package/dist/pages/settings-labels.mjs +1 -1
- package/dist/pages/settings-labels.mjs.map +1 -1
- package/dist/pages/settings-members.mjs +1 -1
- package/dist/pages/settings-members.mjs.map +1 -1
- package/dist/pages/settings-status.mjs +2 -1
- package/dist/pages/settings-status.mjs.map +1 -1
- package/dist/pages/settings-tests.mjs +3 -3
- package/dist/pages/settings-tests.mjs.map +1 -1
- package/dist/pages/settings-tokens.mjs.map +1 -1
- package/dist/pages/settings-webhooks.mjs.map +1 -1
- package/dist/pages/storybook.mjs +38 -1
- package/dist/pages/storybook.mjs.map +1 -1
- package/dist/paths.mjs.map +1 -0
- package/dist/retention/purge.mjs +6 -3
- package/dist/retention/purge.mjs.map +1 -1
- package/dist/routers/admin.mjs +1 -1
- package/dist/routers/admin.mjs.map +1 -1
- package/dist/routers/assets.mjs.map +1 -1
- package/dist/routers/auth.mjs.map +1 -1
- package/dist/routers/builds.handlers.mjs +106 -21
- package/dist/routers/builds.handlers.mjs.map +1 -1
- package/dist/routers/builds.mjs +54 -34
- package/dist/routers/builds.mjs.map +1 -1
- package/dist/routers/comments.mjs.map +1 -1
- package/dist/routers/health.mjs +137 -0
- package/dist/routers/health.mjs.map +1 -0
- package/dist/routers/helpers.mjs +2 -2
- package/dist/routers/helpers.mjs.map +1 -1
- package/dist/routers/htmx.mjs.map +1 -1
- package/dist/routers/labels.mjs +1 -1
- package/dist/routers/labels.mjs.map +1 -1
- package/dist/routers/media.mjs +2 -2
- package/dist/routers/media.mjs.map +1 -1
- package/dist/routers/members.mjs.map +1 -1
- package/dist/routers/projects.mjs +1 -1
- package/dist/routers/projects.mjs.map +1 -1
- package/dist/routers/schemas.mjs.map +1 -1
- package/dist/routers/settings-general.mjs +95 -0
- package/dist/routers/settings-general.mjs.map +1 -0
- package/dist/routers/settings-labels.mjs +49 -0
- package/dist/routers/settings-labels.mjs.map +1 -0
- package/dist/routers/settings-members.mjs +30 -0
- package/dist/routers/settings-members.mjs.map +1 -0
- package/dist/routers/settings-status.mjs +73 -0
- package/dist/routers/settings-status.mjs.map +1 -0
- package/dist/routers/settings-tokens.mjs +40 -0
- package/dist/routers/settings-tokens.mjs.map +1 -0
- package/dist/routers/settings-webhooks.mjs +63 -0
- package/dist/routers/settings-webhooks.mjs.map +1 -0
- package/dist/routers/settings.handlers.mjs +93 -0
- package/dist/routers/settings.handlers.mjs.map +1 -0
- package/dist/routers/settings.mjs +13 -247
- package/dist/routers/settings.mjs.map +1 -1
- package/dist/routers/snapshots.mjs +1 -1
- package/dist/routers/snapshots.mjs.map +1 -1
- package/dist/routers/status-configs.mjs +1 -1
- package/dist/routers/status-configs.mjs.map +1 -1
- package/dist/routers/storybook.mjs +11 -4
- package/dist/routers/storybook.mjs.map +1 -1
- package/dist/routers/tokens.mjs +1 -1
- package/dist/routers/tokens.mjs.map +1 -1
- package/dist/routers/ui.mjs +4 -4
- package/dist/routers/ui.mjs.map +1 -1
- package/dist/routers/webhooks.mjs +6 -6
- package/dist/routers/webhooks.mjs.map +1 -1
- package/dist/schema/baseline.d.mts +18 -0
- package/dist/schema/baseline.mjs +22 -0
- package/dist/schema/baseline.mjs.map +1 -0
- package/dist/schema/build.d.mts +26 -0
- package/dist/schema/build.mjs +29 -0
- package/dist/schema/build.mjs.map +1 -0
- package/dist/schema/comment.d.mts +19 -0
- package/dist/schema/comment.mjs +26 -0
- package/dist/schema/comment.mjs.map +1 -0
- package/dist/schema/label.d.mts +25 -0
- package/dist/schema/label.mjs +30 -0
- package/dist/schema/label.mjs.map +1 -0
- package/dist/schema/member.d.mts +15 -0
- package/dist/schema/member.mjs +19 -0
- package/dist/schema/member.mjs.map +1 -0
- package/dist/schema/project.d.mts +34 -0
- package/dist/schema/project.mjs +25 -0
- package/dist/schema/project.mjs.map +1 -0
- package/dist/schema/snapshot.d.mts +30 -0
- package/dist/schema/snapshot.mjs +34 -0
- package/dist/schema/snapshot.mjs.map +1 -0
- package/dist/schema/status-config.d.mts +16 -0
- package/dist/schema/status-config.mjs +20 -0
- package/dist/schema/status-config.mjs.map +1 -0
- package/dist/schema/token.d.mts +15 -0
- package/dist/schema/token.mjs +19 -0
- package/dist/schema/token.mjs.map +1 -0
- package/dist/schema/user.d.mts +17 -0
- package/dist/schema/user.mjs +19 -0
- package/dist/schema/user.mjs.map +1 -0
- package/dist/schema/webhook.d.mts +16 -0
- package/dist/schema/webhook.mjs +20 -0
- package/dist/schema/webhook.mjs.map +1 -0
- package/dist/schema.d.mts +13 -168
- package/dist/schema.mjs +12 -2
- package/dist/schema.mjs.map +1 -1
- package/dist/store.mjs.map +1 -1
- package/dist/types.d.mts +1 -2
- package/dist/types.mjs.map +1 -1
- package/dist/ui/document.mjs.map +1 -1
- package/dist/ui/feedback.mjs +24 -0
- package/dist/ui/feedback.mjs.map +1 -0
- package/dist/ui/{components.mjs → forms.mjs} +3 -18
- package/dist/ui/forms.mjs.map +1 -0
- package/dist/ui/styles.mjs.map +1 -1
- package/dist/urls.mjs.map +1 -1
- package/package.json +47 -45
- package/dist/adapters/metadata.d.mts +0 -24
- package/dist/models/baseline.d.mts +0 -21
- package/dist/models/build.d.mts +0 -53
- package/dist/models/comment.d.mts +0 -22
- package/dist/models/label.d.mts +0 -49
- package/dist/models/member.d.mts +0 -27
- package/dist/models/project.d.mts +0 -43
- package/dist/models/snapshot.d.mts +0 -47
- package/dist/models/token.d.mts +0 -33
- package/dist/models/webhook.d.mts +0 -38
- package/dist/retention/purge.d.mts +0 -31
- package/dist/schema-tables.mjs +0 -166
- package/dist/schema-tables.mjs.map +0 -1
- package/dist/ui/components.mjs.map +0 -1
- package/dist/ui/document.d.mts +0 -9
- package/dist/utils/paths.mjs.map +0 -1
- package/dist/utils/ulid.d.mts +0 -10
- /package/dist/{utils/paths.d.mts → paths.d.mts} +0 -0
- /package/dist/{utils/paths.mjs → paths.mjs} +0 -0
package/README.md
CHANGED
|
@@ -20,12 +20,12 @@ npm install @storyshelf/core
|
|
|
20
20
|
import { createShelfRouter } from "@storyshelf/core";
|
|
21
21
|
|
|
22
22
|
const app = createShelfRouter({
|
|
23
|
-
database,
|
|
24
|
-
storage,
|
|
25
|
-
captureRunner,
|
|
26
|
-
auth,
|
|
27
|
-
gitHosts,
|
|
28
|
-
logger,
|
|
23
|
+
database, // DatabaseAdapter
|
|
24
|
+
storage, // StorageAdapter
|
|
25
|
+
captureRunner, // CaptureRunner (optional)
|
|
26
|
+
auth, // AuthAdapter (optional)
|
|
27
|
+
gitHosts, // GitHostProvider[] (optional)
|
|
28
|
+
logger, // pino Logger (optional; built internally if omitted)
|
|
29
29
|
ui: { name: "My Shelf" }, // UIConfig (optional)
|
|
30
30
|
config: { captureConcurrency: 2, purgeTtlDays: 30 }, // ShelfConfig (optional)
|
|
31
31
|
});
|
|
@@ -40,27 +40,30 @@ serve({ fetch: app.fetch, port: 3000 });
|
|
|
40
40
|
|
|
41
41
|
Assembles the router from the provided adapters. `ShelfOptions`:
|
|
42
42
|
|
|
43
|
-
| Option
|
|
44
|
-
|
|
|
45
|
-
| `database`
|
|
46
|
-
| `storage`
|
|
47
|
-
| `captureRunner` | `CaptureRunner`
|
|
48
|
-
| `captureQueue`
|
|
49
|
-
| `auth`
|
|
50
|
-
| `gitHosts`
|
|
51
|
-
| `logger`
|
|
52
|
-
| `ui`
|
|
53
|
-
| `config`
|
|
43
|
+
| Option | Type | Description |
|
|
44
|
+
| --------------- | ------------------- | ---------------------------------------------------------------------------------------- |
|
|
45
|
+
| `database` | `DatabaseAdapter` | **Required.** Data access. |
|
|
46
|
+
| `storage` | `StorageAdapter` | **Required.** Blob storage for screenshots, diffs, storybook archives. |
|
|
47
|
+
| `captureRunner` | `CaptureRunner` | Optional. Enables the async capture pipeline (pure renderer). |
|
|
48
|
+
| `captureQueue` | `CaptureQueue` | Optional. Queue adapter; defaults to `InMemoryCaptureQueue`. |
|
|
49
|
+
| `auth` | `AuthAdapter` | Optional. Enables auth and the login UI. |
|
|
50
|
+
| `gitHosts` | `GitHostProvider[]` | Optional. Git-host adapters (GitHub/GitLab) for status checks, merge gates, PR comments. |
|
|
51
|
+
| `logger` | `Logger` (pino) | Optional. Shared logger. Construct a fallback via `createShelfLogger()`. |
|
|
52
|
+
| `ui` | `UIConfig` | Optional. UI branding. |
|
|
53
|
+
| `config` | `ShelfConfig` | Optional. Server behavior. |
|
|
54
54
|
|
|
55
55
|
### `ShelfConfig`
|
|
56
56
|
|
|
57
57
|
```ts
|
|
58
58
|
interface ShelfConfig {
|
|
59
|
-
secret?: string;
|
|
59
|
+
secret?: string; // session signing secret
|
|
60
60
|
publishedBaseDomain?: string; // for published storybook URLs
|
|
61
|
-
captureConcurrency?: number;
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
captureConcurrency?: number; // concurrent capture jobs (default 2)
|
|
62
|
+
scratchDir?: string; // capture working directory (required with captureRunner)
|
|
63
|
+
purgeTtlDays?: number; // purge builds older than N days
|
|
64
|
+
maxUploadBytes?: number; // single-zip upload cap (default 1 GiB)
|
|
65
|
+
maxInlineUnzipSize?: number; // inline statics extraction cap; unset = capture-only
|
|
66
|
+
viewports?: Viewport[]; // capture viewports
|
|
64
67
|
}
|
|
65
68
|
```
|
|
66
69
|
|
|
@@ -78,33 +81,34 @@ interface UIConfig {
|
|
|
78
81
|
|
|
79
82
|
### Adapter interfaces
|
|
80
83
|
|
|
81
|
-
All adapters are constructor-injected (no AsyncLocalStorage). See `docs/architecture.md` for the entity model and workflow.
|
|
84
|
+
All adapters are constructor-injected (no AsyncLocalStorage). See `docs/architecture.md` for the entity model and workflow. Import each from its subpath — the barrel (`@storyshelf/core`) exports only the router and its types.
|
|
82
85
|
|
|
83
|
-
- `DatabaseAdapter` — `insert`, `update`, `get`, `remove`, `list`, `count`, `all`, `migrate`, `close`. Also exports `ListOptions
|
|
84
|
-
- `StorageAdapter` — `read`, `write`, `delete`, `exists`, `list(prefix)`.
|
|
85
|
-
- `AuthAdapter` — `check(request)`, `createSession(user)`, `destroySession(sessionId)`, optional `handleCallback(callback)`. Also exports `AuthUser`, `AuthCallback`, and the shared `SESSION_COOKIE`.
|
|
86
|
-
- `CaptureRunner` — a **pure capture renderer**: `render(input) => RenderResult`, `cancel(buildId)`. Also exports `
|
|
87
|
-
- `
|
|
86
|
+
- `DatabaseAdapter` (`core/adapter/database`) — `insert`, `update`, `get`, `remove`, `list`, `count`, `all`, `migrate`, `close`. Also exports `ListOptions` and the `createDrizzleAdapter` factory.
|
|
87
|
+
- `StorageAdapter` (`core/adapter/storage`) — `read`, `write`, `delete`, `exists`, `list(prefix)`.
|
|
88
|
+
- `AuthAdapter` (`core/adapter/auth`) — `check(request)`, `createSession(user)`, `destroySession(sessionId)`, optional `handleCallback(callback)`. Also exports `AuthUser`, `AuthCallback`, and the shared `SESSION_COOKIE`.
|
|
89
|
+
- `CaptureRunner` (`core/adapter/capture-runner`) — a **pure capture renderer**: `render(input) => RenderResult`, `cancel(buildId)`. Also exports `RenderedSnapshot`, `RenderResult`, `StoryEntry`, `StorySourceAdapter`, `Viewport`.
|
|
90
|
+
- `CaptureQueue` (`core/adapter/capture-queue`) — `enqueue({ buildId, reqId? })`, plus `status`, `active`, `recent`. Also exports `CaptureJob`, `QueueEntry`, `JobStatus`.
|
|
91
|
+
- `GitHostProvider` / `GitHostAdapter` (`core/adapter/git-host`) — set commit status checks, detect merges, and upsert PR comments. Real providers ship in `@storyshelf/git-github` and `@storyshelf/git-gitlab`. Also exports `CheckStatus`.
|
|
88
92
|
|
|
89
93
|
### Logging
|
|
90
94
|
|
|
91
|
-
`core` uses **pino** for structured JSON logging. `createShelfLogger({ level, transports, env })` builds a logger writing to stdout by default, with optional extra pino worker transports (Sentry, PostHog, Datadog, GCP, OTEL collector, etc.). Pass the resulting `Logger` to `createShelfRouter({ logger })` (or construct it at your composition root) so request and background logs share one stream. The capture orchestrator derives a `reqId`-scoped child for background capture work, correlating each capture back to the triggering HTTP request. See ADR 0014.
|
|
95
|
+
`core` uses **pino** for structured JSON logging. `createShelfLogger({ level, transports, env })` (from `core/logger`) builds a logger writing to stdout by default, with optional extra pino worker transports (Sentry, PostHog, Datadog, GCP, OTEL collector, etc.). Pass the resulting `Logger` to `createShelfRouter({ logger })` (or construct it at your composition root) so request and background logs share one stream. The capture orchestrator derives a `reqId`-scoped child for background capture work, correlating each capture back to the triggering HTTP request. See ADR 0014.
|
|
92
96
|
|
|
93
97
|
### Capture, diff, and retention
|
|
94
98
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
- `
|
|
98
|
-
- `
|
|
99
|
-
- `
|
|
100
|
-
- `
|
|
99
|
+
Import from `core/capture`, `core/diff`, and the model entries — never from the barrel:
|
|
100
|
+
|
|
101
|
+
- `executeCaptureJob({ buildId, reqId }, deps)` (`core/capture`) — the capture **orchestrator**: loads the build, marks it `capturing`, extracts the uploaded archive into `scratchDir`, discovers stories, delegates rendering to a pure `CaptureRunner`, and persists. `createShelfRouter` wires it into a `CaptureQueue` when `capture` is supplied (and requires `ShelfConfig.scratchDir`). Also exports `CaptureJobOptions`.
|
|
102
|
+
- `persistCapture(ctx: CaptureContext)` (`core/capture`) — writes screenshots, diffs against the branch baseline, creates snapshots, and finalizes a build from a pure renderer's `captures`. Also exports `CaptureContext`.
|
|
103
|
+
- `StorybookAdapter` (`core/capture`) — reads a built Storybook's `index.json`/`stories.json`.
|
|
104
|
+
- `InMemoryCaptureQueue` (`core/capture`) — in-process, concurrency-limited queue for long-lived hosts; supply a remote queue with a separate worker for serverless.
|
|
105
|
+
- `diffImages(baseline: Buffer, current: Buffer, options: DiffOptions): DiffResult` (`core/diff`) — pixelmatch-based diff. Also exports `DiffOptions`, `DiffResult`.
|
|
106
|
+
- Models, schema, and row types back every entity; retention runs inside the router. (Models are private implementation details with no public entry — reach row types via `core/schema`.)
|
|
101
107
|
|
|
102
108
|
### Helpers
|
|
103
109
|
|
|
104
|
-
- `createUrlBuilder(baseUrl, publishedBaseDomain?)` — type-safe URL builder. Also exports `UrlBuilder`.
|
|
105
|
-
- `
|
|
106
|
-
- Path helpers: `screenshotPath`, `diffPath`, `baselinePath`, `storybookDir`, `storybookZipPath`.
|
|
107
|
-
- `RenderedContent` type for the server-rendered UI.
|
|
110
|
+
- `createUrlBuilder(baseUrl, publishedBaseDomain?)` (`core/urls`) — type-safe URL builder. Also exports `UrlBuilder`.
|
|
111
|
+
- Path helpers (`core/paths`): `screenshotPath`, `diffPath`, `baselinePath`, `storybookDir`, `storybookZipPath`.
|
|
108
112
|
|
|
109
113
|
## How it fits in
|
|
110
114
|
|
|
@@ -116,13 +120,13 @@ See `docs/architecture.md` and the ADRs in `docs/adr/`.
|
|
|
116
120
|
|
|
117
121
|
The core router is runtime-agnostic (Web `Request`/`Response`, `fetch`, `crypto`, `URL`). The only Node-specific piece is the in-process `InMemoryCaptureQueue`, which suits long-lived Node servers; serverless runtimes swap in a remote `CaptureQueue` (e.g. `@storyshelf/queue-sqs`) plus a separate worker. You assemble a server for any platform — `storyshelf server init` generates a scaffold with the adapters you choose:
|
|
118
122
|
|
|
119
|
-
| Platform
|
|
120
|
-
|
|
121
|
-
| **Vercel**
|
|
122
|
-
| **Cloudflare Workers**
|
|
123
|
-
| **Azure Functions**
|
|
124
|
-
| **AWS Lambda**
|
|
125
|
-
| **Deno Deploy**
|
|
126
|
-
| **Bun / Node (VPS, Fly, Railway, Render)** | `db-sqlite` / `db-turso` | `storage-local` / `storage-s3`
|
|
123
|
+
| Platform | Database | Storage | Capture queue | Server entry |
|
|
124
|
+
| ------------------------------------------ | ------------------------ | -------------------------------- | ------------------------------- | -------------------------- |
|
|
125
|
+
| **Vercel** | `@storyshelf/db-turso` | `@storyshelf/storage-s3` (R2/S3) | Remote `CaptureQueue` + worker | Hono + `@hono/vercel-edge` |
|
|
126
|
+
| **Cloudflare Workers** | `@storyshelf/db-turso` | `@storyshelf/storage-s3` (R2) | Workers Queues `CaptureQueue` | Hono + Workers entry |
|
|
127
|
+
| **Azure Functions** | `@storyshelf/db-turso` | `@storyshelf/storage-s3` (Blob) | Azure Queues `CaptureQueue` | Hono + Azure handler |
|
|
128
|
+
| **AWS Lambda** | `@storyshelf/db-turso` | `@storyshelf/storage-s3` | SQS `CaptureQueue` | Hono + Lambda handler |
|
|
129
|
+
| **Deno Deploy** | `@storyshelf/db-turso` | `@storyshelf/storage-s3` | Custom `CaptureQueue` (Deno KV) | Hono + Deno entry |
|
|
130
|
+
| **Bun / Node (VPS, Fly, Railway, Render)** | `db-sqlite` / `db-turso` | `storage-local` / `storage-s3` | `InMemoryCaptureQueue` | `storyshelf-server serve` |
|
|
127
131
|
|
|
128
132
|
All clouds are equal — pick the adapters that match your infrastructure. See the **Deployment** guide for recipes including a minimal Turso + S3 + `InMemoryCaptureQueue` example.
|
package/dist/adapter/auth.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
|
-
import { AdapterMetadata } from "../adapters/metadata.mjs";
|
|
4
3
|
import { ProjectRole, SiteRole } from "../types.mjs";
|
|
4
|
+
import { Adapter } from "./metadata.mjs";
|
|
5
5
|
//#region src/adapters/auth.d.ts
|
|
6
6
|
/** Shared session cookie name used by all auth adapters (ADR 0008). */
|
|
7
7
|
declare const SESSION_COOKIE = "storyshelf_session";
|
|
@@ -33,9 +33,9 @@ interface ProjectAccess {
|
|
|
33
33
|
role: ProjectRole;
|
|
34
34
|
}
|
|
35
35
|
/** Pluggable authentication abstraction (ADR 0008). */
|
|
36
|
-
interface AuthAdapter {
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
interface AuthAdapter extends Adapter<{
|
|
37
|
+
readonly category: "auth";
|
|
38
|
+
}> {
|
|
39
39
|
/** Resolve the current user from a request, or null if unauthenticated. */
|
|
40
40
|
check(request: Request): Promise<AuthUser | null>;
|
|
41
41
|
/** Create a session for a user and return a session token. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.mjs","names":[],"sources":["../../src/adapters/auth.ts"],"sourcesContent":["/**\n * Auth adapter interface: authenticate users and resolve project access.\n */\nimport type {
|
|
1
|
+
{"version":3,"file":"auth.mjs","names":[],"sources":["../../src/adapters/auth.ts"],"sourcesContent":["import type { ProjectRole, SiteRole } from \"../types.ts\";\n/**\n * Auth adapter interface: authenticate users and resolve project access.\n */\nimport type { Adapter } from \"./metadata.ts\";\n\n/** Shared session cookie name used by all auth adapters (ADR 0008). */\nexport const SESSION_COOKIE = \"storyshelf_session\";\n\n/** An authenticated user of the platform. */\nexport interface AuthUser {\n /** User ID. */\n id: string;\n /** User email. */\n email: string;\n /** Display name. */\n name: string;\n /** Optional avatar URL. */\n avatarUrl?: string;\n /** Site-wide role. */\n role: SiteRole;\n}\n\n/** Data passed to an auth adapter when an OAuth callback is received. */\nexport interface AuthCallback {\n /** Provider name. */\n provider: string;\n /** Authorization code from the provider. */\n code: string;\n /** Anti-CSRF state value. */\n state: string;\n}\n\n/** Access controls granted to a user for a specific project. */\nexport interface ProjectAccess {\n /** Project role. */\n role: ProjectRole;\n}\n\n/** Pluggable authentication abstraction (ADR 0008). */\nexport interface AuthAdapter extends Adapter<{ readonly category: \"auth\" }> {\n /** Resolve the current user from a request, or null if unauthenticated. */\n check(request: Request): Promise<AuthUser | null>;\n /** Create a session for a user and return a session token. */\n createSession(user: AuthUser): Promise<string>;\n /** Destroy a session by its id. */\n destroySession(sessionId: string): Promise<void>;\n /** Handle an OAuth callback and return the resolved user, if supported. */\n handleCallback?(callback: AuthCallback): Promise<AuthUser | null>;\n}\n"],"mappings":";;;;;AAOA,MAAa,iBAAiB"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import __tsdown_shims_path from 'node:path';
|
|
2
2
|
import __tsdown_shims_url from 'node:url';
|
|
3
|
-
import {
|
|
3
|
+
import { Adapter } from "./metadata.mjs";
|
|
4
4
|
//#region src/adapters/capture-queue.d.ts
|
|
5
5
|
/** Lifecycle status of a queued capture job. */
|
|
6
6
|
type JobStatus = "queued" | "running" | "completed" | "failed" | "cancelled";
|
|
@@ -34,9 +34,9 @@ interface CaptureJob {
|
|
|
34
34
|
*
|
|
35
35
|
* `status`/`active`/`recent` back the live queue view regardless of transport.
|
|
36
36
|
*/
|
|
37
|
-
interface CaptureQueue {
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
interface CaptureQueue extends Adapter<{
|
|
38
|
+
readonly category: "capture-queue";
|
|
39
|
+
}> {
|
|
40
40
|
/** Submit a build for capture. Resolves once the build is queued. */
|
|
41
41
|
enqueue(job: CaptureJob): Promise<void>;
|
|
42
42
|
/** Return the current status entry for a build, or null if untracked. */
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import __tsdown_shims_path from 'node:path';
|
|
2
2
|
import __tsdown_shims_url from 'node:url';
|
|
3
|
-
import {
|
|
4
|
-
import { StoryEntry, Viewport } from "../capture/adapter.mjs";
|
|
5
|
-
import {
|
|
3
|
+
import { JobStatus } from "./capture-queue.mjs";
|
|
4
|
+
import { StoryEntry, StorySourceAdapter, Viewport } from "../capture/adapter.mjs";
|
|
5
|
+
import { Adapter } from "./metadata.mjs";
|
|
6
6
|
import { Logger } from "pino";
|
|
7
7
|
//#region src/adapters/capture-runner.d.ts
|
|
8
8
|
/** A screenshot produced by a capture renderer. */
|
|
@@ -41,9 +41,9 @@ interface RenderResult {
|
|
|
41
41
|
* orchestrator's job (see `capture/orchestrator.ts`), keeping every adapter
|
|
42
42
|
* implementation free of server concerns.
|
|
43
43
|
*/
|
|
44
|
-
interface CaptureRunner {
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
interface CaptureRunner extends Adapter<{
|
|
45
|
+
readonly category: "capture-runner";
|
|
46
|
+
}> {
|
|
47
47
|
/**
|
|
48
48
|
* Render configured viewports for the given stories of an extracted
|
|
49
49
|
* Storybook and return the screenshot buffers.
|
|
@@ -70,5 +70,5 @@ interface CaptureRunner {
|
|
|
70
70
|
cancel(buildId: string): Promise<void>;
|
|
71
71
|
}
|
|
72
72
|
//#endregion
|
|
73
|
-
export { CaptureRunner, type JobStatus, RenderFailure, RenderResult, RenderedSnapshot };
|
|
73
|
+
export { CaptureRunner, type JobStatus, RenderFailure, RenderResult, RenderedSnapshot, type StoryEntry, type StorySourceAdapter, type Viewport };
|
|
74
74
|
//# sourceMappingURL=capture-runner.d.mts.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import __tsdown_shims_path from 'node:path';
|
|
2
2
|
import __tsdown_shims_url from 'node:url';
|
|
3
|
-
import { AdapterMetadata } from "
|
|
3
|
+
import { Adapter, AdapterMetadata } from "./metadata.mjs";
|
|
4
4
|
import { SQL } from "drizzle-orm";
|
|
5
5
|
import { AnySQLiteTable } from "drizzle-orm/sqlite-core";
|
|
6
6
|
//#region src/adapters/database.d.ts
|
|
@@ -16,9 +16,9 @@ interface ListOptions {
|
|
|
16
16
|
offset?: number;
|
|
17
17
|
}
|
|
18
18
|
/** Database abstraction over Drizzle tables, agnostic of the underlying driver. */
|
|
19
|
-
interface DatabaseAdapter {
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
interface DatabaseAdapter extends Adapter<{
|
|
20
|
+
readonly category: "database";
|
|
21
|
+
}> {
|
|
22
22
|
/** Insert a row and return the inserted record. */
|
|
23
23
|
insert<T extends AnySQLiteTable>(table: T, values: T["$inferInsert"]): Promise<T["$inferSelect"]>;
|
|
24
24
|
/** Update a row by id and return the updated record. */
|
|
@@ -33,11 +33,22 @@ interface DatabaseAdapter {
|
|
|
33
33
|
count(table: AnySQLiteTable, where?: SQL): Promise<number>;
|
|
34
34
|
/** Run an arbitrary SQL query and return typed rows. */
|
|
35
35
|
all<T>(query: SQL): Promise<T[]>;
|
|
36
|
-
/** Apply any pending schema migrations. */
|
|
37
|
-
migrate(): Promise<void>;
|
|
38
|
-
/** Close the underlying database connection. */
|
|
39
|
-
close(): Promise<void>;
|
|
40
36
|
}
|
|
37
|
+
/** Driver-supplied identity plus lifecycle hooks. */
|
|
38
|
+
interface DrizzleAdapterOptions {
|
|
39
|
+
metadata: AdapterMetadata & {
|
|
40
|
+
readonly category: "database";
|
|
41
|
+
};
|
|
42
|
+
migrate: () => Promise<void> | void;
|
|
43
|
+
close: () => Promise<void> | void;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Build a {@link DatabaseAdapter} over any SQLite-compatible Drizzle
|
|
47
|
+
* dialect. Sync results (node:sqlite) and promises (libSQL) are both
|
|
48
|
+
* awaited, so drivers only supply connection setup, metadata, and the
|
|
49
|
+
* driver-specific migrate/close.
|
|
50
|
+
*/
|
|
51
|
+
declare function createDrizzleAdapter(db: unknown, options: DrizzleAdapterOptions): DatabaseAdapter;
|
|
41
52
|
//#endregion
|
|
42
|
-
export { DatabaseAdapter, ListOptions };
|
|
53
|
+
export { DatabaseAdapter, DrizzleAdapterOptions, ListOptions, createDrizzleAdapter };
|
|
43
54
|
//# sourceMappingURL=database.d.mts.map
|
|
@@ -1,4 +1,63 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
|
|
4
|
+
import { eq, getTableColumns } from "drizzle-orm";
|
|
5
|
+
//#region src/adapters/database.ts
|
|
6
|
+
function idOf(table) {
|
|
7
|
+
const id = getTableColumns(table)["id"];
|
|
8
|
+
if (!id) throw new Error("Table has no id column");
|
|
9
|
+
return id;
|
|
10
|
+
}
|
|
11
|
+
function applyListOptions(query, opts) {
|
|
12
|
+
let current = query;
|
|
13
|
+
if (opts.where) current = current.where(opts.where);
|
|
14
|
+
if (opts.orderBy) current = current.orderBy(opts.orderBy);
|
|
15
|
+
if (opts.limit !== void 0) current = current.limit(opts.limit);
|
|
16
|
+
if (opts.offset !== void 0) current = current.offset(opts.offset);
|
|
17
|
+
return current;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Build the database lifecycle: migrations run in `init`, the connection
|
|
21
|
+
* closes in `close`. Close tolerates repeated calls (drivers may not).
|
|
22
|
+
*/
|
|
23
|
+
function buildLifecycle(options) {
|
|
24
|
+
let closed = false;
|
|
25
|
+
return {
|
|
26
|
+
init: async () => {
|
|
27
|
+
await options.migrate();
|
|
28
|
+
},
|
|
29
|
+
close: async () => {
|
|
30
|
+
if (closed) return;
|
|
31
|
+
closed = true;
|
|
32
|
+
await options.close();
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Build a {@link DatabaseAdapter} over any SQLite-compatible Drizzle
|
|
38
|
+
* dialect. Sync results (node:sqlite) and promises (libSQL) are both
|
|
39
|
+
* awaited, so drivers only supply connection setup, metadata, and the
|
|
40
|
+
* driver-specific migrate/close.
|
|
41
|
+
*/
|
|
42
|
+
function createDrizzleAdapter(db, options) {
|
|
43
|
+
const drizzle = db;
|
|
44
|
+
return {
|
|
45
|
+
metadata: options.metadata,
|
|
46
|
+
lifecycle: buildLifecycle(options),
|
|
47
|
+
insert: async (table, values) => await drizzle.insert(table).values(values).returning().get(),
|
|
48
|
+
update: async (table, id, values) => await drizzle.update(table).set(values).where(eq(idOf(table), id)).returning().get(),
|
|
49
|
+
get: async (table, id) => await drizzle.select().from(table).where(eq(idOf(table), id)).limit(1).get() ?? null,
|
|
50
|
+
remove: async (table, id) => {
|
|
51
|
+
await drizzle.delete(table).where(eq(idOf(table), id)).run();
|
|
52
|
+
},
|
|
53
|
+
list: async (table, opts = {}) => {
|
|
54
|
+
return await applyListOptions(drizzle.select().from(table), opts).all();
|
|
55
|
+
},
|
|
56
|
+
count: async (table, where) => await drizzle.$count(table, where),
|
|
57
|
+
all: async (query) => await drizzle.all(query)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
export { createDrizzleAdapter };
|
|
62
|
+
|
|
63
|
+
//# sourceMappingURL=database.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.mjs","names":[],"sources":["../../src/adapters/database.ts"],"sourcesContent":["/**\n * Database adapter interface: query builders over explicitly typed tables.\n */\nimport type { SQL } from \"drizzle-orm\";\nimport { eq, getTableColumns } from \"drizzle-orm\";\nimport type { AnySQLiteTable, SQLiteColumn } from \"drizzle-orm/sqlite-core\";\nimport type { Adapter, AdapterLifecycle, AdapterMetadata } from \"./metadata.ts\";\n\n/** Options that narrow and page a list query. */\nexport interface ListOptions {\n /** SQL WHERE condition. */\n where?: SQL;\n /** SQL ORDER BY condition. */\n orderBy?: SQL;\n /** Maximum number of rows to return. */\n limit?: number;\n /** Number of rows to skip. */\n offset?: number;\n}\n\n/** Database abstraction over Drizzle tables, agnostic of the underlying driver. */\nexport interface DatabaseAdapter extends Adapter<{ readonly category: \"database\" }> {\n /** Insert a row and return the inserted record. */\n insert<T extends AnySQLiteTable>(table: T, values: T[\"$inferInsert\"]): Promise<T[\"$inferSelect\"]>;\n /** Update a row by id and return the updated record. */\n update<T extends AnySQLiteTable>(\n table: T,\n id: string,\n values: Partial<T[\"$inferInsert\"]>,\n ): Promise<T[\"$inferSelect\"]>;\n /** Fetch a single row by id, or null if not found. */\n get<T extends AnySQLiteTable>(table: T, id: string): Promise<T[\"$inferSelect\"] | null>;\n /** Delete a row by id. */\n remove(table: AnySQLiteTable, id: string): Promise<void>;\n /** List rows matching the given options. */\n list<T extends AnySQLiteTable>(table: T, opts?: ListOptions): Promise<T[\"$inferSelect\"][]>;\n /** Count rows matching an optional where condition. */\n count(table: AnySQLiteTable, where?: SQL): Promise<number>;\n /** Run an arbitrary SQL query and return typed rows. */\n all<T>(query: SQL): Promise<T[]>;\n}\n\n/** A value or a promise of one (sync node:sqlite vs async libSQL drivers). */\ntype MaybePromise<T> = T | Promise<T>;\n\n/** Chainable select surface shared by the sync and async Drizzle dialects. */\ninterface DrizzleSelectChain {\n where(where: SQL): DrizzleSelectChain;\n orderBy(order: SQL): DrizzleSelectChain;\n limit(n: number): DrizzleSelectChain;\n offset(n: number): DrizzleSelectChain;\n get(): MaybePromise<unknown>;\n all(): MaybePromise<unknown[]>;\n}\n\n/**\n * Minimal Drizzle surface consumed by {@link createDrizzleAdapter}. Drivers\n * pass their dialect instance as `unknown`; the cast lives here, once, so\n * driver packages stay fully typed. Covered by the adapter contract tests.\n */\ninterface DrizzleLike {\n insert(table: AnySQLiteTable): {\n values(values: unknown): { returning(): { get(): MaybePromise<unknown> } };\n };\n update(table: AnySQLiteTable): {\n set(values: unknown): { where(where: SQL): { returning(): { get(): MaybePromise<unknown> } } };\n };\n select(): { from(table: AnySQLiteTable): DrizzleSelectChain };\n delete(table: AnySQLiteTable): { where(where: SQL): { run(): MaybePromise<unknown> } };\n $count(table: AnySQLiteTable, where?: SQL): MaybePromise<number>;\n all<T>(query: SQL): MaybePromise<T[]>;\n}\n\n/** Driver-supplied identity plus lifecycle hooks. */\nexport interface DrizzleAdapterOptions {\n metadata: AdapterMetadata & { readonly category: \"database\" };\n migrate: () => Promise<void> | void;\n close: () => Promise<void> | void;\n}\n\nfunction idOf(table: AnySQLiteTable): SQLiteColumn {\n const id = getTableColumns(table)[\"id\"];\n if (!id) {\n throw new Error(\"Table has no id column\");\n }\n return id;\n}\n\nfunction applyListOptions(query: DrizzleSelectChain, opts: ListOptions): DrizzleSelectChain {\n let current = query;\n if (opts.where) {\n current = current.where(opts.where);\n }\n if (opts.orderBy) {\n current = current.orderBy(opts.orderBy);\n }\n if (opts.limit !== undefined) {\n current = current.limit(opts.limit);\n }\n if (opts.offset !== undefined) {\n current = current.offset(opts.offset);\n }\n return current;\n}\n\n/**\n * Build the database lifecycle: migrations run in `init`, the connection\n * closes in `close`. Close tolerates repeated calls (drivers may not).\n */\nfunction buildLifecycle(options: DrizzleAdapterOptions): AdapterLifecycle {\n let closed = false;\n return {\n init: async () => {\n await options.migrate();\n },\n close: async () => {\n if (closed) {\n return;\n }\n closed = true;\n await options.close();\n },\n };\n}\n\n/**\n * Build a {@link DatabaseAdapter} over any SQLite-compatible Drizzle\n * dialect. Sync results (node:sqlite) and promises (libSQL) are both\n * awaited, so drivers only supply connection setup, metadata, and the\n * driver-specific migrate/close.\n */\nexport function createDrizzleAdapter(db: unknown, options: DrizzleAdapterOptions): DatabaseAdapter {\n const drizzle = db as DrizzleLike;\n return {\n metadata: options.metadata,\n lifecycle: buildLifecycle(options),\n insert: async <T extends AnySQLiteTable>(\n table: T,\n values: T[\"$inferInsert\"],\n ): Promise<T[\"$inferSelect\"]> =>\n (await drizzle.insert(table).values(values).returning().get()) as T[\"$inferSelect\"],\n update: async <T extends AnySQLiteTable>(\n table: T,\n id: string,\n values: Partial<T[\"$inferInsert\"]>,\n ): Promise<T[\"$inferSelect\"]> =>\n (await drizzle\n .update(table)\n .set(values)\n .where(eq(idOf(table), id))\n .returning()\n .get()) as T[\"$inferSelect\"],\n get: async <T extends AnySQLiteTable>(\n table: T,\n id: string,\n ): Promise<T[\"$inferSelect\"] | null> =>\n ((await drizzle\n .select()\n .from(table)\n .where(eq(idOf(table), id))\n .limit(1)\n .get()) as T[\"$inferSelect\"] | undefined) ?? null,\n remove: async (table: AnySQLiteTable, id: string): Promise<void> => {\n await drizzle\n .delete(table)\n .where(eq(idOf(table), id))\n .run();\n },\n list: async <T extends AnySQLiteTable>(\n table: T,\n opts: ListOptions = {},\n ): Promise<T[\"$inferSelect\"][]> => {\n const query = applyListOptions(drizzle.select().from(table), opts);\n const rows: unknown = await query.all();\n return rows as T[\"$inferSelect\"][];\n },\n count: async (table: AnySQLiteTable, where?: SQL): Promise<number> =>\n await drizzle.$count(table, where),\n all: async <T>(query: SQL): Promise<T[]> => await drizzle.all(query),\n };\n}\n"],"mappings":";;;;;AAgFA,SAAS,KAAK,OAAqC;CACjD,MAAM,KAAK,gBAAgB,KAAK,CAAC,CAAC;CAClC,IAAI,CAAC,IACH,MAAM,IAAI,MAAM,wBAAwB;CAE1C,OAAO;AACT;AAEA,SAAS,iBAAiB,OAA2B,MAAuC;CAC1F,IAAI,UAAU;CACd,IAAI,KAAK,OACP,UAAU,QAAQ,MAAM,KAAK,KAAK;CAEpC,IAAI,KAAK,SACP,UAAU,QAAQ,QAAQ,KAAK,OAAO;CAExC,IAAI,KAAK,UAAU,KAAA,GACjB,UAAU,QAAQ,MAAM,KAAK,KAAK;CAEpC,IAAI,KAAK,WAAW,KAAA,GAClB,UAAU,QAAQ,OAAO,KAAK,MAAM;CAEtC,OAAO;AACT;;;;;AAMA,SAAS,eAAe,SAAkD;CACxE,IAAI,SAAS;CACb,OAAO;EACL,MAAM,YAAY;GAChB,MAAM,QAAQ,QAAQ;EACxB;EACA,OAAO,YAAY;GACjB,IAAI,QACF;GAEF,SAAS;GACT,MAAM,QAAQ,MAAM;EACtB;CACF;AACF;;;;;;;AAQA,SAAgB,qBAAqB,IAAa,SAAiD;CACjG,MAAM,UAAU;CAChB,OAAO;EACL,UAAU,QAAQ;EAClB,WAAW,eAAe,OAAO;EACjC,QAAQ,OACN,OACA,WAEC,MAAM,QAAQ,OAAO,KAAK,CAAC,CAAC,OAAO,MAAM,CAAC,CAAC,UAAU,CAAC,CAAC,IAAI;EAC9D,QAAQ,OACN,OACA,IACA,WAEC,MAAM,QACJ,OAAO,KAAK,CAAC,CACb,IAAI,MAAM,CAAC,CACX,MAAM,GAAG,KAAK,KAAK,GAAG,EAAE,CAAC,CAAC,CAC1B,UAAU,CAAC,CACX,IAAI;EACT,KAAK,OACH,OACA,OAEE,MAAM,QACL,OAAO,CAAC,CACR,KAAK,KAAK,CAAC,CACX,MAAM,GAAG,KAAK,KAAK,GAAG,EAAE,CAAC,CAAC,CAC1B,MAAM,CAAC,CAAC,CACR,IAAI,KAAwC;EACjD,QAAQ,OAAO,OAAuB,OAA8B;GAClE,MAAM,QACH,OAAO,KAAK,CAAC,CACb,MAAM,GAAG,KAAK,KAAK,GAAG,EAAE,CAAC,CAAC,CAC1B,IAAI;EACT;EACA,MAAM,OACJ,OACA,OAAoB,CAAC,MACY;GAGjC,OAAO,MAFO,iBAAiB,QAAQ,OAAO,CAAC,CAAC,KAAK,KAAK,GAAG,IAC7B,CAAC,CAAC,IAAI;EAExC;EACA,OAAO,OAAO,OAAuB,UACnC,MAAM,QAAQ,OAAO,OAAO,KAAK;EACnC,KAAK,OAAU,UAA6B,MAAM,QAAQ,IAAI,KAAK;CACrE;AACF"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { Logger } from "pino";
|
|
4
|
+
//#region src/adapters/git-host/comments.d.ts
|
|
5
|
+
/** A review thread (PR or MR) hosting a single StoryShelf comment. */
|
|
6
|
+
interface ReviewThread {
|
|
7
|
+
/** List existing comments as id/body pairs. */
|
|
8
|
+
list(): Promise<{
|
|
9
|
+
id: string | number;
|
|
10
|
+
body: string;
|
|
11
|
+
}[]>;
|
|
12
|
+
/** Replace the body of an existing comment, returning its id. */
|
|
13
|
+
update(id: string | number, body: string): Promise<string>;
|
|
14
|
+
/** Create a comment, returning its id. */
|
|
15
|
+
create(body: string): Promise<string>;
|
|
16
|
+
}
|
|
17
|
+
/** Dependencies for an idempotent review-comment upsert. */
|
|
18
|
+
interface UpsertReviewCommentOptions {
|
|
19
|
+
/** Canonical build/review URL — anchors the idempotency marker. */
|
|
20
|
+
url: string;
|
|
21
|
+
/** Markdown body appended below the marker. */
|
|
22
|
+
markdown: string;
|
|
23
|
+
/** Known thread number, skipping the lookup when provided. */
|
|
24
|
+
prNumber?: number;
|
|
25
|
+
/** Commit SHA used for the thread lookup and log context. */
|
|
26
|
+
sha: string;
|
|
27
|
+
logger?: Logger;
|
|
28
|
+
/** Resolve the thread number for the commit (PR number or MR iid). */
|
|
29
|
+
resolveNumber: () => Promise<number | undefined>;
|
|
30
|
+
/** Open the thread's comment store once the number is known. */
|
|
31
|
+
thread: (number: number) => ReviewThread;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Create or update the single StoryShelf review comment for a build,
|
|
35
|
+
* idempotent via the url marker. Returns the comment id, or `""` when no
|
|
36
|
+
* review thread exists for the commit.
|
|
37
|
+
*/
|
|
38
|
+
declare function upsertReviewComment(opts: UpsertReviewCommentOptions): Promise<string>;
|
|
39
|
+
//#endregion
|
|
40
|
+
export { ReviewThread, UpsertReviewCommentOptions, upsertReviewComment };
|
|
41
|
+
//# sourceMappingURL=comments.d.mts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { commentMarker } from "./helpers.mjs";
|
|
5
|
+
//#region src/adapters/git-host/comments.ts
|
|
6
|
+
/** Resolve the thread number, preferring the explicit value. */
|
|
7
|
+
async function resolveThreadNumber(opts) {
|
|
8
|
+
return opts.prNumber ?? await opts.resolveNumber();
|
|
9
|
+
}
|
|
10
|
+
/** Update the marked comment if present, else create it. */
|
|
11
|
+
async function findOrCreateComment(thread, marker, body, prNumber, logger) {
|
|
12
|
+
const existing = (await thread.list()).find((comment) => comment.body.includes(marker));
|
|
13
|
+
if (existing) {
|
|
14
|
+
const id = await thread.update(existing.id, body);
|
|
15
|
+
logger?.info({
|
|
16
|
+
prNumber,
|
|
17
|
+
commentId: id
|
|
18
|
+
}, "review comment updated");
|
|
19
|
+
return id;
|
|
20
|
+
}
|
|
21
|
+
const id = await thread.create(body);
|
|
22
|
+
logger?.info({
|
|
23
|
+
prNumber,
|
|
24
|
+
commentId: id
|
|
25
|
+
}, "review comment created");
|
|
26
|
+
return id;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Create or update the single StoryShelf review comment for a build,
|
|
30
|
+
* idempotent via the url marker. Returns the comment id, or `""` when no
|
|
31
|
+
* review thread exists for the commit.
|
|
32
|
+
*/
|
|
33
|
+
async function upsertReviewComment(opts) {
|
|
34
|
+
const marker = commentMarker(opts.url);
|
|
35
|
+
const body = `${marker}\n${opts.markdown}`;
|
|
36
|
+
const number = await resolveThreadNumber(opts);
|
|
37
|
+
if (number === void 0) {
|
|
38
|
+
opts.logger?.debug({ sha: opts.sha }, "no review thread found for comment, skipping");
|
|
39
|
+
return "";
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
return await findOrCreateComment(opts.thread(number), marker, body, number, opts.logger);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
opts.logger?.error({
|
|
45
|
+
err: error,
|
|
46
|
+
prNumber: number
|
|
47
|
+
}, "failed to upsert review comment");
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
export { upsertReviewComment };
|
|
53
|
+
|
|
54
|
+
//# sourceMappingURL=comments.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comments.mjs","names":[],"sources":["../../../src/adapters/git-host/comments.ts"],"sourcesContent":["import type { Logger } from \"pino\";\nimport { commentMarker } from \"./helpers.ts\";\n\n/** A review thread (PR or MR) hosting a single StoryShelf comment. */\nexport interface ReviewThread {\n /** List existing comments as id/body pairs. */\n list(): Promise<{ id: string | number; body: string }[]>;\n /** Replace the body of an existing comment, returning its id. */\n update(id: string | number, body: string): Promise<string>;\n /** Create a comment, returning its id. */\n create(body: string): Promise<string>;\n}\n\n/** Dependencies for an idempotent review-comment upsert. */\nexport interface UpsertReviewCommentOptions {\n /** Canonical build/review URL — anchors the idempotency marker. */\n url: string;\n /** Markdown body appended below the marker. */\n markdown: string;\n /** Known thread number, skipping the lookup when provided. */\n prNumber?: number;\n /** Commit SHA used for the thread lookup and log context. */\n sha: string;\n logger?: Logger;\n /** Resolve the thread number for the commit (PR number or MR iid). */\n resolveNumber: () => Promise<number | undefined>;\n /** Open the thread's comment store once the number is known. */\n thread: (number: number) => ReviewThread;\n}\n\n/** Resolve the thread number, preferring the explicit value. */\nasync function resolveThreadNumber(opts: UpsertReviewCommentOptions): Promise<number | undefined> {\n return opts.prNumber ?? (await opts.resolveNumber());\n}\n\n/** Update the marked comment if present, else create it. */\nasync function findOrCreateComment(\n thread: ReviewThread,\n marker: string,\n body: string,\n prNumber: number,\n logger?: Logger,\n): Promise<string> {\n const existing = (await thread.list()).find((comment) => comment.body.includes(marker));\n if (existing) {\n const id = await thread.update(existing.id, body);\n logger?.info({ prNumber, commentId: id }, \"review comment updated\");\n return id;\n }\n const id = await thread.create(body);\n logger?.info({ prNumber, commentId: id }, \"review comment created\");\n return id;\n}\n\n/**\n * Create or update the single StoryShelf review comment for a build,\n * idempotent via the url marker. Returns the comment id, or `\"\"` when no\n * review thread exists for the commit.\n */\nexport async function upsertReviewComment(opts: UpsertReviewCommentOptions): Promise<string> {\n const marker = commentMarker(opts.url);\n const body = `${marker}\\n${opts.markdown}`;\n const number = await resolveThreadNumber(opts);\n if (number === undefined) {\n opts.logger?.debug({ sha: opts.sha }, \"no review thread found for comment, skipping\");\n return \"\";\n }\n try {\n return await findOrCreateComment(opts.thread(number), marker, body, number, opts.logger);\n } catch (error) {\n opts.logger?.error({ err: error, prNumber: number }, \"failed to upsert review comment\");\n throw error;\n }\n}\n"],"mappings":";;;;;;AA+BA,eAAe,oBAAoB,MAA+D;CAChG,OAAO,KAAK,YAAa,MAAM,KAAK,cAAc;AACpD;;AAGA,eAAe,oBACb,QACA,QACA,MACA,UACA,QACiB;CACjB,MAAM,YAAY,MAAM,OAAO,KAAK,EAAA,CAAG,MAAM,YAAY,QAAQ,KAAK,SAAS,MAAM,CAAC;CACtF,IAAI,UAAU;EACZ,MAAM,KAAK,MAAM,OAAO,OAAO,SAAS,IAAI,IAAI;EAChD,QAAQ,KAAK;GAAE;GAAU,WAAW;EAAG,GAAG,wBAAwB;EAClE,OAAO;CACT;CACA,MAAM,KAAK,MAAM,OAAO,OAAO,IAAI;CACnC,QAAQ,KAAK;EAAE;EAAU,WAAW;CAAG,GAAG,wBAAwB;CAClE,OAAO;AACT;;;;;;AAOA,eAAsB,oBAAoB,MAAmD;CAC3F,MAAM,SAAS,cAAc,KAAK,GAAG;CACrC,MAAM,OAAO,GAAG,OAAO,IAAI,KAAK;CAChC,MAAM,SAAS,MAAM,oBAAoB,IAAI;CAC7C,IAAI,WAAW,KAAA,GAAW;EACxB,KAAK,QAAQ,MAAM,EAAE,KAAK,KAAK,IAAI,GAAG,8CAA8C;EACpF,OAAO;CACT;CACA,IAAI;EACF,OAAO,MAAM,oBAAoB,KAAK,OAAO,MAAM,GAAG,QAAQ,MAAM,QAAQ,KAAK,MAAM;CACzF,SAAS,OAAO;EACd,KAAK,QAAQ,MAAM;GAAE,KAAK;GAAO,UAAU;EAAO,GAAG,iCAAiC;EACtF,MAAM;CACR;AACF"}
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import __tsdown_shims_path from 'node:path';
|
|
2
2
|
import __tsdown_shims_url from 'node:url';
|
|
3
|
-
import { GitAdapterMetadata } from "
|
|
3
|
+
import { Adapter, GitAdapterMetadata } from "./metadata.mjs";
|
|
4
4
|
import { Logger } from "pino";
|
|
5
5
|
//#region src/adapters/git-host/index.d.ts
|
|
6
6
|
/** Status of a git-host provider check. */
|
|
7
7
|
type CheckStatus = "pending" | "success" | "failure";
|
|
8
8
|
/** Descriptor — registered at startup, validates per-project config. */
|
|
9
|
-
interface GitHostProvider {
|
|
10
|
-
/** Adapter identity (name, version, description, kind="github", logo, schema). */
|
|
11
|
-
readonly metadata: GitAdapterMetadata;
|
|
9
|
+
interface GitHostProvider extends Adapter<GitAdapterMetadata> {
|
|
12
10
|
/** Create a configured instance bound to a project config + decrypted token. */
|
|
13
11
|
create(opts: {
|
|
14
12
|
config: unknown;
|
|
@@ -17,9 +15,7 @@ interface GitHostProvider {
|
|
|
17
15
|
}): GitHostAdapter;
|
|
18
16
|
}
|
|
19
17
|
/** Runtime — bound to a project config + token, posts statuses/comments. */
|
|
20
|
-
interface GitHostAdapter {
|
|
21
|
-
/** Adapter identity. */
|
|
22
|
-
readonly metadata: GitAdapterMetadata;
|
|
18
|
+
interface GitHostAdapter extends Adapter<GitAdapterMetadata> {
|
|
23
19
|
/** Set the status of a commit. */
|
|
24
20
|
setStatus(opts: {
|
|
25
21
|
context: string;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { CaptureQueue } from "./capture-queue.mjs";
|
|
4
|
+
import { CaptureRunner } from "./capture-runner.mjs";
|
|
5
|
+
import { DatabaseAdapter } from "./database.mjs";
|
|
6
|
+
import { GitHostProvider } from "./git-host.mjs";
|
|
7
|
+
import { StorageAdapter } from "./storage.mjs";
|
|
8
|
+
import { AdapterInitContext } from "./metadata.mjs";
|
|
9
|
+
import { AuthAdapter } from "./auth.mjs";
|
|
10
|
+
import { Logger } from "pino";
|
|
11
|
+
//#region src/adapters/init.d.ts
|
|
12
|
+
/** Adapters that can take part in lifecycle runs. */
|
|
13
|
+
interface AdapterInitSources {
|
|
14
|
+
database: DatabaseAdapter;
|
|
15
|
+
storage: StorageAdapter;
|
|
16
|
+
captureRunner?: CaptureRunner;
|
|
17
|
+
captureQueue?: CaptureQueue;
|
|
18
|
+
auth?: AuthAdapter;
|
|
19
|
+
gitHosts?: GitHostProvider[];
|
|
20
|
+
}
|
|
21
|
+
/** One collected hook bound to its adapter identity. */
|
|
22
|
+
interface HookEntry {
|
|
23
|
+
readonly category: string;
|
|
24
|
+
readonly kind: string;
|
|
25
|
+
readonly name: string;
|
|
26
|
+
readonly run: (ctx: AdapterInitContext) => Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
/** One failed hook, with its adapter identity attached. */
|
|
29
|
+
interface AdapterInitFailure {
|
|
30
|
+
readonly category: string;
|
|
31
|
+
readonly kind: string;
|
|
32
|
+
readonly name: string;
|
|
33
|
+
readonly error: string;
|
|
34
|
+
}
|
|
35
|
+
/** Settled outcome of a lifecycle run (never rejects — use `ok`). */
|
|
36
|
+
interface AdapterInitResult {
|
|
37
|
+
readonly ok: boolean;
|
|
38
|
+
readonly failures: AdapterInitFailure[];
|
|
39
|
+
}
|
|
40
|
+
/** Lifecycle phase a run covers. */
|
|
41
|
+
type LifecyclePhase = "init" | "close";
|
|
42
|
+
/** Thrown when an `app.lifecycle.init()` or `close()` run has failures. */
|
|
43
|
+
declare class AdapterLifecycleError extends Error {
|
|
44
|
+
readonly phase: LifecyclePhase;
|
|
45
|
+
readonly failures: AdapterInitFailure[];
|
|
46
|
+
constructor(phase: LifecyclePhase, failures: AdapterInitFailure[]);
|
|
47
|
+
}
|
|
48
|
+
/** Collect every available `init` hook in deterministic order. */
|
|
49
|
+
declare function collectInits(sources: AdapterInitSources): HookEntry[];
|
|
50
|
+
/** Collect every available `close` hook in deterministic order. */
|
|
51
|
+
declare function collectCloses(sources: AdapterInitSources): HookEntry[];
|
|
52
|
+
/** Run init hooks concurrently; failures are collected, never thrown. */
|
|
53
|
+
declare function runAdapterInits(entries: HookEntry[], ctx: AdapterInitContext, logger: Logger): Promise<AdapterInitResult>;
|
|
54
|
+
/** Run close hooks concurrently; failures are collected, never thrown. */
|
|
55
|
+
declare function runAdapterCloses(entries: HookEntry[], ctx: AdapterInitContext, logger: Logger): Promise<AdapterInitResult>;
|
|
56
|
+
//#endregion
|
|
57
|
+
export { AdapterInitFailure, AdapterInitResult, AdapterInitSources, AdapterLifecycleError, HookEntry, LifecyclePhase, collectCloses, collectInits, runAdapterCloses, runAdapterInits };
|
|
58
|
+
//# sourceMappingURL=init.d.mts.map
|