@elytracms/studio 0.0.94 → 0.0.95

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.
Files changed (126) hide show
  1. package/convex/asset-alt-migration.test.ts +2 -2
  2. package/convex/asset-meta-patch.test.ts +218 -0
  3. package/convex/asset-replace-delete-race.test.ts +127 -0
  4. package/convex/assets.ts +27 -2
  5. package/convex/delivery-redirects.test.ts +167 -0
  6. package/convex/delivery.ts +15 -5
  7. package/convex/rail-write-parity.test.ts +6 -6
  8. package/convex/redirects.ts +1 -1
  9. package/convex/schema.ts +6 -0
  10. package/dist/addon.d.ts +1 -1
  11. package/dist/addon.js.map +1 -1
  12. package/dist/components/assets/asset-detail.js +26 -11
  13. package/dist/components/assets/asset-detail.js.map +1 -1
  14. package/dist/components/assets/asset-locale.d.ts +9 -0
  15. package/dist/components/assets/asset-locale.js +16 -0
  16. package/dist/components/assets/asset-locale.js.map +1 -0
  17. package/dist/components/assets/asset-meta-label.d.ts +12 -0
  18. package/dist/components/assets/asset-meta-label.js +15 -0
  19. package/dist/components/assets/asset-meta-label.js.map +1 -0
  20. package/dist/components/assets/asset-metadata.d.ts +69 -15
  21. package/dist/components/assets/asset-metadata.js +99 -11
  22. package/dist/components/assets/asset-metadata.js.map +1 -1
  23. package/dist/components/assets/asset-search.js +8 -2
  24. package/dist/components/assets/asset-search.js.map +1 -1
  25. package/dist/components/assets/asset-thumb.js +6 -2
  26. package/dist/components/assets/asset-thumb.js.map +1 -1
  27. package/dist/components/assets/assets-collection-view-source.d.ts +23 -9
  28. package/dist/components/assets/assets-collection-view-source.js +78 -26
  29. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  30. package/dist/components/assets/assets-store.js +17 -4
  31. package/dist/components/assets/assets-store.js.map +1 -1
  32. package/dist/components/builder/builder-store.d.ts +21 -0
  33. package/dist/components/builder/builder-store.js +22 -0
  34. package/dist/components/builder/builder-store.js.map +1 -1
  35. package/dist/components/builder/canvas-commit-boundary.d.ts +22 -0
  36. package/dist/components/builder/canvas-commit-boundary.js +27 -0
  37. package/dist/components/builder/canvas-commit-boundary.js.map +1 -0
  38. package/dist/components/builder/canvas-rebase.d.ts +77 -0
  39. package/dist/components/builder/canvas-rebase.js +169 -0
  40. package/dist/components/builder/canvas-rebase.js.map +1 -0
  41. package/dist/components/builder/canvas-render-context.d.ts +6 -3
  42. package/dist/components/builder/canvas-render-context.js +17 -9
  43. package/dist/components/builder/canvas-render-context.js.map +1 -1
  44. package/dist/components/builder/composition-field-editor.d.ts +20 -2
  45. package/dist/components/builder/composition-field-editor.js +45 -27
  46. package/dist/components/builder/composition-field-editor.js.map +1 -1
  47. package/dist/components/builder/node-factory.js +15 -3
  48. package/dist/components/builder/node-factory.js.map +1 -1
  49. package/dist/components/builder/property-inspector.js +3 -1
  50. package/dist/components/builder/property-inspector.js.map +1 -1
  51. package/dist/components/builder/run-inspector.js +10 -6
  52. package/dist/components/builder/run-inspector.js.map +1 -1
  53. package/dist/components/cms/asset-field.d.ts +5 -2
  54. package/dist/components/cms/asset-field.js +34 -17
  55. package/dist/components/cms/asset-field.js.map +1 -1
  56. package/dist/components/cms/asset-picker.js +3 -1
  57. package/dist/components/cms/asset-picker.js.map +1 -1
  58. package/dist/components/cms/canvas-commit.d.ts +56 -0
  59. package/dist/components/cms/canvas-commit.js +35 -0
  60. package/dist/components/cms/canvas-commit.js.map +1 -0
  61. package/dist/components/cms/collection-table.js +23 -7
  62. package/dist/components/cms/collection-table.js.map +1 -1
  63. package/dist/components/cms/commit-policy.d.ts +6 -2
  64. package/dist/components/cms/commit-policy.js +1 -1
  65. package/dist/components/cms/commit-policy.js.map +1 -1
  66. package/dist/components/cms/detail-editor.js +11 -1
  67. package/dist/components/cms/detail-editor.js.map +1 -1
  68. package/dist/components/cms/use-document-draft.d.ts +62 -4
  69. package/dist/components/cms/use-document-draft.js +102 -32
  70. package/dist/components/cms/use-document-draft.js.map +1 -1
  71. package/dist/components/cms/use-live-url.d.ts +6 -1
  72. package/dist/components/cms/use-live-url.js +12 -9
  73. package/dist/components/cms/use-live-url.js.map +1 -1
  74. package/dist/components/collection-view/cms-collection-view-source.js +9 -5
  75. package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
  76. package/dist/components/deployment-setup-gate.js +10 -1
  77. package/dist/components/deployment-setup-gate.js.map +1 -1
  78. package/dist/components/records/record-lens.js +3 -8
  79. package/dist/components/records/record-lens.js.map +1 -1
  80. package/dist/lib/assets/upload.d.ts +25 -7
  81. package/dist/lib/assets/upload.js +30 -21
  82. package/dist/lib/assets/upload.js.map +1 -1
  83. package/dist/lib/cms/published-documents.d.ts +24 -6
  84. package/dist/lib/cms/published-documents.js +39 -6
  85. package/dist/lib/cms/published-documents.js.map +1 -1
  86. package/dist/lib/cms-views/view-model.d.ts +14 -101
  87. package/dist/lib/cms-views/view-model.js +20 -101
  88. package/dist/lib/cms-views/view-model.js.map +1 -1
  89. package/dist/lib/collection-view/collection-view-source.d.ts +6 -1
  90. package/dist/lib/collection-view/collection-view-source.js.map +1 -1
  91. package/dist/lib/engine-version.d.ts +1 -1
  92. package/dist/lib/engine-version.js +1 -1
  93. package/dist/lib/engine-version.js.map +1 -1
  94. package/dist/lib/i18n/messages.de.js +7 -3
  95. package/dist/lib/i18n/messages.de.js.map +1 -1
  96. package/dist/lib/i18n/messages.en.d.ts +5 -1
  97. package/dist/lib/i18n/messages.en.js +8 -4
  98. package/dist/lib/i18n/messages.en.js.map +1 -1
  99. package/dist/lib/operations/config-manifest-lookup.d.ts +1 -1
  100. package/dist/lib/operations/config-manifest-lookup.js +3 -3
  101. package/dist/lib/operations/config-manifest-lookup.js.map +1 -1
  102. package/dist/lib/persistence/deployment-readiness.js +14 -11
  103. package/dist/lib/persistence/deployment-readiness.js.map +1 -1
  104. package/dist/lib/persona/labels.js +1 -0
  105. package/dist/lib/persona/labels.js.map +1 -1
  106. package/dist/lib/query/index.d.ts +2 -2
  107. package/dist/lib/query/index.js +1 -1
  108. package/dist/lib/query/index.js.map +1 -1
  109. package/dist/lib/query/operations-query.d.ts +26 -0
  110. package/dist/lib/query/operations-query.js +12 -0
  111. package/dist/lib/query/operations-query.js.map +1 -1
  112. package/dist/lib/registry/index.js +2 -2
  113. package/dist/lib/registry/index.js.map +1 -1
  114. package/dist/lib/registry/preview.d.ts +10 -0
  115. package/dist/lib/registry/preview.js +13 -4
  116. package/dist/lib/registry/preview.js.map +1 -1
  117. package/dist/lib/routes/route-issues.js +6 -3
  118. package/dist/lib/routes/route-issues.js.map +1 -1
  119. package/dist/lib/workspace/workspace-source.js +6 -7
  120. package/dist/lib/workspace/workspace-source.js.map +1 -1
  121. package/dist/previews/entry-node.d.ts +14 -0
  122. package/dist/previews/entry-node.js +22 -0
  123. package/dist/previews/entry-node.js.map +1 -0
  124. package/dist/previews/harness.js +2 -16
  125. package/dist/previews/harness.js.map +1 -1
  126. package/package.json +3 -3
