@elytracms/studio 0.0.92 → 0.0.94
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/analytics-cost.test.ts +817 -0
- 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/auth.ts +5 -2
- package/convex/authority.ts +35 -11
- package/convex/cms.ts +25 -11
- package/convex/contract-parity.test.ts +25 -3
- package/convex/delivery.ts +12 -9
- package/convex/document-revalidate.test.ts +10 -12
- package/convex/guard.ts +9 -7
- package/convex/http-routes.test.ts +18 -8
- package/convex/http.ts +23 -17
- package/convex/ingest.test.ts +326 -0
- package/convex/ingest.ts +281 -0
- package/convex/invites.ts +13 -5
- package/convex/members.ts +10 -4
- package/convex/orphan-hydration.test.ts +116 -0
- package/convex/rail-write-parity.test.ts +11 -8
- package/convex/record-functions.ts +325 -0
- package/convex/records.test.ts +156 -0
- package/convex/redirect-migration.test.ts +205 -0
- package/convex/redirect-revalidate.test.ts +5 -1
- package/convex/redirects.ts +120 -79
- package/convex/role-policy.test.ts +315 -0
- package/convex/roles.ts +83 -0
- package/convex/schema.ts +36 -0
- package/convex/triggers.ts +21 -1
- package/dist/addon.d.ts +71 -0
- package/dist/addon.js +67 -0
- package/dist/addon.js.map +1 -0
- package/dist/components/account/account-settings.js +2 -2
- package/dist/components/account/account-settings.js.map +1 -1
- package/dist/components/account/credential-panel.js +2 -2
- package/dist/components/account/credential-panel.js.map +1 -1
- package/dist/components/account/self-account-schema.d.ts +6 -15
- package/dist/components/account/self-account-schema.js +52 -34
- package/dist/components/account/self-account-schema.js.map +1 -1
- package/dist/components/account/self-account-view-source.d.ts +1 -1
- package/dist/components/account/self-account-view-source.js +6 -4
- package/dist/components/account/self-account-view-source.js.map +1 -1
- package/dist/components/ai/ai-chat-panel-lazy.js +1 -1
- package/dist/components/ai/ai-chat-panel-lazy.js.map +1 -1
- package/dist/components/ai/ai-chat-panel.js +6 -6
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/ai/ai-cms-panel.js +2 -2
- package/dist/components/ai/ai-cms-panel.js.map +1 -1
- package/dist/components/ai/ai-panel-toggle.js +4 -4
- package/dist/components/ai/ai-panel-toggle.js.map +1 -1
- package/dist/components/ai/chat-ui.js +2 -2
- package/dist/components/ai/chat-ui.js.map +1 -1
- package/dist/components/ai/proposal-card.d.ts +2 -2
- package/dist/components/ai/proposal-card.js +4 -3
- package/dist/components/ai/proposal-card.js.map +1 -1
- package/dist/components/ai/proposal-issues.js +2 -2
- package/dist/components/ai/proposal-issues.js.map +1 -1
- package/dist/components/ai/translate-document-action.js +2 -2
- package/dist/components/ai/translate-document-action.js.map +1 -1
- package/dist/components/ai/translate-document-dialog.js +10 -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 +50 -2
- package/dist/components/app-views/default-modules.js +65 -20
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/app-views/module.d.ts +53 -3
- package/dist/components/app-views/module.js +14 -7
- package/dist/components/app-views/module.js.map +1 -1
- package/dist/components/app-views/sidebar.js +1 -1
- package/dist/components/app-views/sidebar.js.map +1 -1
- package/dist/components/app-views/studio-config.d.ts +11 -2
- package/dist/components/app-views/studio-config.js.map +1 -1
- package/dist/components/assets/asset-detail.js +20 -10
- package/dist/components/assets/asset-detail.js.map +1 -1
- package/dist/components/assets/asset-grid.js +2 -2
- package/dist/components/assets/asset-grid.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 +6 -5
- package/dist/components/assets/asset-thumb.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.d.ts +5 -5
- 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-module.js +1 -1
- package/dist/components/assets/assets-module.js.map +1 -1
- package/dist/components/assets/assets-store.d.ts +16 -2
- package/dist/components/assets/assets-store.js +28 -6
- package/dist/components/assets/assets-store.js.map +1 -1
- package/dist/components/assets/assets-view-preference.js +1 -1
- package/dist/components/assets/assets-view-preference.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/auth-context.d.ts +1 -1
- package/dist/components/auth-context.js +6 -6
- package/dist/components/auth-context.js.map +1 -1
- package/dist/components/builder/blocks-model.d.ts +15 -27
- package/dist/components/builder/blocks-model.js +38 -44
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/blocks-panel.d.ts +8 -4
- package/dist/components/builder/blocks-panel.js +10 -9
- package/dist/components/builder/blocks-panel.js.map +1 -1
- package/dist/components/builder/builder-store.d.ts +104 -15
- package/dist/components/builder/builder-store.js +157 -32
- 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-breakpoints.js +1 -1
- package/dist/components/builder/canvas-breakpoints.js.map +1 -1
- 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 +9 -9
- package/dist/components/builder/canvas-dnd.js +6 -6
- package/dist/components/builder/canvas-dnd.js.map +1 -1
- package/dist/components/builder/canvas-inline-editing.d.ts +1 -1
- package/dist/components/builder/canvas-inline-editing.js.map +1 -1
- package/dist/components/builder/canvas-overlay-geometry.d.ts +1 -1
- package/dist/components/builder/canvas-overlay-geometry.js.map +1 -1
- package/dist/components/builder/canvas-render-context.d.ts +28 -20
- package/dist/components/builder/canvas-render-context.js +46 -47
- 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-theme-notice.js +2 -2
- package/dist/components/builder/canvas-theme-notice.js.map +1 -1
- package/dist/components/builder/canvas.js +35 -8
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/builder/clipboard-actions.d.ts +16 -1
- package/dist/components/builder/clipboard-actions.js +48 -11
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/builder/composition-field-editor-lazy.js +1 -1
- package/dist/components/builder/composition-field-editor-lazy.js.map +1 -1
- package/dist/components/builder/composition-field-editor.d.ts +4 -16
- package/dist/components/builder/composition-field-editor.js +32 -43
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- 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/drag-session.d.ts +1 -1
- package/dist/components/builder/drag-session.js.map +1 -1
- package/dist/components/builder/fixture-thumbnail.js +1 -1
- package/dist/components/builder/fixture-thumbnail.js.map +1 -1
- package/dist/components/builder/insert-context.d.ts +18 -26
- package/dist/components/builder/insert-context.js +47 -94
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/layout-inspector.js +1 -1
- package/dist/components/builder/layout-inspector.js.map +1 -1
- package/dist/components/builder/node-title.js +1 -1
- package/dist/components/builder/node-title.js.map +1 -1
- package/dist/components/builder/page-tree.d.ts +4 -4
- package/dist/components/builder/page-tree.js +8 -8
- 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 +8 -4
- package/dist/components/builder/property-inspector.js.map +1 -1
- package/dist/components/builder/reuse-context.js +68 -30
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/builder/reuse.d.ts +79 -1
- package/dist/components/builder/reuse.js +70 -3
- package/dist/components/builder/reuse.js.map +1 -1
- package/dist/components/builder/rich-text-toolbar.js +1 -1
- package/dist/components/builder/rich-text-toolbar.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/builder/selection-overlay.js +3 -3
- package/dist/components/builder/selection-overlay.js.map +1 -1
- package/dist/components/builder/spacing-handles.d.ts +2 -2
- package/dist/components/builder/spacing-handles.js +3 -3
- package/dist/components/builder/spacing-handles.js.map +1 -1
- package/dist/components/cms/asset-field.d.ts +38 -1
- package/dist/components/cms/asset-field.js +184 -60
- 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 +14 -20
- 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 +5 -26
- package/dist/components/cms/bulk-actions.js.map +1 -1
- package/dist/components/cms/bulk-bar.d.ts +2 -2
- package/dist/components/cms/bulk-bar.js +2 -2
- package/dist/components/cms/bulk-bar.js.map +1 -1
- package/dist/components/cms/cell-edit.d.ts +1 -1
- package/dist/components/cms/cell-edit.js +1 -1
- package/dist/components/cms/cell-edit.js.map +1 -1
- package/dist/components/cms/cell.d.ts +1 -1
- package/dist/components/cms/cell.js +3 -3
- package/dist/components/cms/cell.js.map +1 -1
- package/dist/components/cms/cms-index.js +2 -2
- package/dist/components/cms/cms-index.js.map +1 -1
- package/dist/components/cms/cms-labels.js +2 -2
- package/dist/components/cms/cms-labels.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +7 -6
- package/dist/components/cms/cms-store.js +23 -43
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +7 -7
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/detail-editor.d.ts +5 -2
- package/dist/components/cms/detail-editor.js +20 -15
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/details-pane.js +1 -1
- package/dist/components/cms/details-pane.js.map +1 -1
- package/dist/components/cms/doc-state.js +1 -1
- package/dist/components/cms/doc-state.js.map +1 -1
- package/dist/components/cms/document-diff-view.d.ts +3 -1
- package/dist/components/cms/document-diff-view.js +17 -12
- package/dist/components/cms/document-diff-view.js.map +1 -1
- package/dist/components/cms/document-editor-context.js +1 -1
- package/dist/components/cms/document-editor-context.js.map +1 -1
- package/dist/components/cms/document-staging.js +1 -1
- package/dist/components/cms/document-staging.js.map +1 -1
- package/dist/components/cms/document-url-path.js +2 -2
- package/dist/components/cms/document-url-path.js.map +1 -1
- package/dist/components/cms/duplicate-document.d.ts +1 -1
- package/dist/components/cms/duplicate-document.js.map +1 -1
- package/dist/components/cms/editable-cell.js +3 -3
- package/dist/components/cms/editable-cell.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 +1 -1
- package/dist/components/cms/field-collaboration.js +1 -1
- package/dist/components/cms/field-collaboration.js.map +1 -1
- package/dist/components/cms/field-connection.d.ts +1 -1
- package/dist/components/cms/field-connection.js +2 -2
- package/dist/components/cms/field-connection.js.map +1 -1
- package/dist/components/cms/field-control-lib.d.ts +15 -1
- package/dist/components/cms/field-control-lib.js +23 -4
- package/dist/components/cms/field-control-lib.js.map +1 -1
- package/dist/components/cms/field-control.js +4 -4
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/field-renderer.d.ts +1 -1
- package/dist/components/cms/field-renderer.js +2 -2
- package/dist/components/cms/field-renderer.js.map +1 -1
- package/dist/components/cms/filter-builder.d.ts +1 -1
- package/dist/components/cms/filter-builder.js +3 -3
- package/dist/components/cms/filter-builder.js.map +1 -1
- package/dist/components/cms/gate-validation.d.ts +21 -30
- package/dist/components/cms/gate-validation.js +11 -32
- package/dist/components/cms/gate-validation.js.map +1 -1
- package/dist/components/cms/import-export.js +2 -2
- package/dist/components/cms/import-export.js.map +1 -1
- package/dist/components/cms/issue-copy.js +1 -1
- package/dist/components/cms/issue-copy.js.map +1 -1
- package/dist/components/cms/issues-pane.js +3 -3
- package/dist/components/cms/issues-pane.js.map +1 -1
- package/dist/components/cms/object-array-field.js +6 -6
- 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 +14 -7
- package/dist/components/cms/prop-fields.js.map +1 -1
- package/dist/components/cms/reference-confirm.d.ts +1 -1
- package/dist/components/cms/reference-confirm.js +2 -2
- package/dist/components/cms/reference-confirm.js.map +1 -1
- package/dist/components/cms/relation-picker.js +2 -2
- package/dist/components/cms/relation-picker.js.map +1 -1
- package/dist/components/cms/rich-text/block-style.js +1 -1
- package/dist/components/cms/rich-text/block-style.js.map +1 -1
- package/dist/components/cms/rich-text/component-embed.js +4 -4
- package/dist/components/cms/rich-text/component-embed.js.map +1 -1
- package/dist/components/cms/rich-text/editor.js +3 -3
- package/dist/components/cms/rich-text/editor.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 +3 -3
- package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
- package/dist/components/cms/rich-text/embed-props.js +3 -3
- package/dist/components/cms/rich-text/embed-props.js.map +1 -1
- package/dist/components/cms/rich-text/extensions.js +1 -1
- package/dist/components/cms/rich-text/extensions.js.map +1 -1
- package/dist/components/cms/rich-text/font-size-control.d.ts +1 -1
- package/dist/components/cms/rich-text/font-size-control.js +2 -2
- package/dist/components/cms/rich-text/font-size-control.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/rich-text/lazy.js +1 -1
- package/dist/components/cms/rich-text/lazy.js.map +1 -1
- package/dist/components/cms/rich-text/link-control.js +2 -2
- package/dist/components/cms/rich-text/link-control.js.map +1 -1
- package/dist/components/cms/rich-text/slash-command.js +3 -3
- package/dist/components/cms/rich-text/slash-command.js.map +1 -1
- package/dist/components/cms/rich-text/toolbar.js +1 -1
- package/dist/components/cms/rich-text/toolbar.js.map +1 -1
- package/dist/components/cms/rich-text/unknown-node.js +2 -2
- package/dist/components/cms/rich-text/unknown-node.js.map +1 -1
- package/dist/components/cms/rich-text-preview.js +2 -2
- package/dist/components/cms/rich-text-preview.js.map +1 -1
- package/dist/components/cms/row-actions.d.ts +1 -1
- package/dist/components/cms/row-actions.js.map +1 -1
- package/dist/components/cms/scalar-value-editor.d.ts +1 -1
- package/dist/components/cms/scalar-value-editor.js +2 -2
- package/dist/components/cms/scalar-value-editor.js.map +1 -1
- package/dist/components/cms/use-discard-controller.d.ts +1 -1
- package/dist/components/cms/use-discard-controller.js +1 -1
- package/dist/components/cms/use-discard-controller.js.map +1 -1
- package/dist/components/cms/use-document-draft.d.ts +7 -1
- package/dist/components/cms/use-document-draft.js +16 -2
- package/dist/components/cms/use-document-draft.js.map +1 -1
- package/dist/components/cms/use-document-versions.js +1 -1
- package/dist/components/cms/use-document-versions.js.map +1 -1
- package/dist/components/cms/use-field-focus.d.ts +1 -1
- package/dist/components/cms/use-field-focus.js +2 -2
- package/dist/components/cms/use-field-focus.js.map +1 -1
- package/dist/components/cms/use-live-url.d.ts +1 -1
- package/dist/components/cms/use-live-url.js +3 -3
- package/dist/components/cms/use-live-url.js.map +1 -1
- package/dist/components/cms/use-publish-controller.d.ts +2 -3
- package/dist/components/cms/use-publish-controller.js +8 -20
- package/dist/components/cms/use-publish-controller.js.map +1 -1
- package/dist/components/cms/use-published-perspective.d.ts +1 -1
- package/dist/components/cms/use-published-perspective.js.map +1 -1
- package/dist/components/cms/use-restore-version.d.ts +1 -1
- package/dist/components/cms/use-restore-version.js +1 -1
- package/dist/components/cms/use-restore-version.js.map +1 -1
- package/dist/components/cms/version-compare-lazy.js +1 -1
- package/dist/components/cms/version-compare-lazy.js.map +1 -1
- package/dist/components/cms/version-compare.js +1 -1
- package/dist/components/cms/version-compare.js.map +1 -1
- package/dist/components/cms/versions-pane.js +11 -4
- package/dist/components/cms/versions-pane.js.map +1 -1
- package/dist/components/cms/view-bar.js +3 -3
- package/dist/components/cms/view-bar.js.map +1 -1
- package/dist/components/collection-view/cms-collection-view-source.d.ts +1 -1
- package/dist/components/collection-view/cms-collection-view-source.js +5 -3
- package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
- package/dist/components/collection-view/collection-view-context.d.ts +1 -1
- package/dist/components/collection-view/collection-view-context.js +1 -1
- package/dist/components/collection-view/collection-view-context.js.map +1 -1
- package/dist/components/collection-view/collection-view-surface.d.ts +13 -0
- package/dist/components/collection-view/collection-view-surface.js +18 -0
- package/dist/components/collection-view/collection-view-surface.js.map +1 -0
- package/dist/components/components/component-detail.js +10 -10
- package/dist/components/components/component-detail.js.map +1 -1
- package/dist/components/components/component-preview.d.ts +2 -2
- package/dist/components/components/component-preview.js +5 -5
- package/dist/components/components/component-preview.js.map +1 -1
- package/dist/components/components/component-status.d.ts +1 -1
- package/dist/components/components/component-status.js +1 -1
- package/dist/components/components/component-status.js.map +1 -1
- package/dist/components/components/components-index.js +2 -2
- package/dist/components/components/components-index.js.map +1 -1
- package/dist/components/components/field-inspector.js +1 -1
- package/dist/components/components/field-inspector.js.map +1 -1
- package/dist/components/components/registry-browser.js +3 -3
- package/dist/components/components/registry-browser.js.map +1 -1
- package/dist/components/components/taxonomy.d.ts +1 -1
- package/dist/components/components/taxonomy.js.map +1 -1
- package/dist/components/components/transfer-dialog.js +4 -3
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/components/components/transfer.d.ts +10 -10
- package/dist/components/components/transfer.js +13 -12
- package/dist/components/components/transfer.js.map +1 -1
- package/dist/components/current-user-menu.js +1 -1
- package/dist/components/current-user-menu.js.map +1 -1
- package/dist/components/delivery-health-banner.js +2 -2
- package/dist/components/delivery-health-banner.js.map +1 -1
- package/dist/components/deployment-setup-gate.js +3 -3
- package/dist/components/deployment-setup-gate.js.map +1 -1
- package/dist/components/dev-login-badge.js +2 -2
- package/dist/components/dev-login-badge.js.map +1 -1
- package/dist/components/guards.js +1 -1
- package/dist/components/guards.js.map +1 -1
- package/dist/components/inspector/document-panes.d.ts +54 -0
- package/dist/components/inspector/document-panes.js +41 -0
- package/dist/components/inspector/document-panes.js.map +1 -0
- package/dist/components/inspector/inspector-sidebar.js +1 -1
- package/dist/components/inspector/inspector-sidebar.js.map +1 -1
- package/dist/components/nav-issues-registry.d.ts +53 -0
- package/dist/components/nav-issues-registry.js +69 -0
- package/dist/components/nav-issues-registry.js.map +1 -0
- package/dist/components/nav-issues.js +4 -4
- package/dist/components/nav-issues.js.map +1 -1
- package/dist/components/operation-error.d.ts +1 -1
- package/dist/components/operation-error.js +4 -4
- package/dist/components/operation-error.js.map +1 -1
- package/dist/components/playground-badge.js +2 -2
- package/dist/components/playground-badge.js.map +1 -1
- package/dist/components/project-list.js +3 -3
- package/dist/components/project-list.js.map +1 -1
- package/dist/components/project-session.d.ts +2 -2
- package/dist/components/project-session.js +5 -5
- package/dist/components/project-session.js.map +1 -1
- package/dist/components/project-switcher.js +2 -2
- package/dist/components/project-switcher.js.map +1 -1
- package/dist/components/rail-store-status.js +1 -1
- package/dist/components/rail-store-status.js.map +1 -1
- package/dist/components/rail-view-source.d.ts +6 -5
- package/dist/components/rail-view-source.js +4 -3
- package/dist/components/rail-view-source.js.map +1 -1
- package/dist/components/records/record-lens.d.ts +51 -0
- package/dist/components/records/record-lens.js +156 -0
- package/dist/components/records/record-lens.js.map +1 -0
- package/dist/components/records/record-rail.d.ts +86 -0
- package/dist/components/records/record-rail.js +241 -0
- package/dist/components/records/record-rail.js.map +1 -0
- package/dist/components/redirects/redirects-schema.js +1 -1
- package/dist/components/redirects/redirects-schema.js.map +1 -1
- package/dist/components/references/references-panel.js +2 -2
- package/dist/components/references/references-panel.js.map +1 -1
- package/dist/components/section-nav.js +27 -11
- package/dist/components/section-nav.js.map +1 -1
- package/dist/components/section-state.js +2 -2
- package/dist/components/section-state.js.map +1 -1
- package/dist/components/section-surface.js +2 -2
- package/dist/components/section-surface.js.map +1 -1
- package/dist/components/session-expired-overlay.js +3 -3
- package/dist/components/session-expired-overlay.js.map +1 -1
- package/dist/components/settings-form.js +1 -1
- package/dist/components/settings-form.js.map +1 -1
- package/dist/components/sign-in-needed-badge.js +1 -1
- package/dist/components/sign-in-needed-badge.js.map +1 -1
- package/dist/components/sign-in-view.d.ts +2 -2
- package/dist/components/sign-in-view.js +5 -5
- package/dist/components/sign-in-view.js.map +1 -1
- package/dist/components/studio-context.js +3 -3
- package/dist/components/studio-context.js.map +1 -1
- package/dist/components/studio-project-screen.js +30 -3
- package/dist/components/studio-project-screen.js.map +1 -1
- package/dist/components/studio-shell.js +6 -5
- package/dist/components/studio-shell.js.map +1 -1
- package/dist/components/theme-context.d.ts +1 -1
- package/dist/components/theme-context.js +2 -2
- package/dist/components/theme-context.js.map +1 -1
- package/dist/components/use-hydration-phase.d.ts +1 -1
- package/dist/components/use-hydration-phase.js +1 -1
- package/dist/components/use-hydration-phase.js.map +1 -1
- package/dist/components/users/invite-user-bar.d.ts +27 -0
- package/dist/components/users/invite-user-bar.js +25 -9
- package/dist/components/users/invite-user-bar.js.map +1 -1
- package/dist/components/users/role-policy-drift-issues.d.ts +38 -0
- package/dist/components/users/role-policy-drift-issues.js +71 -0
- package/dist/components/users/role-policy-drift-issues.js.map +1 -0
- package/dist/components/users/users-collection-view-source.d.ts +1 -1
- package/dist/components/users/users-collection-view-source.js +4 -2
- package/dist/components/users/users-collection-view-source.js.map +1 -1
- package/dist/components/users/users-module.js +1 -1
- package/dist/components/users/users-module.js.map +1 -1
- package/dist/components/users/users-schema.d.ts +10 -1
- package/dist/components/users/users-schema.js +45 -25
- package/dist/components/users/users-schema.js.map +1 -1
- package/dist/components/users/users-store.js +3 -3
- package/dist/components/users/users-store.js.map +1 -1
- package/dist/components/workspace-projects.d.ts +1 -1
- package/dist/components/workspace-projects.js +1 -1
- package/dist/components/workspace-projects.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 +6 -0
- 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 +3 -1
- package/dist/lib/ai/index.js +3 -1
- 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/node-handler.d.ts +1 -1
- package/dist/lib/ai/node-handler.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 +83 -87
- package/dist/lib/ai/tools.js.map +1 -1
- package/dist/lib/ai/translate-document.d.ts +32 -4
- package/dist/lib/ai/translate-document.js +145 -47
- 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 +15 -12
- package/dist/lib/ai/update-fields.js +112 -188
- 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/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-issues.js +1 -1
- package/dist/lib/clipboard/paste-issues.js.map +1 -1
- package/dist/lib/clipboard/paste-target.d.ts +18 -9
- package/dist/lib/clipboard/paste-target.js +10 -9
- package/dist/lib/clipboard/paste-target.js.map +1 -1
- package/dist/lib/cms/field-patch.d.ts +0 -10
- package/dist/lib/cms/field-patch.js +4 -30
- package/dist/lib/cms/field-patch.js.map +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 +9 -9
- package/dist/lib/cms-views/view-model.js +2 -2
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/collection-view/collection-view-source.d.ts +14 -5
- package/dist/lib/collection-view/collection-view-source.js.map +1 -1
- package/dist/lib/data/collection-content.js +3 -2
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/data/content-session-refresh.d.ts +1 -1
- package/dist/lib/data/content-session-refresh.js.map +1 -1
- package/dist/lib/data/playground-unload-guard.d.ts +1 -1
- package/dist/lib/data/playground-unload-guard.js +1 -1
- package/dist/lib/data/playground-unload-guard.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/i18n.js +1 -1
- package/dist/lib/i18n/i18n.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +44 -3
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +38 -2
- package/dist/lib/i18n/messages.en.js +51 -3
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/project-runtime-config.d.ts +34 -6
- package/dist/lib/operations/project-runtime-config.js +50 -10
- package/dist/lib/operations/project-runtime-config.js.map +1 -1
- package/dist/lib/operations/publish-with-revision.d.ts +33 -1
- package/dist/lib/operations/publish-with-revision.js +32 -0
- package/dist/lib/operations/publish-with-revision.js.map +1 -1
- package/dist/lib/operations/runtime-validator.d.ts +19 -68
- package/dist/lib/operations/runtime-validator.js +12 -138
- package/dist/lib/operations/runtime-validator.js.map +1 -1
- package/dist/lib/operations/studio-operations.d.ts +17 -2
- package/dist/lib/operations/studio-operations.js +18 -11
- package/dist/lib/operations/studio-operations.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.d.ts +16 -3
- package/dist/lib/persistence/convex-adapter.js +83 -47
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persistence/index.js +14 -1
- package/dist/lib/persistence/index.js.map +1 -1
- package/dist/lib/persona/labels.js +4 -2
- 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/reference-model.js +1 -1
- package/dist/lib/references/reference-model.js.map +1 -1
- package/dist/lib/references/references-store.js +1 -1
- package/dist/lib/references/references-store.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +14 -11
- package/dist/lib/workspace/index.js +20 -24
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/mount.d.ts +28 -0
- package/dist/mount.js +27 -0
- package/dist/mount.js.map +1 -0
- package/dist/router/root.js +10 -10
- package/dist/router/root.js.map +1 -1
- package/dist/ui/badge.d.ts +1 -1
- package/dist/ui/button.d.ts +2 -2
- package/package.json +15 -11
- 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/redirects/redirects-collection-view-source.d.ts +0 -53
- package/dist/components/redirects/redirects-collection-view-source.js +0 -146
- package/dist/components/redirects/redirects-collection-view-source.js.map +0 -1
- package/dist/components/redirects/redirects-module.d.ts +0 -21
- package/dist/components/redirects/redirects-module.js +0 -40
- package/dist/components/redirects/redirects-module.js.map +0 -1
- package/dist/components/redirects/redirects-store.d.ts +0 -68
- package/dist/components/redirects/redirects-store.js +0 -132
- package/dist/components/redirects/redirects-store.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
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { convexTest } from 'convex-test'
|
|
3
|
+
import { makeFunctionReference } from 'convex/server'
|
|
4
|
+
import { measureMaxDepth } from '@elytracms/core/cms-core'
|
|
5
|
+
import { InMemoryAssetRepository, assetAlt, liftLegacyAssetAlts } from '@elytracms/core/persistence'
|
|
6
|
+
import type { AssetRecord } from '@elytracms/core/persistence'
|
|
7
|
+
import schema from './schema'
|
|
8
|
+
|
|
9
|
+
// convex-test fires a `runAfter(0)` on a REAL timer; fake timers keep the scheduled
|
|
10
|
+
// revalidations queued (rows written, the `webhooks:deliver` fetch never fired).
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
vi.useFakeTimers()
|
|
13
|
+
})
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
vi.useRealTimers()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* EC-587 — the one-shot migration of the legacy `alt` column into `meta.alt`, its one
|
|
20
|
+
* home: the REAL internal mutation (`assets:liftLegacyAlt`, the line the operator runs
|
|
21
|
+
* with `npx convex run`) on convex-test's backend, and the in-memory runner
|
|
22
|
+
* (`liftLegacyAssetAlts`) held to the same expectations over the same rows.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const modules = {
|
|
26
|
+
...import.meta.glob(['./**/*.ts', '!./**/*.test.ts', '!./_generated/**']),
|
|
27
|
+
'./_generated/api.ts': async () => ({}),
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const liftLegacyAlt = makeFunctionReference<'mutation'>('assets:liftLegacyAlt')
|
|
31
|
+
|
|
32
|
+
const STAMP = '2026-10-26T00:00:00.000Z'
|
|
33
|
+
|
|
34
|
+
/** The four shapes a stored asset row can hold its alt in. */
|
|
35
|
+
const ROWS = [
|
|
36
|
+
{ assetId: 'column_only', alt: 'Harbour' },
|
|
37
|
+
{ assetId: 'both', alt: 'stale column', meta: { alt: 'Meta wins', credit: 'Ada' } },
|
|
38
|
+
{ assetId: 'meta_only', meta: { alt: 'Already home' } },
|
|
39
|
+
{ assetId: 'none', meta: { credit: 'Jo' } },
|
|
40
|
+
] as const
|
|
41
|
+
|
|
42
|
+
function row(input: (typeof ROWS)[number]) {
|
|
43
|
+
return {
|
|
44
|
+
filename: `${input.assetId}.jpg`,
|
|
45
|
+
contentType: 'image/jpeg',
|
|
46
|
+
byteSize: 1,
|
|
47
|
+
storageKey: `k_${input.assetId}`,
|
|
48
|
+
createdAt: STAMP,
|
|
49
|
+
updatedAt: STAMP,
|
|
50
|
+
...input,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** What every reader sees BEFORE the lift — the migration must not change it. */
|
|
55
|
+
const READ_BEFORE = { column_only: 'Harbour', both: 'Meta wins', meta_only: 'Already home', none: null }
|
|
56
|
+
|
|
57
|
+
const EXPECTED_META = {
|
|
58
|
+
column_only: { alt: 'Harbour' },
|
|
59
|
+
both: { alt: 'Meta wins', credit: 'Ada' },
|
|
60
|
+
meta_only: { alt: 'Already home' },
|
|
61
|
+
none: { credit: 'Jo' },
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function seededBackend() {
|
|
65
|
+
const t = convexTest(schema, modules)
|
|
66
|
+
await t.run(async (ctx) => {
|
|
67
|
+
for (const input of ROWS) await ctx.db.insert('assets', row(input) as never)
|
|
68
|
+
})
|
|
69
|
+
return t
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function storedRows(t: Awaited<ReturnType<typeof seededBackend>>) {
|
|
73
|
+
return t.run(async (ctx) => {
|
|
74
|
+
const rows = await ctx.db.query('assets').collect()
|
|
75
|
+
return Object.fromEntries(rows.map((r) => [r.assetId, { alt: r.alt, meta: r.meta }]))
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
describe('EC-587 — assets:liftLegacyAlt moves the legacy column home, once', () => {
|
|
80
|
+
it('lifts the column into meta.alt (meta.alt wins when both), unsets it, and keeps every alt reading', async () => {
|
|
81
|
+
const t = await seededBackend()
|
|
82
|
+
const before = await storedRows(t)
|
|
83
|
+
expect(Object.fromEntries(Object.entries(before).map(([id, r]) => [id, assetAlt(r)]))).toEqual(READ_BEFORE)
|
|
84
|
+
|
|
85
|
+
const report = (await t.mutation(liftLegacyAlt, {})) as { scanned: number; lifted: string[] }
|
|
86
|
+
expect(report.scanned).toBe(4)
|
|
87
|
+
expect([...report.lifted].sort()).toEqual(['both', 'column_only'])
|
|
88
|
+
|
|
89
|
+
const after = await storedRows(t)
|
|
90
|
+
for (const [id, stored] of Object.entries(after)) {
|
|
91
|
+
expect(stored.alt, `${id}: the column is gone`).toBeUndefined()
|
|
92
|
+
expect(stored.meta, `${id}: meta`).toEqual(EXPECTED_META[id as keyof typeof EXPECTED_META])
|
|
93
|
+
// convex-test enforces no nesting depth — the stored shape asserts it itself.
|
|
94
|
+
expect(measureMaxDepth(stored.meta).depth).toBeLessThanOrEqual(16)
|
|
95
|
+
}
|
|
96
|
+
// The lift changes no reading: every alt is what it was.
|
|
97
|
+
expect(Object.fromEntries(Object.entries(after).map(([id, r]) => [id, assetAlt(r)]))).toEqual(READ_BEFORE)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('is idempotent: a second run lifts nothing and writes nothing', async () => {
|
|
101
|
+
const t = await seededBackend()
|
|
102
|
+
await t.mutation(liftLegacyAlt, {})
|
|
103
|
+
const once = await storedRows(t)
|
|
104
|
+
const scheduledAfterFirst = await t.run(async (ctx) => (await ctx.db.system.query('_scheduled_functions').collect()).length)
|
|
105
|
+
const again = (await t.mutation(liftLegacyAlt, {})) as { scanned: number; lifted: string[] }
|
|
106
|
+
expect(again).toEqual({ scanned: 4, lifted: [] })
|
|
107
|
+
expect(await storedRows(t)).toEqual(once)
|
|
108
|
+
const scheduledAfterSecond = await t.run(async (ctx) => (await ctx.db.system.query('_scheduled_functions').collect()).length)
|
|
109
|
+
expect(scheduledAfterSecond).toBe(scheduledAfterFirst)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('the in-memory runner does the same to the same rows (the playground / headless equivalent)', async () => {
|
|
113
|
+
const records: AssetRecord[] = ROWS.map((input) => {
|
|
114
|
+
const { assetId, ...rest } = row(input)
|
|
115
|
+
return { ...rest, id: assetId, projectId: 'p' } as AssetRecord
|
|
116
|
+
})
|
|
117
|
+
const repo = new InMemoryAssetRepository({ seed: records })
|
|
118
|
+
const report = await liftLegacyAssetAlts(repo, 'p')
|
|
119
|
+
expect(report.scanned).toBe(4)
|
|
120
|
+
expect([...report.lifted].sort()).toEqual(['both', 'column_only'])
|
|
121
|
+
const after = await repo.listAssets('p')
|
|
122
|
+
expect(Object.fromEntries(after.map((r) => [r.id, { alt: r.alt, meta: r.meta }]))).toEqual(
|
|
123
|
+
Object.fromEntries(Object.entries(EXPECTED_META).map(([id, meta]) => [id, { alt: undefined, meta }])),
|
|
124
|
+
)
|
|
125
|
+
expect((await liftLegacyAssetAlts(repo, 'p')).lifted).toEqual([])
|
|
126
|
+
})
|
|
127
|
+
})
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { convexTest } from 'convex-test'
|
|
4
|
+
import type { ConvexHttpClient } from 'convex/browser'
|
|
5
|
+
import type { FunctionReference } from 'convex/server'
|
|
6
|
+
import { defaultAssetCollection } from '@elytracms/core/cms-core'
|
|
7
|
+
import { InMemoryBlobStorage, InMemoryPersistenceAdapter } from '@elytracms/core/persistence'
|
|
8
|
+
import type { AssetRepository } from '@elytracms/core/persistence'
|
|
9
|
+
import { ConvexPersistenceAdapter } from '../src/lib/persistence/convex-adapter'
|
|
10
|
+
import { runUpload } from '../src/lib/assets/upload'
|
|
11
|
+
import schema from './schema'
|
|
12
|
+
|
|
13
|
+
// convex-test fires a `runAfter(0)` on a REAL timer; fake timers keep the scheduled
|
|
14
|
+
// revalidation queued (its row written, the `webhooks:deliver` fetch never fired).
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
vi.useFakeTimers()
|
|
17
|
+
})
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
vi.useRealTimers()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* EC-568 — "replace works in both persistence modes, same id, derived data regenerated
|
|
24
|
+
* through the upload path" and "verify the revalidation covers a replace, do not assume".
|
|
25
|
+
* The REAL `runUpload` (replace mode) against the in-memory adapter and against the
|
|
26
|
+
* Convex adapter over convex-test (the real `assets:upsert` through the real trigger
|
|
27
|
+
* wrapper); the replace's revalidation is read off `_scheduled_functions`, as
|
|
28
|
+
* `asset-revalidate.test.ts` does (its redirect write is that file's known-positive
|
|
29
|
+
* control for the instrument).
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const modules = {
|
|
33
|
+
...import.meta.glob(['./**/*.ts', '!./**/*.test.ts', '!./_generated/**']),
|
|
34
|
+
'./_generated/api.ts': async () => ({}),
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const PROJECT_ID = 'asset-replace'
|
|
38
|
+
const SESSION_USER_ID = 'user_admin'
|
|
39
|
+
|
|
40
|
+
async function convexBackend() {
|
|
41
|
+
const t = convexTest(schema, modules)
|
|
42
|
+
const now = new Date().toISOString()
|
|
43
|
+
await t.run(async (ctx) => {
|
|
44
|
+
await ctx.db.insert('members', { userId: SESSION_USER_ID, role: 'admin', createdAt: now, updatedAt: now })
|
|
45
|
+
})
|
|
46
|
+
const asAdmin = t.withIdentity({ subject: SESSION_USER_ID, issuer: 'https://convex.test' })
|
|
47
|
+
const client = {
|
|
48
|
+
query: (ref: FunctionReference<'query'>, args?: Record<string, unknown>) => asAdmin.query(ref, args ?? {}),
|
|
49
|
+
mutation: (ref: FunctionReference<'mutation'>, args?: Record<string, unknown>) => asAdmin.mutation(ref, args ?? {}),
|
|
50
|
+
} as unknown as ConvexHttpClient
|
|
51
|
+
return { t, adapter: new ConvexPersistenceAdapter(client, PROJECT_ID) }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function withRevalidationEnv<T>(run: () => Promise<T>): Promise<T> {
|
|
55
|
+
const env = (globalThis as { process: { env: Record<string, string | undefined> } }).process.env
|
|
56
|
+
const keys = ['CANVAS_REVALIDATE_ENDPOINT', 'CANVAS_REVALIDATE_SECRET'] as const
|
|
57
|
+
const previous = keys.map((key) => env[key])
|
|
58
|
+
env['CANVAS_REVALIDATE_ENDPOINT'] = 'https://host.invalid/api/revalidate'
|
|
59
|
+
env['CANVAS_REVALIDATE_SECRET'] = 'ec568-secret'
|
|
60
|
+
return run().finally(() => {
|
|
61
|
+
keys.forEach((key, index) => {
|
|
62
|
+
const value = previous[index]
|
|
63
|
+
if (value === undefined) delete env[key]
|
|
64
|
+
else env[key] = value
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const iptcJpeg = new Uint8Array(
|
|
70
|
+
readFileSync(new URL('../../core/src/cms-core/__fixtures__/embedded-metadata/iptc-xmp.jpg', import.meta.url)),
|
|
71
|
+
)
|
|
72
|
+
const newFile = {
|
|
73
|
+
name: 'new.jpg',
|
|
74
|
+
type: 'image/jpeg',
|
|
75
|
+
size: iptcJpeg.byteLength,
|
|
76
|
+
arrayBuffer: async () => iptcJpeg.slice().buffer as ArrayBuffer,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function seedHero(assets: AssetRepository) {
|
|
80
|
+
await assets.upsertAsset(PROJECT_ID, {
|
|
81
|
+
id: 'hero',
|
|
82
|
+
filename: 'old.jpg',
|
|
83
|
+
contentType: 'image/jpeg',
|
|
84
|
+
byteSize: 10,
|
|
85
|
+
width: 4000,
|
|
86
|
+
height: 3000,
|
|
87
|
+
storageKey: 'blob_old',
|
|
88
|
+
url: 'https://assets.test/old.jpg',
|
|
89
|
+
focalPoint: { x: 0.2, y: 0.8 },
|
|
90
|
+
thumb: { storageKey: 'blob_thumb_old', url: 'https://assets.test/old-t.webp', width: 480, height: 360 },
|
|
91
|
+
lqip: 'data:image/webp;base64,OLD',
|
|
92
|
+
meta: { alt: 'Editor alt', note: 'kept' },
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Replace hero's file through THE upload path; the comparable record after it. */
|
|
97
|
+
async function replaceHero(assets: AssetRepository) {
|
|
98
|
+
await seedHero(assets)
|
|
99
|
+
const result = await runUpload(
|
|
100
|
+
{
|
|
101
|
+
blobs: new InMemoryBlobStorage(),
|
|
102
|
+
assets,
|
|
103
|
+
assetFields: defaultAssetCollection().fields,
|
|
104
|
+
detectDimensions: async () => ({ width: 8, height: 6 }),
|
|
105
|
+
generateVariants: async () => ({ lqip: 'data:image/webp;base64,NEW' }),
|
|
106
|
+
},
|
|
107
|
+
{ projectId: PROJECT_ID, file: newFile, replace: { assetId: 'hero' } },
|
|
108
|
+
)
|
|
109
|
+
expect(result.ok).toBe(true)
|
|
110
|
+
const after = await assets.getAsset(PROJECT_ID, 'hero')
|
|
111
|
+
return {
|
|
112
|
+
ids: (await assets.listAssets(PROJECT_ID)).map((a) => a.id),
|
|
113
|
+
filename: after.filename,
|
|
114
|
+
byteSize: after.byteSize,
|
|
115
|
+
storageKey: after.storageKey,
|
|
116
|
+
width: after.width,
|
|
117
|
+
height: after.height,
|
|
118
|
+
thumb: after.thumb ?? null,
|
|
119
|
+
lqip: after.lqip ?? null,
|
|
120
|
+
focalPoint: after.focalPoint ?? null,
|
|
121
|
+
meta: after.meta,
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
describe('EC-568 — a file replace means the same thing on both adapters', () => {
|
|
126
|
+
it('same id; new bytes and every derived column regenerated (or cleared); meta overridden per key; focal kept', async () => {
|
|
127
|
+
const memory = await replaceHero(new InMemoryPersistenceAdapter().assets)
|
|
128
|
+
const convex = await replaceHero((await convexBackend()).adapter.assets)
|
|
129
|
+
const expected = {
|
|
130
|
+
ids: ['hero'],
|
|
131
|
+
filename: 'new.jpg',
|
|
132
|
+
byteSize: iptcJpeg.byteLength,
|
|
133
|
+
storageKey: 'blob-1',
|
|
134
|
+
width: 8,
|
|
135
|
+
height: 6,
|
|
136
|
+
thumb: null,
|
|
137
|
+
lqip: 'data:image/webp;base64,NEW',
|
|
138
|
+
focalPoint: { x: 0.2, y: 0.8 },
|
|
139
|
+
meta: {
|
|
140
|
+
alt: 'A harbour at dusk & boats',
|
|
141
|
+
note: 'kept',
|
|
142
|
+
credit: 'Ana Credit (XMP)',
|
|
143
|
+
copyright: '© 2026 Example (XMP)',
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
expect({ memory, convex }).toEqual({ memory: expected, convex: expected })
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('the replace reaches the live site: it schedules the asset revalidation (kind asset, the asset id)', async () => {
|
|
150
|
+
await withRevalidationEnv(async () => {
|
|
151
|
+
const { t, adapter } = await convexBackend()
|
|
152
|
+
await seedHero(adapter.assets) // a create schedules nothing (EC-579)
|
|
153
|
+
const scheduled = async () =>
|
|
154
|
+
t.run(async (ctx) =>
|
|
155
|
+
(await ctx.db.system.query('_scheduled_functions').collect())
|
|
156
|
+
.filter((row) => row.name.startsWith('webhooks'))
|
|
157
|
+
.map((row) => (row.args as [{ event?: unknown }])[0]?.event),
|
|
158
|
+
)
|
|
159
|
+
expect(await scheduled()).toEqual([])
|
|
160
|
+
const result = await runUpload(
|
|
161
|
+
{ blobs: new InMemoryBlobStorage(), assets: adapter.assets, assetFields: defaultAssetCollection().fields },
|
|
162
|
+
{ projectId: PROJECT_ID, file: newFile, replace: { assetId: 'hero' } },
|
|
163
|
+
)
|
|
164
|
+
expect(result.ok).toBe(true)
|
|
165
|
+
expect(await scheduled()).toEqual([{ kind: 'asset', id: 'hero' }])
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
})
|
|
@@ -101,7 +101,7 @@ const assetArgs = {
|
|
|
101
101
|
byteSize: 1,
|
|
102
102
|
storageKey: 'placeholder',
|
|
103
103
|
url: 'https://assets.invalid/photo.png',
|
|
104
|
-
alt: 'old',
|
|
104
|
+
meta: { alt: 'old' },
|
|
105
105
|
createdAt: STAMP,
|
|
106
106
|
updatedAt: STAMP,
|
|
107
107
|
}
|
|
@@ -128,13 +128,13 @@ describe('EC-579 — the write chokepoint revalidates on every asset edit', () =
|
|
|
128
128
|
await asAdmin.mutation(upsertAsset, assetArgs)
|
|
129
129
|
expect(await scheduledRevalidations(t)).toHaveLength(1)
|
|
130
130
|
|
|
131
|
-
await asAdmin.mutation(upsertAsset, { ...assetArgs, alt: 'new' })
|
|
131
|
+
await asAdmin.mutation(upsertAsset, { ...assetArgs, meta: { alt: 'new' } })
|
|
132
132
|
|
|
133
133
|
// The edit IS live output: the published snapshot already serves it.
|
|
134
134
|
const snapshot = (await t.query(deliverySnapshot, { projectId: 'ec579' })) as {
|
|
135
|
-
assets: { alt?: string }[]
|
|
135
|
+
assets: { meta?: { alt?: string } }[]
|
|
136
136
|
}
|
|
137
|
-
expect(snapshot.assets[0]?.alt).toBe('new')
|
|
137
|
+
expect(snapshot.assets[0]?.meta?.alt).toBe('new')
|
|
138
138
|
|
|
139
139
|
expect(await scheduledRevalidations(t)).toEqual([{ kind: 'redirects' }, ASSET_EVENT])
|
|
140
140
|
})
|
|
@@ -179,13 +179,13 @@ describe('EC-579 — the write chokepoint revalidates on every asset edit', () =
|
|
|
179
179
|
const t = await freshBackend()
|
|
180
180
|
const asAdmin = t.withIdentity({ subject: SESSION_USER_ID, issuer: 'https://convex.test' })
|
|
181
181
|
await asAdmin.mutation(upsertAsset, assetArgs)
|
|
182
|
-
await asAdmin.mutation(upsertAsset, { ...assetArgs, alt: 'new' })
|
|
182
|
+
await asAdmin.mutation(upsertAsset, { ...assetArgs, meta: { alt: 'new' } })
|
|
183
183
|
// The first test is the known-positive control: the same edit on the same
|
|
184
184
|
// harness DOES schedule when the scope is configured.
|
|
185
185
|
expect(await scheduledRevalidations(t)).toEqual([])
|
|
186
186
|
// The write itself still happened — silence is at the webhook, not the save.
|
|
187
187
|
expect(
|
|
188
|
-
await t.run(async (ctx) => (await ctx.db.query('assets').first())?.alt),
|
|
188
|
+
await t.run(async (ctx) => (await ctx.db.query('assets').first())?.meta?.alt),
|
|
189
189
|
).toBe('new')
|
|
190
190
|
})
|
|
191
191
|
})
|
package/convex/assets.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { ConvexError, v } from 'convex/values'
|
|
2
2
|
import type { GenericId, Validator } from 'convex/values'
|
|
3
3
|
import type { DocumentByName, WithoutSystemFields } from 'convex/server'
|
|
4
|
-
import {
|
|
4
|
+
import { liftLegacyAlt as legacyAltLift, rejectedWriteMessage, resolveAssetWrite } from '@elytracms/core/persistence'
|
|
5
5
|
import type { DataModel } from './functions'
|
|
6
|
-
import { mutation, query } from './functions'
|
|
6
|
+
import { internalMutation, mutation, query } from './functions'
|
|
7
7
|
import { requireCapability } from './guard'
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -23,9 +23,10 @@ const clearable = <T, F extends string>(validator: Validator<T, 'required', F>)
|
|
|
23
23
|
v.optional(v.union(v.null(), validator))
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
-
* Write one asset (EC-577). The ONE write rule (`
|
|
26
|
+
* Write one asset (EC-577). The ONE write rule (`resolveAssetWrite`, shared with
|
|
27
27
|
* the in-memory adapter) runs here, inside the transaction: a column absent from
|
|
28
|
-
* the args keeps its stored value, `null` clears it, a value sets it
|
|
28
|
+
* the args keeps its stored value, `null` clears it, a value sets it — and the legacy
|
|
29
|
+
* `alt` column can only be cleared (EC-587: an asset's alt is `meta.alt`). An edit is
|
|
29
30
|
* ONE `db.patch` — the rule's store patch, where a cleared column is `undefined`,
|
|
30
31
|
* which `db.patch` removes — so the write-chokepoint triggers fire once. No stored
|
|
31
32
|
* asset → ONE `db.insert` of the complete row (the `defaults` fill what the write
|
|
@@ -41,6 +42,12 @@ export const upsert = mutation({
|
|
|
41
42
|
storageKey: v.optional(v.string()),
|
|
42
43
|
width: clearable(v.number()),
|
|
43
44
|
height: clearable(v.number()),
|
|
45
|
+
/**
|
|
46
|
+
* LEGACY column (EC-587): accepted on the wire only so a write that still sets it
|
|
47
|
+
* is refused BY THE RULE with a message naming `meta.alt` (not a bare validator
|
|
48
|
+
* error); `null` clears it. Dropped with the column once `liftLegacyAlt` ran on
|
|
49
|
+
* every deployment.
|
|
50
|
+
*/
|
|
44
51
|
alt: clearable(v.string()),
|
|
45
52
|
title: clearable(v.string()),
|
|
46
53
|
url: clearable(v.string()),
|
|
@@ -73,7 +80,7 @@ export const upsert = mutation({
|
|
|
73
80
|
.query('assets')
|
|
74
81
|
.withIndex('by_asset', (q) => q.eq('assetId', assetId))
|
|
75
82
|
.first()
|
|
76
|
-
const resolved =
|
|
83
|
+
const resolved = resolveAssetWrite(existing ?? undefined, write, defaults ?? {})
|
|
77
84
|
if (!resolved.ok) {
|
|
78
85
|
throw new ConvexError({
|
|
79
86
|
code: 'validation',
|
|
@@ -91,6 +98,35 @@ export const upsert = mutation({
|
|
|
91
98
|
},
|
|
92
99
|
})
|
|
93
100
|
|
|
101
|
+
/**
|
|
102
|
+
* EC-587 — the one-shot migration of the legacy `alt` column: every asset row holding
|
|
103
|
+
* it gets the value lifted into `meta.alt` (unless `meta` already carries an `alt`
|
|
104
|
+
* key, which wins — it is what every reader already reads) and the column unset, by
|
|
105
|
+
* the ONE pure rule (core `liftLegacyAlt`, shared with the in-memory runner
|
|
106
|
+
* `liftLegacyAssetAlts`). Idempotent: a second run lifts nothing.
|
|
107
|
+
*
|
|
108
|
+
* Internal: run by an admin from the CLI, never by a client —
|
|
109
|
+
* `npx convex run assets:liftLegacyAlt` (no args). Each lifted row is one `db.patch`
|
|
110
|
+
* through the write-chokepoint triggers, so it schedules the asset's revalidation like
|
|
111
|
+
* any asset edit (the delivered alt is unchanged by the lift; the sweep is harmless).
|
|
112
|
+
* One transaction: Convex bounds it at 8192 written rows, far above any v1 library —
|
|
113
|
+
* documented, not paginated (as `cms:clearDocuments`).
|
|
114
|
+
*/
|
|
115
|
+
export const liftLegacyAlt = internalMutation({
|
|
116
|
+
args: {},
|
|
117
|
+
handler: async (ctx) => {
|
|
118
|
+
const rows = await ctx.db.query('assets').collect()
|
|
119
|
+
const lifted: string[] = []
|
|
120
|
+
for (const row of rows) {
|
|
121
|
+
const patch = legacyAltLift(row)
|
|
122
|
+
if (!patch) continue
|
|
123
|
+
await ctx.db.patch(row._id, patch as Partial<AssetDoc>)
|
|
124
|
+
lifted.push(row.assetId)
|
|
125
|
+
}
|
|
126
|
+
return { scanned: rows.length, lifted }
|
|
127
|
+
},
|
|
128
|
+
})
|
|
129
|
+
|
|
94
130
|
export const get = query({
|
|
95
131
|
args: { assetId: v.string() },
|
|
96
132
|
handler: async (ctx, args) => {
|
package/convex/auth.ts
CHANGED
|
@@ -4,8 +4,9 @@ import { actionGeneric } from 'convex/server'
|
|
|
4
4
|
import type { AnyDataModel, GenericMutationCtx } from 'convex/server'
|
|
5
5
|
import { ConvexError, v } from 'convex/values'
|
|
6
6
|
import type { GenericId } from 'convex/values'
|
|
7
|
-
import { classifyInviteSignup, normalizeInviteEmail } from '@elytracms/core/persistence'
|
|
7
|
+
import { bootstrapRole, classifyInviteSignup, normalizeInviteEmail } from '@elytracms/core/persistence'
|
|
8
8
|
import type { InviteSignupDecision } from '@elytracms/core/persistence'
|
|
9
|
+
import { deployedRolePolicy } from './roles'
|
|
9
10
|
|
|
10
11
|
/** The provider list `convexAuth` accepts, derived from its own signature. */
|
|
11
12
|
type ConvexAuthProviders = Parameters<typeof convexAuth>[0]['providers']
|
|
@@ -210,7 +211,9 @@ export async function gateSessionCreation(
|
|
|
210
211
|
case 'bootstrap-admin':
|
|
211
212
|
await ctx.db.insert('members', {
|
|
212
213
|
userId: args.userId,
|
|
213
|
-
|
|
214
|
+
// EC-605: the deployed policy's strongest role (which the policy rules require
|
|
215
|
+
// to grant `project.administer`) — `admin` under the default policy.
|
|
216
|
+
role: bootstrapRole(deployedRolePolicy()),
|
|
214
217
|
createdAt: now,
|
|
215
218
|
updatedAt: now,
|
|
216
219
|
})
|
package/convex/authority.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { checkAuthorityFloor } from '@elytracms/core/operations'
|
|
2
2
|
import {
|
|
3
3
|
buildAuthorityAuditEntry,
|
|
4
|
-
|
|
4
|
+
isKnownRole,
|
|
5
|
+
policyRoleIds,
|
|
5
6
|
roleGrantsCapability,
|
|
6
7
|
} from '@elytracms/core/persistence'
|
|
7
8
|
import type { AuthorityAuditOutcome, ProjectRole } from '@elytracms/core/persistence'
|
|
8
9
|
import type { MutationCtx } from './functions'
|
|
9
10
|
import { memberRole, type GuardCtx } from './guard'
|
|
11
|
+
import { deployedRolePolicy } from './roles'
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* The engine integrity FLOOR at the Convex trust boundary (EC-240 / AD-18 tenet
|
|
@@ -16,9 +18,10 @@ import { memberRole, type GuardCtx } from './guard'
|
|
|
16
18
|
* invariants on authority-mutating handlers (`members:set`, `members:remove` —
|
|
17
19
|
* and therefore `users.save`/`users.remove`, which delegate to them):
|
|
18
20
|
*
|
|
19
|
-
* - **(a) assign only roles you hold** —
|
|
20
|
-
*
|
|
21
|
-
*
|
|
21
|
+
* - **(a) assign only roles you hold** — never fires under the default policy (the
|
|
22
|
+
* only administrator, `admin`, is the strongest role); under a declared policy
|
|
23
|
+
* with an administering role below the strongest (EC-605) it is what stops that
|
|
24
|
+
* role granting — or inviting someone into (`invites:create`) — a rank above its own.
|
|
22
25
|
* - **(b) the last admin can't be removed or demoted** — the hole this closes:
|
|
23
26
|
* before EC-240 the handlers did a blind upsert/delete, so an admin could
|
|
24
27
|
* remove the sole admin and lock everyone out (the empty-table bootstrap in
|
|
@@ -27,28 +30,32 @@ import { memberRole, type GuardCtx } from './guard'
|
|
|
27
30
|
* The decision logic is the ONE shared `checkAuthorityFloor` primitive that the
|
|
28
31
|
* engine resolver also calls — this file only supplies the primitives it reads
|
|
29
32
|
* from `ctx.db`, so client and server can never drift. Denials throw
|
|
30
|
-
* `unauthorized: …`, matching the guard convention.
|
|
33
|
+
* `unauthorized: …`, matching the guard convention. The policy is the deployment's
|
|
34
|
+
* (EC-605, `deployedRolePolicy`) — the one `requireCapability` granted by.
|
|
31
35
|
*/
|
|
32
36
|
export async function assertAuthorityFloor(
|
|
33
37
|
ctx: GuardCtx,
|
|
34
38
|
params: {
|
|
35
39
|
/** The caller (already `requireCapability('project.administer')`-checked). */
|
|
36
40
|
readonly callerId: string
|
|
37
|
-
/**
|
|
38
|
-
|
|
41
|
+
/**
|
|
42
|
+
* The member whose role/membership this op changes; absent for an invite (EC-605 —
|
|
43
|
+
* it grants a role to someone who is not a member yet, so only (a) can apply).
|
|
44
|
+
*/
|
|
45
|
+
readonly targetUserId?: string
|
|
39
46
|
/** Roles this op grants the target (empty for a pure removal). */
|
|
40
47
|
readonly rolesGranted: readonly ProjectRole[]
|
|
41
48
|
/** True when the op removes the membership entirely. */
|
|
42
49
|
readonly removesMembership: boolean
|
|
43
50
|
},
|
|
44
51
|
): Promise<void> {
|
|
52
|
+
const policy = deployedRolePolicy()
|
|
45
53
|
const callerRole = await memberRole(ctx, params.callerId)
|
|
46
|
-
const targetRole = await memberRole(ctx, params.targetUserId)
|
|
54
|
+
const targetRole = params.targetUserId !== undefined ? await memberRole(ctx, params.targetUserId) : null
|
|
47
55
|
const members = await ctx.db.query('members').collect()
|
|
48
56
|
// "admin" generalizes to "grants project.administer" (EC-240c) — the same
|
|
49
57
|
// policy the engine resolver uses, so client and server can never drift.
|
|
50
|
-
const isAdministrator = (role: string) =>
|
|
51
|
-
roleGrantsCapability(role, 'project.administer', defaultRolePolicy)
|
|
58
|
+
const isAdministrator = (role: string) => roleGrantsCapability(role, 'project.administer', policy)
|
|
52
59
|
const adminCount = members.filter((m) => isAdministrator(m.role)).length
|
|
53
60
|
|
|
54
61
|
const decision = checkAuthorityFloor(
|
|
@@ -59,11 +66,28 @@ export async function assertAuthorityFloor(
|
|
|
59
66
|
removesMembership: params.removesMembership,
|
|
60
67
|
adminCount,
|
|
61
68
|
},
|
|
62
|
-
|
|
69
|
+
policy,
|
|
63
70
|
)
|
|
64
71
|
if (!decision.allow) throw new Error(`unauthorized: ${decision.reason}`)
|
|
65
72
|
}
|
|
66
73
|
|
|
74
|
+
/**
|
|
75
|
+
* EC-605 — refuse a role the deployed policy does not declare, at the WRITE (a member's
|
|
76
|
+
* role, an invite's role), visibly: `invalid-role: …` naming the declared set. The
|
|
77
|
+
* studio validates against the project's config first; this is the trust boundary's own
|
|
78
|
+
* check, so a hand-crafted client or a drifted deployment can never store a role nobody
|
|
79
|
+
* can enforce (an unknown role grants nothing — it would fail closed on every read,
|
|
80
|
+
* silently).
|
|
81
|
+
*/
|
|
82
|
+
export function assertDeclaredRole(role: string): void {
|
|
83
|
+
const policy = deployedRolePolicy()
|
|
84
|
+
if (!isKnownRole(role, policy)) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`invalid-role: role "${role}" is not declared in this deployment's role policy (declared: ${policyRoleIds(policy).join(', ')}). Declare it in elytra.config.ts \`roles\` and re-provision (\`elytra init\`), or pick a declared role.`,
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
67
91
|
/**
|
|
68
92
|
* Append one {@link buildAuthorityAuditEntry} row to the `authorityAudit` table
|
|
69
93
|
* (EC-240 H-tail item 6). Called at the floor chokepoint (`members:set` /
|
package/convex/cms.ts
CHANGED
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
unfoldValue,
|
|
11
11
|
} from '@elytracms/core/cms-core'
|
|
12
12
|
import type { CmsDocument, DocumentPatch, PatchLogKind, ReplacePrecondition } from '@elytracms/core/cms-core'
|
|
13
|
+
import { isPinnableVersion } from '@elytracms/core/persistence'
|
|
14
|
+
import type { DocumentVersionKind } from '@elytracms/core/persistence'
|
|
13
15
|
import { mutation, query } from './functions'
|
|
14
16
|
import type { MutationCtx, QueryCtx } from './functions'
|
|
15
17
|
import { requireCapability } from './guard'
|
|
@@ -454,10 +456,15 @@ export const clearDocuments = mutation({
|
|
|
454
456
|
},
|
|
455
457
|
})
|
|
456
458
|
|
|
457
|
-
/**
|
|
459
|
+
/**
|
|
460
|
+
* Insert the next monotonic version row of a document (folded); returns it. `kind` is the
|
|
461
|
+
* mutation that records it (EC-585) — decided HERE, never taken from a client argument, so
|
|
462
|
+
* a `content.write` caller cannot mint a pinnable row through `saveDocumentVersion`.
|
|
463
|
+
*/
|
|
458
464
|
async function insertVersion(
|
|
459
465
|
ctx: MutationCtx,
|
|
460
466
|
args: { collection: string; docId: string; snapshot: unknown; label?: string; origin?: string; createdAt: string },
|
|
467
|
+
kind: DocumentVersionKind,
|
|
461
468
|
) {
|
|
462
469
|
// EC-302: versions are appended monotonically, so the latest-created row holds the
|
|
463
470
|
// highest version — read just that one instead of the whole history.
|
|
@@ -475,6 +482,7 @@ async function insertVersion(
|
|
|
475
482
|
snapshot: foldDocument(args.snapshot as CmsDocument),
|
|
476
483
|
...(args.label ? { label: args.label } : {}),
|
|
477
484
|
...(args.origin ? { origin: args.origin } : {}),
|
|
485
|
+
kind,
|
|
478
486
|
createdAt: args.createdAt,
|
|
479
487
|
})
|
|
480
488
|
return (await ctx.db.get(id))!
|
|
@@ -484,7 +492,8 @@ async function insertVersion(
|
|
|
484
492
|
* Document version history (EC-224): append-only snapshots in `documentVersions`; the
|
|
485
493
|
* live one is pinned by `cmsDocuments.publishedVersion`. Version numbers are 1-based,
|
|
486
494
|
* monotonic per document. Used by the pre-restore snapshot and `documents.snapshot`;
|
|
487
|
-
* publish records its version inside `publishDocument` (one step).
|
|
495
|
+
* publish records its version inside `publishDocument` (one step). A row written here is
|
|
496
|
+
* a `'snapshot'` (EC-585): a restore point `publishDocumentVersion` refuses to pin.
|
|
488
497
|
*/
|
|
489
498
|
export const saveDocumentVersion = mutation({
|
|
490
499
|
args: {
|
|
@@ -498,7 +507,7 @@ export const saveDocumentVersion = mutation({
|
|
|
498
507
|
handler: async (ctx, args) => {
|
|
499
508
|
await requireCapability(ctx, 'content.write') // EC-150
|
|
500
509
|
assertWithinWriteLimits(args.snapshot, 'snapshot') // EC-304 B-4
|
|
501
|
-
const row = await insertVersion(ctx, args)
|
|
510
|
+
const row = await insertVersion(ctx, args, 'snapshot')
|
|
502
511
|
return withUnfoldedSnapshot(row)
|
|
503
512
|
},
|
|
504
513
|
})
|
|
@@ -534,7 +543,7 @@ export const publishDocument = mutation({
|
|
|
534
543
|
label: args.label ?? 'published',
|
|
535
544
|
...(args.origin ? { origin: args.origin } : {}),
|
|
536
545
|
createdAt: args.at,
|
|
537
|
-
})
|
|
546
|
+
}, 'publish')
|
|
538
547
|
const revision = existing.revision + 1
|
|
539
548
|
// EC-595 retention: compact the log behind the PREVIOUS publish (one cycle kept).
|
|
540
549
|
const previousPublish = (await logAfter(ctx, args.collection, args.docId, 0))
|
|
@@ -636,10 +645,15 @@ export const listAllDocumentVersions = query({
|
|
|
636
645
|
/**
|
|
637
646
|
* Pin an EXISTING version live (EC-224 `pinVersion`, changeset revert). Pin integrity is
|
|
638
647
|
* enforced here, in the transaction: the version must exist for THIS document
|
|
639
|
-
* (`missing-version` otherwise)
|
|
640
|
-
* (
|
|
641
|
-
*
|
|
642
|
-
*
|
|
648
|
+
* (`missing-version` otherwise) and must have been recorded by `publishDocument`
|
|
649
|
+
* (`not-pinnable` otherwise — EC-585: a `saveDocumentVersion` row was never validated, so
|
|
650
|
+
* an editor calling this mutation directly cannot put one live; Codex P03). The changed
|
|
651
|
+
* flag is DERIVED against the pinned snapshot (a pin of an older version against a newer
|
|
652
|
+
* draft reads `true` — Codex P05's sibling), the revision bumps, a `pin` row is logged.
|
|
653
|
+
*
|
|
654
|
+
* Server-side trust (EC-565 1e, owner-ratified): the checks here are DATA-ONLY — revision,
|
|
655
|
+
* pin integrity, authority. Schema validation stays the client gate (`documents.publish`
|
|
656
|
+
* runs it, with the hidden-clear, before `publishDocument` records the row this pins).
|
|
643
657
|
*/
|
|
644
658
|
export const publishDocumentVersion = mutation({
|
|
645
659
|
args: {
|
|
@@ -654,9 +668,9 @@ export const publishDocumentVersion = mutation({
|
|
|
654
668
|
const actor = await requireCapability(ctx, 'content.write') // EC-150
|
|
655
669
|
const doc = await draftRow(ctx, args.collection, args.docId)
|
|
656
670
|
if (!doc) return { status: 'missing' as const }
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
}
|
|
671
|
+
const target = await versionRow(ctx, args.collection, args.docId, args.version)
|
|
672
|
+
if (!target) return { status: 'missing-version' as const }
|
|
673
|
+
if (!isPinnableVersion(target)) return { status: 'not-pinnable' as const }
|
|
660
674
|
const revision = doc.revision + 1
|
|
661
675
|
const patch = {
|
|
662
676
|
revision,
|