@elytracms/studio 0.0.94 → 0.0.96

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 (164) 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/cms.ts +124 -8
  6. package/convex/delivery-redirects.test.ts +167 -0
  7. package/convex/delivery.ts +15 -5
  8. package/convex/rail-write-parity.test.ts +6 -6
  9. package/convex/redirects.ts +1 -1
  10. package/convex/schema.ts +6 -0
  11. package/convex/strict-reference-race.test.ts +139 -0
  12. package/dist/addon.d.ts +1 -1
  13. package/dist/addon.js.map +1 -1
  14. package/dist/components/ai/ai-chat-panel.js +15 -2
  15. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  16. package/dist/components/ai/translate-document-action.d.ts +6 -0
  17. package/dist/components/ai/translate-document-action.js.map +1 -1
  18. package/dist/components/ai/translate-document-dialog.d.ts +1 -1
  19. package/dist/components/ai/translate-document-dialog.js +5 -5
  20. package/dist/components/ai/translate-document-dialog.js.map +1 -1
  21. package/dist/components/assets/asset-detail.js +29 -11
  22. package/dist/components/assets/asset-detail.js.map +1 -1
  23. package/dist/components/assets/asset-locale.d.ts +16 -0
  24. package/dist/components/assets/asset-locale.js +30 -0
  25. package/dist/components/assets/asset-locale.js.map +1 -0
  26. package/dist/components/assets/asset-meta-drift-notice.d.ts +13 -0
  27. package/dist/components/assets/asset-meta-drift-notice.js +22 -0
  28. package/dist/components/assets/asset-meta-drift-notice.js.map +1 -0
  29. package/dist/components/assets/asset-meta-label.d.ts +13 -0
  30. package/dist/components/assets/asset-meta-label.js +17 -0
  31. package/dist/components/assets/asset-meta-label.js.map +1 -0
  32. package/dist/components/assets/asset-metadata.d.ts +89 -15
  33. package/dist/components/assets/asset-metadata.js +123 -11
  34. package/dist/components/assets/asset-metadata.js.map +1 -1
  35. package/dist/components/assets/asset-search.d.ts +6 -2
  36. package/dist/components/assets/asset-search.js +19 -3
  37. package/dist/components/assets/asset-search.js.map +1 -1
  38. package/dist/components/assets/asset-thumb.js +6 -2
  39. package/dist/components/assets/asset-thumb.js.map +1 -1
  40. package/dist/components/assets/assets-collection-view-source.d.ts +25 -9
  41. package/dist/components/assets/assets-collection-view-source.js +80 -26
  42. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  43. package/dist/components/assets/assets-store.js +17 -4
  44. package/dist/components/assets/assets-store.js.map +1 -1
  45. package/dist/components/builder/builder-store.d.ts +21 -0
  46. package/dist/components/builder/builder-store.js +22 -0
  47. package/dist/components/builder/builder-store.js.map +1 -1
  48. package/dist/components/builder/canvas-commit-boundary.d.ts +22 -0
  49. package/dist/components/builder/canvas-commit-boundary.js +27 -0
  50. package/dist/components/builder/canvas-commit-boundary.js.map +1 -0
  51. package/dist/components/builder/canvas-rebase.d.ts +101 -0
  52. package/dist/components/builder/canvas-rebase.js +179 -0
  53. package/dist/components/builder/canvas-rebase.js.map +1 -0
  54. package/dist/components/builder/canvas-render-context.d.ts +6 -3
  55. package/dist/components/builder/canvas-render-context.js +17 -8
  56. package/dist/components/builder/canvas-render-context.js.map +1 -1
  57. package/dist/components/builder/composition-field-editor.d.ts +26 -2
  58. package/dist/components/builder/composition-field-editor.js +46 -27
  59. package/dist/components/builder/composition-field-editor.js.map +1 -1
  60. package/dist/components/builder/node-factory.js +15 -3
  61. package/dist/components/builder/node-factory.js.map +1 -1
  62. package/dist/components/builder/property-inspector.js +3 -1
  63. package/dist/components/builder/property-inspector.js.map +1 -1
  64. package/dist/components/builder/run-inspector.js +10 -6
  65. package/dist/components/builder/run-inspector.js.map +1 -1
  66. package/dist/components/cms/asset-field.d.ts +5 -2
  67. package/dist/components/cms/asset-field.js +34 -17
  68. package/dist/components/cms/asset-field.js.map +1 -1
  69. package/dist/components/cms/asset-picker-core.d.ts +2 -2
  70. package/dist/components/cms/asset-picker-core.js +2 -2
  71. package/dist/components/cms/asset-picker-core.js.map +1 -1
  72. package/dist/components/cms/asset-picker.js +6 -2
  73. package/dist/components/cms/asset-picker.js.map +1 -1
  74. package/dist/components/cms/canvas-commit.d.ts +62 -0
  75. package/dist/components/cms/canvas-commit.js +39 -0
  76. package/dist/components/cms/canvas-commit.js.map +1 -0
  77. package/dist/components/cms/cms-store.d.ts +21 -8
  78. package/dist/components/cms/cms-store.js +19 -10
  79. package/dist/components/cms/cms-store.js.map +1 -1
  80. package/dist/components/cms/collection-table.js +27 -18
  81. package/dist/components/cms/collection-table.js.map +1 -1
  82. package/dist/components/cms/commit-policy.d.ts +6 -2
  83. package/dist/components/cms/commit-policy.js +1 -1
  84. package/dist/components/cms/commit-policy.js.map +1 -1
  85. package/dist/components/cms/detail-editor.js +12 -2
  86. package/dist/components/cms/detail-editor.js.map +1 -1
  87. package/dist/components/cms/draft-commit.d.ts +16 -3
  88. package/dist/components/cms/draft-commit.js +3 -2
  89. package/dist/components/cms/draft-commit.js.map +1 -1
  90. package/dist/components/cms/use-document-draft.d.ts +121 -5
  91. package/dist/components/cms/use-document-draft.js +197 -54
  92. package/dist/components/cms/use-document-draft.js.map +1 -1
  93. package/dist/components/cms/use-live-url.d.ts +6 -1
  94. package/dist/components/cms/use-live-url.js +12 -9
  95. package/dist/components/cms/use-live-url.js.map +1 -1
  96. package/dist/components/collection-view/cms-collection-view-source.js +11 -5
  97. package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
  98. package/dist/components/components/transfer-dialog.js +6 -4
  99. package/dist/components/components/transfer-dialog.js.map +1 -1
  100. package/dist/components/deployment-setup-gate.js +10 -1
  101. package/dist/components/deployment-setup-gate.js.map +1 -1
  102. package/dist/components/records/record-lens.js +3 -8
  103. package/dist/components/records/record-lens.js.map +1 -1
  104. package/dist/lib/ai/chat-state.d.ts +7 -0
  105. package/dist/lib/ai/chat-state.js.map +1 -1
  106. package/dist/lib/ai/tools.d.ts +8 -0
  107. package/dist/lib/ai/tools.js +4 -0
  108. package/dist/lib/ai/tools.js.map +1 -1
  109. package/dist/lib/assets/upload.d.ts +25 -7
  110. package/dist/lib/assets/upload.js +30 -21
  111. package/dist/lib/assets/upload.js.map +1 -1
  112. package/dist/lib/cms/published-documents.d.ts +24 -6
  113. package/dist/lib/cms/published-documents.js +39 -6
  114. package/dist/lib/cms/published-documents.js.map +1 -1
  115. package/dist/lib/cms-views/view-model.d.ts +25 -105
  116. package/dist/lib/cms-views/view-model.js +36 -103
  117. package/dist/lib/cms-views/view-model.js.map +1 -1
  118. package/dist/lib/collection-view/__fixtures__/document-writes.js +1 -0
  119. package/dist/lib/collection-view/__fixtures__/document-writes.js.map +1 -1
  120. package/dist/lib/collection-view/collection-view-source.d.ts +21 -8
  121. package/dist/lib/collection-view/collection-view-source.js.map +1 -1
  122. package/dist/lib/data/collection-content.js +5 -0
  123. package/dist/lib/data/collection-content.js.map +1 -1
  124. package/dist/lib/engine-version.d.ts +1 -1
  125. package/dist/lib/engine-version.js +1 -1
  126. package/dist/lib/engine-version.js.map +1 -1
  127. package/dist/lib/i18n/messages.de.js +20 -4
  128. package/dist/lib/i18n/messages.de.js.map +1 -1
  129. package/dist/lib/i18n/messages.en.d.ts +16 -1
  130. package/dist/lib/i18n/messages.en.js +22 -5
  131. package/dist/lib/i18n/messages.en.js.map +1 -1
  132. package/dist/lib/operations/cms-persistence.d.ts +80 -35
  133. package/dist/lib/operations/cms-persistence.js +348 -85
  134. package/dist/lib/operations/cms-persistence.js.map +1 -1
  135. package/dist/lib/operations/config-manifest-lookup.d.ts +1 -1
  136. package/dist/lib/operations/config-manifest-lookup.js +3 -3
  137. package/dist/lib/operations/config-manifest-lookup.js.map +1 -1
  138. package/dist/lib/persistence/convex-adapter.js +54 -8
  139. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  140. package/dist/lib/persistence/deployment-readiness.js +14 -11
  141. package/dist/lib/persistence/deployment-readiness.js.map +1 -1
  142. package/dist/lib/persona/labels.js +2 -0
  143. package/dist/lib/persona/labels.js.map +1 -1
  144. package/dist/lib/query/index.d.ts +2 -2
  145. package/dist/lib/query/index.js +1 -1
  146. package/dist/lib/query/index.js.map +1 -1
  147. package/dist/lib/query/operations-query.d.ts +26 -0
  148. package/dist/lib/query/operations-query.js +12 -0
  149. package/dist/lib/query/operations-query.js.map +1 -1
  150. package/dist/lib/registry/index.js +2 -2
  151. package/dist/lib/registry/index.js.map +1 -1
  152. package/dist/lib/registry/preview.d.ts +10 -0
  153. package/dist/lib/registry/preview.js +13 -4
  154. package/dist/lib/registry/preview.js.map +1 -1
  155. package/dist/lib/routes/route-issues.js +6 -3
  156. package/dist/lib/routes/route-issues.js.map +1 -1
  157. package/dist/lib/workspace/workspace-source.js +6 -7
  158. package/dist/lib/workspace/workspace-source.js.map +1 -1
  159. package/dist/previews/entry-node.d.ts +14 -0
  160. package/dist/previews/entry-node.js +22 -0
  161. package/dist/previews/entry-node.js.map +1 -0
  162. package/dist/previews/harness.js +2 -16
  163. package/dist/previews/harness.js.map +1 -1
  164. package/package.json +3 -3