@@ -1 +1 @@
1
- {"version":3,"file":"upload.js","sourceRoot":"","sources":["../../../src/lib/assets/upload.ts"],"names":[],"mappings":"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","sourcesContent":["import { embeddedMetaValues, readEmbeddedMetadata } from '@elytracms/core/cms-core'\nimport type { FieldDef } from '@elytracms/core/cms-core'\nimport { liftLegacyAlt } from '@elytracms/core/persistence'\nimport type {\n AssetRecord,\n AssetRepository,\n AssetWriteInput,\n BlobStorageAdapter,\n} from '@elytracms/core/persistence'\nimport { detectImageDimensions } from './image-dimensions'\nimport type { ImageDimensions } from './image-dimensions'\nimport { generateVariants } from './thumbnail'\nimport type { GeneratedVariants } from './thumbnail'\nimport { initialUploadState, uploadReducer } from './upload-state'\nimport type { UploadEvent, UploadFailure, UploadState } from './upload-state'\n\n/**\n * Upload orchestration (EC-151): request upload URL → transfer bytes →\n * confirm by writing the EC-044 metadata record (dimensions, mime, serve URL).\n * Storage is reached *only* through the {@link BlobStorageAdapter}; every\n * effect is injectable so the whole flow unit-tests without a browser or a\n * live Convex deployment.\n */\n\n/** The structural slice of a browser `File` the upload flow needs. */\nexport interface UploadableFile {\n name: string\n type: string\n size: number\n arrayBuffer(): Promise<ArrayBuffer>\n}\n\n/** Sends bytes to an upload URL, reporting progress; returns the assigned storage key. */\nexport type UploadTransport = (\n uploadUrl: string,\n file: UploadableFile,\n onProgress: (loaded: number, total: number) => void,\n) => Promise<{ storageKey: string }>\n\n/**\n * Default browser transport: XMLHttpRequest POST for real byte progress\n * (fetch has no upload progress without streams support everywhere).\n */\nexport const xhrUploadTransport: UploadTransport = (uploadUrl, file, onProgress) =>\n new Promise((resolve, reject) => {\n const xhr = new XMLHttpRequest()\n xhr.open('POST', uploadUrl)\n if (file.type) xhr.setRequestHeader('Content-Type', file.type)\n xhr.upload.onprogress = (event) => {\n if (event.lengthComputable) onProgress(event.loaded, event.total)\n }\n xhr.onerror = () => reject(new Error('Network error while uploading bytes.'))\n xhr.onabort = () => reject(new Error('Upload was aborted.'))\n xhr.onload = () => {\n if (xhr.status < 200 || xhr.status >= 300) {\n reject(new Error(`Upload failed with HTTP ${xhr.status}.`))\n return\n }\n try {\n const payload = JSON.parse(xhr.responseText) as { storageId?: unknown }\n if (typeof payload.storageId === 'string' && payload.storageId.length > 0) {\n resolve({ storageKey: payload.storageId })\n } else {\n reject(new Error('Upload response did not include a storageId.'))\n }\n } catch {\n reject(new Error('Upload response was not valid JSON.'))\n }\n }\n void file.arrayBuffer().then(\n (bytes) => xhr.send(bytes),\n () => reject(new Error('Could not read file bytes.')),\n )\n })\n\nexport interface UploadDeps {\n blobs: BlobStorageAdapter\n assets: AssetRepository\n /**\n * The project's asset collection fields (EC-568): the `source` declarations that map\n * the file's embedded IPTC facts onto `meta` keys. Absent → no embedded metadata is\n * written (a field without `source` is never filled).\n */\n assetFields?: readonly FieldDef[]\n /** Client-direct transport; only used when `blobs.createUploadUrl` exists. */\n transport?: UploadTransport\n /** Injectable dimension detection; defaults to `createImageBitmap`-based. */\n detectDimensions?: (file: UploadableFile) => Promise<ImageDimensions | undefined>\n /**\n * Injectable preview generation (EC-400 thumb + EC-420 lqip). Defaults to a\n * single `createImageBitmap` + canvas decode that stores the thumb via a second\n * `blobs.store()` and inlines the lqip data URI. Returns `{}` (or a partial) for\n * skipped/failed variants — a preview NEVER fails the upload (AD-2). Tests inject\n * a deterministic implementation.\n */\n generateVariants?: (\n file: UploadableFile,\n dimensions: ImageDimensions | undefined,\n ) => Promise<GeneratedVariants>\n}\n\nexport interface UploadRequest {\n projectId: string\n file: UploadableFile\n /**\n * EC-568 — REPLACE the file inside this existing asset instead of creating one: the\n * same id, the new bytes and everything derived from them (storage key, url,\n * dimensions, type, size, thumb, lqip) through this one upload path; `meta` overridden\n * per key where the new file carries a value (else kept); the focal point, the title\n * and every other key kept. The previous blobs are kept (see EC-568 § Done).\n */\n replace?: { assetId: string }\n /** Observe every state transition (drives progress UI). */\n onState?: (state: UploadState) => void\n}\n\nexport type UploadResult =\n | { ok: true; asset: AssetRecord; state: UploadState }\n | { ok: false; failure: UploadFailure; state: UploadState }\n\nasync function defaultDetectDimensions(file: UploadableFile): Promise<ImageDimensions | undefined> {\n if (typeof Blob === 'undefined' || !(file instanceof Blob)) return undefined\n return detectImageDimensions(file as Blob & { type: string })\n}\n\n/**\n * Default preview step (EC-400 thumb + EC-420 lqip): decode the real `File` (which\n * IS a `Blob` in the browser) ONCE and produce both variants — store the thumb on\n * `blobs`, inline the lqip data URI. Guards the `Blob` narrowing so a non-browser\n * `UploadableFile` (or a backend without `store()`) simply yields no thumb — never\n * an error that could fail the upload.\n */\nfunction defaultGenerateVariants(blobs: BlobStorageAdapter) {\n return async (\n file: UploadableFile,\n dimensions: ImageDimensions | undefined,\n ): Promise<GeneratedVariants> => {\n if (typeof Blob === 'undefined' || !(file instanceof Blob)) return {}\n return generateVariants({ blobs }, file, {\n contentType: file.type,\n ...(dimensions ? { width: dimensions.width, height: dimensions.height } : {}),\n })\n }\n}\n\nfunction failureOf(stage: UploadFailure['stage'], error: unknown): UploadFailure {\n return {\n stage,\n message: error instanceof Error ? error.message : String(error),\n retryable: true,\n }\n}\n\n/**\n * Run one upload attempt. Resolves with a discriminated result — it never\n * throws, and a failed transfer leaves no metadata record behind. Pass the\n * previous `state` to retry (the attempt counter carries over).\n */\nexport async function runUpload(\n deps: UploadDeps,\n request: UploadRequest,\n previous?: UploadState,\n): Promise<UploadResult> {\n let state = previous ?? initialUploadState\n const dispatch = (event: UploadEvent): void => {\n state = uploadReducer(state, event)\n request.onState?.(state)\n }\n dispatch(state.phase === 'failed' ? { type: 'retry' } : { type: 'start' })\n\n const { blobs, assets } = deps\n const { projectId, file } = request\n\n // --- prepare + transfer ---------------------------------------------------\n let storageKey: string\n try {\n if (blobs.createUploadUrl && deps.transport) {\n const uploadUrl = await blobs.createUploadUrl()\n dispatch({ type: 'url-ready' })\n try {\n const sent = await deps.transport(uploadUrl, file, (loaded, total) =>\n dispatch({ type: 'progress', loaded, total }),\n )\n storageKey = sent.storageKey\n } catch (error) {\n dispatch({ type: 'failed', ...failureOf('transfer', error) })\n return { ok: false, failure: failureOf('transfer', error), state }\n }\n } else {\n // Backend without client-direct uploads: push bytes through the adapter.\n dispatch({ type: 'url-ready' })\n const bytes = new Uint8Array(await file.arrayBuffer())\n try {\n if (blobs.store) {\n storageKey = await blobs.store(bytes, file.type)\n } else {\n storageKey = `${projectId}/${file.name}`\n await blobs.put(storageKey, bytes, file.type)\n }\n } catch (error) {\n dispatch({ type: 'failed', ...failureOf('transfer', error) })\n return { ok: false, failure: failureOf('transfer', error), state }\n }\n }\n } catch (error) {\n dispatch({ type: 'failed', ...failureOf('prepare', error) })\n return { ok: false, failure: failureOf('prepare', error), state }\n }\n dispatch({ type: 'transferred' })\n\n // --- confirm: metadata record linked to the stored blob --------------------\n try {\n // EC-568: the ONE embedded-metadata reader, for a first upload and a replace alike.\n const embedded = await embeddedAssetMeta(file, deps.assetFields)\n const detect = deps.detectDimensions ?? defaultDetectDimensions\n const dimensions = await detect(file)\n // EC-400 + EC-420: produce the thumb + lqip previews from the bytes already in\n // hand (one decode). A preview failure is swallowed — the asset simply ships\n // without them (AD-2, scope §4); absence = today's behaviour, never an error.\n const generate = deps.generateVariants ?? defaultGenerateVariants(blobs)\n let variants: GeneratedVariants\n try {\n variants = await generate(file, dimensions)\n } catch {\n variants = {}\n }\n const url = (await blobs.resolveUrl?.(storageKey)) ?? blobs.url(storageKey)\n const derived = {\n filename: file.name,\n contentType: file.type || 'application/octet-stream',\n byteSize: file.size,\n storageKey,\n url,\n }\n let write: AssetWriteInput\n if (request.replace) {\n const stored = await assets.findAsset(projectId, request.replace.assetId)\n if (!stored) throw new Error(`No asset “${request.replace.assetId}” is stored to replace the file of.`)\n write = replaceWrite(stored, derived, dimensions, variants, embedded)\n } else {\n write = {\n ...derived,\n ...(dimensions ? { width: dimensions.width, height: dimensions.height } : {}),\n ...(variants.thumb ? { thumb: variants.thumb } : {}),\n ...(variants.lqip ? { lqip: variants.lqip } : {}),\n ...(Object.keys(embedded).length > 0 ? { meta: embedded } : {}),\n }\n }\n const asset = await assets.upsertAsset(projectId, write)\n dispatch({ type: 'confirmed', assetId: asset.id })\n return { ok: true, asset, state }\n } catch (error) {\n dispatch({ type: 'failed', ...failureOf('confirm', error) })\n return { ok: false, failure: failureOf('confirm', error), state }\n }\n}\n\n/**\n * The file's embedded facts as `meta` values (EC-568): `readEmbeddedMetadata` over the\n * bytes, mapped through the asset collection's `source` declarations. A file that cannot\n * be read yields `{}` — metadata never fails an upload.\n */\nasync function embeddedAssetMeta(\n file: UploadableFile,\n fields: readonly FieldDef[] | undefined,\n): Promise<Record<string, unknown>> {\n if (!fields || !fields.some((field) => field.source !== undefined)) return {}\n try {\n return embeddedMetaValues(fields, readEmbeddedMetadata(new Uint8Array(await file.arrayBuffer())))\n } catch {\n return {}\n }\n}\n\n/**\n * The write of a file REPLACE (EC-568, the owner's ruling: \"override the values with\n * available metadata, or keep the existing if no metadata is available\"):\n * - the bytes' columns are SET — and a derived column the new file does not have is\n * CLEARED (`null`), so an old thumb, lqip or dimension never outlives its file;\n * - `meta` is the stored bag (a legacy `alt` column lifted in, EC-587) with each key the\n * new file brings overridden; written only when the file brings something, and then\n * the legacy column is cleared, as every `meta` write does;\n * - the id, the focal point, the title and every other column are absent → kept.\n */\nexport function replaceWrite(\n stored: AssetRecord,\n derived: Pick<AssetRecord, 'filename' | 'contentType' | 'byteSize' | 'storageKey'> & { url: string },\n dimensions: ImageDimensions | undefined,\n variants: GeneratedVariants,\n embedded: Record<string, unknown>,\n): AssetWriteInput {\n const brings = Object.keys(embedded).length > 0\n const base = liftLegacyAlt(stored)?.meta ?? stored.meta ?? {}\n return {\n id: stored.id,\n ...derived,\n width: dimensions?.width ?? null,\n height: dimensions?.height ?? null,\n thumb: variants.thumb ?? null,\n lqip: variants.lqip ?? null,\n ...(brings ? { meta: { ...base, ...embedded }, alt: null } : {}),\n }\n}\n"]}
1
+ {"version":3,"file":"upload.js","sourceRoot":"","sources":["../../../src/lib/assets/upload.ts"],"names":[],"mappings":"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","sourcesContent":["import { embeddedAssetMeta, readEmbeddedMetadata } from '@elytracms/core/cms-core'\nimport type { FieldDef, Locale } from '@elytracms/core/cms-core'\nimport type {\n AssetRecord,\n AssetRepository,\n AssetWriteInput,\n BlobStorageAdapter,\n} from '@elytracms/core/persistence'\nimport { detectImageDimensions } from './image-dimensions'\nimport type { ImageDimensions } from './image-dimensions'\nimport { generateVariants } from './thumbnail'\nimport type { GeneratedVariants } from './thumbnail'\nimport { initialUploadState, uploadReducer } from './upload-state'\nimport type { UploadEvent, UploadFailure, UploadState } from './upload-state'\n\n/**\n * Upload orchestration (EC-151): request upload URL → transfer bytes →\n * confirm by writing the EC-044 metadata record (dimensions, mime, serve URL).\n * Storage is reached *only* through the {@link BlobStorageAdapter}; every\n * effect is injectable so the whole flow unit-tests without a browser or a\n * live Convex deployment.\n */\n\n/** The structural slice of a browser `File` the upload flow needs. */\nexport interface UploadableFile {\n name: string\n type: string\n size: number\n arrayBuffer(): Promise<ArrayBuffer>\n}\n\n/** Sends bytes to an upload URL, reporting progress; returns the assigned storage key. */\nexport type UploadTransport = (\n uploadUrl: string,\n file: UploadableFile,\n onProgress: (loaded: number, total: number) => void,\n) => Promise<{ storageKey: string }>\n\n/**\n * Default browser transport: XMLHttpRequest POST for real byte progress\n * (fetch has no upload progress without streams support everywhere).\n */\nexport const xhrUploadTransport: UploadTransport = (uploadUrl, file, onProgress) =>\n new Promise((resolve, reject) => {\n const xhr = new XMLHttpRequest()\n xhr.open('POST', uploadUrl)\n if (file.type) xhr.setRequestHeader('Content-Type', file.type)\n xhr.upload.onprogress = (event) => {\n if (event.lengthComputable) onProgress(event.loaded, event.total)\n }\n xhr.onerror = () => reject(new Error('Network error while uploading bytes.'))\n xhr.onabort = () => reject(new Error('Upload was aborted.'))\n xhr.onload = () => {\n if (xhr.status < 200 || xhr.status >= 300) {\n reject(new Error(`Upload failed with HTTP ${xhr.status}.`))\n return\n }\n try {\n const payload = JSON.parse(xhr.responseText) as { storageId?: unknown }\n if (typeof payload.storageId === 'string' && payload.storageId.length > 0) {\n resolve({ storageKey: payload.storageId })\n } else {\n reject(new Error('Upload response did not include a storageId.'))\n }\n } catch {\n reject(new Error('Upload response was not valid JSON.'))\n }\n }\n void file.arrayBuffer().then(\n (bytes) => xhr.send(bytes),\n () => reject(new Error('Could not read file bytes.')),\n )\n })\n\nexport interface UploadDeps {\n blobs: BlobStorageAdapter\n assets: AssetRepository\n /**\n * The project's asset collection fields (EC-568): the `source` declarations that map\n * the file's embedded IPTC facts onto `meta` keys. Absent → no embedded metadata is\n * written (a field without `source` is never filled).\n */\n assetFields?: readonly FieldDef[]\n /**\n * The project's locales (EC-632): a `localized: true` asset field's embedded value goes\n * to the locale buckets — each locale its `xml:lang` alternative, the default the\n * `x-default` reading. Absent → every field is written to the shared `meta`.\n */\n locales?: { readonly default: Locale; readonly locales: readonly Locale[] }\n /** Client-direct transport; only used when `blobs.createUploadUrl` exists. */\n transport?: UploadTransport\n /** Injectable dimension detection; defaults to `createImageBitmap`-based. */\n detectDimensions?: (file: UploadableFile) => Promise<ImageDimensions | undefined>\n /**\n * Injectable preview generation (EC-400 thumb + EC-420 lqip). Defaults to a\n * single `createImageBitmap` + canvas decode that stores the thumb via a second\n * `blobs.store()` and inlines the lqip data URI. Returns `{}` (or a partial) for\n * skipped/failed variants — a preview NEVER fails the upload (AD-2). Tests inject\n * a deterministic implementation.\n */\n generateVariants?: (\n file: UploadableFile,\n dimensions: ImageDimensions | undefined,\n ) => Promise<GeneratedVariants>\n}\n\nexport interface UploadRequest {\n projectId: string\n file: UploadableFile\n /**\n * EC-568 — REPLACE the file inside this existing asset instead of creating one: the\n * same id, the new bytes and everything derived from them (storage key, url,\n * dimensions, type, size, thumb, lqip) through this one upload path; `meta` overridden\n * per key where the new file carries a value (else kept); the focal point, the title\n * and every other key kept. The previous blobs are kept (see EC-568 § Done).\n */\n replace?: { assetId: string }\n /** Observe every state transition (drives progress UI). */\n onState?: (state: UploadState) => void\n}\n\nexport type UploadResult =\n | { ok: true; asset: AssetRecord; state: UploadState }\n | { ok: false; failure: UploadFailure; state: UploadState }\n\nasync function defaultDetectDimensions(file: UploadableFile): Promise<ImageDimensions | undefined> {\n if (typeof Blob === 'undefined' || !(file instanceof Blob)) return undefined\n return detectImageDimensions(file as Blob & { type: string })\n}\n\n/**\n * Default preview step (EC-400 thumb + EC-420 lqip): decode the real `File` (which\n * IS a `Blob` in the browser) ONCE and produce both variants — store the thumb on\n * `blobs`, inline the lqip data URI. Guards the `Blob` narrowing so a non-browser\n * `UploadableFile` (or a backend without `store()`) simply yields no thumb — never\n * an error that could fail the upload.\n */\nfunction defaultGenerateVariants(blobs: BlobStorageAdapter) {\n return async (\n file: UploadableFile,\n dimensions: ImageDimensions | undefined,\n ): Promise<GeneratedVariants> => {\n if (typeof Blob === 'undefined' || !(file instanceof Blob)) return {}\n return generateVariants({ blobs }, file, {\n contentType: file.type,\n ...(dimensions ? { width: dimensions.width, height: dimensions.height } : {}),\n })\n }\n}\n\nfunction failureOf(stage: UploadFailure['stage'], error: unknown): UploadFailure {\n return {\n stage,\n message: error instanceof Error ? error.message : String(error),\n retryable: true,\n }\n}\n\n/**\n * Run one upload attempt. Resolves with a discriminated result — it never\n * throws, and a failed transfer leaves no metadata record behind. Pass the\n * previous `state` to retry (the attempt counter carries over).\n */\nexport async function runUpload(\n deps: UploadDeps,\n request: UploadRequest,\n previous?: UploadState,\n): Promise<UploadResult> {\n let state = previous ?? initialUploadState\n const dispatch = (event: UploadEvent): void => {\n state = uploadReducer(state, event)\n request.onState?.(state)\n }\n dispatch(state.phase === 'failed' ? { type: 'retry' } : { type: 'start' })\n\n const { blobs, assets } = deps\n const { projectId, file } = request\n\n // --- prepare + transfer ---------------------------------------------------\n let storageKey: string\n try {\n if (blobs.createUploadUrl && deps.transport) {\n const uploadUrl = await blobs.createUploadUrl()\n dispatch({ type: 'url-ready' })\n try {\n const sent = await deps.transport(uploadUrl, file, (loaded, total) =>\n dispatch({ type: 'progress', loaded, total }),\n )\n storageKey = sent.storageKey\n } catch (error) {\n dispatch({ type: 'failed', ...failureOf('transfer', error) })\n return { ok: false, failure: failureOf('transfer', error), state }\n }\n } else {\n // Backend without client-direct uploads: push bytes through the adapter.\n dispatch({ type: 'url-ready' })\n const bytes = new Uint8Array(await file.arrayBuffer())\n try {\n if (blobs.store) {\n storageKey = await blobs.store(bytes, file.type)\n } else {\n storageKey = `${projectId}/${file.name}`\n await blobs.put(storageKey, bytes, file.type)\n }\n } catch (error) {\n dispatch({ type: 'failed', ...failureOf('transfer', error) })\n return { ok: false, failure: failureOf('transfer', error), state }\n }\n }\n } catch (error) {\n dispatch({ type: 'failed', ...failureOf('prepare', error) })\n return { ok: false, failure: failureOf('prepare', error), state }\n }\n dispatch({ type: 'transferred' })\n\n // --- confirm: metadata record linked to the stored blob --------------------\n try {\n // EC-568: the ONE embedded-metadata reader, for a first upload and a replace alike.\n const embedded = await embeddedMetaOf(file, deps.assetFields, deps.locales)\n const detect = deps.detectDimensions ?? defaultDetectDimensions\n const dimensions = await detect(file)\n // EC-400 + EC-420: produce the thumb + lqip previews from the bytes already in\n // hand (one decode). A preview failure is swallowed — the asset simply ships\n // without them (AD-2, scope §4); absence = today's behaviour, never an error.\n const generate = deps.generateVariants ?? defaultGenerateVariants(blobs)\n let variants: GeneratedVariants\n try {\n variants = await generate(file, dimensions)\n } catch {\n variants = {}\n }\n const url = (await blobs.resolveUrl?.(storageKey)) ?? blobs.url(storageKey)\n const derived = {\n filename: file.name,\n contentType: file.type || 'application/octet-stream',\n byteSize: file.size,\n storageKey,\n url,\n }\n let write: AssetWriteInput\n if (request.replace) {\n // A replace edits an asset, never creates one — decided by the write itself\n // (`mustExist`), against the row as it is when the write lands: a delete by another\n // session in the meantime makes the write fail here, visibly, and creates nothing.\n write = replaceWrite(request.replace.assetId, derived, dimensions, variants, embedded)\n } else {\n write = {\n ...derived,\n ...(dimensions ? { width: dimensions.width, height: dimensions.height } : {}),\n ...(variants.thumb ? { thumb: variants.thumb } : {}),\n ...(variants.lqip ? { lqip: variants.lqip } : {}),\n ...(Object.keys(embedded.meta).length > 0 ? { meta: embedded.meta } : {}),\n ...(Object.keys(embedded.localizedMeta).length > 0 ? { localizedMeta: embedded.localizedMeta } : {}),\n }\n }\n const asset = await assets.upsertAsset(projectId, write)\n dispatch({ type: 'confirmed', assetId: asset.id })\n return { ok: true, asset, state }\n } catch (error) {\n dispatch({ type: 'failed', ...failureOf('confirm', error) })\n return { ok: false, failure: failureOf('confirm', error), state }\n }\n}\n\n/**\n * The file's embedded facts as `meta` values (EC-568): `readEmbeddedMetadata` over the\n * bytes, mapped through the asset collection's `source` declarations. A file that cannot\n * be read yields `{}` — metadata never fails an upload.\n */\nasync function embeddedMetaOf(\n file: UploadableFile,\n fields: readonly FieldDef[] | undefined,\n locales: UploadDeps['locales'],\n): Promise<EmbeddedAssetWrite> {\n const none: EmbeddedAssetWrite = { meta: {}, localizedMeta: {} }\n if (!fields || !fields.some((field) => field.source !== undefined)) return none\n try {\n return embeddedAssetMeta(fields, readEmbeddedMetadata(new Uint8Array(await file.arrayBuffer())), locales)\n } catch {\n return none\n }\n}\n\n/** What a file's embedded facts write (EC-568 → EC-632): shared `meta` keys, and per-locale buckets of the localized fields. */\nexport interface EmbeddedAssetWrite {\n meta: Record<string, unknown>\n localizedMeta: Record<string, Record<string, unknown>>\n}\n\n/**\n * The write of a file REPLACE (EC-568, the owner's ruling: \"override the values with\n * available metadata, or keep the existing if no metadata is available\"):\n * - the bytes' columns are SET — and a derived column the new file does not have is\n * CLEARED (`null`), so an old thumb, lqip or dimension never outlives its file;\n * - each `meta` key the new file brings is a per-key `metaPatch` (EC-634), applied to the\n * bag as STORED inside the write — never a copy read before the upload, so an editor's\n * metadata save that lands while the bytes travel is kept. Sent only when the file\n * brings something; the write rule then lifts a legacy `alt` column into the bag and\n * clears it (EC-587), as for every patch;\n * - the id, the focal point, the title and every other column are absent → kept;\n * - `mustExist`: the write only updates the stored asset — gone (deleted meanwhile), it is\n * refused inside the write and nothing is created.\n */\nexport function replaceWrite(\n assetId: string,\n derived: Pick<AssetRecord, 'filename' | 'contentType' | 'byteSize' | 'storageKey'> & { url: string },\n dimensions: ImageDimensions | undefined,\n variants: GeneratedVariants,\n embedded: EmbeddedAssetWrite,\n): AssetWriteInput {\n const brings = Object.keys(embedded.meta).length > 0\n const bringsLocales = Object.keys(embedded.localizedMeta).length > 0\n return {\n id: assetId,\n // EC-634 follow-on: an update of a stored asset, never a create (a concurrent delete wins).\n mustExist: true,\n ...derived,\n width: dimensions?.width ?? null,\n height: dimensions?.height ?? null,\n thumb: variants.thumb ?? null,\n lqip: variants.lqip ?? null,\n ...(brings ? { metaPatch: embedded.meta } : {}),\n // EC-632: a localized field's embedded values patch their locale buckets per key.\n ...(bringsLocales ? { localizedMetaPatch: embedded.localizedMeta } : {}),\n }\n}\n"]}
@@ -1,5 +1,7 @@
1
1
  import type { CmsDocument } from '@elytracms/core/cms-core';
