@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,325 @@
|
|
|
1
|
+
import { ConvexError, v } from 'convex/values'
|
|
2
|
+
import type { GenericId } from 'convex/values'
|
|
3
|
+
import type { RegisteredMutation, RegisteredQuery } from 'convex/server'
|
|
4
|
+
import {
|
|
5
|
+
PersistenceValidationError,
|
|
6
|
+
matchesRecordQuery,
|
|
7
|
+
orderRecords,
|
|
8
|
+
recordSlots,
|
|
9
|
+
recordWireRow,
|
|
10
|
+
resolveRecordQuery,
|
|
11
|
+
resolveRecordSave,
|
|
12
|
+
} from '@elytracms/core/persistence'
|
|
13
|
+
import type {
|
|
14
|
+
AnyRecordKind,
|
|
15
|
+
IndexedRecord,
|
|
16
|
+
RecordColumns,
|
|
17
|
+
RecordQuery,
|
|
18
|
+
RecordWireRow,
|
|
19
|
+
ResolvedRecordQuery,
|
|
20
|
+
} from '@elytracms/core/persistence'
|
|
21
|
+
import { mutation, query } from './functions'
|
|
22
|
+
import type { MutationCtx, QueryCtx } from './functions'
|
|
23
|
+
import { requireCapability } from './guard'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* EC-616 — the Convex half of the generic record store: ONE implementation, instantiated
|
|
27
|
+
* per record kind. A kind's module is one line,
|
|
28
|
+
*
|
|
29
|
+
* ```ts
|
|
30
|
+
* // an addon's convex module (and the engine's own kinds) — the same way
|
|
31
|
+
* export const { save, get, list, remove } = defineRecordFunctions(submissionRecordKind)
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* and reaches a deployment through the consumer's one re-export line for that module
|
|
35
|
+
* (`convex/forms.ts`: `export * from '@elytracms/forms/convex'`), so the functions are
|
|
36
|
+
* addressed `<module>:save` (the kind declares its `module`). The engine never imports a
|
|
37
|
+
* kind it did not declare: an addon's convex module brings its own declaration.
|
|
38
|
+
*
|
|
39
|
+
* This file registers NO functions itself — it is a factory, imported by the modules
|
|
40
|
+
* that do (`./record-functions` is not a consumer re-export).
|
|
41
|
+
*
|
|
42
|
+
* **The wire is the kind's own:** `save` takes the kind's `idKey` (`redirectId`) and its
|
|
43
|
+
* columns flat (plus `meta`, or a per-key `metaPatch`) (each `v.optional(v.any())` — the kind's zod schema is the one validator,
|
|
44
|
+
* run below, never a hand-mirrored Convex copy), plus `createdAt`/`updatedAt`; every
|
|
45
|
+
* function returns rows flat (`recordWireRow`). A kind's columns are therefore its
|
|
46
|
+
* Convex argument names — `defineRecordKind` refuses the names that would collide.
|
|
47
|
+
*
|
|
48
|
+
* **The write** is the EC-577 rule (`resolveRecordSave`, shared with the in-memory store),
|
|
49
|
+
* inside this transaction: absent keeps, `null` clears an optional column, a value sets,
|
|
50
|
+
* no stored record → a complete create or a `validation` `ConvexError` (the client maps
|
|
51
|
+
* it to `PersistenceValidationError`). ONE `db.patch` or ONE `db.insert` into `records`,
|
|
52
|
+
* so the write-chokepoint trigger fires once per save. Access is the kind's (`access`,
|
|
53
|
+
* default `content.read` / `content.write`), checked first.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
type RecordDoc = {
|
|
57
|
+
_id: GenericId<'records'>
|
|
58
|
+
kind: string
|
|
59
|
+
recordId: string
|
|
60
|
+
data: RecordColumns
|
|
61
|
+
key1?: string
|
|
62
|
+
key2?: string
|
|
63
|
+
at: string
|
|
64
|
+
createdAt: string
|
|
65
|
+
updatedAt: string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* TOLERANT READ (marked; EC-616's redirect move, the EC-587 precedent): a kind whose rows
|
|
70
|
+
* used to live in a table of their own declares where, so a deployment whose functions
|
|
71
|
+
* moved but whose migration has not run yet serves exactly what it served before — no
|
|
72
|
+
* gap. Reads union the legacy rows a record does not shadow; a save of a legacy-only id
|
|
73
|
+
* lifts it (the record is created from the legacy columns, the legacy row deleted, one
|
|
74
|
+
* transaction); a remove deletes both. Writes NEVER go to the legacy table. Removed with
|
|
75
|
+
* the legacy table once its migration is reported run on every deployment (a gated item).
|
|
76
|
+
*/
|
|
77
|
+
export interface LegacyRecordSource {
|
|
78
|
+
/** The legacy row for one id: its columns and stamps, and a handle to delete it. */
|
|
79
|
+
find(ctx: QueryCtx | MutationCtx, recordId: string): Promise<LegacyRecord | null>
|
|
80
|
+
/** Every legacy row (the tables are small editorial sets: read whole). */
|
|
81
|
+
list(ctx: QueryCtx | MutationCtx): Promise<LegacyRecord[]>
|
|
82
|
+
/** Delete one legacy row (after its record exists). */
|
|
83
|
+
remove(ctx: MutationCtx, record: LegacyRecord): Promise<void>
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface LegacyRecord {
|
|
87
|
+
recordId: string
|
|
88
|
+
columns: RecordColumns
|
|
89
|
+
createdAt: string
|
|
90
|
+
updatedAt: string
|
|
91
|
+
/** The legacy row's own id, opaque to the factory. */
|
|
92
|
+
handle: unknown
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** A `PersistenceValidationError` from the shared rule, as the `ConvexError` the client maps back. */
|
|
96
|
+
function asConvexValidation(error: unknown): unknown {
|
|
97
|
+
if (!(error instanceof PersistenceValidationError)) return error
|
|
98
|
+
return new ConvexError({ code: 'validation', message: error.message, issues: [...error.issues] })
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function rethrowAsConvex<T>(work: () => T): T {
|
|
102
|
+
try {
|
|
103
|
+
return work()
|
|
104
|
+
} catch (error) {
|
|
105
|
+
throw asConvexValidation(error)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** One stored record of a kind by id (`by_kind_record`). */
|
|
110
|
+
export async function findRecordDoc(
|
|
111
|
+
ctx: QueryCtx | MutationCtx,
|
|
112
|
+
kind: AnyRecordKind,
|
|
113
|
+
recordId: string,
|
|
114
|
+
): Promise<RecordDoc | null> {
|
|
115
|
+
return (await ctx.db
|
|
116
|
+
.query('records')
|
|
117
|
+
.withIndex('by_kind_record', (q) => q.eq('kind', kind.kind).eq('recordId', recordId))
|
|
118
|
+
.first()) as RecordDoc | null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** A stored row as the kind's wire row. */
|
|
122
|
+
export function recordDocWire(kind: AnyRecordKind, doc: RecordDoc): RecordWireRow {
|
|
123
|
+
return recordWireRow(kind, doc.recordId, doc.data, doc.createdAt, doc.updatedAt)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The rows of one kind a resolved query selects, straight from the index the query
|
|
128
|
+
* resolved to: its equalities, its time range, `.order('desc')` and `.take(limit)` —
|
|
129
|
+
* which break ties exactly as `orderRecords` does (insertion order). An unbounded list
|
|
130
|
+
* of an `orderBy` kind is read in index order and ordered by the shared function.
|
|
131
|
+
*/
|
|
132
|
+
export async function queryRecordDocs(
|
|
133
|
+
ctx: QueryCtx | MutationCtx,
|
|
134
|
+
kind: AnyRecordKind,
|
|
135
|
+
resolved: ResolvedRecordQuery,
|
|
136
|
+
): Promise<RecordDoc[]> {
|
|
137
|
+
// The index range builder is typed per index; the four shapes share `kind` + `at`.
|
|
138
|
+
type Range = {
|
|
139
|
+
eq(field: string, value: string): Range
|
|
140
|
+
gte(field: string, value: string): Range
|
|
141
|
+
lt(field: string, value: string): Range
|
|
142
|
+
}
|
|
143
|
+
const range = (q: Range): Range => {
|
|
144
|
+
let r = q.eq('kind', kind.kind)
|
|
145
|
+
if (resolved.index === 'by_kind_key1' || resolved.index === 'by_kind_keys') r = r.eq('key1', resolved.key1!)
|
|
146
|
+
if (resolved.index === 'by_kind_key2' || resolved.index === 'by_kind_keys') r = r.eq('key2', resolved.key2!)
|
|
147
|
+
if (resolved.since !== undefined) r = r.gte('at', resolved.since)
|
|
148
|
+
if (resolved.before !== undefined) r = r.lt('at', resolved.before)
|
|
149
|
+
return r
|
|
150
|
+
}
|
|
151
|
+
const byColumn = kind.orderBy !== undefined && resolved.limit === undefined
|
|
152
|
+
const indexed = ctx.db.query('records').withIndex(resolved.index, range as never)
|
|
153
|
+
const read = !byColumn && resolved.order === 'desc' ? indexed.order('desc') : indexed
|
|
154
|
+
const docs = (resolved.limit !== undefined ? await read.take(resolved.limit) : await read.collect()) as RecordDoc[]
|
|
155
|
+
if (!byColumn) return docs
|
|
156
|
+
return orderRecords(kind, docs.map(indexedDoc), resolved).map((entry) => entry.doc)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function indexedDoc(doc: RecordDoc): IndexedRecord & { doc: RecordDoc } {
|
|
160
|
+
return { id: doc.recordId, slots: { ...doc }, columns: doc.data, doc }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The wire rows of one kind for a query — the records, plus (while a legacy source is
|
|
165
|
+
* declared) the legacy rows no record shadows, matched and ordered by the SAME pure
|
|
166
|
+
* functions the in-memory store uses. The one list reader: the kind's `list` and any
|
|
167
|
+
* server-side reader of the kind (the delivery snapshot) go through it.
|
|
168
|
+
*/
|
|
169
|
+
export async function listRecordWire(
|
|
170
|
+
ctx: QueryCtx | MutationCtx,
|
|
171
|
+
kind: AnyRecordKind,
|
|
172
|
+
resolved: ResolvedRecordQuery,
|
|
173
|
+
legacy?: LegacyRecordSource,
|
|
174
|
+
): Promise<RecordWireRow[]> {
|
|
175
|
+
if (!legacy) return (await queryRecordDocs(ctx, kind, resolved)).map((doc) => recordDocWire(kind, doc))
|
|
176
|
+
// TOLERANT READ (marked): the union is read unbounded and ordered once, here.
|
|
177
|
+
const { limit: _limit, ...unbounded } = resolved
|
|
178
|
+
const docs = await queryRecordDocs(ctx, kind, { ...unbounded, order: 'asc' })
|
|
179
|
+
const shadowed = new Set(docs.map((doc) => doc.recordId))
|
|
180
|
+
const legacyRows = (await legacy.list(ctx))
|
|
181
|
+
.filter((row) => !shadowed.has(row.recordId))
|
|
182
|
+
.map((row) => ({
|
|
183
|
+
id: row.recordId,
|
|
184
|
+
slots: recordSlots(kind, row.columns, row.createdAt),
|
|
185
|
+
columns: row.columns,
|
|
186
|
+
wire: recordWireRow(kind, row.recordId, row.columns, row.createdAt, row.updatedAt),
|
|
187
|
+
}))
|
|
188
|
+
.filter((row) => matchesRecordQuery(row, resolved))
|
|
189
|
+
// Legacy rows predate every record, so they lead in insertion order.
|
|
190
|
+
const union = [...legacyRows, ...docs.map((doc) => ({ ...indexedDoc(doc), wire: recordDocWire(kind, doc) }))]
|
|
191
|
+
return orderRecords(kind, union, resolved).map((row) => row.wire)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** One record's wire row by id — the record, else (while declared) its legacy row. */
|
|
195
|
+
export async function getRecordWire(
|
|
196
|
+
ctx: QueryCtx | MutationCtx,
|
|
197
|
+
kind: AnyRecordKind,
|
|
198
|
+
recordId: string,
|
|
199
|
+
legacy?: LegacyRecordSource,
|
|
200
|
+
): Promise<RecordWireRow | null> {
|
|
201
|
+
const doc = await findRecordDoc(ctx, kind, recordId)
|
|
202
|
+
if (doc) return recordDocWire(kind, doc)
|
|
203
|
+
const old = legacy ? await legacy.find(ctx, recordId) : null // TOLERANT READ (marked)
|
|
204
|
+
return old ? recordWireRow(kind, old.recordId, old.columns, old.createdAt, old.updatedAt) : null
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Save one record of a kind (the shared write path — the kind's `save` mutation and an
|
|
209
|
+
* addon's internal ingest mutation both call it). Returns the stored wire row.
|
|
210
|
+
*/
|
|
211
|
+
export async function saveRecord(
|
|
212
|
+
ctx: MutationCtx,
|
|
213
|
+
kind: AnyRecordKind,
|
|
214
|
+
recordId: string,
|
|
215
|
+
write: Readonly<Record<string, unknown>>,
|
|
216
|
+
stamps: { createdAt: string; updatedAt: string },
|
|
217
|
+
legacy?: LegacyRecordSource,
|
|
218
|
+
): Promise<RecordWireRow> {
|
|
219
|
+
const existing = await findRecordDoc(ctx, kind, recordId)
|
|
220
|
+
// TOLERANT READ (marked): a legacy-only record is edited from its legacy columns.
|
|
221
|
+
const lifted = !existing && legacy ? await legacy.find(ctx, recordId) : null
|
|
222
|
+
const stored = existing?.data ?? lifted?.columns
|
|
223
|
+
const columns = rethrowAsConvex(() => resolveRecordSave(kind, recordId, stored, write))
|
|
224
|
+
const createdAt = existing?.createdAt ?? lifted?.createdAt ?? stamps.createdAt
|
|
225
|
+
const slots = recordSlots(kind, columns, createdAt)
|
|
226
|
+
if (existing) {
|
|
227
|
+
// `key1`/`key2` are always written: an edit that empties a key unsets its slot.
|
|
228
|
+
await ctx.db.patch(existing._id, {
|
|
229
|
+
data: columns,
|
|
230
|
+
key1: slots.key1,
|
|
231
|
+
key2: slots.key2,
|
|
232
|
+
at: slots.at,
|
|
233
|
+
updatedAt: stamps.updatedAt,
|
|
234
|
+
})
|
|
235
|
+
} else {
|
|
236
|
+
await ctx.db.insert('records', {
|
|
237
|
+
kind: kind.kind,
|
|
238
|
+
recordId,
|
|
239
|
+
data: columns,
|
|
240
|
+
...(slots.key1 !== undefined ? { key1: slots.key1 } : {}),
|
|
241
|
+
...(slots.key2 !== undefined ? { key2: slots.key2 } : {}),
|
|
242
|
+
at: slots.at,
|
|
243
|
+
createdAt,
|
|
244
|
+
updatedAt: stamps.updatedAt,
|
|
245
|
+
})
|
|
246
|
+
if (lifted && legacy) await legacy.remove(ctx, lifted)
|
|
247
|
+
}
|
|
248
|
+
return recordWireRow(kind, recordId, columns, createdAt, stamps.updatedAt)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** The registered functions of one record kind (the module's exports). */
|
|
252
|
+
export interface RecordFunctions {
|
|
253
|
+
save: RegisteredMutation<'public', Record<string, unknown>, Promise<RecordWireRow>>
|
|
254
|
+
get: RegisteredQuery<'public', Record<string, unknown>, Promise<RecordWireRow | null>>
|
|
255
|
+
list: RegisteredQuery<'public', RecordQuery & Record<string, unknown>, Promise<RecordWireRow[]>>
|
|
256
|
+
remove: RegisteredMutation<'public', Record<string, unknown>, Promise<void>>
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export interface RecordFunctionsOptions {
|
|
260
|
+
/** TOLERANT READ (marked) — see {@link LegacyRecordSource}. Only a kind that moved declares one. */
|
|
261
|
+
legacy?: LegacyRecordSource
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** The public CRUD functions of one record kind — see the module header. */
|
|
265
|
+
export function defineRecordFunctions(kind: AnyRecordKind, options: RecordFunctionsOptions = {}): RecordFunctions {
|
|
266
|
+
const { legacy } = options
|
|
267
|
+
const idArg = { [kind.idKey]: v.string() }
|
|
268
|
+
const columnArgs = Object.fromEntries(
|
|
269
|
+
[...Object.keys(kind.fields), 'meta', 'metaPatch'].map((column) => [column, v.optional(v.any())]),
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
const save = mutation({
|
|
273
|
+
args: { ...idArg, ...columnArgs, createdAt: v.string(), updatedAt: v.string() },
|
|
274
|
+
handler: async (ctx, args: Record<string, unknown>) => {
|
|
275
|
+
await requireCapability(ctx, kind.access.write)
|
|
276
|
+
const { [kind.idKey]: recordId, createdAt, updatedAt, ...write } = args
|
|
277
|
+
return saveRecord(
|
|
278
|
+
ctx,
|
|
279
|
+
kind,
|
|
280
|
+
recordId as string,
|
|
281
|
+
write,
|
|
282
|
+
{ createdAt: createdAt as string, updatedAt: updatedAt as string },
|
|
283
|
+
legacy,
|
|
284
|
+
)
|
|
285
|
+
},
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
const get = query({
|
|
289
|
+
args: idArg,
|
|
290
|
+
handler: async (ctx, args: Record<string, unknown>) => {
|
|
291
|
+
await requireCapability(ctx, kind.access.read)
|
|
292
|
+
return getRecordWire(ctx, kind, args[kind.idKey] as string, legacy)
|
|
293
|
+
},
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
const list = query({
|
|
297
|
+
args: {
|
|
298
|
+
where: v.optional(v.record(v.string(), v.string())),
|
|
299
|
+
since: v.optional(v.string()),
|
|
300
|
+
before: v.optional(v.string()),
|
|
301
|
+
order: v.optional(v.union(v.literal('asc'), v.literal('desc'))),
|
|
302
|
+
limit: v.optional(v.number()),
|
|
303
|
+
},
|
|
304
|
+
handler: async (ctx, args: RecordQuery) => {
|
|
305
|
+
await requireCapability(ctx, kind.access.read)
|
|
306
|
+
const resolved = rethrowAsConvex(() => resolveRecordQuery(kind, args))
|
|
307
|
+
return listRecordWire(ctx, kind, resolved, legacy)
|
|
308
|
+
},
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
const remove = mutation({
|
|
312
|
+
args: idArg,
|
|
313
|
+
handler: async (ctx, args: Record<string, unknown>) => {
|
|
314
|
+
await requireCapability(ctx, kind.access.write)
|
|
315
|
+
const recordId = args[kind.idKey] as string
|
|
316
|
+
const doc = await findRecordDoc(ctx, kind, recordId)
|
|
317
|
+
if (doc) await ctx.db.delete(doc._id)
|
|
318
|
+
// TOLERANT READ (marked): a legacy row of the same id goes too, or it would resurface.
|
|
319
|
+
const old = legacy ? await legacy.find(ctx, recordId) : null
|
|
320
|
+
if (old && legacy) await legacy.remove(ctx, old)
|
|
321
|
+
},
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
return { save, get, list, remove }
|
|
325
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { convexTest } from 'convex-test'
|
|
3
|
+
import { makeFunctionReference } from 'convex/server'
|
|
4
|
+
import { z } from 'zod'
|
|
5
|
+
import { CONVEX_MAX_NESTING_DEPTH, measureMaxDepth } from '@elytracms/core/cms-core'
|
|
6
|
+
import { CONTRACT_NOTE_KIND, defineRecordKind, recordRowDepth } from '@elytracms/core/persistence'
|
|
7
|
+
import schema from './schema'
|
|
8
|
+
import { defineRecordFunctions } from './record-functions'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* EC-616 — the Convex record store beyond the shared contract scenarios (those run in
|
|
12
|
+
* `contract-parity.test.ts`): the stored row's nesting depth against Convex's 16-level
|
|
13
|
+
* ceiling (convex-test does NOT enforce it, so the row is measured here), the kind's
|
|
14
|
+
* wire (its columns are its argument names, nothing else is accepted), and access (the
|
|
15
|
+
* kind's capabilities, checked before anything is read or written).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** A kind whose writes need administration — proves `access` is the kind's, not a constant. */
|
|
19
|
+
const ADMIN_NOTE_KIND = defineRecordKind({
|
|
20
|
+
kind: 'adminNote',
|
|
21
|
+
module: 'adminNotes',
|
|
22
|
+
fields: { text: z.string().min(1) },
|
|
23
|
+
access: { read: 'content.read', write: 'project.administer' },
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const modules = {
|
|
27
|
+
...import.meta.glob(['./**/*.ts', '!./**/*.test.ts', '!./_generated/**']),
|
|
28
|
+
'./_generated/api.ts': async () => ({}),
|
|
29
|
+
[`./${CONTRACT_NOTE_KIND.module}.ts`]: async () => defineRecordFunctions(CONTRACT_NOTE_KIND),
|
|
30
|
+
[`./${ADMIN_NOTE_KIND.module}.ts`]: async () => defineRecordFunctions(ADMIN_NOTE_KIND),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const saveNote = makeFunctionReference<'mutation'>('contractNotes:save')
|
|
34
|
+
const listNotes = makeFunctionReference<'query'>('contractNotes:list')
|
|
35
|
+
const getNote = makeFunctionReference<'query'>('contractNotes:get')
|
|
36
|
+
const saveAdminNote = makeFunctionReference<'mutation'>('adminNotes:save')
|
|
37
|
+
|
|
38
|
+
const STAMP = '2026-10-26T00:00:00.000Z'
|
|
39
|
+
|
|
40
|
+
async function backendWith(members: Array<{ userId: string; role: string }>) {
|
|
41
|
+
const t = convexTest(schema, modules)
|
|
42
|
+
await t.run(async (ctx) => {
|
|
43
|
+
for (const member of members) {
|
|
44
|
+
await ctx.db.insert('members', { ...member, createdAt: STAMP, updatedAt: STAMP })
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
const as = (userId: string) => t.withIdentity({ subject: userId, issuer: 'https://convex.test' })
|
|
48
|
+
return { t, as }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A meta value whose stored row measures exactly `rowDepth`. */
|
|
52
|
+
function metaForRowDepth(rowDepth: number): Record<string, unknown> {
|
|
53
|
+
// row (1) → data (2) → meta (3) → … : the bag itself sits at row depth 3.
|
|
54
|
+
let value: Record<string, unknown> = { leaf: true }
|
|
55
|
+
for (let depth = 3; depth < rowDepth; depth++) value = { nested: value }
|
|
56
|
+
return value
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe('the records row and Convex’s stored-depth ceiling (EC-616)', () => {
|
|
60
|
+
it('a record at the ceiling is stored, and its stored row measures ≤ 16', async () => {
|
|
61
|
+
const { t, as } = await backendWith([{ userId: 'editor', role: 'editor' }])
|
|
62
|
+
const meta = metaForRowDepth(CONVEX_MAX_NESTING_DEPTH)
|
|
63
|
+
expect(recordRowDepth({ topic: 't', postedAt: STAMP, meta })).toBe(CONVEX_MAX_NESTING_DEPTH)
|
|
64
|
+
|
|
65
|
+
await as('editor').mutation(saveNote, {
|
|
66
|
+
contractNoteId: 'deepest',
|
|
67
|
+
topic: 't',
|
|
68
|
+
postedAt: STAMP,
|
|
69
|
+
meta,
|
|
70
|
+
createdAt: STAMP,
|
|
71
|
+
updatedAt: STAMP,
|
|
72
|
+
})
|
|
73
|
+
const stored = await t.run(async (ctx) =>
|
|
74
|
+
ctx.db
|
|
75
|
+
.query('records')
|
|
76
|
+
.withIndex('by_kind_record', (q) => q.eq('kind', 'contractNote').eq('recordId', 'deepest'))
|
|
77
|
+
.unique(),
|
|
78
|
+
)
|
|
79
|
+
expect(stored).not.toBeNull()
|
|
80
|
+
// Measured on the WHOLE stored document (system fields included) — what Convex measures.
|
|
81
|
+
expect(measureMaxDepth(stored).depth).toBe(CONVEX_MAX_NESTING_DEPTH)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('one level deeper is refused with a validation error, and nothing is stored', async () => {
|
|
85
|
+
const { t, as } = await backendWith([{ userId: 'editor', role: 'editor' }])
|
|
86
|
+
await expect(
|
|
87
|
+
as('editor').mutation(saveNote, {
|
|
88
|
+
contractNoteId: 'too_deep',
|
|
89
|
+
topic: 't',
|
|
90
|
+
postedAt: STAMP,
|
|
91
|
+
meta: metaForRowDepth(CONVEX_MAX_NESTING_DEPTH + 1),
|
|
92
|
+
createdAt: STAMP,
|
|
93
|
+
updatedAt: STAMP,
|
|
94
|
+
}),
|
|
95
|
+
).rejects.toThrow(/nests 17 levels deep/)
|
|
96
|
+
expect(await t.run(async (ctx) => (await ctx.db.query('records').collect()).length)).toBe(0)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe('a kind’s wire is its own declaration (EC-616)', () => {
|
|
101
|
+
it('accepts exactly the id key, the columns, meta and the stamps — an unknown argument is refused', async () => {
|
|
102
|
+
const { as } = await backendWith([{ userId: 'editor', role: 'editor' }])
|
|
103
|
+
await expect(
|
|
104
|
+
as('editor').mutation(saveNote, {
|
|
105
|
+
contractNoteId: 'n',
|
|
106
|
+
topic: 't',
|
|
107
|
+
postedAt: STAMP,
|
|
108
|
+
bogus: 1,
|
|
109
|
+
createdAt: STAMP,
|
|
110
|
+
updatedAt: STAMP,
|
|
111
|
+
}),
|
|
112
|
+
).rejects.toThrow(/bogus/)
|
|
113
|
+
// Control: the same call without the stray argument is stored and returns its wire row.
|
|
114
|
+
const row = await as('editor').mutation(saveNote, {
|
|
115
|
+
contractNoteId: 'n',
|
|
116
|
+
topic: 't',
|
|
117
|
+
postedAt: STAMP,
|
|
118
|
+
createdAt: STAMP,
|
|
119
|
+
updatedAt: STAMP,
|
|
120
|
+
})
|
|
121
|
+
expect(row).toEqual({
|
|
122
|
+
contractNoteId: 'n',
|
|
123
|
+
topic: 't',
|
|
124
|
+
status: 'open',
|
|
125
|
+
postedAt: STAMP,
|
|
126
|
+
createdAt: STAMP,
|
|
127
|
+
updatedAt: STAMP,
|
|
128
|
+
})
|
|
129
|
+
expect(await as('editor').query(getNote, { contractNoteId: 'n' })).toEqual(row)
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
describe('access is the kind’s, checked first (EC-616)', () => {
|
|
134
|
+
it('a viewer reads and cannot write; a non-member cannot read', async () => {
|
|
135
|
+
const { as } = await backendWith([
|
|
136
|
+
{ userId: 'editor', role: 'editor' },
|
|
137
|
+
{ userId: 'viewer', role: 'viewer' },
|
|
138
|
+
])
|
|
139
|
+
await as('editor').mutation(saveNote, { contractNoteId: 'n', topic: 't', postedAt: STAMP, createdAt: STAMP, updatedAt: STAMP })
|
|
140
|
+
expect(await as('viewer').query(listNotes, {})).toHaveLength(1)
|
|
141
|
+
await expect(
|
|
142
|
+
as('viewer').mutation(saveNote, { contractNoteId: 'v', topic: 't', postedAt: STAMP, createdAt: STAMP, updatedAt: STAMP }),
|
|
143
|
+
).rejects.toThrow(/unauthorized|forbidden|capability/i)
|
|
144
|
+
await expect(as('stranger').query(listNotes, {})).rejects.toThrow(/unauthorized|forbidden|member/i)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('a kind that declares administration refuses an editor and admits an admin', async () => {
|
|
148
|
+
const { as } = await backendWith([
|
|
149
|
+
{ userId: 'editor', role: 'editor' },
|
|
150
|
+
{ userId: 'admin', role: 'admin' },
|
|
151
|
+
])
|
|
152
|
+
const write = { adminNoteId: 'a', text: 'x', createdAt: STAMP, updatedAt: STAMP }
|
|
153
|
+
await expect(as('editor').mutation(saveAdminNote, write)).rejects.toThrow(/unauthorized|forbidden|capability/i)
|
|
154
|
+
await expect(as('admin').mutation(saveAdminNote, write)).resolves.toMatchObject({ adminNoteId: 'a', text: 'x' })
|
|
155
|
+
})
|
|
156
|
+
})
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { convexTest } from 'convex-test'
|
|
3
|
+
import { makeFunctionReference } from 'convex/server'
|
|
4
|
+
import schema from './schema'
|
|
5
|
+
|
|
6
|
+
// convex-test fires `runAfter(0)` on a REAL timer; fake timers keep any scheduled
|
|
7
|
+
// revalidation queued (rows written, the `webhooks:deliver` fetch never fired).
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
vi.useFakeTimers()
|
|
10
|
+
})
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
vi.useRealTimers()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* EC-616 — the redirect move from the `redirects` table into `records` (kind `redirect`)
|
|
17
|
+
* is a STORED-DATA move (Konditorei and Naturkinder hold real redirects), done the EC-587
|
|
18
|
+
* way: writes to the new home, a marked tolerant read of the old one, an idempotent
|
|
19
|
+
* per-deployment migration. These run the REAL functions on convex-test over legacy rows
|
|
20
|
+
* inserted as a pre-EC-616 deployment holds them, and pin "no gap": a deployment with the
|
|
21
|
+
* new functions pushed and the migration NOT yet run serves exactly what it served before.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const modules = {
|
|
25
|
+
...import.meta.glob(['./**/*.ts', '!./**/*.test.ts', '!./_generated/**']),
|
|
26
|
+
'./_generated/api.ts': async () => ({}),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const listRedirects = makeFunctionReference<'query'>('redirects:list')
|
|
30
|
+
const getRedirect = makeFunctionReference<'query'>('redirects:get')
|
|
31
|
+
const saveRedirect = makeFunctionReference<'mutation'>('redirects:save')
|
|
32
|
+
const removeRedirect = makeFunctionReference<'mutation'>('redirects:remove')
|
|
33
|
+
const migrateLegacy = makeFunctionReference<'mutation'>('redirects:migrateLegacy')
|
|
34
|
+
const snapshot = makeFunctionReference<'query'>('delivery:snapshot')
|
|
35
|
+
|
|
36
|
+
const STAMP = '2026-08-01T00:00:00.000Z'
|
|
37
|
+
const LATER = '2026-10-26T00:00:00.000Z'
|
|
38
|
+
|
|
39
|
+
/** Rows as a pre-EC-616 deployment stores them in the legacy `redirects` table. */
|
|
40
|
+
const LEGACY = [
|
|
41
|
+
{ redirectId: 'r_spring', from: '/spring', to: '/offers', permanent: true, enabled: true },
|
|
42
|
+
{ redirectId: 'r_paused', from: '/paused', to: '/x', permanent: false, enabled: false, meta: { note: 'later' } },
|
|
43
|
+
{ redirectId: 'r_blog', from: '/blog-old', to: '/blog', permanent: true, enabled: true },
|
|
44
|
+
// Stored before EC-577 refused it: a meta key naming a column. The current schema
|
|
45
|
+
// refuses this row as a create, so the migration must REPORT it, never drop it.
|
|
46
|
+
{ redirectId: 'r_shadow', from: '/shadow', to: '/real', permanent: true, enabled: true, meta: { to: '/fake' } },
|
|
47
|
+
] as const
|
|
48
|
+
|
|
49
|
+
async function legacyDeployment(configuredRevalidation = false) {
|
|
50
|
+
const env = (globalThis as { process: { env: Record<string, string | undefined> } }).process.env
|
|
51
|
+
if (configuredRevalidation) {
|
|
52
|
+
env['CANVAS_REVALIDATE_ENDPOINT'] = 'https://host.invalid/api/revalidate'
|
|
53
|
+
env['CANVAS_REVALIDATE_SECRET'] = 'ec616-secret'
|
|
54
|
+
}
|
|
55
|
+
const t = convexTest(schema, modules)
|
|
56
|
+
await t.run(async (ctx) => {
|
|
57
|
+
await ctx.db.insert('members', { userId: 'editor', role: 'editor', createdAt: STAMP, updatedAt: STAMP })
|
|
58
|
+
for (const row of LEGACY) await ctx.db.insert('redirects', { ...row, createdAt: STAMP, updatedAt: STAMP })
|
|
59
|
+
})
|
|
60
|
+
const asEditor = t.withIdentity({ subject: 'editor', issuer: 'https://convex.test' })
|
|
61
|
+
return { t, asEditor }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
afterEach(() => {
|
|
65
|
+
const env = (globalThis as { process: { env: Record<string, string | undefined> } }).process.env
|
|
66
|
+
delete env['CANVAS_REVALIDATE_ENDPOINT']
|
|
67
|
+
delete env['CANVAS_REVALIDATE_SECRET']
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
/** What the pre-EC-616 functions returned for the legacy rows: each row flat, ordered by `from`. */
|
|
71
|
+
const SERVED_BEFORE = [...LEGACY]
|
|
72
|
+
.sort((a, b) => (a.from < b.from ? -1 : 1))
|
|
73
|
+
.map((row) => ({ ...row, createdAt: STAMP, updatedAt: STAMP }))
|
|
74
|
+
|
|
75
|
+
/** What the pre-EC-616 snapshot served: the enabled rows as routing records. */
|
|
76
|
+
const LIVE_BEFORE = LEGACY.filter((row) => row.enabled).map((row) => ({
|
|
77
|
+
id: row.redirectId,
|
|
78
|
+
from: row.from,
|
|
79
|
+
to: row.to,
|
|
80
|
+
permanent: row.permanent,
|
|
81
|
+
}))
|
|
82
|
+
|
|
83
|
+
const byId = (a: { id: string }, b: { id: string }) => (a.id < b.id ? -1 : 1)
|
|
84
|
+
|
|
85
|
+
async function counts(t: Awaited<ReturnType<typeof legacyDeployment>>['t']) {
|
|
86
|
+
return t.run(async (ctx) => ({
|
|
87
|
+
legacy: (await ctx.db.query('redirects').collect()).length,
|
|
88
|
+
records: (await ctx.db.query('records').collect()).filter((row) => row.kind === 'redirect').length,
|
|
89
|
+
}))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
describe('the redirect move: no gap before the migration runs (EC-616)', () => {
|
|
93
|
+
it('list, get and the delivery snapshot serve the legacy rows exactly as before', async () => {
|
|
94
|
+
const { asEditor } = await legacyDeployment()
|
|
95
|
+
expect(await asEditor.query(listRedirects, {})).toEqual(SERVED_BEFORE)
|
|
96
|
+
expect(await asEditor.query(getRedirect, { redirectId: 'r_paused' })).toEqual(
|
|
97
|
+
SERVED_BEFORE.find((row) => row.redirectId === 'r_paused'),
|
|
98
|
+
)
|
|
99
|
+
const served = (await asEditor.query(snapshot, { projectId: 'demo' })) as { redirects: { id: string }[] }
|
|
100
|
+
expect([...served.redirects].sort(byId)).toEqual([...LIVE_BEFORE].sort(byId))
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('a save of a legacy-only redirect lifts it: an edit, not a create; the legacy row goes', async () => {
|
|
104
|
+
const { t, asEditor } = await legacyDeployment()
|
|
105
|
+
// An edit that leaves `enabled`/`permanent` absent keeps the STORED (legacy) values.
|
|
106
|
+
const saved = await asEditor.mutation(saveRedirect, {
|
|
107
|
+
redirectId: 'r_paused',
|
|
108
|
+
to: '/y',
|
|
109
|
+
createdAt: LATER,
|
|
110
|
+
updatedAt: LATER,
|
|
111
|
+
})
|
|
112
|
+
expect(saved).toEqual({
|
|
113
|
+
redirectId: 'r_paused',
|
|
114
|
+
from: '/paused',
|
|
115
|
+
to: '/y',
|
|
116
|
+
permanent: false,
|
|
117
|
+
enabled: false,
|
|
118
|
+
meta: { note: 'later' },
|
|
119
|
+
createdAt: STAMP,
|
|
120
|
+
updatedAt: LATER,
|
|
121
|
+
})
|
|
122
|
+
expect(await counts(t)).toEqual({ legacy: 3, records: 1 })
|
|
123
|
+
expect((await asEditor.query(listRedirects, {})) as unknown[]).toHaveLength(4)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('a remove of a legacy-only redirect deletes it (it never resurfaces)', async () => {
|
|
127
|
+
const { t, asEditor } = await legacyDeployment()
|
|
128
|
+
await asEditor.mutation(removeRedirect, { redirectId: 'r_blog' })
|
|
129
|
+
expect(await asEditor.query(getRedirect, { redirectId: 'r_blog' })).toBeNull()
|
|
130
|
+
expect(await counts(t)).toEqual({ legacy: 3, records: 0 })
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
describe('redirects:migrateLegacy (EC-616)', () => {
|
|
135
|
+
it('moves every valid legacy row, reports the refused one, and serves the same set after', async () => {
|
|
136
|
+
const { t, asEditor } = await legacyDeployment()
|
|
137
|
+
const first = await t.mutation(migrateLegacy, {})
|
|
138
|
+
expect(first).toEqual({
|
|
139
|
+
scanned: 4,
|
|
140
|
+
moved: 3,
|
|
141
|
+
shadowed: 0,
|
|
142
|
+
refused: [{ redirectId: 'r_shadow', message: expect.stringMatching(/meta may not use a column name/) }],
|
|
143
|
+
movedIds: ['r_blog', 'r_paused', 'r_spring'],
|
|
144
|
+
})
|
|
145
|
+
expect(await counts(t)).toEqual({ legacy: 1, records: 3 })
|
|
146
|
+
// The served set is unchanged by the move — the refused row still serves (tolerant read).
|
|
147
|
+
expect(await asEditor.query(listRedirects, {})).toEqual(SERVED_BEFORE)
|
|
148
|
+
const served = (await asEditor.query(snapshot, { projectId: 'demo' })) as { redirects: { id: string }[] }
|
|
149
|
+
expect([...served.redirects].sort(byId)).toEqual([...LIVE_BEFORE].sort(byId))
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('is idempotent: a second run moves nothing', async () => {
|
|
153
|
+
const { t } = await legacyDeployment()
|
|
154
|
+
await t.mutation(migrateLegacy, {})
|
|
155
|
+
expect(await t.mutation(migrateLegacy, {})).toMatchObject({ scanned: 1, moved: 0, shadowed: 0 })
|
|
156
|
+
expect(await counts(t)).toEqual({ legacy: 1, records: 3 })
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('drops a legacy row a record already shadows (a save lifted it meanwhile)', async () => {
|
|
160
|
+
const { t, asEditor } = await legacyDeployment()
|
|
161
|
+
// Simulate the race the transaction cannot have: a record and its legacy row both present.
|
|
162
|
+
await t.run(async (ctx) => {
|
|
163
|
+
await ctx.db.insert('redirects', {
|
|
164
|
+
redirectId: 'r_twice',
|
|
165
|
+
from: '/twice',
|
|
166
|
+
to: '/a',
|
|
167
|
+
permanent: true,
|
|
168
|
+
enabled: true,
|
|
169
|
+
createdAt: STAMP,
|
|
170
|
+
updatedAt: STAMP,
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
await asEditor.mutation(saveRedirect, { redirectId: 'r_twice', from: '/twice', to: '/b', createdAt: LATER, updatedAt: LATER })
|
|
174
|
+
await t.run(async (ctx) => {
|
|
175
|
+
await ctx.db.insert('redirects', {
|
|
176
|
+
redirectId: 'r_twice',
|
|
177
|
+
from: '/twice',
|
|
178
|
+
to: '/a',
|
|
179
|
+
permanent: true,
|
|
180
|
+
enabled: true,
|
|
181
|
+
createdAt: STAMP,
|
|
182
|
+
updatedAt: STAMP,
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
// The record wins every read while both exist…
|
|
186
|
+
expect(await asEditor.query(getRedirect, { redirectId: 'r_twice' })).toMatchObject({ to: '/b' })
|
|
187
|
+
// …and the migration drops the shadowed legacy row instead of overwriting the record.
|
|
188
|
+
expect(await t.mutation(migrateLegacy, {})).toMatchObject({ shadowed: 1 })
|
|
189
|
+
expect(await asEditor.query(getRedirect, { redirectId: 'r_twice' })).toMatchObject({ to: '/b' })
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('schedules no revalidation: the served set is identical before and after', async () => {
|
|
193
|
+
const { t } = await legacyDeployment(true)
|
|
194
|
+
await t.mutation(migrateLegacy, {})
|
|
195
|
+
const scheduled = await t.run(async (ctx) => ctx.db.system.query('_scheduled_functions').collect())
|
|
196
|
+
expect(scheduled).toEqual([])
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('control: a redirect SAVE on the same configured deployment does schedule one', async () => {
|
|
200
|
+
const { t, asEditor } = await legacyDeployment(true)
|
|
201
|
+
await asEditor.mutation(saveRedirect, { redirectId: 'r_new', from: '/n', to: '/m', createdAt: LATER, updatedAt: LATER })
|
|
202
|
+
const scheduled = await t.run(async (ctx) => ctx.db.system.query('_scheduled_functions').collect())
|
|
203
|
+
expect(scheduled.map((row) => (row.args as [{ event: unknown }])[0].event)).toEqual([{ kind: 'redirects' }])
|
|
204
|
+
})
|
|
205
|
+
})
|