@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.
- package/convex/asset-alt-migration.test.ts +2 -2
- package/convex/asset-meta-patch.test.ts +218 -0
- package/convex/asset-replace-delete-race.test.ts +127 -0
- package/convex/assets.ts +27 -2
- package/convex/cms.ts +124 -8
- package/convex/delivery-redirects.test.ts +167 -0
- package/convex/delivery.ts +15 -5
- package/convex/rail-write-parity.test.ts +6 -6
- package/convex/redirects.ts +1 -1
- package/convex/schema.ts +6 -0
- package/convex/strict-reference-race.test.ts +139 -0
- package/dist/addon.d.ts +1 -1
- package/dist/addon.js.map +1 -1
- package/dist/components/ai/ai-chat-panel.js +15 -2
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/ai/translate-document-action.d.ts +6 -0
- package/dist/components/ai/translate-document-action.js.map +1 -1
- package/dist/components/ai/translate-document-dialog.d.ts +1 -1
- package/dist/components/ai/translate-document-dialog.js +5 -5
- package/dist/components/ai/translate-document-dialog.js.map +1 -1
- package/dist/components/assets/asset-detail.js +29 -11
- package/dist/components/assets/asset-detail.js.map +1 -1
- package/dist/components/assets/asset-locale.d.ts +16 -0
- package/dist/components/assets/asset-locale.js +30 -0
- package/dist/components/assets/asset-locale.js.map +1 -0
- package/dist/components/assets/asset-meta-drift-notice.d.ts +13 -0
- package/dist/components/assets/asset-meta-drift-notice.js +22 -0
- package/dist/components/assets/asset-meta-drift-notice.js.map +1 -0
- package/dist/components/assets/asset-meta-label.d.ts +13 -0
- package/dist/components/assets/asset-meta-label.js +17 -0
- package/dist/components/assets/asset-meta-label.js.map +1 -0
- package/dist/components/assets/asset-metadata.d.ts +89 -15
- package/dist/components/assets/asset-metadata.js +123 -11
- package/dist/components/assets/asset-metadata.js.map +1 -1
- package/dist/components/assets/asset-search.d.ts +6 -2
- package/dist/components/assets/asset-search.js +19 -3
- package/dist/components/assets/asset-search.js.map +1 -1
- package/dist/components/assets/asset-thumb.js +6 -2
- package/dist/components/assets/asset-thumb.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.d.ts +25 -9
- package/dist/components/assets/assets-collection-view-source.js +80 -26
- package/dist/components/assets/assets-collection-view-source.js.map +1 -1
- package/dist/components/assets/assets-store.js +17 -4
- package/dist/components/assets/assets-store.js.map +1 -1
- package/dist/components/builder/builder-store.d.ts +21 -0
- package/dist/components/builder/builder-store.js +22 -0
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/canvas-commit-boundary.d.ts +22 -0
- package/dist/components/builder/canvas-commit-boundary.js +27 -0
- package/dist/components/builder/canvas-commit-boundary.js.map +1 -0
- package/dist/components/builder/canvas-rebase.d.ts +101 -0
- package/dist/components/builder/canvas-rebase.js +179 -0
- package/dist/components/builder/canvas-rebase.js.map +1 -0
- package/dist/components/builder/canvas-render-context.d.ts +6 -3
- package/dist/components/builder/canvas-render-context.js +17 -8
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/composition-field-editor.d.ts +26 -2
- package/dist/components/builder/composition-field-editor.js +46 -27
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/node-factory.js +15 -3
- package/dist/components/builder/node-factory.js.map +1 -1
- package/dist/components/builder/property-inspector.js +3 -1
- package/dist/components/builder/property-inspector.js.map +1 -1
- package/dist/components/builder/run-inspector.js +10 -6
- package/dist/components/builder/run-inspector.js.map +1 -1
- package/dist/components/cms/asset-field.d.ts +5 -2
- package/dist/components/cms/asset-field.js +34 -17
- package/dist/components/cms/asset-field.js.map +1 -1
- package/dist/components/cms/asset-picker-core.d.ts +2 -2
- package/dist/components/cms/asset-picker-core.js +2 -2
- package/dist/components/cms/asset-picker-core.js.map +1 -1
- package/dist/components/cms/asset-picker.js +6 -2
- package/dist/components/cms/asset-picker.js.map +1 -1
- package/dist/components/cms/canvas-commit.d.ts +62 -0
- package/dist/components/cms/canvas-commit.js +39 -0
- package/dist/components/cms/canvas-commit.js.map +1 -0
- package/dist/components/cms/cms-store.d.ts +21 -8
- package/dist/components/cms/cms-store.js +19 -10
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +27 -18
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/commit-policy.d.ts +6 -2
- package/dist/components/cms/commit-policy.js +1 -1
- package/dist/components/cms/commit-policy.js.map +1 -1
- package/dist/components/cms/detail-editor.js +12 -2
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/draft-commit.d.ts +16 -3
- package/dist/components/cms/draft-commit.js +3 -2
- package/dist/components/cms/draft-commit.js.map +1 -1
- package/dist/components/cms/use-document-draft.d.ts +121 -5
- package/dist/components/cms/use-document-draft.js +197 -54
- package/dist/components/cms/use-document-draft.js.map +1 -1
- package/dist/components/cms/use-live-url.d.ts +6 -1
- package/dist/components/cms/use-live-url.js +12 -9
- package/dist/components/cms/use-live-url.js.map +1 -1
- package/dist/components/collection-view/cms-collection-view-source.js +11 -5
- package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
- package/dist/components/components/transfer-dialog.js +6 -4
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/components/deployment-setup-gate.js +10 -1
- package/dist/components/deployment-setup-gate.js.map +1 -1
- package/dist/components/records/record-lens.js +3 -8
- package/dist/components/records/record-lens.js.map +1 -1
- package/dist/lib/ai/chat-state.d.ts +7 -0
- package/dist/lib/ai/chat-state.js.map +1 -1
- package/dist/lib/ai/tools.d.ts +8 -0
- package/dist/lib/ai/tools.js +4 -0
- package/dist/lib/ai/tools.js.map +1 -1
- package/dist/lib/assets/upload.d.ts +25 -7
- package/dist/lib/assets/upload.js +30 -21
- package/dist/lib/assets/upload.js.map +1 -1
- package/dist/lib/cms/published-documents.d.ts +24 -6
- package/dist/lib/cms/published-documents.js +39 -6
- package/dist/lib/cms/published-documents.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +25 -105
- package/dist/lib/cms-views/view-model.js +36 -103
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/collection-view/__fixtures__/document-writes.js +1 -0
- package/dist/lib/collection-view/__fixtures__/document-writes.js.map +1 -1
- package/dist/lib/collection-view/collection-view-source.d.ts +21 -8
- package/dist/lib/collection-view/collection-view-source.js.map +1 -1
- package/dist/lib/data/collection-content.js +5 -0
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/engine-version.d.ts +1 -1
- package/dist/lib/engine-version.js +1 -1
- package/dist/lib/engine-version.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +20 -4
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +16 -1
- package/dist/lib/i18n/messages.en.js +22 -5
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/cms-persistence.d.ts +80 -35
- package/dist/lib/operations/cms-persistence.js +348 -85
- package/dist/lib/operations/cms-persistence.js.map +1 -1
- package/dist/lib/operations/config-manifest-lookup.d.ts +1 -1
- package/dist/lib/operations/config-manifest-lookup.js +3 -3
- package/dist/lib/operations/config-manifest-lookup.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.js +54 -8
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persistence/deployment-readiness.js +14 -11
- package/dist/lib/persistence/deployment-readiness.js.map +1 -1
- package/dist/lib/persona/labels.js +2 -0
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/query/index.d.ts +2 -2
- package/dist/lib/query/index.js +1 -1
- package/dist/lib/query/index.js.map +1 -1
- package/dist/lib/query/operations-query.d.ts +26 -0
- package/dist/lib/query/operations-query.js +12 -0
- package/dist/lib/query/operations-query.js.map +1 -1
- package/dist/lib/registry/index.js +2 -2
- package/dist/lib/registry/index.js.map +1 -1
- package/dist/lib/registry/preview.d.ts +10 -0
- package/dist/lib/registry/preview.js +13 -4
- package/dist/lib/registry/preview.js.map +1 -1
- package/dist/lib/routes/route-issues.js +6 -3
- package/dist/lib/routes/route-issues.js.map +1 -1
- package/dist/lib/workspace/workspace-source.js +6 -7
- package/dist/lib/workspace/workspace-source.js.map +1 -1
- package/dist/previews/entry-node.d.ts +14 -0
- package/dist/previews/entry-node.js +22 -0
- package/dist/previews/entry-node.js.map +1 -0
- package/dist/previews/harness.js +2 -16
- package/dist/previews/harness.js.map +1 -1
- 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`
|
|
88
|
-
*
|
|
89
|
-
* the
|
|
90
|
-
*
|
|
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(
|
|
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:
|
|
112
|
+
}, dimensions: ImageDimensions | undefined, variants: GeneratedVariants, embedded: EmbeddedAssetWrite): AssetWriteInput;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
write = replaceWrite(
|
|
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
|
|
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
|
|
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`
|
|
194
|
-
*
|
|
195
|
-
* the
|
|
196
|
-
*
|
|
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(
|
|
199
|
-
const brings = Object.keys(embedded).length > 0;
|
|
200
|
-
const
|
|
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:
|
|
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 ? {
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
46
|
-
return
|
|
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":"
|
|
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
|
|
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
|
|
21
|
-
export
|
|
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
|
-
|
|
66
|
-
desc: "desc";
|
|
42
|
+
direction: z.ZodDefault<z.ZodEnum<{
|
|
67
43
|
asc: "asc";
|
|
68
|
-
|
|
69
|
-
|
|
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:
|
|
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
|
-
*
|
|
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
|
|
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.
|