2
+ import type { OperationError } from '@elytracms/core/operations';
2
3
  import type { OperationsClient } from '../operations/index.js';
4
+ import type { ReadState } from '../query/index.js';
3
5
  /**
4
6
  * The PUBLISHED-perspective documents of a collection (EC-205, "live = published")
5
7
  * — the pinned published snapshot of each currently-published document, keyed by
@@ -13,12 +15,13 @@ import type { OperationsClient } from '../operations/index.js';
13
15
  * seam — invalidated only when a command that moves a live pin succeeds (NOT on
14
16
  * every autosave upsert — a draft edit doesn't change what's published). Cost: ONE
15
17
  * bulk `documents.listPublished` read (EC-307).
18
+ *
19
+ * EC-628: the pool is a {@link ReadState} — `ready` carries the pinned published snapshot
20
+ * per published document id; a failed read is `error` with the localized reason, and
21
+ * every surface reading the pool (the list's URL column, the editor header's live link)
22
+ * shows it. A failure is never an empty pool: an empty pool says "nothing is published".
16
23
  */
17
- export interface PublishedDocuments {
18
- /** Pinned published snapshot per published document id. Empty while loading. */
19
- byId: ReadonlyMap<string, CmsDocument>;
20
- status: 'loading' | 'ready';
21
- }
24
+ export type PublishedDocuments = ReadState<ReadonlyMap<string, CmsDocument>>;
22
25
  /**
23
26
  * Commands that change which version is pinned live (and thus the pool).
24
27
  *
@@ -30,12 +33,27 @@ export interface PublishedDocuments {
30
33
  * move or the delete of a pinned row changes published output.
31
34
  */
