handoff-app 1.2.2-7 → 2.0.0-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Changelog.md +37 -15
- package/README.md +372 -118
- package/dist/app-builder/build.d.ts +41 -2
- package/dist/app-builder/build.js +455 -18
- package/dist/app-builder/client-config.d.ts +15 -2
- package/dist/app-builder/client-config.js +43 -4
- package/dist/app-builder/config-diff/snapshot.d.ts +1 -6
- package/dist/app-builder/config-diff/snapshot.js +2 -9
- package/dist/app-builder/index.d.ts +1 -1
- package/dist/app-builder/index.js +3 -1
- package/dist/app-builder/nav-shell.d.ts +23 -0
- package/dist/app-builder/nav-shell.js +80 -0
- package/dist/app-builder/static-export.d.ts +17 -0
- package/dist/app-builder/static-export.js +181 -0
- package/dist/app-builder/vercel-output.d.ts +39 -0
- package/dist/app-builder/vercel-output.js +259 -0
- package/dist/app-builder/watchers/utils.d.ts +2 -0
- package/dist/app-builder/watchers.d.ts +14 -4
- package/dist/app-builder/watchers.js +123 -28
- package/dist/app-builder/websocket.js +1 -1
- package/dist/artifacts/index.d.ts +2 -0
- package/dist/artifacts/index.js +13 -0
- package/dist/artifacts/types.d.ts +136 -0
- package/dist/artifacts/types.js +24 -0
- package/dist/artifacts/url.d.ts +49 -0
- package/dist/artifacts/url.js +86 -0
- package/dist/cache/build-cache.js +48 -4
- package/dist/cli.js +0 -0
- package/dist/commands/build/app.js +1 -2
- package/dist/commands/build/index.d.ts +17 -0
- package/dist/commands/build/index.js +66 -0
- package/dist/commands/checkout/index.d.ts +24 -0
- package/dist/commands/checkout/index.js +71 -0
- package/dist/commands/db/migrate.d.ts +11 -0
- package/dist/commands/db/migrate.js +33 -0
- package/dist/commands/index.js +8 -0
- package/dist/commands/publish/index.d.ts +21 -0
- package/dist/commands/publish/index.js +68 -0
- package/dist/config/defaults.js +15 -1
- package/dist/config/loader.js +6 -1
- package/dist/config/normalizers/declaration.js +4 -1
- package/dist/config/normalizers/page.d.ts +18 -0
- package/dist/config/normalizers/page.js +34 -0
- package/dist/config/normalizers/pattern.js +7 -1
- package/dist/config/runtime.d.ts +4 -0
- package/dist/config/runtime.js +53 -5
- package/dist/index.d.ts +65 -1
- package/dist/index.js +123 -3
- package/dist/nav/index.d.ts +83 -0
- package/dist/nav/index.js +9 -0
- package/dist/nav/registry.d.ts +15 -0
- package/dist/nav/registry.js +42 -0
- package/dist/nav/resolver.d.ts +8 -0
- package/dist/nav/resolver.js +192 -0
- package/dist/nav/workspace.d.ts +18 -0
- package/dist/nav/workspace.js +103 -0
- package/dist/pipeline/styles.js +7 -35
- package/dist/pipeline/token-transformers.d.ts +45 -0
- package/dist/pipeline/token-transformers.js +71 -0
- package/dist/registry/asset-storage/adapters/vercel-blob.d.ts +17 -0
- package/dist/registry/asset-storage/adapters/vercel-blob.js +87 -0
- package/dist/registry/asset-storage/define.d.ts +10 -0
- package/dist/registry/asset-storage/define.js +13 -0
- package/dist/registry/asset-storage/resolve.d.ts +45 -0
- package/dist/registry/asset-storage/resolve.js +36 -0
- package/dist/registry/asset-storage/types.d.ts +51 -0
- package/dist/registry/asset-storage/types.js +9 -0
- package/dist/registry/assets/layout.d.ts +22 -0
- package/dist/registry/assets/layout.js +30 -0
- package/dist/registry/assets/sets.d.ts +18 -0
- package/dist/registry/assets/sets.js +44 -0
- package/dist/registry/assets/transfer.d.ts +45 -0
- package/dist/registry/assets/transfer.js +11 -0
- package/dist/registry/checkout/assets.d.ts +16 -0
- package/dist/registry/checkout/assets.js +150 -0
- package/dist/registry/checkout/index.d.ts +33 -0
- package/dist/registry/checkout/index.js +610 -0
- package/dist/registry/checkout/tokens.d.ts +15 -0
- package/dist/registry/checkout/tokens.js +171 -0
- package/dist/registry/client.d.ts +61 -0
- package/dist/registry/client.js +211 -0
- package/dist/registry/connection.d.ts +30 -0
- package/dist/registry/connection.js +36 -0
- package/dist/registry/db/adapter.d.ts +43 -0
- package/dist/registry/db/adapter.js +59 -0
- package/dist/registry/db/client.d.ts +35 -0
- package/dist/registry/db/client.js +94 -0
- package/dist/registry/db/index.d.ts +5 -0
- package/dist/registry/db/index.js +24 -0
- package/dist/registry/db/migrate.d.ts +23 -0
- package/dist/registry/db/migrate.js +64 -0
- package/dist/registry/db/schema.d.ts +4869 -0
- package/dist/registry/db/schema.js +347 -0
- package/dist/registry/path.d.ts +4 -0
- package/dist/registry/path.js +31 -0
- package/dist/registry/publish/artifacts.d.ts +16 -0
- package/dist/registry/publish/artifacts.js +132 -0
- package/dist/registry/publish/assets.d.ts +16 -0
- package/dist/registry/publish/assets.js +129 -0
- package/dist/registry/publish/errors.d.ts +5 -0
- package/dist/registry/publish/errors.js +26 -0
- package/dist/registry/publish/index.d.ts +27 -0
- package/dist/registry/publish/index.js +126 -0
- package/dist/registry/publish/package.d.ts +27 -0
- package/dist/registry/publish/package.js +257 -0
- package/dist/registry/publish/publish-build.d.ts +12 -0
- package/dist/registry/publish/publish-build.js +43 -0
- package/dist/registry/publish/tokens.d.ts +18 -0
- package/dist/registry/publish/tokens.js +112 -0
- package/dist/registry/tokens/sets.d.ts +59 -0
- package/dist/registry/tokens/sets.js +109 -0
- package/dist/registry/tokens/transfer.d.ts +48 -0
- package/dist/registry/tokens/transfer.js +10 -0
- package/dist/registry/transfer.d.ts +75 -0
- package/dist/registry/transfer.js +11 -0
- package/dist/store/filesystem-assets.d.ts +17 -0
- package/dist/store/filesystem-assets.js +147 -0
- package/dist/store/filesystem.d.ts +76 -0
- package/dist/store/filesystem.js +208 -0
- package/dist/store/index.d.ts +20 -0
- package/dist/store/index.js +38 -0
- package/dist/store/registry.d.ts +77 -0
- package/dist/store/registry.js +350 -0
- package/dist/store/source-files.d.ts +24 -0
- package/dist/store/source-files.js +147 -0
- package/dist/store/types.d.ts +185 -0
- package/dist/store/types.js +12 -0
- package/dist/transformers/plugins/csf-render.js +17 -8
- package/dist/transformers/plugins/handlebars-previews.js +10 -3
- package/dist/transformers/plugins/ssr-render.js +116 -49
- package/dist/transformers/preview/component/api.js +3 -1
- package/dist/transformers/preview/component/artifacts.js +8 -1
- package/dist/transformers/preview/component/builder.js +13 -5
- package/dist/transformers/preview/component/css.js +9 -1
- package/dist/transformers/preview/component/javascript.d.ts +9 -3
- package/dist/transformers/preview/component/javascript.js +45 -17
- package/dist/transformers/preview/component/shared-artifacts.d.ts +83 -0
- package/dist/transformers/preview/component/shared-artifacts.js +92 -0
- package/dist/transformers/preview/component.js +5 -2
- package/dist/transformers/preview/pattern/builder.js +28 -9
- package/dist/transformers/preview/pattern/html.d.ts +13 -10
- package/dist/transformers/preview/pattern/html.js +62 -27
- package/dist/transformers/preview/types.d.ts +38 -0
- package/dist/transformers/utils/handlebars.d.ts +3 -0
- package/dist/transformers/utils/handlebars.js +19 -5
- package/dist/types/config.d.ts +118 -25
- package/dist/utils/menu-shell.d.ts +60 -0
- package/dist/utils/menu-shell.js +221 -0
- package/dist/utils/stable-stringify.d.ts +2 -0
- package/dist/utils/stable-stringify.js +8 -0
- package/docs/cli.md +4 -2
- package/drizzle/0000_init_registry_storage.sql +95 -0
- package/drizzle/0001_add_review_metadata.sql +2 -0
- package/drizzle/0002_add_pages.sql +28 -0
- package/drizzle/0003_add_tokens.sql +29 -0
- package/drizzle/0004_add_assets.sql +37 -0
- package/drizzle/meta/0000_snapshot.json +642 -0
- package/drizzle/meta/0001_snapshot.json +654 -0
- package/drizzle/meta/0002_snapshot.json +835 -0
- package/drizzle/meta/0003_snapshot.json +1023 -0
- package/drizzle/meta/0004_snapshot.json +1252 -0
- package/drizzle/meta/_journal.json +41 -0
- package/drizzle.config.ts +16 -0
- package/package.json +10 -1
- package/src/app/components/Component/Preview.tsx +3 -2
- package/src/app/components/Component/RegistryActions.tsx +150 -0
- package/src/app/components/Foundations/DisplayLogo.tsx +5 -3
- package/src/app/components/Layout/Header.tsx +21 -3
- package/src/app/components/Layout/Main.tsx +11 -3
- package/src/app/components/Navigation/MainNav.tsx +4 -2
- package/src/app/components/Navigation/MobileNav.tsx +4 -2
- package/src/app/components/Navigation/SideNav.tsx +46 -11
- package/src/app/components/NotFound.tsx +24 -0
- package/src/app/components/context/NavProvider.tsx +71 -0
- package/src/app/components/old/Header.tsx +3 -3
- package/src/app/components/util/index.ts +203 -380
- package/src/app/components/util/useCollectionAssets.ts +125 -0
- package/src/app/components/util/useFoundationTokens.ts +67 -0
- package/src/app/generated/default-pages.json +1 -0
- package/src/app/generated/nav-shell.json +1 -0
- package/src/app/lib/api/query.ts +6 -0
- package/src/app/lib/api/redact.ts +46 -0
- package/src/app/lib/asset-storage.ts +81 -0
- package/src/app/lib/docs-api/artifacts.ts +116 -0
- package/src/app/lib/docs-api/backend.ts +151 -0
- package/src/app/lib/docs-api/errors.ts +52 -0
- package/src/app/lib/docs-api/handler.ts +34 -0
- package/src/app/lib/docs-api/index.ts +7 -0
- package/src/app/lib/docs-api/records.ts +189 -0
- package/src/app/lib/docs-api/registry.ts +130 -0
- package/src/app/lib/docs-api/runtime-config.ts +173 -0
- package/src/app/lib/docs-api/serve.ts +37 -0
- package/src/app/lib/docs-api/token-detail.ts +19 -0
- package/src/app/lib/registry-api/allowlist.ts +163 -0
- package/src/app/lib/registry-api/asset-transfer.ts +363 -0
- package/src/app/lib/registry-api/auth.ts +68 -0
- package/src/app/lib/registry-api/entity-routes.ts +197 -0
- package/src/app/lib/registry-api/errors.ts +51 -0
- package/src/app/lib/registry-api/files.ts +135 -0
- package/src/app/lib/registry-api/handler.ts +96 -0
- package/src/app/lib/registry-api/index.ts +4 -0
- package/src/app/lib/registry-api/meta.ts +87 -0
- package/src/app/lib/registry-api/revalidate.ts +36 -0
- package/src/app/lib/registry-api/store.ts +348 -0
- package/src/app/lib/registry-api/token-transfer.ts +231 -0
- package/src/app/lib/registry-api/transfer.ts +513 -0
- package/src/app/lib/registry-api/validation.ts +74 -0
- package/src/app/lib/registry-connection.ts +58 -0
- package/src/app/lib/utils.ts +14 -0
- package/src/app/next.config.mjs +72 -13
- package/src/app/pages/[...slug]/index.tsx +74 -20
- package/src/app/pages/_app.tsx +5 -2
- package/src/app/pages/api/docs/artifacts/[...artifactPath].ts +16 -0
- package/src/app/pages/api/docs/assets/[collection]/[...path].ts +50 -0
- package/src/app/pages/api/docs/assets/[collection]/index.ts +22 -0
- package/src/app/pages/api/docs/components/[id]/assets/[asset].ts +33 -0
- package/src/app/pages/api/docs/components/[id]/inspect/[previewId].ts +20 -0
- package/src/app/pages/api/docs/components/[id]/previews/[previewId].ts +21 -0
- package/src/app/pages/api/docs/components/[id].ts +24 -0
- package/src/app/pages/api/docs/components.json.ts +12 -0
- package/src/app/pages/api/docs/nav.json.ts +48 -0
- package/src/app/pages/api/docs/patterns/[id]/preview.ts +19 -0
- package/src/app/pages/api/docs/patterns/[id].ts +24 -0
- package/src/app/pages/api/docs/patterns.json.ts +12 -0
- package/src/app/pages/api/docs/tokens/components/[id].ts +30 -0
- package/src/app/pages/api/docs/tokens/foundations/[type].ts +32 -0
- package/src/app/pages/api/registry/components/[id]/files/[...filePath].ts +19 -0
- package/src/app/pages/api/registry/components/[id]/files/index.ts +19 -0
- package/src/app/pages/api/registry/components/[id].ts +20 -0
- package/src/app/pages/api/registry/components/index.ts +19 -0
- package/src/app/pages/api/registry/health.ts +20 -0
- package/src/app/pages/api/registry/patterns/[id]/files/[...filePath].ts +19 -0
- package/src/app/pages/api/registry/patterns/[id]/files/index.ts +19 -0
- package/src/app/pages/api/registry/patterns/[id].ts +20 -0
- package/src/app/pages/api/registry/patterns/index.ts +19 -0
- package/src/app/pages/api/registry/transfer/[kind]/[id].ts +39 -0
- package/src/app/pages/api/registry/transfer/assets/[collection].ts +24 -0
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +23 -0
- package/src/app/pages/api/registry/transfer/assets/blobs/have.ts +11 -0
- package/src/app/pages/api/registry/transfer/assets/index.ts +10 -0
- package/src/app/pages/api/registry/transfer/tokens/[...setId].ts +26 -0
- package/src/app/pages/api/registry/transfer/tokens/index.ts +11 -0
- package/src/app/pages/assets/fonts.tsx +41 -17
- package/src/app/pages/assets/index.tsx +2 -2
- package/src/app/pages/assets/logos.tsx +2 -2
- package/src/app/pages/foundations/colors.tsx +18 -8
- package/src/app/pages/foundations/effects.tsx +18 -8
- package/src/app/pages/foundations/icons/[name]/index.tsx +26 -14
- package/src/app/pages/foundations/icons/index.tsx +13 -9
- package/src/app/pages/foundations/index.tsx +1 -1
- package/src/app/pages/foundations/logo.tsx +7 -4
- package/src/app/pages/foundations/typography.tsx +19 -9
- package/src/app/pages/index.tsx +1 -1
- package/src/app/pages/system/component/[component]/index.tsx +87 -17
- package/src/app/pages/system/index.tsx +13 -4
- package/src/app/pages/system/pattern/[pattern]/index.tsx +70 -14
- package/src/app/pages/system/tokens/components/[component]/index.tsx +42 -14
- package/src/app/pages/system/tokens/foundations/colors.tsx +7 -5
- package/src/app/pages/system/tokens/foundations/effects.tsx +7 -5
- package/src/app/pages/system/tokens/foundations/typography.tsx +8 -4
- package/src/app/tsconfig.json +2 -3
- package/src/app-builder/build.ts +555 -11
- package/src/app-builder/client-config.ts +61 -4
- package/src/app-builder/config-diff/snapshot.ts +1 -11
- package/src/app-builder/index.ts +1 -1
- package/src/app-builder/nav-shell.ts +74 -0
- package/src/app-builder/static-export.ts +215 -0
- package/src/app-builder/vercel-output.ts +297 -0
- package/src/app-builder/watchers/utils.ts +2 -0
- package/src/app-builder/watchers.ts +149 -25
- package/src/app-builder/websocket.ts +2 -2
- package/src/artifacts/README.md +14 -0
- package/src/artifacts/index.ts +17 -0
- package/src/artifacts/types.ts +146 -0
- package/src/artifacts/url.ts +90 -0
- package/src/cache/build-cache.ts +47 -2
- package/src/commands/build/app.ts +1 -2
- package/src/commands/build/index.ts +61 -0
- package/src/commands/checkout/index.ts +66 -0
- package/src/commands/db/migrate.ts +25 -0
- package/src/commands/index.ts +8 -0
- package/src/commands/publish/index.ts +63 -0
- package/src/config/defaults.ts +15 -0
- package/src/config/loader.ts +10 -1
- package/src/config/normalizers/declaration.ts +3 -0
- package/src/config/normalizers/page.ts +45 -0
- package/src/config/normalizers/pattern.ts +4 -1
- package/src/config/runtime.ts +48 -2
- package/src/index.ts +126 -3
- package/src/nav/index.ts +94 -0
- package/src/nav/registry.ts +54 -0
- package/src/nav/resolver.ts +217 -0
- package/src/nav/workspace.ts +87 -0
- package/src/pipeline/styles.ts +4 -35
- package/src/pipeline/token-transformers.ts +97 -0
- package/src/registry/asset-storage/adapters/vercel-blob.ts +51 -0
- package/src/registry/asset-storage/define.ts +15 -0
- package/src/registry/asset-storage/resolve.ts +72 -0
- package/src/registry/asset-storage/types.ts +46 -0
- package/src/registry/assets/layout.ts +38 -0
- package/src/registry/assets/sets.ts +48 -0
- package/src/registry/assets/transfer.ts +50 -0
- package/src/registry/checkout/assets.ts +126 -0
- package/src/registry/checkout/index.ts +596 -0
- package/src/registry/checkout/tokens.ts +152 -0
- package/src/registry/client.ts +234 -0
- package/src/registry/connection.ts +50 -0
- package/src/registry/db/adapter.ts +74 -0
- package/src/registry/db/client.ts +88 -0
- package/src/registry/db/index.ts +35 -0
- package/src/registry/db/migrate.ts +56 -0
- package/src/registry/db/schema.ts +444 -0
- package/src/registry/path.ts +26 -0
- package/src/registry/publish/artifacts.ts +147 -0
- package/src/registry/publish/assets.ts +129 -0
- package/src/registry/publish/errors.ts +34 -0
- package/src/registry/publish/index.ts +128 -0
- package/src/registry/publish/package.ts +272 -0
- package/src/registry/publish/publish-build.ts +51 -0
- package/src/registry/publish/tokens.ts +119 -0
- package/src/registry/tokens/sets.ts +132 -0
- package/src/registry/tokens/transfer.ts +53 -0
- package/src/registry/transfer.ts +82 -0
- package/src/store/filesystem-assets.ts +152 -0
- package/src/store/filesystem.ts +237 -0
- package/src/store/index.ts +62 -0
- package/src/store/registry.ts +334 -0
- package/src/store/source-files.ts +155 -0
- package/src/store/types.ts +210 -0
- package/src/transformers/plugins/csf-render.ts +31 -7
- package/src/transformers/plugins/handlebars-previews.ts +22 -3
- package/src/transformers/plugins/ssr-render.ts +145 -57
- package/src/transformers/preview/component/api.ts +2 -1
- package/src/transformers/preview/component/artifacts.ts +10 -1
- package/src/transformers/preview/component/builder.ts +9 -2
- package/src/transformers/preview/component/css.ts +10 -1
- package/src/transformers/preview/component/javascript.ts +55 -20
- package/src/transformers/preview/component/shared-artifacts.ts +146 -0
- package/src/transformers/preview/component.ts +5 -2
- package/src/transformers/preview/pattern/builder.ts +33 -7
- package/src/transformers/preview/pattern/html.ts +71 -27
- package/src/transformers/preview/types.ts +42 -0
- package/src/transformers/utils/handlebars.ts +31 -5
- package/src/types/config.ts +123 -25
- package/src/utils/menu-shell.ts +241 -0
- package/src/utils/stable-stringify.ts +7 -0
- package/templates/default/package.json.tpl +1 -1
- package/handoff.config.js +0 -90
- package/src/app/components/ComponentSearch.tsx +0 -93
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { and, eq } from 'drizzle-orm';
|
|
2
|
+
import type { RegistryDatabase } from '@handoff/registry/db/client';
|
|
3
|
+
import {
|
|
4
|
+
buildMetadata,
|
|
5
|
+
componentFiles,
|
|
6
|
+
components,
|
|
7
|
+
docsArtifacts,
|
|
8
|
+
pageFiles,
|
|
9
|
+
pages,
|
|
10
|
+
patternFiles,
|
|
11
|
+
patterns,
|
|
12
|
+
type RegistryReviewMetadata,
|
|
13
|
+
} from '@handoff/registry/db/schema';
|
|
14
|
+
import type { ComponentListObject, PageListObject, PatternListObject } from '@handoff/transformers/preview/types';
|
|
15
|
+
import { mergeReviewMetadata, type ManagedEntityKind, type ValidatedMetadataWrite } from './allowlist';
|
|
16
|
+
import type { ValidatedFile } from './files';
|
|
17
|
+
import { resolveBuildMeta, type RegistryBuildMeta } from './meta';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Database operations behind the registry management API.
|
|
21
|
+
*
|
|
22
|
+
* Reads return the normalized record merged with registry-only review metadata; writes are limited
|
|
23
|
+
* to the metadata allowlist and the text-file record groups. Crucially, metadata create/update
|
|
24
|
+
* never touches `docs_artifacts` or `build_metadata` — published artifacts and build state are only
|
|
25
|
+
* changed by transfer/publish. Entity deletion removes only that entity's owned
|
|
26
|
+
* artifacts; shared/global artifacts (owned by `asset`) are preserved.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** A registry record as served by the management API: the normalized record plus review metadata. */
|
|
30
|
+
export type RegistryEntityData = Record<string, unknown> & { metadata: RegistryReviewMetadata | null };
|
|
31
|
+
|
|
32
|
+
/** A single entity read result: its served data plus build/artifact state. */
|
|
33
|
+
export interface EntityReadResult {
|
|
34
|
+
data: RegistryEntityData;
|
|
35
|
+
build: RegistryBuildMeta;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Binds an entity kind to its tables and the conventions used to build/store its records. */
|
|
39
|
+
interface EntitySpec {
|
|
40
|
+
table: typeof components | typeof patterns | typeof pages;
|
|
41
|
+
filesTable: typeof componentFiles | typeof patternFiles | typeof pageFiles;
|
|
42
|
+
fileFkColumn: typeof componentFiles.componentId;
|
|
43
|
+
fileFkName: 'componentId' | 'patternId' | 'pageId';
|
|
44
|
+
pathPrefix: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const ENTITY: Record<ManagedEntityKind, EntitySpec> = {
|
|
48
|
+
component: {
|
|
49
|
+
table: components,
|
|
50
|
+
filesTable: componentFiles,
|
|
51
|
+
fileFkColumn: componentFiles.componentId,
|
|
52
|
+
fileFkName: 'componentId',
|
|
53
|
+
pathPrefix: 'component',
|
|
54
|
+
},
|
|
55
|
+
pattern: {
|
|
56
|
+
table: patterns,
|
|
57
|
+
filesTable: patternFiles as unknown as typeof componentFiles,
|
|
58
|
+
fileFkColumn: patternFiles.patternId as unknown as typeof componentFiles.componentId,
|
|
59
|
+
fileFkName: 'patternId',
|
|
60
|
+
pathPrefix: 'pattern',
|
|
61
|
+
},
|
|
62
|
+
page: {
|
|
63
|
+
table: pages,
|
|
64
|
+
filesTable: pageFiles as unknown as typeof componentFiles,
|
|
65
|
+
fileFkColumn: pageFiles.pageId as unknown as typeof componentFiles.componentId,
|
|
66
|
+
fileFkName: 'pageId',
|
|
67
|
+
pathPrefix: 'page',
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const asString = (value: unknown): string | undefined => (typeof value === 'string' ? value : undefined);
|
|
72
|
+
const asStringArray = (value: unknown): string[] | undefined => (Array.isArray(value) ? (value as string[]) : undefined);
|
|
73
|
+
|
|
74
|
+
/** Attach review metadata to a normalized record for serving. */
|
|
75
|
+
const withMetadata = (record: unknown, metadata: RegistryReviewMetadata | null | undefined): RegistryEntityData => ({
|
|
76
|
+
...(record as Record<string, unknown>),
|
|
77
|
+
metadata: metadata ?? null,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
/** Build a minimal, valid metadata-only component record from allowlisted fields. */
|
|
81
|
+
const buildComponentRecord = (id: string, path: string, fields: Record<string, unknown>): ComponentListObject => ({
|
|
82
|
+
id,
|
|
83
|
+
path,
|
|
84
|
+
title: asString(fields.title) ?? id,
|
|
85
|
+
description: asString(fields.description) ?? '',
|
|
86
|
+
image: '',
|
|
87
|
+
group: asString(fields.group) ?? '',
|
|
88
|
+
type: '',
|
|
89
|
+
properties: {},
|
|
90
|
+
previews: {},
|
|
91
|
+
...(asStringArray(fields.tags) ? { tags: asStringArray(fields.tags) } : {}),
|
|
92
|
+
...(asStringArray(fields.categories) ? { categories: asStringArray(fields.categories) } : {}),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
/** Build a minimal, valid metadata-only pattern record from allowlisted fields. */
|
|
96
|
+
const buildPatternRecord = (id: string, path: string, fields: Record<string, unknown>): PatternListObject => ({
|
|
97
|
+
id,
|
|
98
|
+
path,
|
|
99
|
+
title: asString(fields.title) ?? id,
|
|
100
|
+
description: asString(fields.description) ?? '',
|
|
101
|
+
group: asString(fields.group) ?? '',
|
|
102
|
+
components: [],
|
|
103
|
+
...(asStringArray(fields.tags) ? { tags: asStringArray(fields.tags) } : {}),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/** Build a minimal, valid metadata-only page record from allowlisted fields. */
|
|
107
|
+
const buildPageRecord = (id: string, path: string, fields: Record<string, unknown>): PageListObject => ({
|
|
108
|
+
id,
|
|
109
|
+
path,
|
|
110
|
+
title: asString(fields.title) ?? id,
|
|
111
|
+
description: asString(fields.description) ?? '',
|
|
112
|
+
group: asString(fields.group) ?? '',
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
/** Whether an entity exists by id. */
|
|
116
|
+
export const entityExists = async (db: RegistryDatabase, kind: ManagedEntityKind, id: string): Promise<boolean> => {
|
|
117
|
+
const spec = ENTITY[kind];
|
|
118
|
+
const rows = await db.select({ id: spec.table.id }).from(spec.table).where(eq(spec.table.id, id)).limit(1);
|
|
119
|
+
return Boolean(rows[0]);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/** List all entities of a kind, each merged with its review metadata. */
|
|
123
|
+
export const listEntities = async (db: RegistryDatabase, kind: ManagedEntityKind): Promise<RegistryEntityData[]> => {
|
|
124
|
+
const spec = ENTITY[kind];
|
|
125
|
+
const rows = await db.select({ record: spec.table.record, metadata: spec.table.metadata }).from(spec.table);
|
|
126
|
+
return rows.map((row) => withMetadata(row.record, row.metadata));
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/** Read a single entity with its build/artifact state, or `null` when absent. */
|
|
130
|
+
export const getEntity = async (db: RegistryDatabase, kind: ManagedEntityKind, id: string): Promise<EntityReadResult | null> => {
|
|
131
|
+
const spec = ENTITY[kind];
|
|
132
|
+
const rows = await db
|
|
133
|
+
.select({ record: spec.table.record, metadata: spec.table.metadata })
|
|
134
|
+
.from(spec.table)
|
|
135
|
+
.where(eq(spec.table.id, id))
|
|
136
|
+
.limit(1);
|
|
137
|
+
const row = rows[0];
|
|
138
|
+
if (!row) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
return { data: withMetadata(row.record, row.metadata), build: await resolveBuildMeta(db, kind, id) };
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/** Create a metadata-only entity record. Returns `null` when an entity with that id already exists. */
|
|
145
|
+
export const createEntity = async (
|
|
146
|
+
db: RegistryDatabase,
|
|
147
|
+
kind: ManagedEntityKind,
|
|
148
|
+
write: ValidatedMetadataWrite
|
|
149
|
+
): Promise<EntityReadResult | null> => {
|
|
150
|
+
const spec = ENTITY[kind];
|
|
151
|
+
const id = write.id as string;
|
|
152
|
+
const path = `${spec.pathPrefix}/${id}`;
|
|
153
|
+
const { fields } = write;
|
|
154
|
+
const metadata = write.metadata ?? null;
|
|
155
|
+
const record =
|
|
156
|
+
kind === 'component'
|
|
157
|
+
? buildComponentRecord(id, path, fields)
|
|
158
|
+
: kind === 'pattern'
|
|
159
|
+
? buildPatternRecord(id, path, fields)
|
|
160
|
+
: buildPageRecord(id, path, fields);
|
|
161
|
+
|
|
162
|
+
// `tags` is promoted on components/patterns only; the pages table has no such column.
|
|
163
|
+
const baseValues = {
|
|
164
|
+
id,
|
|
165
|
+
path,
|
|
166
|
+
title: asString(fields.title),
|
|
167
|
+
description: asString(fields.description),
|
|
168
|
+
group: asString(fields.group),
|
|
169
|
+
record,
|
|
170
|
+
metadata,
|
|
171
|
+
};
|
|
172
|
+
const values =
|
|
173
|
+
kind === 'component'
|
|
174
|
+
? { ...baseValues, tags: asStringArray(fields.tags), type: '', categories: asStringArray(fields.categories) }
|
|
175
|
+
: kind === 'pattern'
|
|
176
|
+
? { ...baseValues, tags: asStringArray(fields.tags), components: [] }
|
|
177
|
+
: baseValues;
|
|
178
|
+
|
|
179
|
+
const inserted = await db
|
|
180
|
+
.insert(spec.table)
|
|
181
|
+
.values(values as any)
|
|
182
|
+
.onConflictDoNothing({ target: spec.table.id })
|
|
183
|
+
.returning({ id: spec.table.id });
|
|
184
|
+
if (inserted.length === 0) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
return { data: withMetadata(record, metadata), build: await resolveBuildMeta(db, kind, id) };
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Apply an allowlisted metadata update: allowlisted top-level fields are written to both the
|
|
192
|
+
* normalized record and their promoted columns, and review metadata is merged over the existing
|
|
193
|
+
* value. Artifacts and build state are deliberately untouched. Returns `null` when absent.
|
|
194
|
+
*/
|
|
195
|
+
export const updateEntityMetadata = async (
|
|
196
|
+
db: RegistryDatabase,
|
|
197
|
+
kind: ManagedEntityKind,
|
|
198
|
+
id: string,
|
|
199
|
+
write: ValidatedMetadataWrite
|
|
200
|
+
): Promise<EntityReadResult | null> => {
|
|
201
|
+
const spec = ENTITY[kind];
|
|
202
|
+
const rows = await db
|
|
203
|
+
.select({ record: spec.table.record, metadata: spec.table.metadata })
|
|
204
|
+
.from(spec.table)
|
|
205
|
+
.where(eq(spec.table.id, id))
|
|
206
|
+
.limit(1);
|
|
207
|
+
const row = rows[0];
|
|
208
|
+
if (!row) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const record = { ...(row.record as Record<string, unknown>) };
|
|
213
|
+
const { fields } = write;
|
|
214
|
+
const promoted: Record<string, unknown> = { updatedAt: new Date() };
|
|
215
|
+
|
|
216
|
+
if ('title' in fields) {
|
|
217
|
+
record.title = fields.title;
|
|
218
|
+
promoted.title = fields.title;
|
|
219
|
+
}
|
|
220
|
+
if ('description' in fields) {
|
|
221
|
+
record.description = fields.description;
|
|
222
|
+
promoted.description = fields.description;
|
|
223
|
+
}
|
|
224
|
+
if ('group' in fields) {
|
|
225
|
+
record.group = fields.group;
|
|
226
|
+
promoted.group = fields.group;
|
|
227
|
+
}
|
|
228
|
+
if ('tags' in fields) {
|
|
229
|
+
record.tags = fields.tags;
|
|
230
|
+
promoted.tags = fields.tags;
|
|
231
|
+
}
|
|
232
|
+
if (kind === 'component' && 'categories' in fields) {
|
|
233
|
+
record.categories = fields.categories;
|
|
234
|
+
promoted.categories = fields.categories;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const metadata = mergeReviewMetadata(row.metadata, write.metadata);
|
|
238
|
+
promoted.record = record;
|
|
239
|
+
promoted.metadata = metadata;
|
|
240
|
+
|
|
241
|
+
await db
|
|
242
|
+
.update(spec.table)
|
|
243
|
+
.set(promoted as any)
|
|
244
|
+
.where(eq(spec.table.id, id));
|
|
245
|
+
return { data: withMetadata(record, metadata), build: await resolveBuildMeta(db, kind, id) };
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Delete an entity and only the artifacts it owns. Cascade removes its text-file records; shared
|
|
250
|
+
* artifacts owned by `asset` are preserved. Returns `false` when the entity does not exist.
|
|
251
|
+
*/
|
|
252
|
+
export const deleteEntity = async (db: RegistryDatabase, kind: ManagedEntityKind, id: string): Promise<boolean> => {
|
|
253
|
+
return db.transaction(async (tx) => {
|
|
254
|
+
const transactionalDb = tx as unknown as RegistryDatabase;
|
|
255
|
+
const spec = ENTITY[kind];
|
|
256
|
+
if (!(await entityExists(transactionalDb, kind, id))) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
await transactionalDb.delete(docsArtifacts).where(and(eq(docsArtifacts.entityKind, kind), eq(docsArtifacts.entityId, id)));
|
|
260
|
+
if (kind !== 'page') {
|
|
261
|
+
await transactionalDb.delete(docsArtifacts).where(and(eq(docsArtifacts.ownerKind, kind), eq(docsArtifacts.ownerId, id)));
|
|
262
|
+
}
|
|
263
|
+
await transactionalDb.delete(buildMetadata).where(and(eq(buildMetadata.entityKind, kind), eq(buildMetadata.entityId, id)));
|
|
264
|
+
await transactionalDb.delete(spec.table).where(eq(spec.table.id, id));
|
|
265
|
+
return true;
|
|
266
|
+
});
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
/** List an entity's text-file records (inline content only). */
|
|
270
|
+
export const listEntityFiles = async (db: RegistryDatabase, kind: ManagedEntityKind, id: string): Promise<ValidatedFile[]> => {
|
|
271
|
+
const spec = ENTITY[kind];
|
|
272
|
+
const rows = await db
|
|
273
|
+
.select({
|
|
274
|
+
path: spec.filesTable.path,
|
|
275
|
+
kind: spec.filesTable.kind,
|
|
276
|
+
content: spec.filesTable.content,
|
|
277
|
+
contentType: spec.filesTable.contentType,
|
|
278
|
+
})
|
|
279
|
+
.from(spec.filesTable)
|
|
280
|
+
.where(eq(spec.fileFkColumn, id));
|
|
281
|
+
return rows
|
|
282
|
+
.filter((row) => row.content != null)
|
|
283
|
+
.map((row) => ({ path: row.path, kind: row.kind, content: row.content as string, contentType: row.contentType }));
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
/** Read one text-file record by path, or `null` when absent (or stored without inline content). */
|
|
287
|
+
export const getEntityFile = async (
|
|
288
|
+
db: RegistryDatabase,
|
|
289
|
+
kind: ManagedEntityKind,
|
|
290
|
+
id: string,
|
|
291
|
+
filePath: string
|
|
292
|
+
): Promise<ValidatedFile | null> => {
|
|
293
|
+
const spec = ENTITY[kind];
|
|
294
|
+
const rows = await db
|
|
295
|
+
.select({
|
|
296
|
+
path: spec.filesTable.path,
|
|
297
|
+
kind: spec.filesTable.kind,
|
|
298
|
+
content: spec.filesTable.content,
|
|
299
|
+
contentType: spec.filesTable.contentType,
|
|
300
|
+
})
|
|
301
|
+
.from(spec.filesTable)
|
|
302
|
+
.where(and(eq(spec.fileFkColumn, id), eq(spec.filesTable.path, filePath)))
|
|
303
|
+
.limit(1);
|
|
304
|
+
const row = rows[0];
|
|
305
|
+
if (!row || row.content == null) {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
return { path: row.path, kind: row.kind, content: row.content, contentType: row.contentType };
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
/** Insert or update a text-file record for an entity. */
|
|
312
|
+
export const upsertEntityFile = async (
|
|
313
|
+
db: RegistryDatabase,
|
|
314
|
+
kind: ManagedEntityKind,
|
|
315
|
+
id: string,
|
|
316
|
+
file: ValidatedFile
|
|
317
|
+
): Promise<ValidatedFile> => {
|
|
318
|
+
const spec = ENTITY[kind];
|
|
319
|
+
await db
|
|
320
|
+
.insert(spec.filesTable)
|
|
321
|
+
.values({
|
|
322
|
+
[spec.fileFkName]: id,
|
|
323
|
+
path: file.path,
|
|
324
|
+
kind: file.kind,
|
|
325
|
+
content: file.content,
|
|
326
|
+
contentType: file.contentType,
|
|
327
|
+
} as any)
|
|
328
|
+
.onConflictDoUpdate({
|
|
329
|
+
target: [spec.fileFkColumn, spec.filesTable.path],
|
|
330
|
+
set: { kind: file.kind, content: file.content, contentType: file.contentType, updatedAt: new Date() },
|
|
331
|
+
});
|
|
332
|
+
return file;
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
/** Delete a text-file record by path. Returns `false` when no such file exists. */
|
|
336
|
+
export const deleteEntityFile = async (db: RegistryDatabase, kind: ManagedEntityKind, id: string, filePath: string): Promise<boolean> => {
|
|
337
|
+
const spec = ENTITY[kind];
|
|
338
|
+
const rows = await db
|
|
339
|
+
.select({ path: spec.filesTable.path })
|
|
340
|
+
.from(spec.filesTable)
|
|
341
|
+
.where(and(eq(spec.fileFkColumn, id), eq(spec.filesTable.path, filePath)))
|
|
342
|
+
.limit(1);
|
|
343
|
+
if (!rows[0]) {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
await db.delete(spec.filesTable).where(and(eq(spec.fileFkColumn, id), eq(spec.filesTable.path, filePath)));
|
|
347
|
+
return true;
|
|
348
|
+
};
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
2
|
+
import { eq } from 'drizzle-orm';
|
|
3
|
+
import type { RegistryDatabase } from '@handoff/registry/db/client';
|
|
4
|
+
import { tokenArtifacts, tokenSets } from '@handoff/registry/db/schema';
|
|
5
|
+
import { isTokenSetId, kindForSetId } from '@handoff/registry/tokens/sets';
|
|
6
|
+
import type { TokenSetTransferArtifact, TokenSetTransferPackage } from '@handoff/registry/tokens/transfer';
|
|
7
|
+
import { joinedQueryValue } from '../api/query';
|
|
8
|
+
import { sendRegistryError } from './errors';
|
|
9
|
+
import { buildMeta } from './meta';
|
|
10
|
+
import { handleRegistryRoute, sendRegistryData } from './handler';
|
|
11
|
+
import {
|
|
12
|
+
asString,
|
|
13
|
+
invalidPackage as invalid,
|
|
14
|
+
isPlainObject,
|
|
15
|
+
normalizeSafeRelativePath,
|
|
16
|
+
type PackageValidation,
|
|
17
|
+
validateTransferBuild,
|
|
18
|
+
} from './validation';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Token-set transfer ingestion for the registry.
|
|
22
|
+
*
|
|
23
|
+
* - `GET /api/registry/transfer/tokens` lists set summaries (id + kind + source hash) for the CLI's
|
|
24
|
+
* skip-unchanged / bulk-checkout logic.
|
|
25
|
+
* - `GET /api/registry/transfer/tokens/:setId` is checkout: the set record + its generated artifacts.
|
|
26
|
+
* - `PUT /api/registry/transfer/tokens/:setId` is publish ingestion: it validates the package and
|
|
27
|
+
* atomically upserts the `token_sets` row and replaces its `token_artifacts` in one transaction, so
|
|
28
|
+
* a set is never left partially written — either the new version commits or the previous one stays.
|
|
29
|
+
* An unchanged set (matching `sourceHash`) is a no-op. Absent sets are never deleted.
|
|
30
|
+
*
|
|
31
|
+
* Runs behind {@link handleRegistryRoute}: runtime-mode, method, bearer-token (on PUT), and database
|
|
32
|
+
* guards apply before any of this executes.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** Validate a token-set publish package against the transfer contract. */
|
|
36
|
+
const validateTokenPackage = (body: unknown, setId: string): PackageValidation<TokenSetTransferPackage> => {
|
|
37
|
+
if (!isPlainObject(body)) {
|
|
38
|
+
return invalid('Request body must be a JSON object.');
|
|
39
|
+
}
|
|
40
|
+
if (!isTokenSetId(setId)) {
|
|
41
|
+
return invalid(`Unknown token set id "${setId}".`, { rejectedFields: ['id'] });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const bodyId = asString(body.id);
|
|
45
|
+
if (bodyId !== undefined && bodyId !== setId) {
|
|
46
|
+
return invalid(`Package set id "${bodyId}" does not match the publish target "${setId}".`, { rejectedFields: ['id'] });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const expectedKind = kindForSetId(setId);
|
|
50
|
+
const kind = asString(body.kind) ?? expectedKind;
|
|
51
|
+
if (kind !== 'foundation' && kind !== 'component') {
|
|
52
|
+
return invalid('`kind` must be "foundation" or "component".', { rejectedFields: ['kind'] });
|
|
53
|
+
}
|
|
54
|
+
if (kind !== expectedKind) {
|
|
55
|
+
return invalid(`Package kind "${kind}" is inconsistent with set id "${setId}".`, { rejectedFields: ['kind'] });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (body.record === undefined || body.record === null) {
|
|
59
|
+
return invalid('A token `record` is required to publish.', { rejectedFields: ['record'] });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const rawArtifacts = body.artifacts ?? [];
|
|
63
|
+
if (!Array.isArray(rawArtifacts)) {
|
|
64
|
+
return invalid('`artifacts` must be an array.', { rejectedFields: ['artifacts'] });
|
|
65
|
+
}
|
|
66
|
+
const artifacts: TokenSetTransferArtifact[] = [];
|
|
67
|
+
const seenPaths = new Set<string>();
|
|
68
|
+
for (let i = 0; i < rawArtifacts.length; i += 1) {
|
|
69
|
+
const raw = rawArtifacts[i];
|
|
70
|
+
const field = (name: string) => `artifacts[${i}].${name}`;
|
|
71
|
+
if (!isPlainObject(raw)) {
|
|
72
|
+
return invalid('Each artifact must be an object.', { rejectedFields: [`artifacts[${i}]`] });
|
|
73
|
+
}
|
|
74
|
+
const artifactPath = normalizeSafeRelativePath(raw.path);
|
|
75
|
+
if (!artifactPath) {
|
|
76
|
+
return invalid('Artifact path must be a registry-safe relative path.', { rejectedFields: [field('path')] });
|
|
77
|
+
}
|
|
78
|
+
if (seenPaths.has(artifactPath)) {
|
|
79
|
+
return invalid(`Duplicate artifact path "${artifactPath}" in package.`, { rejectedFields: [field('path')] });
|
|
80
|
+
}
|
|
81
|
+
seenPaths.add(artifactPath);
|
|
82
|
+
if (typeof raw.content !== 'string') {
|
|
83
|
+
return invalid(`Artifact "${artifactPath}" is missing string content.`, { rejectedFields: [field('content')] });
|
|
84
|
+
}
|
|
85
|
+
const format = asString(raw.format);
|
|
86
|
+
if (!format) {
|
|
87
|
+
return invalid(`Artifact "${artifactPath}" is missing a format.`, { rejectedFields: [field('format')] });
|
|
88
|
+
}
|
|
89
|
+
artifacts.push({
|
|
90
|
+
path: artifactPath,
|
|
91
|
+
format,
|
|
92
|
+
content: raw.content as string,
|
|
93
|
+
contentType: asString(raw.contentType) || 'text/plain; charset=utf-8',
|
|
94
|
+
hash: asString(raw.hash),
|
|
95
|
+
size: typeof raw.size === 'number' ? raw.size : undefined,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const buildValidation = validateTransferBuild(body.build, {
|
|
100
|
+
requiredHashField: 'sourceHash',
|
|
101
|
+
currentMessage: 'A "current" token build requires builtAt, builderVersion, and sourceHash.',
|
|
102
|
+
});
|
|
103
|
+
if (!buildValidation.ok) {
|
|
104
|
+
return invalid(buildValidation.message, buildValidation.details);
|
|
105
|
+
}
|
|
106
|
+
const build = buildValidation.value;
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
ok: true,
|
|
110
|
+
value: {
|
|
111
|
+
id: setId,
|
|
112
|
+
kind,
|
|
113
|
+
record: body.record,
|
|
114
|
+
artifacts,
|
|
115
|
+
build: {
|
|
116
|
+
status: build.status,
|
|
117
|
+
builtAt: build.builtAt,
|
|
118
|
+
builderVersion: build.builderVersion,
|
|
119
|
+
sourceHash: build.sourceHash,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/** The stored source hash for a set, or `undefined` when the set does not exist yet. */
|
|
126
|
+
const storedSourceHash = async (db: RegistryDatabase, id: string): Promise<string | null | undefined> => {
|
|
127
|
+
const rows = await db.select({ sourceHash: tokenSets.sourceHash }).from(tokenSets).where(eq(tokenSets.id, id)).limit(1);
|
|
128
|
+
return rows.length ? rows[0].sourceHash : undefined;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/** Atomically upsert the set row and replace its artifacts in one transaction. */
|
|
132
|
+
const ingestTokenSet = async (db: RegistryDatabase, pkg: TokenSetTransferPackage): Promise<void> => {
|
|
133
|
+
const setValues = {
|
|
134
|
+
kind: pkg.kind,
|
|
135
|
+
record: pkg.record,
|
|
136
|
+
sourceHash: pkg.build.sourceHash ?? null,
|
|
137
|
+
status: pkg.build.status,
|
|
138
|
+
builtAt: pkg.build.builtAt ? new Date(pkg.build.builtAt) : null,
|
|
139
|
+
builderVersion: pkg.build.builderVersion ?? null,
|
|
140
|
+
updatedAt: new Date(),
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
await db.transaction(async (tx) => {
|
|
144
|
+
await tx
|
|
145
|
+
.insert(tokenSets)
|
|
146
|
+
.values({ id: pkg.id, metadata: null, ...setValues } as any)
|
|
147
|
+
.onConflictDoUpdate({ target: tokenSets.id, set: setValues as any });
|
|
148
|
+
|
|
149
|
+
await tx.delete(tokenArtifacts).where(eq(tokenArtifacts.tokenSetId, pkg.id));
|
|
150
|
+
for (const artifact of pkg.artifacts) {
|
|
151
|
+
await tx.insert(tokenArtifacts).values({
|
|
152
|
+
tokenSetId: pkg.id,
|
|
153
|
+
path: artifact.path,
|
|
154
|
+
format: artifact.format,
|
|
155
|
+
content: artifact.content,
|
|
156
|
+
contentType: artifact.contentType,
|
|
157
|
+
hash: artifact.hash ?? null,
|
|
158
|
+
size: artifact.size ?? null,
|
|
159
|
+
} as any);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/** `GET /api/registry/transfer/tokens` — list set summaries (unauthenticated read behind the guards). */
|
|
165
|
+
export const handleTokenSummaryRoute = (req: NextApiRequest, res: NextApiResponse): Promise<void> =>
|
|
166
|
+
handleRegistryRoute(req, res, ['GET'], async ({ db }) => {
|
|
167
|
+
const rows = await db
|
|
168
|
+
.select({ id: tokenSets.id, kind: tokenSets.kind, sourceHash: tokenSets.sourceHash, status: tokenSets.status })
|
|
169
|
+
.from(tokenSets);
|
|
170
|
+
sendRegistryData(res, 200, { sets: rows }, buildMeta());
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
/** `GET /api/registry/transfer/tokens/:setId` — checkout one set's record + generated artifacts. */
|
|
174
|
+
export const handleTokenCheckoutRoute = (req: NextApiRequest, res: NextApiResponse): Promise<void> =>
|
|
175
|
+
handleRegistryRoute(req, res, ['GET'], async ({ db }) => {
|
|
176
|
+
const id = joinedQueryValue(req.query.setId) ?? '';
|
|
177
|
+
if (!id) {
|
|
178
|
+
sendRegistryError(res, 'not_found', 'Missing token set id.');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (!isTokenSetId(id)) {
|
|
182
|
+
sendRegistryError(res, 'bad_request', `Unknown or unsafe token set id "${id}".`, {
|
|
183
|
+
rejectedFields: ['id'],
|
|
184
|
+
});
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const rows = await db.select({ kind: tokenSets.kind, record: tokenSets.record }).from(tokenSets).where(eq(tokenSets.id, id)).limit(1);
|
|
188
|
+
const set = rows[0];
|
|
189
|
+
if (!set) {
|
|
190
|
+
sendRegistryError(res, 'not_found', `No token set "${id}" exists in the registry.`);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const artifacts = await db
|
|
194
|
+
.select({
|
|
195
|
+
path: tokenArtifacts.path,
|
|
196
|
+
format: tokenArtifacts.format,
|
|
197
|
+
content: tokenArtifacts.content,
|
|
198
|
+
contentType: tokenArtifacts.contentType,
|
|
199
|
+
hash: tokenArtifacts.hash,
|
|
200
|
+
size: tokenArtifacts.size,
|
|
201
|
+
})
|
|
202
|
+
.from(tokenArtifacts)
|
|
203
|
+
.where(eq(tokenArtifacts.tokenSetId, id));
|
|
204
|
+
sendRegistryData(res, 200, { id, kind: set.kind, record: set.record, artifacts }, buildMeta());
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
/** `PUT /api/registry/transfer/tokens/:setId` — validate + atomically ingest one set. */
|
|
208
|
+
export const handleTokenTransferRoute = (req: NextApiRequest, res: NextApiResponse): Promise<void> =>
|
|
209
|
+
handleRegistryRoute(req, res, ['PUT'], async ({ db }) => {
|
|
210
|
+
const id = joinedQueryValue(req.query.setId) ?? '';
|
|
211
|
+
if (!id) {
|
|
212
|
+
sendRegistryError(res, 'not_found', 'Missing token set id.');
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const validation = validateTokenPackage(req.body, id);
|
|
217
|
+
if (!validation.ok) {
|
|
218
|
+
sendRegistryError(res, 'bad_request', validation.message ?? 'Invalid token set package.', validation.details);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const pkg = validation.value;
|
|
222
|
+
|
|
223
|
+
const existingHash = await storedSourceHash(db, id);
|
|
224
|
+
if (existingHash && pkg.build.sourceHash && existingHash === pkg.build.sourceHash) {
|
|
225
|
+
sendRegistryData(res, 200, { id, kind: pkg.kind, published: false, unchanged: true }, buildMeta());
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
await ingestTokenSet(db, pkg);
|
|
230
|
+
sendRegistryData(res, 200, { id, kind: pkg.kind, published: true, artifacts: pkg.artifacts.length }, buildMeta());
|
|
231
|
+
});
|