@elytracms/studio 0.0.91 → 0.0.93
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 +127 -0
- package/convex/asset-replace.test.ts +168 -0
- package/convex/asset-revalidate.test.ts +6 -6
- package/convex/assets.ts +41 -5
- package/convex/cms.ts +537 -177
- package/convex/contract-parity.test.ts +173 -19
- package/convex/delivery.ts +26 -13
- package/convex/document-revalidate.test.ts +17 -16
- package/convex/orphan-hydration.test.ts +116 -0
- package/convex/presence-cost.test.ts +89 -0
- package/convex/rail-write-parity.test.ts +17 -14
- package/convex/referenceMaintenance.ts +15 -15
- package/convex/schema.ts +70 -5
- package/convex/triggers.ts +21 -8
- package/convex/write-limits.ts +1 -1
- package/dist/components/ai/ai-chat-panel.js +33 -22
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/ai/proposal-card.js +2 -1
- package/dist/components/ai/proposal-card.js.map +1 -1
- package/dist/components/ai/translate-document-action.d.ts +8 -2
- 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 +14 -8
- package/dist/components/ai/translate-document-dialog.js.map +1 -1
- package/dist/components/ai/translate-skipped.d.ts +19 -0
- package/dist/components/ai/translate-skipped.js +46 -0
- package/dist/components/ai/translate-skipped.js.map +1 -0
- package/dist/components/app-views/default-modules.d.ts +23 -6
- package/dist/components/app-views/default-modules.js +33 -14
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/assets/asset-detail.js +18 -8
- package/dist/components/assets/asset-detail.js.map +1 -1
- package/dist/components/assets/asset-metadata.d.ts +15 -4
- package/dist/components/assets/asset-metadata.js +15 -2
- package/dist/components/assets/asset-metadata.js.map +1 -1
- package/dist/components/assets/asset-search.js +4 -1
- package/dist/components/assets/asset-search.js.map +1 -1
- package/dist/components/assets/asset-selection.d.ts +0 -8
- package/dist/components/assets/asset-selection.js +4 -23
- package/dist/components/assets/asset-selection.js.map +1 -1
- package/dist/components/assets/asset-thumb.js +3 -2
- package/dist/components/assets/asset-thumb.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.d.ts +3 -3
- package/dist/components/assets/assets-collection-view-source.js +9 -5
- package/dist/components/assets/assets-collection-view-source.js.map +1 -1
- package/dist/components/assets/assets-store.d.ts +14 -0
- package/dist/components/assets/assets-store.js +23 -1
- package/dist/components/assets/assets-store.js.map +1 -1
- package/dist/components/assets/replace-file.d.ts +22 -0
- package/dist/components/assets/replace-file.js +59 -0
- package/dist/components/assets/replace-file.js.map +1 -0
- package/dist/components/builder/blocks-model.d.ts +14 -26
- package/dist/components/builder/blocks-model.js +35 -41
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/blocks-panel.d.ts +7 -3
- package/dist/components/builder/blocks-panel.js +6 -5
- package/dist/components/builder/blocks-panel.js.map +1 -1
- package/dist/components/builder/builder-store.d.ts +126 -30
- package/dist/components/builder/builder-store.js +169 -67
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/canvas-body.d.ts +30 -0
- package/dist/components/builder/canvas-body.js +38 -0
- package/dist/components/builder/canvas-body.js.map +1 -0
- package/dist/components/builder/canvas-chrome-css.js +7 -0
- package/dist/components/builder/canvas-chrome-css.js.map +1 -1
- package/dist/components/builder/canvas-dnd.d.ts +7 -7
- package/dist/components/builder/canvas-dnd.js +5 -5
- package/dist/components/builder/canvas-dnd.js.map +1 -1
- package/dist/components/builder/canvas-render-context.d.ts +30 -22
- package/dist/components/builder/canvas-render-context.js +44 -44
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/canvas-runs.d.ts +45 -0
- package/dist/components/builder/canvas-runs.js +51 -0
- package/dist/components/builder/canvas-runs.js.map +1 -0
- package/dist/components/builder/canvas.d.ts +5 -4
- package/dist/components/builder/canvas.js +34 -6
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/builder/clipboard-actions.d.ts +15 -0
- package/dist/components/builder/clipboard-actions.js +50 -13
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/builder/composition-field-editor.d.ts +10 -16
- package/dist/components/builder/composition-field-editor.js +30 -40
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/composition-patches.d.ts +7 -0
- package/dist/components/builder/composition-patches.js +256 -0
- package/dist/components/builder/composition-patches.js.map +1 -0
- package/dist/components/builder/delete-key.d.ts +5 -0
- package/dist/components/builder/delete-key.js +4 -0
- package/dist/components/builder/delete-key.js.map +1 -1
- package/dist/components/builder/insert-context.d.ts +17 -25
- package/dist/components/builder/insert-context.js +45 -92
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/page-tree.d.ts +3 -3
- package/dist/components/builder/page-tree.js +3 -3
- package/dist/components/builder/page-tree.js.map +1 -1
- package/dist/components/builder/placement-refusal.d.ts +18 -0
- package/dist/components/builder/placement-refusal.js +46 -0
- package/dist/components/builder/placement-refusal.js.map +1 -0
- package/dist/components/builder/property-inspector.js +4 -0
- package/dist/components/builder/property-inspector.js.map +1 -1
- package/dist/components/builder/reuse-context.js +65 -26
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/builder/reuse.d.ts +79 -8
- package/dist/components/builder/reuse.js +71 -23
- package/dist/components/builder/reuse.js.map +1 -1
- package/dist/components/builder/run-inspector.d.ts +11 -0
- package/dist/components/builder/run-inspector.js +33 -0
- package/dist/components/builder/run-inspector.js.map +1 -0
- package/dist/components/cms/asset-field.d.ts +38 -1
- package/dist/components/cms/asset-field.js +182 -58
- package/dist/components/cms/asset-field.js.map +1 -1
- package/dist/components/cms/asset-picker-core.d.ts +16 -0
- package/dist/components/cms/asset-picker-core.js +20 -0
- package/dist/components/cms/asset-picker-core.js.map +1 -1
- package/dist/components/cms/asset-picker.js +12 -18
- package/dist/components/cms/asset-picker.js.map +1 -1
- package/dist/components/cms/bulk-actions.d.ts +4 -20
- package/dist/components/cms/bulk-actions.js +3 -24
- package/dist/components/cms/bulk-actions.js.map +1 -1
- package/dist/components/cms/cell-edit.d.ts +5 -5
- package/dist/components/cms/cell-edit.js.map +1 -1
- package/dist/components/cms/cell.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +87 -45
- package/dist/components/cms/cms-store.js +347 -203
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +7 -2
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/commit-policy.d.ts +24 -0
- package/dist/components/cms/commit-policy.js +37 -0
- package/dist/components/cms/commit-policy.js.map +1 -0
- package/dist/components/cms/detail-editor.js +83 -137
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/document-diff-view.d.ts +3 -1
- package/dist/components/cms/document-diff-view.js +12 -7
- package/dist/components/cms/document-diff-view.js.map +1 -1
- package/dist/components/cms/draft-commit.d.ts +70 -0
- package/dist/components/cms/draft-commit.js +82 -0
- package/dist/components/cms/draft-commit.js.map +1 -0
- package/dist/components/cms/duplicate-document.d.ts +11 -13
- package/dist/components/cms/duplicate-document.js +26 -34
- package/dist/components/cms/duplicate-document.js.map +1 -1
- package/dist/components/cms/field-anchors.d.ts +7 -4
- package/dist/components/cms/field-anchors.js +9 -7
- package/dist/components/cms/field-anchors.js.map +1 -1
- package/dist/components/cms/field-collaboration.d.ts +47 -0
- package/dist/components/cms/field-collaboration.js +87 -0
- package/dist/components/cms/field-collaboration.js.map +1 -0
- package/dist/components/cms/field-control-lib.d.ts +22 -1
- package/dist/components/cms/field-control-lib.js +33 -4
- package/dist/components/cms/field-control-lib.js.map +1 -1
- package/dist/components/cms/field-control.js +17 -14
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/gate-validation.d.ts +21 -30
- package/dist/components/cms/gate-validation.js +11 -22
- package/dist/components/cms/gate-validation.js.map +1 -1
- package/dist/components/cms/object-array-field.js +4 -4
- package/dist/components/cms/object-array-field.js.map +1 -1
- package/dist/components/cms/prop-fields.d.ts +7 -2
- package/dist/components/cms/prop-fields.js +13 -6
- package/dist/components/cms/prop-fields.js.map +1 -1
- package/dist/components/cms/reference-confirm.js +10 -4
- package/dist/components/cms/reference-confirm.js.map +1 -1
- package/dist/components/cms/rich-text/embed-picker.d.ts +1 -1
- package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
- package/dist/components/cms/rich-text/insert.d.ts +20 -9
- package/dist/components/cms/rich-text/insert.js +25 -17
- package/dist/components/cms/rich-text/insert.js.map +1 -1
- package/dist/components/cms/use-document-draft.d.ts +96 -0
- package/dist/components/cms/use-document-draft.js +283 -0
- package/dist/components/cms/use-document-draft.js.map +1 -0
- package/dist/components/cms/use-document-versions.js +2 -2
- package/dist/components/cms/use-document-versions.js.map +1 -1
- package/dist/components/cms/use-live-url.d.ts +6 -12
- package/dist/components/cms/use-live-url.js +16 -15
- package/dist/components/cms/use-live-url.js.map +1 -1
- package/dist/components/cms/use-publish-controller.d.ts +13 -18
- package/dist/components/cms/use-publish-controller.js +24 -41
- package/dist/components/cms/use-publish-controller.js.map +1 -1
- package/dist/components/cms/use-published-perspective.d.ts +2 -3
- package/dist/components/cms/use-published-perspective.js +4 -3
- package/dist/components/cms/use-published-perspective.js.map +1 -1
- package/dist/components/cms/versions-pane.js +8 -1
- package/dist/components/cms/versions-pane.js.map +1 -1
- package/dist/components/collection-view/cms-collection-view-source.js +24 -7
- package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
- package/dist/components/components/transfer-dialog.js +28 -23
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/components/components/transfer.d.ts +31 -21
- package/dist/components/components/transfer.js +49 -35
- package/dist/components/components/transfer.js.map +1 -1
- package/dist/components/project-list.js.map +1 -1
- package/dist/components/project-session.d.ts +6 -3
- package/dist/components/project-session.js +14 -8
- package/dist/components/project-session.js.map +1 -1
- package/dist/components/project-switcher.js.map +1 -1
- package/dist/components/rail-view-source.js +2 -1
- package/dist/components/rail-view-source.js.map +1 -1
- package/dist/components/section-nav.js +1 -1
- package/dist/components/section-nav.js.map +1 -1
- package/dist/lib/ai/apply-content.js +56 -116
- package/dist/lib/ai/apply-content.js.map +1 -1
- package/dist/lib/ai/chat-context.d.ts +8 -0
- package/dist/lib/ai/chat-context.js +37 -6
- package/dist/lib/ai/chat-context.js.map +1 -1
- package/dist/lib/ai/chat-state.d.ts +14 -1
- package/dist/lib/ai/chat-state.js.map +1 -1
- package/dist/lib/ai/constraints.d.ts +20 -5
- package/dist/lib/ai/constraints.js +125 -33
- package/dist/lib/ai/constraints.js.map +1 -1
- package/dist/lib/ai/fake-provider.d.ts +16 -3
- package/dist/lib/ai/fake-provider.js +22 -6
- package/dist/lib/ai/fake-provider.js.map +1 -1
- package/dist/lib/ai/index.d.ts +2 -0
- package/dist/lib/ai/index.js +2 -0
- package/dist/lib/ai/index.js.map +1 -1
- package/dist/lib/ai/leash.d.ts +34 -0
- package/dist/lib/ai/leash.js +28 -0
- package/dist/lib/ai/leash.js.map +1 -0
- package/dist/lib/ai/provenance.d.ts +1 -1
- package/dist/lib/ai/provenance.js.map +1 -1
- package/dist/lib/ai/rich-text-input.d.ts +8 -8
- package/dist/lib/ai/rich-text-input.js +2 -1
- package/dist/lib/ai/rich-text-input.js.map +1 -1
- package/dist/lib/ai/rich-text-translate.d.ts +27 -4
- package/dist/lib/ai/rich-text-translate.js +28 -10
- package/dist/lib/ai/rich-text-translate.js.map +1 -1
- package/dist/lib/ai/shape.d.ts +13 -13
- package/dist/lib/ai/shape.js +69 -93
- package/dist/lib/ai/shape.js.map +1 -1
- package/dist/lib/ai/tools.d.ts +36 -41
- package/dist/lib/ai/tools.js +85 -87
- package/dist/lib/ai/tools.js.map +1 -1
- package/dist/lib/ai/translate-document.d.ts +39 -5
- package/dist/lib/ai/translate-document.js +160 -51
- package/dist/lib/ai/translate-document.js.map +1 -1
- package/dist/lib/ai/types.d.ts +34 -23
- package/dist/lib/ai/types.js.map +1 -1
- package/dist/lib/ai/update-fields.d.ts +21 -12
- package/dist/lib/ai/update-fields.js +118 -177
- package/dist/lib/ai/update-fields.js.map +1 -1
- package/dist/lib/ai/writer-form.d.ts +116 -0
- package/dist/lib/ai/writer-form.js +429 -0
- package/dist/lib/ai/writer-form.js.map +1 -0
- package/dist/lib/ai-current-document.d.ts +11 -11
- package/dist/lib/ai-current-document.js +4 -4
- package/dist/lib/ai-current-document.js.map +1 -1
- package/dist/lib/assets/upload.d.ts +31 -2
- package/dist/lib/assets/upload.js +61 -6
- package/dist/lib/assets/upload.js.map +1 -1
- package/dist/lib/clipboard/os-clipboard.d.ts +10 -2
- package/dist/lib/clipboard/os-clipboard.js +8 -4
- package/dist/lib/clipboard/os-clipboard.js.map +1 -1
- package/dist/lib/clipboard/paste-target.d.ts +18 -9
- package/dist/lib/clipboard/paste-target.js +8 -7
- package/dist/lib/clipboard/paste-target.js.map +1 -1
- package/dist/lib/cms/field-patch.d.ts +15 -0
- package/dist/lib/cms/field-patch.js +25 -0
- package/dist/lib/cms/field-patch.js.map +1 -0
- package/dist/lib/cms/published-documents.d.ts +1 -1
- package/dist/lib/cms/published-documents.js +1 -1
- package/dist/lib/cms/published-documents.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +3 -3
- package/dist/lib/collection-view/__fixtures__/document-writes.d.ts +6 -0
- package/dist/lib/collection-view/__fixtures__/document-writes.js +23 -0
- package/dist/lib/collection-view/__fixtures__/document-writes.js.map +1 -0
- package/dist/lib/collection-view/collection-view-source.d.ts +69 -6
- package/dist/lib/collection-view/collection-view-source.js.map +1 -1
- package/dist/lib/data/collection-content.d.ts +4 -13
- package/dist/lib/data/collection-content.js +11 -5
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/dnd/drop-rules.d.ts +25 -31
- package/dist/lib/dnd/drop-rules.js +53 -59
- package/dist/lib/dnd/drop-rules.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 +72 -15
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +61 -12
- package/dist/lib/i18n/messages.en.js +76 -16
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/__fixtures__/write-document.d.ts +30 -0
- package/dist/lib/operations/__fixtures__/write-document.js +16 -0
- package/dist/lib/operations/__fixtures__/write-document.js.map +1 -0
- package/dist/lib/operations/cms-persistence.d.ts +124 -31
- package/dist/lib/operations/cms-persistence.js +447 -68
- package/dist/lib/operations/cms-persistence.js.map +1 -1
- package/dist/lib/operations/config-manifest-lookup.d.ts +35 -0
- package/dist/lib/operations/config-manifest-lookup.js +56 -0
- package/dist/lib/operations/config-manifest-lookup.js.map +1 -0
- package/dist/lib/operations/project-runtime-config.d.ts +39 -0
- package/dist/lib/operations/project-runtime-config.js +54 -0
- package/dist/lib/operations/project-runtime-config.js.map +1 -0
- package/dist/lib/operations/publish-with-revision.d.ts +64 -0
- package/dist/lib/operations/publish-with-revision.js +66 -0
- package/dist/lib/operations/publish-with-revision.js.map +1 -0
- package/dist/lib/operations/runtime-validator.d.ts +32 -0
- package/dist/lib/operations/runtime-validator.js +18 -0
- package/dist/lib/operations/runtime-validator.js.map +1 -0
- package/dist/lib/operations/session-hydration.d.ts +31 -14
- package/dist/lib/operations/session-hydration.js +12 -16
- package/dist/lib/operations/session-hydration.js.map +1 -1
- package/dist/lib/operations/studio-operations.d.ts +59 -2
- package/dist/lib/operations/studio-operations.js +76 -14
- package/dist/lib/operations/studio-operations.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.d.ts +13 -5
- package/dist/lib/persistence/convex-adapter.js +189 -74
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persistence/convex-client.d.ts +65 -5
- package/dist/lib/persistence/convex-client.js +106 -3
- package/dist/lib/persistence/convex-client.js.map +1 -1
- package/dist/lib/persistence/index.js +17 -18
- package/dist/lib/persistence/index.js.map +1 -1
- package/dist/lib/persona/labels.js +4 -1
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/query/operations-query.d.ts +1 -1
- package/dist/lib/query/operations-query.js +1 -1
- package/dist/lib/query/operations-query.js.map +1 -1
- package/dist/lib/references/impact.d.ts +21 -15
- package/dist/lib/references/impact.js +13 -26
- package/dist/lib/references/impact.js.map +1 -1
- package/dist/lib/references/references-store.d.ts +9 -0
- package/dist/lib/references/references-store.js +24 -4
- package/dist/lib/references/references-store.js.map +1 -1
- package/dist/lib/routes/route-issues.d.ts +6 -6
- package/dist/lib/routes/route-issues.js +26 -80
- package/dist/lib/routes/route-issues.js.map +1 -1
- package/dist/lib/workspace/config-issues.js +2 -1
- package/dist/lib/workspace/config-issues.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +31 -1
- package/dist/lib/workspace/index.js +64 -17
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/lib/workspace/switcher-source.d.ts +3 -3
- package/dist/lib/workspace/switcher-source.js.map +1 -1
- package/dist/lib/workspace/workspace-source.d.ts +2 -2
- package/dist/lib/workspace/workspace-source.js +2 -1
- package/dist/lib/workspace/workspace-source.js.map +1 -1
- package/package.json +3 -3
- package/dist/components/cms/cms-composition-validators.d.ts +0 -16
- package/dist/components/cms/cms-composition-validators.js +0 -133
- package/dist/components/cms/cms-composition-validators.js.map +0 -1
- package/dist/components/cms/publish-clean.d.ts +0 -28
- package/dist/components/cms/publish-clean.js +0 -38
- package/dist/components/cms/publish-clean.js.map +0 -1
- package/dist/components/cms/use-save-mode.d.ts +0 -35
- package/dist/components/cms/use-save-mode.js +0 -54
- package/dist/components/cms/use-save-mode.js.map +0 -1
- package/dist/lib/assets/asset-placements.d.ts +0 -28
- package/dist/lib/assets/asset-placements.js +0 -83
- package/dist/lib/assets/asset-placements.js.map +0 -1
- package/dist/lib/cms/document-gate.d.ts +0 -47
- package/dist/lib/cms/document-gate.js +0 -61
- package/dist/lib/cms/document-gate.js.map +0 -1
|
@@ -5,7 +5,7 @@ import type { OperationResult, OperationsClient } from './operations-package.js'
|
|
|
5
5
|
* Persisted-session hydration (Milestone B sign-off fix, EC-177 gap 1).
|
|
6
6
|
*
|
|
7
7
|
* Against a persistent backend (Convex mode) a studio session must BOOT from
|
|
8
|
-
* what is persisted — the
|
|
8
|
+
* what is persisted — the CMS documents (+ asset records) — and
|
|
9
9
|
* fall back to the deterministic fixtures only when the project has nothing
|
|
10
10
|
* persisted yet (first run). Before this, every session seeded from fixtures,
|
|
11
11
|
* so reloads hid the editor's work.
|
|
@@ -25,10 +25,19 @@ import type { OperationResult, OperationsClient } from './operations-package.js'
|
|
|
25
25
|
* SAME loader (over the seeded in-memory adapter, through operations — AD-17)
|
|
26
26
|
* to hydrate the canvas/preview content session; only the render-gating is
|
|
27
27
|
* Convex-specific.
|
|
28
|
+
*
|
|
29
|
+
* **EC-583 / EC-559 — content only, never a schema.** This loader used to read the
|
|
30
|
+
* schema back through `schema.get`, and `HydratedCms` carried it as `collections`. In
|
|
31
|
+
* the playground that record was the in-memory adapter's JSON projection (config
|
|
32
|
+
* functions stripped), and it WON the CMS surfaces' schema rung over the config — so
|
|
33
|
+
* `validate`, `form.hidden` predicates and function `titleField`s were undefined in
|
|
34
|
+
* the playground and every playground check of them was a false pass. The store holds
|
|
35
|
+
* no schema now: the caller hands the DECLARED collections in (the resolved config),
|
|
36
|
+
* hydration only uses them to keep the declared collections' documents and count the
|
|
37
|
+
* rest, and the CMS surfaces read their schema from the config in both modes.
|
|
28
38
|
*/
|
|
29
|
-
/** The persisted CMS content of a project: the
|
|
39
|
+
/** The persisted CMS content of a project: the documents of its declared collections. */
|
|
30
40
|
export interface HydratedCms {
|
|
31
|
-
collections: CollectionDef[];
|
|
32
41
|
/** Documents of the declared collections only — see {@link orphans} for the rest. */
|
|
33
42
|
documents: CmsDocument[];
|
|
34
43
|
/**
|
|
@@ -85,6 +94,14 @@ export interface HydratedCms {
|
|
|
85
94
|
* pre-EC-449 fixtures/callers stay valid; `loadHydratedCms` always sets it.
|
|
86
95
|
*/
|
|
87
96
|
changedKeys?: string[];
|
|
97
|
+
/**
|
|
98
|
+
* EC-595 A2 — each document's REVISION, keyed by `documentKey`: the base the session's
|
|
99
|
+
* edits are made against. The persister checks a field's patch against the changes
|
|
100
|
+
* since the revision the user's view was hydrated at, so a colleague's earlier change
|
|
101
|
+
* of that field is refused per path instead of overwritten (Codex P06). Optional for
|
|
102
|
+
* fixture sessions; `loadHydratedCms` always sets it.
|
|
103
|
+
*/
|
|
104
|
+
revisions?: Record<string, number>;
|
|
88
105
|
/**
|
|
89
106
|
* The project's asset records (the `assets` table — NOT cms `asset`-collection
|
|
90
107
|
* documents). The preview content engine wires these into its lookup so listing/
|
|
@@ -95,22 +112,22 @@ export interface HydratedCms {
|
|
|
95
112
|
*/
|
|
96
113
|
assets: AssetRecord[];
|
|
97
114
|
}
|
|
98
|
-
/** Everything a Convex-mode session boots from. `null` fields mean "nothing
|
|
115
|
+
/** Everything a Convex-mode session boots from. `null` fields mean "nothing to hydrate". */
|
|
99
116
|
export interface HydratedProjectSession {
|
|
100
|
-
/** Persisted CMS
|
|
117
|
+
/** Persisted CMS documents, or `null` when the project declares no collections. */
|
|
101
118
|
cms: HydratedCms | null;
|
|
102
119
|
}
|
|
103
120
|
/**
|
|
104
|
-
* Load the persisted CMS content state
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
121
|
+
* Load the persisted CMS content state for the project's DECLARED collections (the
|
|
122
|
+
* resolved config's — EC-583: the store holds no schema). `null` when the config
|
|
123
|
+
* declares no collections: there is nothing a CMS surface could show the documents
|
|
124
|
+
* under (every stored row would be an orphan).
|
|
108
125
|
*/
|
|
109
|
-
export declare function loadHydratedCms(operations: OperationsClient, projectId: string): Promise<OperationResult<HydratedCms | null>>;
|
|
126
|
+
export declare function loadHydratedCms(operations: OperationsClient, projectId: string, collections: readonly CollectionDef[]): Promise<OperationResult<HydratedCms | null>>;
|
|
110
127
|
/**
|
|
111
128
|
* Load everything a project session hydrates from. Post-EC-287 that is just the
|
|
112
|
-
* persisted CMS content (
|
|
113
|
-
*
|
|
114
|
-
*
|
|
129
|
+
* persisted CMS content (documents + assets): the project graph and its publish
|
|
130
|
+
* path are gone, so there is no graph revision or graph-publishing state to load.
|
|
131
|
+
* Per-document `publishedVersion` (EC-224) is the only published surface.
|
|
115
132
|
*/
|
|
116
|
-
export declare function loadProjectSession(operations: OperationsClient, projectId: string): Promise<OperationResult<HydratedProjectSession>>;
|
|
133
|
+
export declare function loadProjectSession(operations: OperationsClient, projectId: string, collections: readonly CollectionDef[]): Promise<OperationResult<HydratedProjectSession>>;
|
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
import { documentKey, orphanedCollections } from '@elytracms/core/cms-core';
|
|
2
2
|
/**
|
|
3
|
-
* Load the persisted CMS content state
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* Load the persisted CMS content state for the project's DECLARED collections (the
|
|
4
|
+
* resolved config's — EC-583: the store holds no schema). `null` when the config
|
|
5
|
+
* declares no collections: there is nothing a CMS surface could show the documents
|
|
6
|
+
* under (every stored row would be an orphan).
|
|
7
7
|
*/
|
|
8
|
-
export async function loadHydratedCms(operations, projectId) {
|
|
9
|
-
const schema = await operations.schema.get({ projectId });
|
|
10
|
-
if (!schema.ok) {
|
|
11
|
-
return schema.error.code === 'not-found' ? { ok: true, value: null } : schema;
|
|
12
|
-
}
|
|
13
|
-
const collections = schema.value.collections;
|
|
8
|
+
export async function loadHydratedCms(operations, projectId, collections) {
|
|
14
9
|
if (collections.length === 0)
|
|
15
10
|
return { ok: true, value: null };
|
|
16
11
|
const documents = await operations.documents.list({ projectId });
|
|
@@ -35,7 +30,6 @@ export async function loadHydratedCms(operations, projectId) {
|
|
|
35
30
|
return {
|
|
36
31
|
ok: true,
|
|
37
32
|
value: {
|
|
38
|
-
collections,
|
|
39
33
|
orphans,
|
|
40
34
|
// The live row hydrates AS PERSISTED — a continuous draft (EC-224). We no
|
|
41
35
|
// longer stamp a `state:'published'` mirror: published-ness is the version
|
|
@@ -60,18 +54,20 @@ export async function loadHydratedCms(operations, projectId) {
|
|
|
60
54
|
changedKeys: contentRecords
|
|
61
55
|
.filter((record) => record.hasUnpublishedChanges === true)
|
|
62
56
|
.map((record) => documentKey(record.document)),
|
|
57
|
+
// EC-595 A2: the revision each document was hydrated at — the per-patch base.
|
|
58
|
+
revisions: Object.fromEntries(contentRecords.map((record) => [documentKey(record.document), record.revision])),
|
|
63
59
|
assets: assets.ok ? assets.value : [],
|
|
64
60
|
},
|
|
65
61
|
};
|
|
66
62
|
}
|
|
67
63
|
/**
|
|
68
64
|
* Load everything a project session hydrates from. Post-EC-287 that is just the
|
|
69
|
-
* persisted CMS content (
|
|
70
|
-
*
|
|
71
|
-
*
|
|
65
|
+
* persisted CMS content (documents + assets): the project graph and its publish
|
|
66
|
+
* path are gone, so there is no graph revision or graph-publishing state to load.
|
|
67
|
+
* Per-document `publishedVersion` (EC-224) is the only published surface.
|
|
72
68
|
*/
|
|
73
|
-
export async function loadProjectSession(operations, projectId) {
|
|
74
|
-
const cms = await loadHydratedCms(operations, projectId);
|
|
69
|
+
export async function loadProjectSession(operations, projectId, collections) {
|
|
70
|
+
const cms = await loadHydratedCms(operations, projectId, collections);
|
|
75
71
|
if (!cms.ok)
|
|
76
72
|
return cms;
|
|
77
73
|
return { ok: true, value: { cms: cms.value } };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-hydration.js","sourceRoot":"","sources":["../../../src/lib/operations/session-hydration.ts"],"names":[],"mappings":"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","sourcesContent":["import { documentKey, orphanedCollections } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef, OrphanedCollection } from '@elytracms/core/cms-core'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport type { OperationResult, OperationsClient } from './operations-package'\n\n/**\n * Persisted-session hydration (Milestone B sign-off fix, EC-177 gap 1).\n *\n * Against a persistent backend (Convex mode) a studio session must BOOT from\n * what is persisted — the saved CMS schema + documents (+ asset records) — and\n * fall back to the deterministic fixtures only when the project has nothing\n * persisted yet (first run). Before this, every session seeded from fixtures,\n * so reloads hid the editor's work.\n *\n * EC-287: the DB-resident project graph and its publish path are retired —\n * layouts/config/schema/components are CODE, the DB holds content only. A\n * session no longer hydrates a graph revision or graph-publishing state; CMS\n * content (with per-document `publishedVersion`, EC-224) is the only persisted\n * surface a session boots from.\n *\n * This module is the pure load half: read-only `@elytracms/core/operations` queries\n * composed into one hydration result. `not-found` is a *normal* answer (the\n * first-run fallback); every other failure propagates as the structured\n * operation error so the gate renders it — never a silent fixture fallback.\n * The React gate that applies it lives in\n * `components/studio/project-session.tsx`. EC-361: the playground calls the\n * SAME loader (over the seeded in-memory adapter, through operations — AD-17)\n * to hydrate the canvas/preview content session; only the render-gating is\n * Convex-specific.\n */\n\n/** The persisted CMS content of a project: the declared schema + the documents of its collections. */\nexport interface HydratedCms {\n collections: CollectionDef[]\n /** Documents of the declared collections only — see {@link orphans} for the rest. */\n documents: CmsDocument[]\n /**\n * EC-606 — the stored rows the session does NOT carry: every collection id that\n * stored documents use but the project does not declare, with its row count and\n * how many of those rows are published. Sorted by collection id; empty when every\n * row belongs to a declared collection.\n *\n * Hydration keeps only declared collections' documents (there is no schema to edit\n * the rest under). Before EC-606 the others were dropped right here with no count,\n * so renaming a collection id in config made its documents vanish without a trace.\n * Counted by the one core counter (`orphanedCollections`), which `elytra doctor`\n * also runs over the published snapshot. The rail foot reports this list through\n * the content session (`configureContentSession(contentSessionOf(cms))`).\n *\n * EC-606 B1: there is no \"system collection\" exemption any more. The only one left\n * was `cms-saved-views`, the editor saved views EC-246b retired; nothing reads\n * those rows, so they are counted like any other undeclared collection.\n */\n orphans: OrphanedCollection[]\n /**\n * Published document keys (EC-224), via the version pointer: a record is\n * published iff `publishedVersion` is set. The session boots its\n * `publishedKeys` from this — published-ness is the pointer, nothing else.\n */\n publishedKeys: string[]\n /**\n * Per-document last-write timestamps (EC-445), keyed by `documentKey`\n * (`collection:id`) → the persisted `cmsDocuments.updatedAt` ISO string. Backs the\n * `$updatedAt` list column; the session seeds its `documentUpdatedAt` map from this.\n * Optional so pre-EC-445 fixtures/callers stay valid (the session falls back to a\n * session-start stamp for the seeded documents); `loadHydratedCms` always sets it.\n */\n timestamps?: Record<string, string>\n /**\n * Per-document FIRST-publish instants (EC-447), keyed by `documentKey`\n * (`collection:id`) → the persisted `cmsDocuments.publishedAt` ISO string. Present\n * only for documents that carry one (currently published, with a recorded publish\n * instant); the session seeds its `documentPublishedAt` map from this. Backs the\n * `$publishedAt` list column. UNLIKE {@link timestamps} there is no session-start\n * fallback — an absent entry is a real \"no publish date\" (EmDash), and the store\n * re-stamps/clears it authoritatively on publish/unpublish. Optional so pre-EC-447\n * fixtures/callers stay valid; `loadHydratedCms` always sets it.\n */\n publishTimestamps?: Record<string, string>\n /**\n * Document keys whose working draft carries UNPUBLISHED CHANGES (EC-449) — the\n * persisted `cmsDocuments.hasUnpublishedChanges === true` set, keyed like\n * {@link publishedKeys}. Backs the amber \"changed\" state of the `$state` list\n * dot; the session seeds its changed-set from this and re-stamps it\n * authoritatively on save/publish/unpublish from the returned records. Only\n * `true` flags contribute — an absent entry means \"no unpublished changes\"\n * (`false` or not yet materialized both render the same dot). Optional so\n * pre-EC-449 fixtures/callers stay valid; `loadHydratedCms` always sets it.\n */\n changedKeys?: string[]\n /**\n * The project's asset records (the `assets` table — NOT cms `asset`-collection\n * documents). The preview content engine wires these into its lookup so listing/\n * relation results resolve their `asset`-typed FIELDS (e.g. a ProductGrid product's\n * `gallery`) to delivery shape — matching what the renderer's `resolveAsset` already\n * does for static `asset` props (EC-262). Without them, the studio canvas renders\n * such nested images blank even though the documents reference real assets.\n */\n assets: AssetRecord[]\n}\n\n/** Everything a Convex-mode session boots from. `null` fields mean \"nothing persisted\" (first run). */\nexport interface HydratedProjectSession {\n /** Persisted CMS schema + documents, or `null` when no content schema was ever saved. */\n cms: HydratedCms | null\n}\n\n/**\n * Load the persisted CMS content state. `null` when the project has no\n * persisted content schema, or one that declares no collections (first run —\n * the session seeds fixtures and the persister's baseline will persist them on\n * the first edit).\n */\nexport async function loadHydratedCms(\n operations: OperationsClient,\n projectId: string,\n): Promise<OperationResult<HydratedCms | null>> {\n const schema = await operations.schema.get({ projectId })\n if (!schema.ok) {\n return schema.error.code === 'not-found' ? { ok: true, value: null } : schema\n }\n const collections = schema.value.collections\n if (collections.length === 0) return { ok: true, value: null }\n const documents = await operations.documents.list({ projectId })\n if (!documents.ok) return documents\n // The project's asset records (assets table), wired into the preview lookup so\n // listing/relation results resolve their nested `asset` fields (EC-262). Non-fatal:\n // a failed asset list degrades previews to no images, never blocks the session.\n const assets = await operations.assets.list({ projectId })\n // EC-224: a record is published when its version pointer is set — the same\n // gate delivery reads. There is no flat-state fallback (the field is gone).\n const isRecordPublished = (record: (typeof documents.value)[number]) =>\n record.publishedVersion !== undefined\n // The declared set, read once: the session carries the documents of these\n // collections, and every other stored row is COUNTED (EC-606), never dropped\n // silently.\n const declaredIds = new Set(collections.map((collection) => collection.id))\n const contentRecords = documents.value.filter((record) =>\n declaredIds.has(record.document.collection),\n )\n const orphans = orphanedCollections(\n documents.value.map((record) => ({\n collection: record.document.collection,\n published: isRecordPublished(record),\n })),\n declaredIds,\n )\n return {\n ok: true,\n value: {\n collections,\n orphans,\n // The live row hydrates AS PERSISTED — a continuous draft (EC-224). We no\n // longer stamp a `state:'published'` mirror: published-ness is the version\n // pointer (`publishedKeys`), which every studio reader now consults (the\n // table badge/$state via `isPublished`, the preview engine via the pin).\n documents: contentRecords.map((record) => record.document),\n publishedKeys: contentRecords\n .filter(isRecordPublished)\n .map((record) => documentKey(record.document)),\n // EC-445: carry each record's `updatedAt` onto the session for the $updatedAt\n // column — a per-document metadata accessor, exactly like `publishedKeys`.\n timestamps: Object.fromEntries(\n contentRecords.map((record) => [documentKey(record.document), record.updatedAt]),\n ),\n // EC-447: carry each PUBLISHED record's materialized `publishedAt` for the\n // $publishedAt column. Only records that actually have one contribute (no\n // session-start fallback) — an absent entry is a real \"no publish date\".\n publishTimestamps: Object.fromEntries(\n contentRecords\n .filter((record) => record.publishedAt !== undefined)\n .map((record) => [documentKey(record.document), record.publishedAt as string]),\n ),\n // EC-449: the documents whose draft carries unpublished changes (the amber\n // list dot) — only materialized `true` flags contribute (absent/false both\n // mean \"not changed\"; the flag only exists on published docs).\n changedKeys: contentRecords\n .filter((record) => record.hasUnpublishedChanges === true)\n .map((record) => documentKey(record.document)),\n assets: assets.ok ? assets.value : [],\n },\n }\n}\n\n/**\n * Load everything a project session hydrates from. Post-EC-287 that is just the\n * persisted CMS content (schema + documents + assets): the project graph and its\n * publish path are gone, so there is no graph revision or graph-publishing state\n * to load. Per-document `publishedVersion` (EC-224) is the only published surface.\n */\nexport async function loadProjectSession(\n operations: OperationsClient,\n projectId: string,\n): Promise<OperationResult<HydratedProjectSession>> {\n const cms = await loadHydratedCms(operations, projectId)\n if (!cms.ok) return cms\n return { ok: true, value: { cms: cms.value } }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"session-hydration.js","sourceRoot":"","sources":["../../../src/lib/operations/session-hydration.ts"],"names":[],"mappings":"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","sourcesContent":["import { documentKey, orphanedCollections } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef, OrphanedCollection } from '@elytracms/core/cms-core'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport type { OperationResult, OperationsClient } from './operations-package'\n\n/**\n * Persisted-session hydration (Milestone B sign-off fix, EC-177 gap 1).\n *\n * Against a persistent backend (Convex mode) a studio session must BOOT from\n * what is persisted — the CMS documents (+ asset records) — and\n * fall back to the deterministic fixtures only when the project has nothing\n * persisted yet (first run). Before this, every session seeded from fixtures,\n * so reloads hid the editor's work.\n *\n * EC-287: the DB-resident project graph and its publish path are retired —\n * layouts/config/schema/components are CODE, the DB holds content only. A\n * session no longer hydrates a graph revision or graph-publishing state; CMS\n * content (with per-document `publishedVersion`, EC-224) is the only persisted\n * surface a session boots from.\n *\n * This module is the pure load half: read-only `@elytracms/core/operations` queries\n * composed into one hydration result. `not-found` is a *normal* answer (the\n * first-run fallback); every other failure propagates as the structured\n * operation error so the gate renders it — never a silent fixture fallback.\n * The React gate that applies it lives in\n * `components/studio/project-session.tsx`. EC-361: the playground calls the\n * SAME loader (over the seeded in-memory adapter, through operations — AD-17)\n * to hydrate the canvas/preview content session; only the render-gating is\n * Convex-specific.\n *\n * **EC-583 / EC-559 — content only, never a schema.** This loader used to read the\n * schema back through `schema.get`, and `HydratedCms` carried it as `collections`. In\n * the playground that record was the in-memory adapter's JSON projection (config\n * functions stripped), and it WON the CMS surfaces' schema rung over the config — so\n * `validate`, `form.hidden` predicates and function `titleField`s were undefined in\n * the playground and every playground check of them was a false pass. The store holds\n * no schema now: the caller hands the DECLARED collections in (the resolved config),\n * hydration only uses them to keep the declared collections' documents and count the\n * rest, and the CMS surfaces read their schema from the config in both modes.\n */\n\n/** The persisted CMS content of a project: the documents of its declared collections. */\nexport interface HydratedCms {\n /** Documents of the declared collections only — see {@link orphans} for the rest. */\n documents: CmsDocument[]\n /**\n * EC-606 — the stored rows the session does NOT carry: every collection id that\n * stored documents use but the project does not declare, with its row count and\n * how many of those rows are published. Sorted by collection id; empty when every\n * row belongs to a declared collection.\n *\n * Hydration keeps only declared collections' documents (there is no schema to edit\n * the rest under). Before EC-606 the others were dropped right here with no count,\n * so renaming a collection id in config made its documents vanish without a trace.\n * Counted by the one core counter (`orphanedCollections`), which `elytra doctor`\n * also runs over the published snapshot. The rail foot reports this list through\n * the content session (`configureContentSession(contentSessionOf(cms))`).\n *\n * EC-606 B1: there is no \"system collection\" exemption any more. The only one left\n * was `cms-saved-views`, the editor saved views EC-246b retired; nothing reads\n * those rows, so they are counted like any other undeclared collection.\n */\n orphans: OrphanedCollection[]\n /**\n * Published document keys (EC-224), via the version pointer: a record is\n * published iff `publishedVersion` is set. The session boots its\n * `publishedKeys` from this — published-ness is the pointer, nothing else.\n */\n publishedKeys: string[]\n /**\n * Per-document last-write timestamps (EC-445), keyed by `documentKey`\n * (`collection:id`) → the persisted `cmsDocuments.updatedAt` ISO string. Backs the\n * `$updatedAt` list column; the session seeds its `documentUpdatedAt` map from this.\n * Optional so pre-EC-445 fixtures/callers stay valid (the session falls back to a\n * session-start stamp for the seeded documents); `loadHydratedCms` always sets it.\n */\n timestamps?: Record<string, string>\n /**\n * Per-document FIRST-publish instants (EC-447), keyed by `documentKey`\n * (`collection:id`) → the persisted `cmsDocuments.publishedAt` ISO string. Present\n * only for documents that carry one (currently published, with a recorded publish\n * instant); the session seeds its `documentPublishedAt` map from this. Backs the\n * `$publishedAt` list column. UNLIKE {@link timestamps} there is no session-start\n * fallback — an absent entry is a real \"no publish date\" (EmDash), and the store\n * re-stamps/clears it authoritatively on publish/unpublish. Optional so pre-EC-447\n * fixtures/callers stay valid; `loadHydratedCms` always sets it.\n */\n publishTimestamps?: Record<string, string>\n /**\n * Document keys whose working draft carries UNPUBLISHED CHANGES (EC-449) — the\n * persisted `cmsDocuments.hasUnpublishedChanges === true` set, keyed like\n * {@link publishedKeys}. Backs the amber \"changed\" state of the `$state` list\n * dot; the session seeds its changed-set from this and re-stamps it\n * authoritatively on save/publish/unpublish from the returned records. Only\n * `true` flags contribute — an absent entry means \"no unpublished changes\"\n * (`false` or not yet materialized both render the same dot). Optional so\n * pre-EC-449 fixtures/callers stay valid; `loadHydratedCms` always sets it.\n */\n changedKeys?: string[]\n /**\n * EC-595 A2 — each document's REVISION, keyed by `documentKey`: the base the session's\n * edits are made against. The persister checks a field's patch against the changes\n * since the revision the user's view was hydrated at, so a colleague's earlier change\n * of that field is refused per path instead of overwritten (Codex P06). Optional for\n * fixture sessions; `loadHydratedCms` always sets it.\n */\n revisions?: Record<string, number>\n /**\n * The project's asset records (the `assets` table — NOT cms `asset`-collection\n * documents). The preview content engine wires these into its lookup so listing/\n * relation results resolve their `asset`-typed FIELDS (e.g. a ProductGrid product's\n * `gallery`) to delivery shape — matching what the renderer's `resolveAsset` already\n * does for static `asset` props (EC-262). Without them, the studio canvas renders\n * such nested images blank even though the documents reference real assets.\n */\n assets: AssetRecord[]\n}\n\n/** Everything a Convex-mode session boots from. `null` fields mean \"nothing to hydrate\". */\nexport interface HydratedProjectSession {\n /** Persisted CMS documents, or `null` when the project declares no collections. */\n cms: HydratedCms | null\n}\n\n/**\n * Load the persisted CMS content state for the project's DECLARED collections (the\n * resolved config's — EC-583: the store holds no schema). `null` when the config\n * declares no collections: there is nothing a CMS surface could show the documents\n * under (every stored row would be an orphan).\n */\nexport async function loadHydratedCms(\n operations: OperationsClient,\n projectId: string,\n collections: readonly CollectionDef[],\n): Promise<OperationResult<HydratedCms | null>> {\n if (collections.length === 0) return { ok: true, value: null }\n const documents = await operations.documents.list({ projectId })\n if (!documents.ok) return documents\n // The project's asset records (assets table), wired into the preview lookup so\n // listing/relation results resolve their nested `asset` fields (EC-262). Non-fatal:\n // a failed asset list degrades previews to no images, never blocks the session.\n const assets = await operations.assets.list({ projectId })\n // EC-224: a record is published when its version pointer is set — the same\n // gate delivery reads. There is no flat-state fallback (the field is gone).\n const isRecordPublished = (record: (typeof documents.value)[number]) =>\n record.publishedVersion !== undefined\n // The declared set, read once: the session carries the documents of these\n // collections, and every other stored row is COUNTED (EC-606), never dropped\n // silently.\n const declaredIds = new Set(collections.map((collection) => collection.id))\n const contentRecords = documents.value.filter((record) =>\n declaredIds.has(record.document.collection),\n )\n const orphans = orphanedCollections(\n documents.value.map((record) => ({\n collection: record.document.collection,\n published: isRecordPublished(record),\n })),\n declaredIds,\n )\n return {\n ok: true,\n value: {\n orphans,\n // The live row hydrates AS PERSISTED — a continuous draft (EC-224). We no\n // longer stamp a `state:'published'` mirror: published-ness is the version\n // pointer (`publishedKeys`), which every studio reader now consults (the\n // table badge/$state via `isPublished`, the preview engine via the pin).\n documents: contentRecords.map((record) => record.document),\n publishedKeys: contentRecords\n .filter(isRecordPublished)\n .map((record) => documentKey(record.document)),\n // EC-445: carry each record's `updatedAt` onto the session for the $updatedAt\n // column — a per-document metadata accessor, exactly like `publishedKeys`.\n timestamps: Object.fromEntries(\n contentRecords.map((record) => [documentKey(record.document), record.updatedAt]),\n ),\n // EC-447: carry each PUBLISHED record's materialized `publishedAt` for the\n // $publishedAt column. Only records that actually have one contribute (no\n // session-start fallback) — an absent entry is a real \"no publish date\".\n publishTimestamps: Object.fromEntries(\n contentRecords\n .filter((record) => record.publishedAt !== undefined)\n .map((record) => [documentKey(record.document), record.publishedAt as string]),\n ),\n // EC-449: the documents whose draft carries unpublished changes (the amber\n // list dot) — only materialized `true` flags contribute (absent/false both\n // mean \"not changed\"; the flag only exists on published docs).\n changedKeys: contentRecords\n .filter((record) => record.hasUnpublishedChanges === true)\n .map((record) => documentKey(record.document)),\n // EC-595 A2: the revision each document was hydrated at — the per-patch base.\n revisions: Object.fromEntries(\n contentRecords.map((record) => [documentKey(record.document), record.revision]),\n ),\n assets: assets.ok ? assets.value : [],\n },\n }\n}\n\n/**\n * Load everything a project session hydrates from. Post-EC-287 that is just the\n * persisted CMS content (documents + assets): the project graph and its publish\n * path are gone, so there is no graph revision or graph-publishing state to load.\n * Per-document `publishedVersion` (EC-224) is the only published surface.\n */\nexport async function loadProjectSession(\n operations: OperationsClient,\n projectId: string,\n collections: readonly CollectionDef[],\n): Promise<OperationResult<HydratedProjectSession>> {\n const cms = await loadHydratedCms(operations, projectId, collections)\n if (!cms.ok) return cms\n return { ok: true, value: { cms: cms.value } }\n}\n"]}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { PersistenceAdapter } from '@elytracms/core/persistence';
|
|
2
|
-
import type {
|
|
1
|
+
import type { DocumentListener, IdFactory, PersistenceAdapter, PresenceInput } from '@elytracms/core/persistence';
|
|
2
|
+
import type { DocumentRef } from '@elytracms/core/cms-core';
|
|
3
|
+
import type { ChangesetOperations, CollectionsResolver, DocumentValidator, LocalesResolver, ManifestLookupResolver, OperationsClient, Principal, ReferenceIndexAttachment } from './operations-package.js';
|
|
3
4
|
/**
|
|
4
5
|
* The studio's single operations bootstrap (EC-141, vision AD-7).
|
|
5
6
|
*
|
|
@@ -25,6 +26,21 @@ export interface StudioOperations {
|
|
|
25
26
|
readonly adapter: PersistenceAdapter;
|
|
26
27
|
/** EC-155 maintained-on-write index attachment (flushable in tests). */
|
|
27
28
|
readonly references: ReferenceIndexAttachment;
|
|
29
|
+
/**
|
|
30
|
+
* EC-595 binding point 7 — subscribe to ONE document (the narrow EC-309 slice): the
|
|
31
|
+
* listener gets the current row + the presence on it, then one snapshot per change
|
|
32
|
+
* (Convex: the reactive client's server push; playground: the in-memory store's own
|
|
33
|
+
* writes). An adapter capability exposed through the in-process bundle — the UI never
|
|
34
|
+
* holds a reactive Convex client. A subscription failure is ALSO emitted as a failed
|
|
35
|
+
* `documents.subscribe` event on the operations bus, so an expired session reaches
|
|
36
|
+
* EC-403's one session-expiry chokepoint like any failed operation (never a silently
|
|
37
|
+
* stopped stream). Returns the unsubscribe function.
|
|
38
|
+
*/
|
|
39
|
+
subscribeDocument(projectId: string, ref: DocumentRef, listener: DocumentListener): () => void;
|
|
40
|
+
/** EC-595 — this session's field-presence heartbeat on a document (thin presence, Stage A). */
|
|
41
|
+
reportPresence(projectId: string, ref: DocumentRef, presence: PresenceInput): Promise<void>;
|
|
42
|
+
/** EC-595 — remove this session's presence from a document. */
|
|
43
|
+
clearPresence(projectId: string, ref: DocumentRef, sessionId: string): Promise<void>;
|
|
28
44
|
}
|
|
29
45
|
export interface CreateStudioOperationsOptions {
|
|
30
46
|
/**
|
|
@@ -37,6 +53,41 @@ export interface CreateStudioOperationsOptions {
|
|
|
37
53
|
* unauthenticated users away from every operation surface.
|
|
38
54
|
*/
|
|
39
55
|
actor?: Principal;
|
|
56
|
+
/**
|
|
57
|
+
* EC-583 / EC-559 — the collection definitions on the operations context, resolved for
|
|
58
|
+
* each operation's OWN project id. Defaults to the workspace config's project with that
|
|
59
|
+
* slug (`projectCollections` — the resolved `elytra.config.ts`, functions
|
|
60
|
+
* included), read on every call: the bundle is cached per deployment KEY (EC-179) and
|
|
61
|
+
* one deployment may serve several config projects, so capturing one project's
|
|
62
|
+
* collections at build time would canonicalise another project's documents against
|
|
63
|
+
* them (operator correction to phase 1). The store holds no schema.
|
|
64
|
+
*/
|
|
65
|
+
collections?: CollectionsResolver;
|
|
66
|
+
/** EC-578 — the document-id factory (`documents.create`); UUIDs by default. */
|
|
67
|
+
idFactory?: IdFactory;
|
|
68
|
+
/**
|
|
69
|
+
* EC-595 A1 (critic correction 4) → A2 — the manifest half of the write canonicaliser
|
|
70
|
+
* on the operations context, resolved for each operation's OWN project. Defaults to a
|
|
71
|
+
* lookup over that config project's component entries (`projectManifestLookup`), so
|
|
72
|
+
* every patch / replace canonicalises a composition's props with its project's
|
|
73
|
+
* manifests — never the active project's (A1 found-but-left #1).
|
|
74
|
+
*/
|
|
75
|
+
manifestLookup?: ManifestLookupResolver;
|
|
76
|
+
/**
|
|
77
|
+
* EC-595 A2 (F23) — the publish gate's validator, bound to THIS bundle. Defaults to
|
|
78
|
+
* the runtime validator (`createRuntimeDocumentValidator`: the operation's project's
|
|
79
|
+
* config + oracles prefetched through the adapter). Tests that pin persistence
|
|
80
|
+
* routing rather than gate strength inject their own here — at construction, never
|
|
81
|
+
* through a mount.
|
|
82
|
+
*/
|
|
83
|
+
validateDocument?: DocumentValidator;
|
|
84
|
+
/**
|
|
85
|
+
* EC-585 — the locale config on the operations context, per project: the view
|
|
86
|
+
* `documents.publish` runs its hidden-clear in. Defaults to that config project's
|
|
87
|
+
* `locales` (`projectLocaleConfig`) — the same project, read the same way, as the
|
|
88
|
+
* collections, the manifests and the gate's validator.
|
|
89
|
+
*/
|
|
90
|
+
locales?: LocalesResolver;
|
|
40
91
|
}
|
|
41
92
|
/** Build a studio operations bundle over an adapter (exported for tests). */
|
|
42
93
|
export declare function createStudioOperations(adapter: PersistenceAdapter, options?: CreateStudioOperationsOptions): StudioOperations;
|
|
@@ -55,5 +106,11 @@ export declare function getStudioOperations(): Promise<StudioOperations>;
|
|
|
55
106
|
* later switch picks up the new actor too.
|
|
56
107
|
*/
|
|
57
108
|
export declare function setStudioSession(actor: Principal | null): void;
|
|
109
|
+
/**
|
|
110
|
+
* EC-595 A2 — the signed-in principal the operations clients act as (`null` signed out):
|
|
111
|
+
* what this session's presence heartbeat names ("X is in this field") and how a conflict
|
|
112
|
+
* tells "you, in another tab" from a colleague. Read-only; set by {@link setStudioSession}.
|
|
113
|
+
*/
|
|
114
|
+
export declare function getStudioActor(): Principal | null;
|
|
58
115
|
/** Test seam: replace (or clear) the singleton for the active backend. */
|
|
59
116
|
export declare function setStudioOperationsForTests(next: StudioOperations | null): void;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { activeDeploymentKey, selectPersistenceAdapter } from '../persistence/index.js';
|
|
2
2
|
import { attachSessionExpiryReporting } from '../session-expiry.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { projectManifestLookup } from './config-manifest-lookup.js';
|
|
4
|
+
import { configProjectFor, projectCollections, projectComponentRegistry, projectLocaleConfig, } from './project-runtime-config.js';
|
|
5
|
+
import { createRuntimeDocumentValidator } from './runtime-validator.js';
|
|
6
|
+
import { attachReferenceIndex, createChangesets, createOperations, createRoleAuthorization, summarizeInput, toOperationError, } from './operations-package.js';
|
|
5
7
|
/**
|
|
6
8
|
* A no-op {@link ReferenceIndexAttachment} for self-maintaining adapters (EC-277):
|
|
7
9
|
* nothing is subscribed, so `detach` and `flush` are trivially satisfied (the index
|
|
@@ -15,17 +17,31 @@ export function createStudioOperations(adapter, options = {}) {
|
|
|
15
17
|
const authorized = options.actor
|
|
16
18
|
? { actor: options.actor, authorize: createRoleAuthorization(adapter.members) }
|
|
17
19
|
: {};
|
|
18
|
-
// EC-
|
|
19
|
-
//
|
|
20
|
-
// every
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
// EC-583: the collections come from the config (never the store), per project id;
|
|
21
|
+
// EC-595 A1: so does the manifest lookup. Both ride the changesets runtime too.
|
|
22
|
+
// EC-585: every per-project fact below is read from ONE source — the config project
|
|
23
|
+
// with the operation's slug (`configProjectFor`): collections, manifests, locales.
|
|
24
|
+
const collections = options.collections ?? projectCollections;
|
|
25
|
+
const locales = options.locales ?? projectLocaleConfig;
|
|
26
|
+
const idFactory = options.idFactory ? { idFactory: options.idFactory } : {};
|
|
27
|
+
const manifestLookup = options.manifestLookup ?? projectManifestLookup((projectId) => configProjectFor(projectId)?.components);
|
|
28
|
+
// EC-595 A2 (F23) → EC-582: the publish gate's validator is bound to THIS bundle —
|
|
29
|
+
// core's canonical gate (`@elytracms/core/validation`) over the OPERATION's project:
|
|
30
|
+
// its collections, locales and component registry (never the active project's) +
|
|
31
|
+
// oracles prefetched through the adapter — never whichever CMS session is mounted.
|
|
32
|
+
const validateDocument = options.validateDocument ??
|
|
33
|
+
createRuntimeDocumentValidator({
|
|
34
|
+
collections,
|
|
35
|
+
localeConfig: locales,
|
|
36
|
+
registry: projectComponentRegistry,
|
|
37
|
+
});
|
|
26
38
|
const operations = createOperations({
|
|
27
39
|
adapter,
|
|
28
|
-
|
|
40
|
+
collections,
|
|
41
|
+
validateDocument,
|
|
42
|
+
manifestLookup,
|
|
43
|
+
locales,
|
|
44
|
+
...idFactory,
|
|
29
45
|
...authorized,
|
|
30
46
|
});
|
|
31
47
|
// EC-277: when the adapter maintains the index in its own write path (AD-6 "at the
|
|
@@ -39,9 +55,14 @@ export function createStudioOperations(adapter, options = {}) {
|
|
|
39
55
|
: attachReferenceIndex(operations.events, adapter);
|
|
40
56
|
const changesets = createChangesets({
|
|
41
57
|
adapter,
|
|
58
|
+
collections,
|
|
42
59
|
events: operations.events,
|
|
43
|
-
// EC-497: a changeset-driven publish is gated identically —
|
|
44
|
-
|
|
60
|
+
// EC-497 / EC-595 A2: a changeset-driven publish is gated identically — the same
|
|
61
|
+
// runtime validator, prefetching through the step's own (scratch or real) adapter.
|
|
62
|
+
validateDocument,
|
|
63
|
+
manifestLookup,
|
|
64
|
+
locales,
|
|
65
|
+
...idFactory,
|
|
45
66
|
...authorized,
|
|
46
67
|
});
|
|
47
68
|
// EC-579: no revalidation emitter attaches here. Host revalidation is a property
|
|
@@ -54,7 +75,40 @@ export function createStudioOperations(adapter, options = {}) {
|
|
|
54
75
|
// matter which surface issued the operation (per-surface banners can't cover
|
|
55
76
|
// surfaces that render no banner).
|
|
56
77
|
attachSessionExpiryReporting(operations.events);
|
|
57
|
-
|
|
78
|
+
/** Report a capability failure on the bus (EC-403 sees an expiry like any failed operation). */
|
|
79
|
+
const reportFailure = (operation, input, error) => {
|
|
80
|
+
operations.events.emit({
|
|
81
|
+
operation,
|
|
82
|
+
domain: 'documents',
|
|
83
|
+
kind: 'query',
|
|
84
|
+
status: 'failed',
|
|
85
|
+
inputSummary: summarizeInput(input),
|
|
86
|
+
error: toOperationError(error),
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
return {
|
|
90
|
+
operations,
|
|
91
|
+
changesets,
|
|
92
|
+
adapter,
|
|
93
|
+
references,
|
|
94
|
+
subscribeDocument: (projectId, ref, listener) => adapter.cms.subscribeDocument(projectId, ref, {
|
|
95
|
+
onSnapshot: (snapshot) => listener.onSnapshot(snapshot),
|
|
96
|
+
onError: (error) => {
|
|
97
|
+
reportFailure('documents.subscribe', { projectId, ref }, error);
|
|
98
|
+
listener.onError?.(error);
|
|
99
|
+
},
|
|
100
|
+
}),
|
|
101
|
+
reportPresence: async (projectId, ref, presence) => {
|
|
102
|
+
try {
|
|
103
|
+
await adapter.cms.reportPresence(projectId, ref, presence);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
reportFailure('documents.presence', { projectId, ref }, error);
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
clearPresence: (projectId, ref, sessionId) => adapter.cms.clearPresence(projectId, ref, sessionId),
|
|
111
|
+
};
|
|
58
112
|
}
|
|
59
113
|
// One adapter + one operations bundle per backend (EC-179): keyed by the active
|
|
60
114
|
// deployment so switching to a project on another Convex deployment selects (or
|
|
@@ -113,6 +167,14 @@ export function setStudioSession(actor) {
|
|
|
113
167
|
currentActor = actor;
|
|
114
168
|
instancesByKey.clear();
|
|
115
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* EC-595 A2 — the signed-in principal the operations clients act as (`null` signed out):
|
|
172
|
+
* what this session's presence heartbeat names ("X is in this field") and how a conflict
|
|
173
|
+
* tells "you, in another tab" from a colleague. Read-only; set by {@link setStudioSession}.
|
|
174
|
+
*/
|
|
175
|
+
export function getStudioActor() {
|
|
176
|
+
return currentActor;
|
|
177
|
+
}
|
|
116
178
|
/** Test seam: replace (or clear) the singleton for the active backend. */
|
|
117
179
|
export function setStudioOperationsForTests(next) {
|
|
118
180
|
if (next) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"studio-operations.js","sourceRoot":"","sources":["../../../src/lib/operations/studio-operations.ts"],"names":[],"mappings":"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","sourcesContent":["import type { PersistenceAdapter } from '@elytracms/core/persistence'\nimport { activeDeploymentKey, selectPersistenceAdapter } from '../persistence'\nimport { attachSessionExpiryReporting } from '../session-expiry'\nimport { sessionValidateDocument } from '../cms/document-gate'\nimport {\n attachReferenceIndex,\n createChangesets,\n createOperations,\n createRoleAuthorization,\n} from './operations-package'\nimport type {\n ChangesetOperations,\n OperationsClient,\n Principal,\n ReferenceIndexAttachment,\n} from './operations-package'\n\n/**\n * The studio's single operations bootstrap (EC-141, vision AD-7).\n *\n * One `OperationsClient` + one `ChangesetOperations` instance, built over the\n * selected persistence adapter, shared by every store and surface in the app.\n * The UI never talks to a repository for writes — every mutation is a typed\n * operation command (validate → authorize → mutate → event), so the studio\n * dogfoods the same seam plugins, AI changesets, and the CLI use.\n *\n * The EC-155 reference index is attached to the shared event bus here, so any\n * document/graph/publishing command issued anywhere in the studio keeps the\n * index maintained.\n */\nexport interface StudioOperations {\n readonly operations: OperationsClient\n readonly changesets: ChangesetOperations\n /**\n * The raw adapter, exposed for *transport and read paths only*: blob byte\n * upload/download (`adapter.blobs`) and existing read stores. Studio code\n * must not call repository write methods on it — writes go through\n * `operations`.\n */\n readonly adapter: PersistenceAdapter\n /** EC-155 maintained-on-write index attachment (flushable in tests). */\n readonly references: ReferenceIndexAttachment\n}\n\nexport interface CreateStudioOperationsOptions {\n /**\n * The signed-in principal (EC-150). When present, the EC-150 role hook\n * (`createRoleAuthorization` over `adapter.members`) becomes the\n * authorization chokepoint for every command and query — viewers get\n * structured denials on mutations, editors on administration. Without an\n * actor the client runs pass-through (seeds, fixtures, tests that exercise\n * domain logic rather than permissions); the route guard keeps\n * unauthenticated users away from every operation surface.\n */\n actor?: Principal\n}\n\n/**\n * A no-op {@link ReferenceIndexAttachment} for self-maintaining adapters (EC-277):\n * nothing is subscribed, so `detach` and `flush` are trivially satisfied (the index\n * is kept current in the adapter's write path, not by a queued subscriber).\n */\nfunction noopReferenceIndexAttachment(): ReferenceIndexAttachment {\n return { detach: () => {}, flush: async () => {} }\n}\n\n/** Build a studio operations bundle over an adapter (exported for tests). */\nexport function createStudioOperations(\n adapter: PersistenceAdapter,\n options: CreateStudioOperationsOptions = {},\n): StudioOperations {\n const authorized = options.actor\n ? { actor: options.actor, authorize: createRoleAuthorization(adapter.members) }\n : {}\n // EC-497: the publish gate validates with the CMS session's canonical option\n // bag — the same one the detail editor, bulk publish, cell edit, import and\n // every AI path use — instead of deciding its own (weaker, mode-dependent)\n // strength off the adapter-served schema record. The seam reads a validator the\n // mounted session published; with no session it answers with ONE blocking issue\n // (EC-546) — a publish issued outside a mounted CMS session is refused, never\n // waved through on weaker rules. The validator is required by the operations\n // layer, so this bundle cannot be built without it.\n const operations = createOperations({\n adapter,\n validateDocument: sessionValidateDocument,\n ...authorized,\n })\n // EC-277: when the adapter maintains the index in its own write path (AD-6 \"at the\n // source\" — the Convex provider does this server-side, so a raw `convex run` is\n // caught too), the client-side subscriber would only double the work. Attach it\n // ONLY for providers that don't self-maintain. Where it does run, extraction is a\n // schema-free, manifest-free walk over self-describing content, so it needs no\n // registry (compositions self-typed their relation/asset props on write).\n const references = adapter.maintainsReferenceIndex\n ? noopReferenceIndexAttachment()\n : attachReferenceIndex(operations.events, adapter)\n const changesets = createChangesets({\n adapter,\n events: operations.events,\n // EC-497: a changeset-driven publish is gated identically — same seam, same bag.\n validateDocument: sessionValidateDocument,\n ...authorized,\n })\n // EC-579: no revalidation emitter attaches here. Host revalidation is a property\n // of the Convex write chokepoint (`convex/triggers.ts`), the one emitter — it\n // catches every writer, keeps the signing secret server-side, and the playground's\n // in-browser adapter is read by no host, so there is nothing to emit there.\n // EC-403: session-expiry detection rides the SAME lifecycle bus — every failed\n // command/query/changeset operation is checked against the session-expired\n // predicate here, so expiry raises the ONE app-level blocking re-auth state no\n // matter which surface issued the operation (per-surface banners can't cover\n // surfaces that render no banner).\n attachSessionExpiryReporting(operations.events)\n return { operations, changesets, adapter, references }\n}\n\n// One adapter + one operations bundle per backend (EC-179): keyed by the active\n// deployment so switching to a project on another Convex deployment selects (or\n// lazily builds) that backend's client, rather than reusing the first one.\nconst adaptersByKey = new Map<string, Promise<PersistenceAdapter>>()\nconst instancesByKey = new Map<string, Promise<StudioOperations>>()\nlet currentActor: Principal | null = null\n\n/**\n * The active backend key, tolerant of an unconfigured env: the real app never\n * reaches `getStudioOperations` while misconfigured (the `DeploymentSetupGate` renders a\n * boot error first), but tests inject a bundle directly via the seam below — so\n * a missing backend resolves to one stable key rather than throwing.\n */\nfunction deploymentKey(): string {\n try {\n return activeDeploymentKey()\n } catch {\n return '__unconfigured__'\n }\n}\n\nfunction getAdapter(key: string): Promise<PersistenceAdapter> {\n let adapter = adaptersByKey.get(key)\n if (!adapter) {\n adapter = selectPersistenceAdapter()\n adaptersByKey.set(key, adapter)\n }\n return adapter\n}\n\n/**\n * The shared studio operations client for the active backend (lazily\n * bootstrapped, cached per deployment). In-memory mode this owns the one seeded\n * adapter the whole session mutates; against Convex it owns one client per\n * deployment — the active project (EC-179) selects which.\n */\nexport function getStudioOperations(): Promise<StudioOperations> {\n const key = deploymentKey()\n let instance = instancesByKey.get(key)\n if (!instance) {\n const actor = currentActor\n instance = getAdapter(key).then((adapter) =>\n createStudioOperations(adapter, actor ? { actor } : {}),\n )\n instancesByKey.set(key, instance)\n }\n return instance\n}\n\n/**\n * Bind (or clear) the session principal (EC-150). Called by the auth context\n * on sign-in/sign-out. Rebuilds the operations clients over the *same* adapters\n * — the persisted state (and, in-memory, the whole session's edits) survives;\n * only the authorization identity changes. All cached backends are rebuilt so a\n * later switch picks up the new actor too.\n */\nexport function setStudioSession(actor: Principal | null): void {\n if (currentActor?.id === actor?.id) return // same user (or both null) — keep the clients\n currentActor = actor\n instancesByKey.clear()\n}\n\n/** Test seam: replace (or clear) the singleton for the active backend. */\nexport function setStudioOperationsForTests(next: StudioOperations | null): void {\n if (next) {\n instancesByKey.set(deploymentKey(), Promise.resolve(next))\n return\n }\n instancesByKey.clear()\n adaptersByKey.clear()\n currentActor = null\n}\n"]}
|
|
1
|
+
{"version":3,"file":"studio-operations.js","sourceRoot":"","sources":["../../../src/lib/operations/studio-operations.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n DocumentListener,\n IdFactory,\n PersistenceAdapter,\n PresenceInput,\n} from '@elytracms/core/persistence'\nimport type { DocumentRef } from '@elytracms/core/cms-core'\nimport { activeDeploymentKey, selectPersistenceAdapter } from '../persistence'\nimport { attachSessionExpiryReporting } from '../session-expiry'\nimport { projectManifestLookup } from './config-manifest-lookup'\nimport {\n configProjectFor,\n projectCollections,\n projectComponentRegistry,\n projectLocaleConfig,\n} from './project-runtime-config'\nimport { createRuntimeDocumentValidator } from './runtime-validator'\nimport {\n attachReferenceIndex,\n createChangesets,\n createOperations,\n createRoleAuthorization,\n summarizeInput,\n toOperationError,\n} from './operations-package'\nimport type {\n ChangesetOperations,\n CollectionsResolver,\n DocumentValidator,\n LocalesResolver,\n ManifestLookupResolver,\n OperationsClient,\n Principal,\n ReferenceIndexAttachment,\n} from './operations-package'\n\n/**\n * The studio's single operations bootstrap (EC-141, vision AD-7).\n *\n * One `OperationsClient` + one `ChangesetOperations` instance, built over the\n * selected persistence adapter, shared by every store and surface in the app.\n * The UI never talks to a repository for writes — every mutation is a typed\n * operation command (validate → authorize → mutate → event), so the studio\n * dogfoods the same seam plugins, AI changesets, and the CLI use.\n *\n * The EC-155 reference index is attached to the shared event bus here, so any\n * document/graph/publishing command issued anywhere in the studio keeps the\n * index maintained.\n */\nexport interface StudioOperations {\n readonly operations: OperationsClient\n readonly changesets: ChangesetOperations\n /**\n * The raw adapter, exposed for *transport and read paths only*: blob byte\n * upload/download (`adapter.blobs`) and existing read stores. Studio code\n * must not call repository write methods on it — writes go through\n * `operations`.\n */\n readonly adapter: PersistenceAdapter\n /** EC-155 maintained-on-write index attachment (flushable in tests). */\n readonly references: ReferenceIndexAttachment\n /**\n * EC-595 binding point 7 — subscribe to ONE document (the narrow EC-309 slice): the\n * listener gets the current row + the presence on it, then one snapshot per change\n * (Convex: the reactive client's server push; playground: the in-memory store's own\n * writes). An adapter capability exposed through the in-process bundle — the UI never\n * holds a reactive Convex client. A subscription failure is ALSO emitted as a failed\n * `documents.subscribe` event on the operations bus, so an expired session reaches\n * EC-403's one session-expiry chokepoint like any failed operation (never a silently\n * stopped stream). Returns the unsubscribe function.\n */\n subscribeDocument(projectId: string, ref: DocumentRef, listener: DocumentListener): () => void\n /** EC-595 — this session's field-presence heartbeat on a document (thin presence, Stage A). */\n reportPresence(projectId: string, ref: DocumentRef, presence: PresenceInput): Promise<void>\n /** EC-595 — remove this session's presence from a document. */\n clearPresence(projectId: string, ref: DocumentRef, sessionId: string): Promise<void>\n}\n\nexport interface CreateStudioOperationsOptions {\n /**\n * The signed-in principal (EC-150). When present, the EC-150 role hook\n * (`createRoleAuthorization` over `adapter.members`) becomes the\n * authorization chokepoint for every command and query — viewers get\n * structured denials on mutations, editors on administration. Without an\n * actor the client runs pass-through (seeds, fixtures, tests that exercise\n * domain logic rather than permissions); the route guard keeps\n * unauthenticated users away from every operation surface.\n */\n actor?: Principal\n /**\n * EC-583 / EC-559 — the collection definitions on the operations context, resolved for\n * each operation's OWN project id. Defaults to the workspace config's project with that\n * slug (`projectCollections` — the resolved `elytra.config.ts`, functions\n * included), read on every call: the bundle is cached per deployment KEY (EC-179) and\n * one deployment may serve several config projects, so capturing one project's\n * collections at build time would canonicalise another project's documents against\n * them (operator correction to phase 1). The store holds no schema.\n */\n collections?: CollectionsResolver\n /** EC-578 — the document-id factory (`documents.create`); UUIDs by default. */\n idFactory?: IdFactory\n /**\n * EC-595 A1 (critic correction 4) → A2 — the manifest half of the write canonicaliser\n * on the operations context, resolved for each operation's OWN project. Defaults to a\n * lookup over that config project's component entries (`projectManifestLookup`), so\n * every patch / replace canonicalises a composition's props with its project's\n * manifests — never the active project's (A1 found-but-left #1).\n */\n manifestLookup?: ManifestLookupResolver\n /**\n * EC-595 A2 (F23) — the publish gate's validator, bound to THIS bundle. Defaults to\n * the runtime validator (`createRuntimeDocumentValidator`: the operation's project's\n * config + oracles prefetched through the adapter). Tests that pin persistence\n * routing rather than gate strength inject their own here — at construction, never\n * through a mount.\n */\n validateDocument?: DocumentValidator\n /**\n * EC-585 — the locale config on the operations context, per project: the view\n * `documents.publish` runs its hidden-clear in. Defaults to that config project's\n * `locales` (`projectLocaleConfig`) — the same project, read the same way, as the\n * collections, the manifests and the gate's validator.\n */\n locales?: LocalesResolver\n}\n\n/**\n * A no-op {@link ReferenceIndexAttachment} for self-maintaining adapters (EC-277):\n * nothing is subscribed, so `detach` and `flush` are trivially satisfied (the index\n * is kept current in the adapter's write path, not by a queued subscriber).\n */\nfunction noopReferenceIndexAttachment(): ReferenceIndexAttachment {\n return { detach: () => {}, flush: async () => {} }\n}\n\n/** Build a studio operations bundle over an adapter (exported for tests). */\nexport function createStudioOperations(\n adapter: PersistenceAdapter,\n options: CreateStudioOperationsOptions = {},\n): StudioOperations {\n const authorized = options.actor\n ? { actor: options.actor, authorize: createRoleAuthorization(adapter.members) }\n : {}\n // EC-583: the collections come from the config (never the store), per project id;\n // EC-595 A1: so does the manifest lookup. Both ride the changesets runtime too.\n // EC-585: every per-project fact below is read from ONE source — the config project\n // with the operation's slug (`configProjectFor`): collections, manifests, locales.\n const collections: CollectionsResolver = options.collections ?? projectCollections\n const locales: LocalesResolver = options.locales ?? projectLocaleConfig\n const idFactory = options.idFactory ? { idFactory: options.idFactory } : {}\n const manifestLookup =\n options.manifestLookup ?? projectManifestLookup((projectId) => configProjectFor(projectId)?.components)\n // EC-595 A2 (F23) → EC-582: the publish gate's validator is bound to THIS bundle —\n // core's canonical gate (`@elytracms/core/validation`) over the OPERATION's project:\n // its collections, locales and component registry (never the active project's) +\n // oracles prefetched through the adapter — never whichever CMS session is mounted.\n const validateDocument =\n options.validateDocument ??\n createRuntimeDocumentValidator({\n collections,\n localeConfig: locales,\n registry: projectComponentRegistry,\n })\n const operations = createOperations({\n adapter,\n collections,\n validateDocument,\n manifestLookup,\n locales,\n ...idFactory,\n ...authorized,\n })\n // EC-277: when the adapter maintains the index in its own write path (AD-6 \"at the\n // source\" — the Convex provider does this server-side, so a raw `convex run` is\n // caught too), the client-side subscriber would only double the work. Attach it\n // ONLY for providers that don't self-maintain. Where it does run, extraction is a\n // schema-free, manifest-free walk over self-describing content, so it needs no\n // registry (compositions self-typed their relation/asset props on write).\n const references = adapter.maintainsReferenceIndex\n ? noopReferenceIndexAttachment()\n : attachReferenceIndex(operations.events, adapter)\n const changesets = createChangesets({\n adapter,\n collections,\n events: operations.events,\n // EC-497 / EC-595 A2: a changeset-driven publish is gated identically — the same\n // runtime validator, prefetching through the step's own (scratch or real) adapter.\n validateDocument,\n manifestLookup,\n locales,\n ...idFactory,\n ...authorized,\n })\n // EC-579: no revalidation emitter attaches here. Host revalidation is a property\n // of the Convex write chokepoint (`convex/triggers.ts`), the one emitter — it\n // catches every writer, keeps the signing secret server-side, and the playground's\n // in-browser adapter is read by no host, so there is nothing to emit there.\n // EC-403: session-expiry detection rides the SAME lifecycle bus — every failed\n // command/query/changeset operation is checked against the session-expired\n // predicate here, so expiry raises the ONE app-level blocking re-auth state no\n // matter which surface issued the operation (per-surface banners can't cover\n // surfaces that render no banner).\n attachSessionExpiryReporting(operations.events)\n\n /** Report a capability failure on the bus (EC-403 sees an expiry like any failed operation). */\n const reportFailure = (operation: string, input: unknown, error: unknown) => {\n operations.events.emit({\n operation,\n domain: 'documents',\n kind: 'query',\n status: 'failed',\n inputSummary: summarizeInput(input),\n error: toOperationError(error),\n })\n }\n return {\n operations,\n changesets,\n adapter,\n references,\n subscribeDocument: (projectId, ref, listener) =>\n adapter.cms.subscribeDocument(projectId, ref, {\n onSnapshot: (snapshot) => listener.onSnapshot(snapshot),\n onError: (error) => {\n reportFailure('documents.subscribe', { projectId, ref }, error)\n listener.onError?.(error)\n },\n }),\n reportPresence: async (projectId, ref, presence) => {\n try {\n await adapter.cms.reportPresence(projectId, ref, presence)\n } catch (error) {\n reportFailure('documents.presence', { projectId, ref }, error)\n throw error\n }\n },\n clearPresence: (projectId, ref, sessionId) => adapter.cms.clearPresence(projectId, ref, sessionId),\n }\n}\n\n// One adapter + one operations bundle per backend (EC-179): keyed by the active\n// deployment so switching to a project on another Convex deployment selects (or\n// lazily builds) that backend's client, rather than reusing the first one.\nconst adaptersByKey = new Map<string, Promise<PersistenceAdapter>>()\nconst instancesByKey = new Map<string, Promise<StudioOperations>>()\nlet currentActor: Principal | null = null\n\n/**\n * The active backend key, tolerant of an unconfigured env: the real app never\n * reaches `getStudioOperations` while misconfigured (the `DeploymentSetupGate` renders a\n * boot error first), but tests inject a bundle directly via the seam below — so\n * a missing backend resolves to one stable key rather than throwing.\n */\nfunction deploymentKey(): string {\n try {\n return activeDeploymentKey()\n } catch {\n return '__unconfigured__'\n }\n}\n\nfunction getAdapter(key: string): Promise<PersistenceAdapter> {\n let adapter = adaptersByKey.get(key)\n if (!adapter) {\n adapter = selectPersistenceAdapter()\n adaptersByKey.set(key, adapter)\n }\n return adapter\n}\n\n/**\n * The shared studio operations client for the active backend (lazily\n * bootstrapped, cached per deployment). In-memory mode this owns the one seeded\n * adapter the whole session mutates; against Convex it owns one client per\n * deployment — the active project (EC-179) selects which.\n */\nexport function getStudioOperations(): Promise<StudioOperations> {\n const key = deploymentKey()\n let instance = instancesByKey.get(key)\n if (!instance) {\n const actor = currentActor\n instance = getAdapter(key).then((adapter) =>\n createStudioOperations(adapter, actor ? { actor } : {}),\n )\n instancesByKey.set(key, instance)\n }\n return instance\n}\n\n/**\n * Bind (or clear) the session principal (EC-150). Called by the auth context\n * on sign-in/sign-out. Rebuilds the operations clients over the *same* adapters\n * — the persisted state (and, in-memory, the whole session's edits) survives;\n * only the authorization identity changes. All cached backends are rebuilt so a\n * later switch picks up the new actor too.\n */\nexport function setStudioSession(actor: Principal | null): void {\n if (currentActor?.id === actor?.id) return // same user (or both null) — keep the clients\n currentActor = actor\n instancesByKey.clear()\n}\n\n/**\n * EC-595 A2 — the signed-in principal the operations clients act as (`null` signed out):\n * what this session's presence heartbeat names (\"X is in this field\") and how a conflict\n * tells \"you, in another tab\" from a colleague. Read-only; set by {@link setStudioSession}.\n */\nexport function getStudioActor(): Principal | null {\n return currentActor\n}\n\n/** Test seam: replace (or clear) the singleton for the active backend. */\nexport function setStudioOperationsForTests(next: StudioOperations | null): void {\n if (next) {\n instancesByKey.set(deploymentKey(), Promise.resolve(next))\n return\n }\n instancesByKey.clear()\n adaptersByKey.clear()\n currentActor = null\n}\n"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ConvexHttpClient } from 'convex/browser';
|
|
2
|
-
import type { CollectionDef } from '@elytracms/core/cms-core';
|
|
3
2
|
import type { AssetRepository, BackendStatus, BackendValidationInput, BackendValidationResult, BlobStorageAdapter, CmsRepository, DeliveryRepository, MembersRepository, PersistenceAdapter, ReferenceIndexRepository, RedirectRepository, UserRepository, InviteRepository } from '@elytracms/core/persistence';
|
|
3
|
+
import type { ReactiveDocumentClient } from './convex-client.js';
|
|
4
4
|
/**
|
|
5
5
|
* Convex-backed {@link PersistenceAdapter} (brief §10, EC-039). Every repository
|
|
6
6
|
* is implemented against the Convex functions in `apps/builder/convex/*`, mapping
|
|
@@ -34,11 +34,19 @@ export declare class ConvexPersistenceAdapter implements PersistenceAdapter {
|
|
|
34
34
|
* @param client the Convex client bound to this deployment (one project).
|
|
35
35
|
* @param projectId the slug of this deployment's one project (EC-179). Used to
|
|
36
36
|
* stamp deployment-scoped member records onto the still-project-keyed contract.
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* the
|
|
37
|
+
*
|
|
38
|
+
* @param options.reactive the reactive client a document subscription runs on (EC-595),
|
|
39
|
+
* built lazily — the studio passes `() => getReactiveConvexClient(url)`. Absent (a
|
|
40
|
+
* seed script, a test double without push), `subscribeDocument` fails loudly.
|
|
41
|
+
*
|
|
42
|
+
* EC-583: the third argument is no longer the collections. They only fed the
|
|
43
|
+
* synthesized schema record (`getSchema`/`findSchema`), which the contract no longer
|
|
44
|
+
* has — collections reach the operations layer on its context
|
|
45
|
+
* (`createOperations({ collections })`).
|
|
40
46
|
*/
|
|
41
|
-
constructor(client: ConvexHttpClient, projectId: string,
|
|
47
|
+
constructor(client: ConvexHttpClient, projectId: string, options?: {
|
|
48
|
+
reactive?: () => ReactiveDocumentClient;
|
|
49
|
+
});
|
|
42
50
|
status(): Promise<BackendStatus>;
|
|
43
51
|
validate(input: BackendValidationInput): BackendValidationResult;
|
|
44
52
|
}
|