32
35
  export declare const PUBLISHED_RELOAD_OPS: readonly ["documents.publish", "documents.unpublish", "documents.pinVersion", "documents.delete", "documents.clear"];
36
+ /**
37
+ * A failed `documents.listPublished` read (EC-628), carrying the operation's typed error so
38
+ * the surface localizes it when it renders — the query cache outlives a UI-language switch.
39
+ */
40
+ export declare class PublishedReadError extends Error {
41
+ readonly error: OperationError;
42
+ constructor(error: OperationError);
43
+ }
33
44
  /**
34
45
  * Build the published pool of a collection (pure over the operations client): the
35
46
  * pinned published snapshot of every currently-published document, keyed by id.
36
47
  * EC-307: one bulk `documents.listPublished` read — the backend resolves each
37
48
  * `publishedVersion` pointer to its recorded snapshot — instead of a `list` plus a
38
- * per-document `listVersions` fan-out. Read failures degrade to an empty pool.
49
+ * per-document `listVersions` fan-out. A failed read REJECTS with
50
+ * {@link PublishedReadError} (EC-628) — it used to resolve to an empty pool.
39
51
  */
40
52
  export declare function loadPublishedDocuments(operations: OperationsClient, projectId: string, collectionId: string): Promise<Map<string, CmsDocument>>;
53
+ /** The {@link PublishedDocuments} state of the pool's query (pure — the hook's mapping). */
54
+ export declare function publishedDocumentsState(query: {
55
+ readonly isError: boolean;
56
+ readonly error: unknown;
57
+ readonly data: ReadonlyMap<string, CmsDocument> | undefined;
58
+ }): PublishedDocuments;
41
59
  export declare function usePublishedDocuments(projectId: string | undefined, collectionId: string): PublishedDocuments;