@@ -1,4 +1,4 @@
1
- import type { FieldDef } from '@elytracms/core/cms-core';
1
+ import type { FieldDef, Locale } from '@elytracms/core/cms-core';
2
2
  import type { AssetRecord, AssetRepository, AssetWriteInput, BlobStorageAdapter } from '@elytracms/core/persistence';
3
3
  import type { ImageDimensions } from './image-dimensions.js';
4
4
  import type { GeneratedVariants } from './thumbnail.js';
@@ -35,6 +35,15 @@ export interface UploadDeps {
35
35
  * written (a field without `source` is never filled).
36
36
  */
37
37
  assetFields?: readonly FieldDef[];
38
+ /**
39
+ * The project's locales (EC-632): a `localized: true` asset field's embedded value goes
40
+ * to the locale buckets — each locale its `xml:lang` alternative, the default the
41
+ * `x-default` reading. Absent → every field is written to the shared `meta`.
42
+ */
43
+ locales?: {
44
+ readonly default: Locale;
45
+ readonly locales: readonly Locale[];
46
+ };
38
47
  /** Client-direct transport; only used when `blobs.createUploadUrl` exists. */
39
48
  transport?: UploadTransport;
40
49
  /** Injectable dimension detection; defaults to `createImageBitmap`-based. */
@@ -79,16 +88,25 @@ export type UploadResult = {
79
88
  * previous `state` to retry (the attempt counter carries over).
80
89
  */
81
90
  export declare function runUpload(deps: UploadDeps, request: UploadRequest, previous?: UploadState): Promise<UploadResult>;
91
+ /** What a file's embedded facts write (EC-568 → EC-632): shared `meta` keys, and per-locale buckets of the localized fields. */
92
+ export interface EmbeddedAssetWrite {
93
+ meta: Record<string, unknown>;
94
+ localizedMeta: Record<string, Record<string, unknown>>;
95
+ }
82
96
  /**
83
97
  * The write of a file REPLACE (EC-568, the owner's ruling: "override the values with
84
98
  * available metadata, or keep the existing if no metadata is available"):
85
99
  * - the bytes' columns are SET — and a derived column the new file does not have is
86
100
  * CLEARED (`null`), so an old thumb, lqip or dimension never outlives its file;
87
- * - `meta` is the stored bag (a legacy `alt` column lifted in, EC-587) with each key the
88
- * new file brings overridden; written only when the file brings something, and then
89
- * the legacy column is cleared, as every `meta` write does;
90
- * - the id, the focal point, the title and every other column are absent → kept.
101
+ * - each `meta` key the new file brings is a per-key `metaPatch` (EC-634), applied to the
102
+ * bag as STORED inside the write — never a copy read before the upload, so an editor's
103
+ * metadata save that lands while the bytes travel is kept. Sent only when the file
104
+ * brings something; the write rule then lifts a legacy `alt` column into the bag and
105
+ * clears it (EC-587), as for every patch;
106
+ * - the id, the focal point, the title and every other column are absent → kept;
107
+ * - `mustExist`: the write only updates the stored asset — gone (deleted meanwhile), it is
108
+ * refused inside the write and nothing is created.
91
109
  */
92
- export declare function replaceWrite(stored: AssetRecord, derived: Pick<AssetRecord, 'filename' | 'contentType' | 'byteSize' | 'storageKey'> & {
110
+ export declare function replaceWrite(assetId: string, derived: Pick<AssetRecord, 'filename' | 'contentType' | 'byteSize' | 'storageKey'> & {
93
111
  url: string;
94
- }, dimensions: ImageDimensions | undefined, variants: GeneratedVariants, embedded: Record<string, unknown>): AssetWriteInput;
112
+ }, dimensions: ImageDimensions | undefined, variants: GeneratedVariants, embedded: EmbeddedAssetWrite): AssetWriteInput;
@@ -1,5 +1,4 @@
1
- import { embeddedMetaValues, readEmbeddedMetadata } from '@elytracms/core/cms-core';
2
- import { liftLegacyAlt } from '@elytracms/core/persistence';
1
+ import { embeddedAssetMeta, readEmbeddedMetadata } from '@elytracms/core/cms-core';
3
2
  import { detectImageDimensions } from './image-dimensions.js';
4
3
  import { generateVariants } from './thumbnail.js';
5
4
  import { initialUploadState, uploadReducer } from './upload-state.js';
@@ -123,7 +122,7 @@ export async function runUpload(deps, request, previous) {
123
122
  // --- confirm: metadata record linked to the stored blob --------------------
124
123
  try {
125
124
  // EC-568: the ONE embedded-metadata reader, for a first upload and a replace alike.
126
- const embedded = await embeddedAssetMeta(file, deps.assetFields);
125
+ const embedded = await embeddedMetaOf(file, deps.assetFields, deps.locales);
127
126
  const detect = deps.detectDimensions ?? defaultDetectDimensions;
128
127
  const dimensions = await detect(file);
129
128
  // EC-400 + EC-420: produce the thumb + lqip previews from the bytes already in
@@ -147,10 +146,10 @@ export async function runUpload(deps, request, previous) {
147
146
  };
148
147
  let write;
149
148
  if (request.replace) {
150
- const stored = await assets.findAsset(projectId, request.replace.assetId);
151
- if (!stored)
152
- throw new Error(`No asset “${request.replace.assetId}” is stored to replace the file of.`);
153
- write = replaceWrite(stored, derived, dimensions, variants, embedded);
149
+ // A replace edits an asset, never creates one — decided by the write itself
150
+ // (`mustExist`), against the row as it is when the write lands: a delete by another
151
+ // session in the meantime makes the write fail here, visibly, and creates nothing.
152
+ write = replaceWrite(request.replace.assetId, derived, dimensions, variants, embedded);
154
153
  }
155
154
  else {
156
155
  write = {
@@ -158,7 +157,8 @@ export async function runUpload(deps, request, previous) {
158
157
  ...(dimensions ? { width: dimensions.width, height: dimensions.height } : {}),
159
158
  ...(variants.thumb ? { thumb: variants.thumb } : {}),
160
159
  ...(variants.lqip ? { lqip: variants.lqip } : {}),
161
- ...(Object.keys(embedded).length > 0 ? { meta: embedded } : {}),
160
+ ...(Object.keys(embedded.meta).length > 0 ? { meta: embedded.meta } : {}),
161
+ ...(Object.keys(embedded.localizedMeta).length > 0 ? { localizedMeta: embedded.localizedMeta } : {}),
162
162
  };
163
163
  }
164
164
  const asset = await assets.upsertAsset(projectId, write);
@@ -175,14 +175,15 @@ export async function runUpload(deps, request, previous) {
175
175
  * bytes, mapped through the asset collection's `source` declarations. A file that cannot
176
176
  * be read yields `{}` — metadata never fails an upload.
177
177
  */
178
- async function embeddedAssetMeta(file, fields) {
178
+ async function embeddedMetaOf(file, fields, locales) {
179
+ const none = { meta: {}, localizedMeta: {} };
179
180
  if (!fields || !fields.some((field) => field.source !== undefined))
180
- return {};
181
+ return none;
181
182
  try {
182
- return embeddedMetaValues(fields, readEmbeddedMetadata(new Uint8Array(await file.arrayBuffer())));
183
+ return embeddedAssetMeta(fields, readEmbeddedMetadata(new Uint8Array(await file.arrayBuffer())), locales);
183
184
  }
184
185
  catch {
185
- return {};
186
+ return none;
186
187
  }
187
188
  }
188
189
  /**
@@ -190,22 +191,30 @@ async function embeddedAssetMeta(file, fields) {
190
191
  * available metadata, or keep the existing if no metadata is available"):
191
192
  * - the bytes' columns are SET — and a derived column the new file does not have is
192
193
  * CLEARED (`null`), so an old thumb, lqip or dimension never outlives its file;
193
- * - `meta` is the stored bag (a legacy `alt` column lifted in, EC-587) with each key the
194
- * new file brings overridden; written only when the file brings something, and then
195
- * the legacy column is cleared, as every `meta` write does;
196
- * - the id, the focal point, the title and every other column are absent → kept.
194
+ * - each `meta` key the new file brings is a per-key `metaPatch` (EC-634), applied to the
195
+ * bag as STORED inside the write — never a copy read before the upload, so an editor's
196
+ * metadata save that lands while the bytes travel is kept. Sent only when the file
197
+ * brings something; the write rule then lifts a legacy `alt` column into the bag and
198
+ * clears it (EC-587), as for every patch;
199
+ * - the id, the focal point, the title and every other column are absent → kept;
200
+ * - `mustExist`: the write only updates the stored asset — gone (deleted meanwhile), it is
201
+ * refused inside the write and nothing is created.
197
202
  */
198
- export function replaceWrite(stored, derived, dimensions, variants, embedded) {
199
- const brings = Object.keys(embedded).length > 0;
200
- const base = liftLegacyAlt(stored)?.meta ?? stored.meta ?? {};
203
+ export function replaceWrite(assetId, derived, dimensions, variants, embedded) {
204
+ const brings = Object.keys(embedded.meta).length > 0;
205
+ const bringsLocales = Object.keys(embedded.localizedMeta).length > 0;
201
206
  return {
202
- id: stored.id,
207
+ id: assetId,
208
+ // EC-634 follow-on: an update of a stored asset, never a create (a concurrent delete wins).
209
+ mustExist: true,
203
210
  ...derived,
204
211
  width: dimensions?.width ?? null,
205
212
  height: dimensions?.height ?? null,
206
213
  thumb: variants.thumb ?? null,
207
214
  lqip: variants.lqip ?? null,
208
- ...(brings ? { meta: { ...base, ...embedded }, alt: null } : {}),
215
+ ...(brings ? { metaPatch: embedded.meta } : {}),
216
+ // EC-632: a localized field's embedded values patch their locale buckets per key.
217
+ ...(bringsLocales ? { localizedMetaPatch: embedded.localizedMeta } : {}),
209
218
  };
210
219
  }
211
220
  //# sourceMappingURL=upload.js.map
@@ -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, sortSchema } from '@elytracms/core/cms-core';
3
+ import type { CmsDocument, CollectionDef, ConfigView, FieldDef, FilterClause, Locale, LocaleConfig, LocalizedString, Sort } 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, sortSchema, STATE_FIELD, UPDATED_AT_FIELD, URL_FIELD, };
22
+ export type { ConfigView, FilterClause, Sort };
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
- dir: z.ZodEnum<{
66
- desc: "desc";
42
+ direction: z.ZodDefault<z.ZodEnum<{
67
43
  asc: "asc";
68
- }>;
69
- }, z.core.$strip>>;
44
+ desc: "desc";
45
+ }>>;
46
+ }, z.core.$strict>>;
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.
@@ -240,13 +187,20 @@ export type IsPublished = (doc: CmsDocument) => boolean;
240
187
  * relation membership all behave the same on the table as on the live site).
241
188
  */
242
189
  export declare function clauseMatches(collection: CollectionDef, doc: CmsDocument, localeConfig: LocaleConfig, locale: Locale, clause: FilterClause, isPublished?: IsPublished): boolean;
190
+ /**
191
+ * The sort a column header click leads to (EC-169): a new column sorts ascending, the
192
+ * active one flips to descending, then clears. Reads `direction`, which every parsed
193
+ * sort carries — THE sort shape normalises an omitted one to `'asc'` (EC-660), so a
194
+ * config view declared `{ field }` flips on its first click instead of clearing.
195
+ */
196
+ export declare function nextHeaderSort(prev: Sort | null, field: string): Sort | null;
243
197
  /**
244
198
  * Sort documents by one column in the canonical deterministic order
245
199
  * (`compareListEntries`: typed value order, id-ascending tiebreak). Unlike the
246
200
  * content accessors, the *table* may sort any rendered column — sorting is a
247
201
  * presentation concern here; the `filterable` flag governs filters.
248
202
  */
249
- export declare function sortViewDocuments(collection: CollectionDef, docs: readonly CmsDocument[], localeConfig: LocaleConfig, locale: Locale, sort: ViewSort | null, isPublished?: IsPublished, meta?: PseudoColumnMeta): CmsDocument[];
203
+ export declare function sortViewDocuments(collection: CollectionDef, docs: readonly CmsDocument[], localeConfig: LocaleConfig, locale: Locale, sort: Sort | null, isPublished?: IsPublished, meta?: PseudoColumnMeta): CmsDocument[];
250
204
  /** The filtered + sorted documents of a view, plus any clause issues. */
251
205
  export interface AppliedViewState {
252
206
  docs: CmsDocument[];
@@ -269,49 +223,14 @@ export declare function defaultViewState(collection: CollectionDef, pseudo?: rea
269
223
  * same shape {@link defaultViewState} produces, so {@link resolveColumns} renders
270
224
  * precisely the authored list, visible, in that order. An omitted (or empty) list
271
225
  * 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.
226
+ * An authored name outside the universe — no longer a field, or a reserved column that
227
+ * does not apply to this source — is skipped here so the list still renders; a name that
228
+ * is neither a top-level field nor a reserved `$` key never reaches a resolved config
229
+ * (EC-622: the config gate refuses it, `collectionViewFindings`).
273
230
  */
274
231
  export declare function resolveConfigColumns(collection: CollectionDef, visible: readonly string[] | undefined, pseudo?: readonly string[]): Pick<TableViewState, 'columns' | 'hidden'>;
275
232
  /** The reserved `views[]` id whose entry OVERRIDES the synthesized baseline view (EC-444). */
276
233
  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
234
  /**
316
235
  * A resolved view: identity + the table state it restores (matches `SavedView`).
317
236
  * `name` stays a {@link LocalizedString} — resolved at the render site (EC-343), like
@@ -327,7 +246,8 @@ export interface ResolvedView {
327
246
  /**
328
247
  * Resolve a collection's config-declared `views` into full {@link ResolvedView}s
329
248
  * (EC-246b). Each is validated with {@link configViewSchema}; malformed entries
330
- * and duplicate ids are dropped (defensive). Columns fall back to the collection
249
+ * and duplicate ids are skipped (defensive — a malformed view never reaches a resolved
250
+ * config: the config gate refuses it, EC-622). Columns fall back to the collection
331
251
  * default when a view doesn't pin them, so a filter-only view still renders every
332
252
  * column. Config views are shared and change only in
333
253
  * code — the runtime never mutates them.