@@ -1,4 +1,6 @@
1
- import { useOperationQuery } from '../query/index.js';
1
+ import { readStateOf, useOperationQuery } from '../query/index.js';
2
+ import { translate } from '../i18n/index.js';
3
+ import { operationErrorMessage } from '../persona/labels.js';
2
4
  /**
3
5
  * Commands that change which version is pinned live (and thus the pool).
4
6
  *
@@ -16,23 +18,54 @@ export const PUBLISHED_RELOAD_OPS = [
16
18
  'documents.delete',
17
19
  'documents.clear',
18
20
  ];
19
- const EMPTY_POOL = new Map();
21
+ /** No project (a fixture session without a backend): nothing is published, by construction. */
22
+ const NO_PROJECT_POOL = { status: 'ready', value: new Map() };
23
+ /**
24
+ * A failed `documents.listPublished` read (EC-628), carrying the operation's typed error so
25
+ * the surface localizes it when it renders — the query cache outlives a UI-language switch.
26
+ */
27
+ export class PublishedReadError extends Error {
28
+ error;
29
+ constructor(error) {
30
+ super(error.message);
31
+ this.error = error;
32
+ this.name = 'PublishedReadError';
33
+ }
34
+ }
20
35
  /**
21
36
  * Build the published pool of a collection (pure over the operations client): the
22
37
  * pinned published snapshot of every currently-published document, keyed by id.
23
38
  * EC-307: one bulk `documents.listPublished` read — the backend resolves each
24
39
  * `publishedVersion` pointer to its recorded snapshot — instead of a `list` plus a
25
- * per-document `listVersions` fan-out. Read failures degrade to an empty pool.
40
+ * per-document `listVersions` fan-out. A failed read REJECTS with
41
+ * {@link PublishedReadError} (EC-628) — it used to resolve to an empty pool.
26
42
  */
27
43
  export async function loadPublishedDocuments(operations, projectId, collectionId) {
28
44
  const result = await operations.documents.listPublished({ projectId, collection: collectionId });
29
45
  if (!result.ok)
30
- return new Map();
46
+ throw new PublishedReadError(result.error);
31
47
  const byId = new Map();
32
48
  for (const snapshot of result.value)
33
49
  byId.set(snapshot.id, snapshot);
34
50
  return byId;
35
51
  }
52
+ /**
53
+ * The pool's sentence when its read failed (EC-628): one message, shown by every surface
54
+ * that reads the pool — the operation's reason in the UI language when it carries a
55
+ * catalog reference (EC-494), its English otherwise.
56
+ */
57
+ function publishedReadFailure(error) {
58
+ const reason = error instanceof PublishedReadError
59
+ ? operationErrorMessage(error.error)
60
+ : error instanceof Error
61
+ ? error.message
62
+ : String(error);
63
+ return translate('cms.editor.publishedReadFailed', { reason });
64
+ }
65
+ /** The {@link PublishedDocuments} state of the pool's query (pure — the hook's mapping). */
66
+ export function publishedDocumentsState(query) {
67
+ return readStateOf(query, publishedReadFailure);
68
+ }
36
69
  export function usePublishedDocuments(projectId, collectionId) {
37
70
  const query = useOperationQuery({
38
71
  // Playground/fixture mode (no backend) is `enabled: false` → never queried.
@@ -42,7 +75,7 @@ export function usePublishedDocuments(projectId, collectionId) {
42
75
  enabled: projectId !== undefined,
43
76
  });
44
77
  if (projectId === undefined)
45
- return { byId: EMPTY_POOL, status: 'ready' };
46
- return { byId: query.data ?? EMPTY_POOL, status: query.isPending ? 'loading' : 'ready' };
78
+ return NO_PROJECT_POOL;
79
+ return publishedDocumentsState(query);
47
80
  }
48
81
  //# sourceMappingURL=published-documents.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"published-documents.js","sourceRoot":"","sources":["../../../src/lib/cms/published-documents.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAA;AAsB5C;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,mBAAmB;IACnB,qBAAqB;IACrB,sBAAsB;IACtB,kBAAkB;IAClB,iBAAiB;CACT,CAAA;AAEV,MAAM,UAAU,GAAqC,IAAI,GAAG,EAAE,CAAA;AAE9D;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,UAA4B,EAC5B,SAAiB,EACjB,YAAoB;IAEpB,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,SAAS,CAAC,aAAa,CAAC,EAAE,SAAS,EAAE,UAAU,EAAE,YAAY,EAAE,CAAC,CAAA;IAChG,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,OAAO,IAAI,GAAG,EAAE,CAAA;IAChC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAuB,CAAA;IAC3C,KAAK,MAAM,QAAQ,IAAI,MAAM,CAAC,KAAK;QAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAA;IACpE,OAAO,IAAI,CAAA;AACb,CAAC;AAED,MAAM,UAAU,qBAAqB,CACnC,SAA6B,EAC7B,YAAoB;IAEpB,MAAM,KAAK,GAAG,iBAAiB,CAA2B;QACxD,4EAA4E;QAC5E,QAAQ,EAAE,CAAC,oBAAoB,EAAE,SAAS,IAAI,GAAG,EAAE,YAAY,CAAC;QAChE,MAAM,EAAE,CAAC,UAAU,EAAE,EAAE,CAAC,sBAAsB,CAAC,UAAU,EAAE,SAAmB,EAAE,YAAY,CAAC;QAC7F,YAAY,EAAE,oBAAoB;QAClC,OAAO,EAAE,SAAS,KAAK,SAAS;KACjC,CAAC,CAAA;IACF,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,CAAA;IACzE,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,EAAE,CAAA;AAC1F,CAAC","sourcesContent":["import type { CmsDocument } from '@elytracms/core/cms-core'\nimport type { OperationsClient } from '../operations'\nimport { useOperationQuery } from '../query'\n\n/**\n * The PUBLISHED-perspective documents of a collection (EC-205, \"live = published\")\n * — the pinned published snapshot of each currently-published document, keyed by\n * id. Live surfaces (a document's live URL + \"View live\", EC-216) MUST derive\n * from this, never from the draft rows the studio edits: an unpublished document,\n * or an unpublished `slug`/`parent` edit, must never leak into a \"live\" URL. The\n * pool is exactly the set the live site would serve, so composing a nested page's\n * URL from these ancestors yields the URL the live site actually resolves.\n *\n * EC-308/AD-17: loaded + kept fresh through the one `useOperationQuery` cache\n * seam — invalidated only when a command that moves a live pin succeeds (NOT on\n * every autosave upsert — a draft edit doesn't change what's published). Cost: ONE\n * bulk `documents.listPublished` read (EC-307).\n */\nexport interface PublishedDocuments {\n /** Pinned published snapshot per published document id. Empty while loading. */\n byId: ReadonlyMap<string, CmsDocument>\n status: 'loading' | 'ready'\n}\n\n/**\n * Commands that change which version is pinned live (and thus the pool).\n *\n * EC-579 (RESIDUE-2c): `documents.pinVersion` moves the pin too — it is how a\n * changeset revert restores a prior live version — and was missing, so a revert left\n * the live URLs on the old pool until a reload. `documents.restore` was here and\n * moves nothing: it writes the DRAFT from a recorded version (`documents.replace`),\n * never the pin. The same answer the Convex trigger gives server-side: only a pin\n * move or the delete of a pinned row changes published output.\n */\nexport const PUBLISHED_RELOAD_OPS = [\n 'documents.publish',\n 'documents.unpublish',\n 'documents.pinVersion',\n 'documents.delete',\n 'documents.clear',\n] as const\n\nconst EMPTY_POOL: ReadonlyMap<string, CmsDocument> = new Map()\n\n/**\n * Build the published pool of a collection (pure over the operations client): the\n * pinned published snapshot of every currently-published document, keyed by id.\n * EC-307: one bulk `documents.listPublished` read — the backend resolves each\n * `publishedVersion` pointer to its recorded snapshot — instead of a `list` plus a\n * per-document `listVersions` fan-out. Read failures degrade to an empty pool.\n */\nexport async function loadPublishedDocuments(\n operations: OperationsClient,\n projectId: string,\n collectionId: string,\n): Promise<Map<string, CmsDocument>> {\n const result = await operations.documents.listPublished({ projectId, collection: collectionId })\n if (!result.ok) return new Map()\n const byId = new Map<string, CmsDocument>()\n for (const snapshot of result.value) byId.set(snapshot.id, snapshot)\n return byId\n}\n\nexport function usePublishedDocuments(\n projectId: string | undefined,\n collectionId: string,\n): PublishedDocuments {\n const query = useOperationQuery<Map<string, CmsDocument>>({\n // Playground/fixture mode (no backend) is `enabled: false` → never queried.\n queryKey: ['publishedDocuments', projectId ?? '∅', collectionId],\n loader: (operations) => loadPublishedDocuments(operations, projectId as string, collectionId),\n invalidateOn: PUBLISHED_RELOAD_OPS,\n enabled: projectId !== undefined,\n })\n if (projectId === undefined) return { byId: EMPTY_POOL, status: 'ready' }\n return { byId: query.data ?? EMPTY_POOL, status: query.isPending ? 'loading' : 'ready' }\n}\n"]}
1
+ {"version":3,"file":"published-documents.js","sourceRoot":"","sources":["../../../src/lib/cms/published-documents.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAA;AAEzD,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AACnC,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAA;AAuBzD;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,mBAAmB;IACnB,qBAAqB;IACrB,sBAAsB;IACtB,kBAAkB;IAClB,iBAAiB;CACT,CAAA;AAEV,+FAA+F;AAC/F,MAAM,eAAe,GAAuB,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,EAAE,CAAA;AAEjF;;;GAGG;AACH,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IACtB;IAArB,YAAqB,KAAqB;QACxC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;QADD,UAAK,GAAL,KAAK,CAAgB;QAExC,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAA;IAClC,CAAC;CACF;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,UAA4B,EAC5B,SAAiB,EACjB,YAAoB;IAEpB,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,SAAS,CAAC,aAAa,CAAC,EAAE,SAAS,EAAE,UAAU,EAAE,YAAY,EAAE,CAAC,CAAA;IAChG,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,MAAM,IAAI,kBAAkB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IAC1D,MAAM,IAAI,GAAG,IAAI,GAAG,EAAuB,CAAA;IAC3C,KAAK,MAAM,QAAQ,IAAI,MAAM,CAAC,KAAK;QAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAA;IACpE,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;GAIG;AACH,SAAS,oBAAoB,CAAC,KAAc;IAC1C,MAAM,MAAM,GACV,KAAK,YAAY,kBAAkB;QACjC,CAAC,CAAC,qBAAqB,CAAC,KAAK,CAAC,KAAK,CAAC;QACpC,CAAC,CAAC,KAAK,YAAY,KAAK;YACtB,CAAC,CAAC,KAAK,CAAC,OAAO;YACf,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IACrB,OAAO,SAAS,CAAC,gCAAgC,EAAE,EAAE,MAAM,EAAE,CAAC,CAAA;AAChE,CAAC;AAED,4FAA4F;AAC5F,MAAM,UAAU,uBAAuB,CAAC,KAIvC;IACC,OAAO,WAAW,CAAC,KAAK,EAAE,oBAAoB,CAAC,CAAA;AACjD,CAAC;AAED,MAAM,UAAU,qBAAqB,CACnC,SAA6B,EAC7B,YAAoB;IAEpB,MAAM,KAAK,GAAG,iBAAiB,CAA2B;QACxD,4EAA4E;QAC5E,QAAQ,EAAE,CAAC,oBAAoB,EAAE,SAAS,IAAI,GAAG,EAAE,YAAY,CAAC;QAChE,MAAM,EAAE,CAAC,UAAU,EAAE,EAAE,CAAC,sBAAsB,CAAC,UAAU,EAAE,SAAmB,EAAE,YAAY,CAAC;QAC7F,YAAY,EAAE,oBAAoB;QAClC,OAAO,EAAE,SAAS,KAAK,SAAS;KACjC,CAAC,CAAA;IACF,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,eAAe,CAAA;IACnD,OAAO,uBAAuB,CAAC,KAAK,CAAC,CAAA;AACvC,CAAC","sourcesContent":["import type { CmsDocument } from '@elytracms/core/cms-core'\nimport type { OperationError } from '@elytracms/core/operations'\nimport type { OperationsClient } from '../operations'\nimport { readStateOf, useOperationQuery } from '../query'\nimport type { ReadState } from '../query'\nimport { translate } from '../i18n'\nimport { operationErrorMessage } from '../persona/labels'\n\n/**\n * The PUBLISHED-perspective documents of a collection (EC-205, \"live = published\")\n * — the pinned published snapshot of each currently-published document, keyed by\n * id. Live surfaces (a document's live URL + \"View live\", EC-216) MUST derive\n * from this, never from the draft rows the studio edits: an unpublished document,\n * or an unpublished `slug`/`parent` edit, must never leak into a \"live\" URL. The\n * pool is exactly the set the live site would serve, so composing a nested page's\n * URL from these ancestors yields the URL the live site actually resolves.\n *\n * EC-308/AD-17: loaded + kept fresh through the one `useOperationQuery` cache\n * seam — invalidated only when a command that moves a live pin succeeds (NOT on\n * every autosave upsert — a draft edit doesn't change what's published). Cost: ONE\n * bulk `documents.listPublished` read (EC-307).\n *\n * EC-628: the pool is a {@link ReadState} — `ready` carries the pinned published snapshot\n * per published document id; a failed read is `error` with the localized reason, and\n * every surface reading the pool (the list's URL column, the editor header's live link)\n * shows it. A failure is never an empty pool: an empty pool says \"nothing is published\".\n */\nexport type PublishedDocuments = ReadState<ReadonlyMap<string, CmsDocument>>\n\n/**\n * Commands that change which version is pinned live (and thus the pool).\n *\n * EC-579 (RESIDUE-2c): `documents.pinVersion` moves the pin too — it is how a\n * changeset revert restores a prior live version — and was missing, so a revert left\n * the live URLs on the old pool until a reload. `documents.restore` was here and\n * moves nothing: it writes the DRAFT from a recorded version (`documents.replace`),\n * never the pin. The same answer the Convex trigger gives server-side: only a pin\n * move or the delete of a pinned row changes published output.\n */\nexport const PUBLISHED_RELOAD_OPS = [\n 'documents.publish',\n 'documents.unpublish',\n 'documents.pinVersion',\n 'documents.delete',\n 'documents.clear',\n] as const\n\n/** No project (a fixture session without a backend): nothing is published, by construction. */\nconst NO_PROJECT_POOL: PublishedDocuments = { status: 'ready', value: new Map() }\n\n/**\n * A failed `documents.listPublished` read (EC-628), carrying the operation's typed error so\n * the surface localizes it when it renders — the query cache outlives a UI-language switch.\n */\nexport class PublishedReadError extends Error {\n constructor(readonly error: OperationError) {\n super(error.message)\n this.name = 'PublishedReadError'\n }\n}\n\n/**\n * Build the published pool of a collection (pure over the operations client): the\n * pinned published snapshot of every currently-published document, keyed by id.\n * EC-307: one bulk `documents.listPublished` read — the backend resolves each\n * `publishedVersion` pointer to its recorded snapshot — instead of a `list` plus a\n * per-document `listVersions` fan-out. A failed read REJECTS with\n * {@link PublishedReadError} (EC-628) — it used to resolve to an empty pool.\n */\nexport async function loadPublishedDocuments(\n operations: OperationsClient,\n projectId: string,\n collectionId: string,\n): Promise<Map<string, CmsDocument>> {\n const result = await operations.documents.listPublished({ projectId, collection: collectionId })\n if (!result.ok) throw new PublishedReadError(result.error)\n const byId = new Map<string, CmsDocument>()\n for (const snapshot of result.value) byId.set(snapshot.id, snapshot)\n return byId\n}\n\n/**\n * The pool's sentence when its read failed (EC-628): one message, shown by every surface\n * that reads the pool — the operation's reason in the UI language when it carries a\n * catalog reference (EC-494), its English otherwise.\n */\nfunction publishedReadFailure(error: unknown): string {\n const reason =\n error instanceof PublishedReadError\n ? operationErrorMessage(error.error)\n : error instanceof Error\n ? error.message\n : String(error)\n return translate('cms.editor.publishedReadFailed', { reason })\n}\n\n/** The {@link PublishedDocuments} state of the pool's query (pure — the hook's mapping). */\nexport function publishedDocumentsState(query: {\n readonly isError: boolean\n readonly error: unknown\n readonly data: ReadonlyMap<string, CmsDocument> | undefined\n}): PublishedDocuments {\n return readStateOf(query, publishedReadFailure)\n}\n\nexport function usePublishedDocuments(\n projectId: string | undefined,\n collectionId: string,\n): PublishedDocuments {\n const query = useOperationQuery<Map<string, CmsDocument>>({\n // Playground/fixture mode (no backend) is `enabled: false` → never queried.\n queryKey: ['publishedDocuments', projectId ?? '∅', collectionId],\n loader: (operations) => loadPublishedDocuments(operations, projectId as string, collectionId),\n invalidateOn: PUBLISHED_RELOAD_OPS,\n enabled: projectId !== undefined,\n })\n if (projectId === undefined) return NO_PROJECT_POOL\n return publishedDocumentsState(query)\n}\n"]}
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
- import type { CmsDocument, CollectionDef, FieldDef, Locale, LocaleConfig, LocalizedString } from '@elytracms/core/cms-core';
2
+ import { configViewSchema, filterClauseSchema, filterScalarSchema, isPseudoColumn, PUBLISHED_AT_FIELD, STATE_FIELD, UPDATED_AT_FIELD, URL_FIELD, viewSortSchema } from '@elytracms/core/cms-core';
3
+ import type { CmsDocument, CollectionDef, ConfigView, FieldDef, FilterClause, Locale, LocaleConfig, LocalizedString, ViewSort } from '@elytracms/core/cms-core';
3
4
  import type { FilterOperator, FilterScalar } from '@elytracms/core/content';
4
5
  /**
5
6
  * The pure table-view model for the CMS collection table (EC-169).
@@ -17,90 +18,36 @@ import type { FilterOperator, FilterScalar } from '@elytracms/core/content';
17
18
  * "show me drafts → bulk publish" working set and still speaks only the
18
19
  * closed vocabulary (`eq`/`in`).
19
20
  */
20
- export declare const filterScalarSchema: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
21
- export declare const filterClauseSchema: z.ZodObject<{
22
- field: z.ZodString;
23
- op: z.ZodEnum<{
24
- endsWith: "endsWith";
25
- startsWith: "startsWith";
26
- in: "in";
27
- empty: "empty";
28
- contains: "contains";
29
- eq: "eq";
30
- gt: "gt";
31
- gte: "gte";
32
- lt: "lt";
33
- lte: "lte";
34
- }>;
35
- value: z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>]>;
36
- }, z.core.$strip>;
37
- export type FilterClause = z.infer<typeof filterClauseSchema>;
38
- export declare const viewSortSchema: z.ZodObject<{
39
- field: z.ZodString;
40
- dir: z.ZodEnum<{
41
- desc: "desc";
42
- asc: "asc";
43
- }>;
44
- }, z.core.$strip>;
45
- export type ViewSort = z.infer<typeof viewSortSchema>;
21
+ export { configViewSchema, filterClauseSchema, filterScalarSchema, isPseudoColumn, PUBLISHED_AT_FIELD, STATE_FIELD, UPDATED_AT_FIELD, URL_FIELD, viewSortSchema, };
22
+ export type { ConfigView, FilterClause, ViewSort };
46
23
  export declare const tableViewStateSchema: z.ZodObject<{
47
24
  filters: z.ZodArray<z.ZodObject<{
48
25
  field: z.ZodString;
49
26
  op: z.ZodEnum<{
27
+ in: "in";
50
28
  endsWith: "endsWith";
51
29
  startsWith: "startsWith";
52
- in: "in";
53
- empty: "empty";
54
- contains: "contains";
55
30
  eq: "eq";
56
31
  gt: "gt";
57
32
  gte: "gte";
58
33
  lt: "lt";
59
34
  lte: "lte";
35
+ contains: "contains";
36
+ empty: "empty";
60
37
  }>;
61
38
  value: z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>]>;
62
39
  }, z.core.$strip>>;
63
40
  sort: z.ZodNullable<z.ZodObject<{
64
41
  field: z.ZodString;
65
42
  dir: z.ZodEnum<{
66
- desc: "desc";
67
43
  asc: "asc";
44
+ desc: "desc";
68
45
  }>;
69
46
  }, z.core.$strip>>;
70
47
  columns: z.ZodArray<z.ZodString>;
71
48
  hidden: z.ZodDefault<z.ZodArray<z.ZodString>>;
72
49
  }, z.core.$strip>;
73
50
  export type TableViewState = z.infer<typeof tableViewStateSchema>;
74
- /**
75
- * Sentinel field key for the publication state. `$`-prefixed so it can never
76
- * collide with a schema field name (`fieldDefSchema` names are identifiers).
77
- */
78
- export declare const STATE_FIELD = "$state";
79
- /**
80
- * Reserved pseudo-COLUMN keys — the columns-axis siblings of the filter-axis
81
- * {@link STATE_FIELD}. `$`-prefixed so they can never collide with a schema field
82
- * name; they participate in a view's `columns` list AND the Display picker exactly
83
- * like a schema field (orderable / hideable / includable), but they are NOT drawn
84
- * from `collection.fields` — the column UNIVERSE is `[...schemaFields, ...pseudoColumns]`.
85
- *
86
- * - `$state` — publication status. A reserved `$`-key, but NO LONGER a reorderable
87
- * column: EC-448 promoted it to a FIXED structural leading column (checkbox →
88
- * status-dot → title), so it is NOT emitted by {@link pseudoColumns} and never
89
- * participates in the reorderable columns/hidden/order model. It stays a reserved
90
- * key (filter + sort) and {@link isPseudoColumn} still recognizes it, so an authored
91
- * view listing `$state` is a harmless no-op (see {@link resolveConfigColumns}).
92
- * - `$url` — the derived delivery URL (routable collections only; NON-sortable).
93
- * - `$updatedAt` — the record's last-write timestamp.
94
- * - `$publishedAt` — the record's FIRST-publish instant (EC-447; opt-in, HIDDEN by
95
- * default). Now materialized end-to-end (`cmsDocuments.publishedAt` → source
96
- * `timestamps.publishedAt`) and emitted by {@link pseudoColumns} when the source
97
- * both materializes timestamps and has a publish lifecycle.
98
- */
99
- export declare const URL_FIELD = "$url";
100
- export declare const PUBLISHED_AT_FIELD = "$publishedAt";
101
- export declare const UPDATED_AT_FIELD = "$updatedAt";
102
- /** Is `name` a reserved pseudo-column (a `$`-key, not a schema field)? */
103
- export declare function isPseudoColumn(name: string): boolean;
104
51
  /**
105
52
  * Capability signals that make a pseudo-column applicable to a collection/source
106
53
  * (EC-445). Supplied by the caller from the portable source — the model stays pure.
@@ -269,49 +216,14 @@ export declare function defaultViewState(collection: CollectionDef, pseudo?: rea
269
216
  * same shape {@link defaultViewState} produces, so {@link resolveColumns} renders
270
217
  * precisely the authored list, visible, in that order. An omitted (or empty) list
271
218
  * inherits the collection's default lens (all scalars visible, composites hidden).
272
- * Unknown names in the authored list (a field the schema no longer has) are dropped.
219
+ * An authored name outside the universe — no longer a field, or a reserved column that
220
+ * does not apply to this source — is skipped here so the list still renders; a name that
221
+ * is neither a top-level field nor a reserved `$` key never reaches a resolved config
222
+ * (EC-622: the config gate refuses it, `collectionViewFindings`).
273
223
  */
274
224
  export declare function resolveConfigColumns(collection: CollectionDef, visible: readonly string[] | undefined, pseudo?: readonly string[]): Pick<TableViewState, 'columns' | 'hidden'>;
275
225
  /** The reserved `views[]` id whose entry OVERRIDES the synthesized baseline view (EC-444). */
276
226
  export declare const DEFAULT_VIEW_OVERRIDE_ID = "default";
277
- /**
278
- * A config-declared view (EC-246b/EC-444) — the flat authoring shape a collection
279
- * declares in `cms/` config: `{ name, initial?, filters?, sort?, columns? }`.
280
- * Filters/sort/columns sit at the top level (no nested `state` wrapper). The
281
- * studio validates + interprets these; `cms-core` stores them opaquely (it
282
- * can't reference the filter vocabulary without a layering cycle).
283
- */
284
- export declare const configViewSchema: z.ZodObject<{
285
- id: z.ZodOptional<z.ZodString>;
286
- name: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
287
- initial: z.ZodOptional<z.ZodBoolean>;
288
- default: z.ZodOptional<z.ZodBoolean>;
289
- filters: z.ZodOptional<z.ZodArray<z.ZodObject<{
290
- field: z.ZodString;
291
- op: z.ZodEnum<{
292
- endsWith: "endsWith";
293
- startsWith: "startsWith";
294
- in: "in";
295
- empty: "empty";
296
- contains: "contains";
297
- eq: "eq";
298
- gt: "gt";
299
- gte: "gte";
300
- lt: "lt";
301
- lte: "lte";
302
- }>;
303
- value: z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>]>;
304
- }, z.core.$strip>>>;
305
- sort: z.ZodOptional<z.ZodNullable<z.ZodObject<{
306
- field: z.ZodString;
307
- dir: z.ZodEnum<{
308
- desc: "desc";
309
- asc: "asc";
310
- }>;
311
- }, z.core.$strip>>>;
312
- columns: z.ZodOptional<z.ZodArray<z.ZodString>>;
313
- }, z.core.$strip>;
314
- export type ConfigView = z.infer<typeof configViewSchema>;
315
227
  /**
316
228
  * A resolved view: identity + the table state it restores (matches `SavedView`).
317
229
  * `name` stays a {@link LocalizedString} — resolved at the render site (EC-343), like
@@ -327,7 +239,8 @@ export interface ResolvedView {
327
239
  /**
328
240
  * Resolve a collection's config-declared `views` into full {@link ResolvedView}s
329
241
  * (EC-246b). Each is validated with {@link configViewSchema}; malformed entries
330
- * and duplicate ids are dropped (defensive). Columns fall back to the collection
242
+ * and duplicate ids are skipped (defensive — a malformed view never reaches a resolved
243
+ * config: the config gate refuses it, EC-622). Columns fall back to the collection
331
244
  * default when a view doesn't pin them, so a filter-only view still renders every
332
245
  * column. Config views are shared and change only in
333
246
  * code — the runtime never mutates them.
@@ -1,8 +1,8 @@
1
1
  import { z } from 'zod';
2
- import { canonicalDateValue, localizedStringSchema, resolveField, resolveLabel, titleFieldOf, } from '@elytracms/core/cms-core';
2
+ import { canonicalDateValue, configViewSchema, filterClauseSchema, filterScalarSchema, isPseudoColumn, PUBLISHED_AT_FIELD, resolveField, resolveLabel, STATE_FIELD, titleFieldOf, UPDATED_AT_FIELD, URL_FIELD, viewSortSchema, } from '@elytracms/core/cms-core';
3
3
  import { localizedLabel, translate } from '../i18n/index.js';
4
4
  import { contentQueryErrorMessage } from '../persona/labels.js';
5
- import { FILTER_OPERATORS, compareListEntries, sortScalarOf, filterableFields, matchesFieldValue, validateWhere, } from '@elytracms/core/content';
5
+ import { compareListEntries, sortScalarOf, filterableFields, matchesFieldValue, validateWhere, } from '@elytracms/core/content';
6
6
  /**
7
7
  * The pure table-view model for the CMS collection table (EC-169).
8
8
  *
@@ -20,18 +20,10 @@ import { FILTER_OPERATORS, compareListEntries, sortScalarOf, filterableFields, m
20
20
  * closed vocabulary (`eq`/`in`).
21
21
  */
22
22
  // --- Schemas (single source of truth — persisted view records reuse these) ----
23
- export const filterScalarSchema = z.union([z.string(), z.number(), z.boolean()]);
24
- export const filterClauseSchema = z.object({
25
- /** A schema field name, or {@link STATE_FIELD} for the publication state. */
26
- field: z.string().min(1),
27
- op: z.enum(FILTER_OPERATORS),
28
- /** `in` carries an array of scalars; every other operator one scalar. */
29
- value: z.union([filterScalarSchema, z.array(filterScalarSchema)]),
30
- });
31
- export const viewSortSchema = z.object({
32
- field: z.string().min(1),
33
- dir: z.enum(['asc', 'desc']),
34
- });
23
+ // EC-622: the authoring shape of a view and the reserved `$` keys are declared ONCE, in
24
+ // cms-core (`collection-views.ts`), where the config gate judges every declared view —
25
+ // re-exported here unchanged, so this module's public names stay what they were.
26
+ export { configViewSchema, filterClauseSchema, filterScalarSchema, isPseudoColumn, PUBLISHED_AT_FIELD, STATE_FIELD, UPDATED_AT_FIELD, URL_FIELD, viewSortSchema, };
35
27
  export const tableViewStateSchema = z.object({
36
28
  /** AND-only list of conditions (no OR, no nesting — vision AD-3). */
37
29
  filters: z.array(filterClauseSchema),
@@ -50,53 +42,16 @@ export const tableViewStateSchema = z.object({
50
42
  hidden: z.array(z.string().min(1)).default([]),
51
43
  });
52
44
  // --- The built-in publication-state filter column ------------------------------
53
- /**
54
- * Sentinel field key for the publication state. `$`-prefixed so it can never
55
- * collide with a schema field name (`fieldDefSchema` names are identifiers).
56
- */
57
- export const STATE_FIELD = '$state';
45
+ // `STATE_FIELD` (the publication-state sentinel) and the reserved pseudo-COLUMN keys
46
+ // (`$url`, `$updatedAt`, `$publishedAt` — EC-445/EC-447) are declared in cms-core
47
+ // (`collection-views.ts`, EC-622) and re-exported above. `$state` is a reserved key
48
+ // (filter + sort) but NOT a reorderable column: EC-448 made it the FIXED structural
49
+ // leading column, so {@link pseudoColumns} never emits it and an authored view listing
50
+ // `$state` is a harmless no-op (see {@link resolveConfigColumns}). Which reserved key
51
+ // APPLIES to a collection is this module's runtime decision ({@link pseudoColumns}); the
52
+ // config gate accepts every reserved key.
58
53
  const STATE_VALUES = ['draft', 'published'];
59
54
  // --- Reserved pseudo-COLUMNS (EC-445) ------------------------------------------
60
- /**
61
- * Reserved pseudo-COLUMN keys — the columns-axis siblings of the filter-axis
62
- * {@link STATE_FIELD}. `$`-prefixed so they can never collide with a schema field
63
- * name; they participate in a view's `columns` list AND the Display picker exactly
64
- * like a schema field (orderable / hideable / includable), but they are NOT drawn
65
- * from `collection.fields` — the column UNIVERSE is `[...schemaFields, ...pseudoColumns]`.
66
- *
67
- * - `$state` — publication status. A reserved `$`-key, but NO LONGER a reorderable
68
- * column: EC-448 promoted it to a FIXED structural leading column (checkbox →
69
- * status-dot → title), so it is NOT emitted by {@link pseudoColumns} and never
70
- * participates in the reorderable columns/hidden/order model. It stays a reserved
71
- * key (filter + sort) and {@link isPseudoColumn} still recognizes it, so an authored
72
- * view listing `$state` is a harmless no-op (see {@link resolveConfigColumns}).
73
- * - `$url` — the derived delivery URL (routable collections only; NON-sortable).
74
- * - `$updatedAt` — the record's last-write timestamp.
75
- * - `$publishedAt` — the record's FIRST-publish instant (EC-447; opt-in, HIDDEN by
76
- * default). Now materialized end-to-end (`cmsDocuments.publishedAt` → source
77
- * `timestamps.publishedAt`) and emitted by {@link pseudoColumns} when the source
78
- * both materializes timestamps and has a publish lifecycle.
79
- */
80
- export const URL_FIELD = '$url';
81
- export const PUBLISHED_AT_FIELD = '$publishedAt';
82
- export const UPDATED_AT_FIELD = '$updatedAt';
83
- /**
84
- * Every reserved pseudo-column key. `$state` is included (it is still a reserved
85
- * `$`-key: the filter/sort axis + the FIXED structural status column, EC-448) so
86
- * {@link isPseudoColumn} never mistakes it for a schema field — but it is NOT a
87
- * reorderable column, so it is absent from the `pseudo` sets {@link pseudoColumns}
88
- * emits into the order/hide/picker model.
89
- */
90
- const PSEUDO_COLUMN_FIELDS = [
91
- STATE_FIELD,
92
- URL_FIELD,
93
- PUBLISHED_AT_FIELD,
94
- UPDATED_AT_FIELD,
95
- ];
96
- /** Is `name` a reserved pseudo-column (a `$`-key, not a schema field)? */
97
- export function isPseudoColumn(name) {
98
- return PSEUDO_COLUMN_FIELDS.includes(name);
99
- }
100
55
  /**
101
56
  * Pseudo-columns HIDDEN by default (opt-in): the extra publish timestamp. Everything
102
57
  * else applicable (`$updatedAt`/`$state`/`$url`) is on by default (owner, 2026-07-17).
@@ -460,7 +415,10 @@ export function defaultViewState(collection, pseudo = []) {
460
415
  * same shape {@link defaultViewState} produces, so {@link resolveColumns} renders
461
416
  * precisely the authored list, visible, in that order. An omitted (or empty) list
462
417
  * inherits the collection's default lens (all scalars visible, composites hidden).
463
- * Unknown names in the authored list (a field the schema no longer has) are dropped.
418
+ * An authored name outside the universe — no longer a field, or a reserved column that
419
+ * does not apply to this source — is skipped here so the list still renders; a name that
420
+ * is neither a top-level field nor a reserved `$` key never reaches a resolved config
421
+ * (EC-622: the config gate refuses it, `collectionViewFindings`).
464
422
  */
465
423
  export function resolveConfigColumns(collection, visible, pseudo = []) {
466
424
  if (visible === undefined || visible.length === 0) {
@@ -494,46 +452,6 @@ export function resolveConfigColumns(collection, visible, pseudo = []) {
494
452
  }
495
453
  /** The reserved `views[]` id whose entry OVERRIDES the synthesized baseline view (EC-444). */
496
454
  export const DEFAULT_VIEW_OVERRIDE_ID = 'default';
497
- /**
498
- * A config-declared view (EC-246b/EC-444) — the flat authoring shape a collection
499
- * declares in `cms/` config: `{ name, initial?, filters?, sort?, columns? }`.
500
- * Filters/sort/columns sit at the top level (no nested `state` wrapper). The
501
- * studio validates + interprets these; `cms-core` stores them opaquely (it
502
- * can't reference the filter vocabulary without a layering cycle).
503
- */
504
- export const configViewSchema = z.object({
505
- /**
506
- * Optional stable id; defaults to a slug of `name`. The reserved value
507
- * {@link DEFAULT_VIEW_OVERRIDE_ID} (`'default'`) marks the baseline-override entry.
508
- */
509
- id: z.string().min(1).optional(),
510
- /**
511
- * View label; localizable (EC-343): a plain string or a `{ locale }` map, resolved
512
- * at the render site via the same `resolveLabel` path as `form.label` (EC-241).
513
- */
514
- name: localizedStringSchema,
515
- /**
516
- * When true this view seeds the collection's INITIAL active view (EC-343/EC-444) —
517
- * the curated lens an editor lands on before picking one. At most one per collection;
518
- * a duplicate is a visible validation issue ({@link configViewIssues}), never silent.
519
- * The baseline lens stays available and stays the fallback.
520
- */
521
- initial: z.boolean().optional(),
522
- /**
523
- * @deprecated EC-444 — renamed to {@link ConfigView.initial}. Still read as an alias
524
- * for one release (`initial ?? default`) with a console warning; removed next release.
525
- * Distinct from a field's default VALUE and from the reserved `id: 'default'` override.
526
- */
527
- default: z.boolean().optional(),
528
- filters: z.array(filterClauseSchema).optional(),
529
- sort: viewSortSchema.nullable().optional(),
530
- /**
531
- * The single ordered list of VISIBLE columns (EC-444): what you list is what shows,
532
- * in that order. Omit to inherit the collection's default columns (all scalars). The
533
- * two-axis `{columns, hidden}` internal state is derived by {@link resolveConfigColumns}.
534
- */
535
- columns: z.array(z.string().min(1)).optional(),
536
- });
537
455
  /**
538
456
  * Slug used to derive a config view's stable id from its name. A localized name
539
457
  * (EC-343) slugs its DEFAULT-locale text so the id is stable across the active UI
@@ -550,7 +468,8 @@ function configViewId(name, index) {
550
468
  /**
551
469
  * Resolve a collection's config-declared `views` into full {@link ResolvedView}s
552
470
  * (EC-246b). Each is validated with {@link configViewSchema}; malformed entries
553
- * and duplicate ids are dropped (defensive). Columns fall back to the collection
471
+ * and duplicate ids are skipped (defensive — a malformed view never reaches a resolved
472
+ * config: the config gate refuses it, EC-622). Columns fall back to the collection
554
473
  * default when a view doesn't pin them, so a filter-only view still renders every
555
474
  * column. Config views are shared and change only in
556
475
  * code — the runtime never mutates them.