@elytracms/studio 0.0.93 → 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/auth.ts +5 -2
- package/convex/authority.ts +35 -11
- package/convex/contract-parity.test.ts +7 -0
- package/convex/delivery.ts +12 -9
- 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/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 +30 -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 +2 -2
- 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 +6 -6
- package/dist/components/ai/translate-document-dialog.js.map +1 -1
- package/dist/components/ai/translate-skipped.d.ts +1 -1
- package/dist/components/ai/translate-skipped.js +2 -2
- package/dist/components/ai/translate-skipped.js.map +1 -1
- 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 +2 -2
- 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-thumb.js +3 -3
- package/dist/components/assets/asset-thumb.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.d.ts +2 -2
- 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 +2 -2
- package/dist/components/assets/assets-store.js +6 -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.js +1 -1
- package/dist/components/assets/replace-file.js.map +1 -1
- 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 +1 -1
- package/dist/components/builder/blocks-model.js +3 -3
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/blocks-panel.d.ts +1 -1
- package/dist/components/builder/blocks-panel.js +4 -4
- package/dist/components/builder/blocks-panel.js.map +1 -1
- package/dist/components/builder/builder-store.js +2 -2
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/canvas-breakpoints.js +1 -1
- package/dist/components/builder/canvas-breakpoints.js.map +1 -1
- package/dist/components/builder/canvas-dnd.d.ts +2 -2
- package/dist/components/builder/canvas-dnd.js +1 -1
- 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 +1 -1
- package/dist/components/builder/canvas-render-context.js +4 -4
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- 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 +3 -3
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/builder/clipboard-actions.d.ts +2 -2
- package/dist/components/builder/clipboard-actions.js +4 -4
- 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 +1 -1
- package/dist/components/builder/composition-field-editor.js +3 -3
- package/dist/components/builder/composition-field-editor.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 +1 -1
- package/dist/components/builder/insert-context.js +3 -3
- 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 +1 -1
- package/dist/components/builder/page-tree.js +5 -5
- package/dist/components/builder/page-tree.js.map +1 -1
- package/dist/components/builder/placement-refusal.js +3 -3
- package/dist/components/builder/placement-refusal.js.map +1 -1
- package/dist/components/builder/property-inspector.js +4 -4
- package/dist/components/builder/property-inspector.js.map +1 -1
- package/dist/components/builder/reuse-context.js +4 -4
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/builder/reuse.js +1 -1
- 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.js +1 -1
- package/dist/components/builder/run-inspector.js.map +1 -1
- 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.js +2 -2
- package/dist/components/cms/asset-field.js.map +1 -1
- package/dist/components/cms/asset-picker.js +2 -2
- package/dist/components/cms/asset-picker.js.map +1 -1
- package/dist/components/cms/bulk-actions.d.ts +1 -1
- package/dist/components/cms/bulk-actions.js +2 -2
- 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 +2 -2
- package/dist/components/cms/cms-store.js +10 -10
- 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 -14
- 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.js +5 -5
- 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-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.js +1 -1
- 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.js +1 -1
- 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 +2 -2
- package/dist/components/cms/object-array-field.js.map +1 -1
- package/dist/components/cms/prop-fields.js +2 -2
- 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.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/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 +1 -1
- package/dist/components/cms/use-publish-controller.js +1 -1
- 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 +3 -3
- 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 +2 -2
- 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 +3 -3
- package/dist/components/components/transfer-dialog.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/index.d.ts +1 -1
- package/dist/lib/ai/index.js +1 -1
- package/dist/lib/ai/index.js.map +1 -1
- package/dist/lib/ai/node-handler.d.ts +1 -1
- package/dist/lib/ai/node-handler.js.map +1 -1
- package/dist/lib/ai/translate-document.js +1 -1
- package/dist/lib/ai/translate-document.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.js +2 -2
- package/dist/lib/clipboard/paste-target.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 +12 -12
- 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 +7 -2
- package/dist/lib/collection-view/collection-view-source.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/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 +4 -0
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +4 -0
- package/dist/lib/i18n/messages.en.js +8 -0
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/project-runtime-config.d.ts +9 -0
- package/dist/lib/operations/project-runtime-config.js +13 -0
- package/dist/lib/operations/project-runtime-config.js.map +1 -1
- package/dist/lib/operations/studio-operations.d.ts +9 -1
- package/dist/lib/operations/studio-operations.js +6 -1
- 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 +78 -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 +1 -1
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/query/operations-query.d.ts +1 -1
- package/dist/lib/query/operations-query.js +1 -1
- package/dist/lib/query/operations-query.js.map +1 -1
- package/dist/lib/references/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 +6 -0
- package/dist/lib/workspace/index.js +8 -0
- 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/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
|
@@ -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
|
+
})
|
|
@@ -135,8 +135,12 @@ describe('EC-510 — the write chokepoint revalidates on every redirect write',
|
|
|
135
135
|
// is the gate working rather than the instrument being blind.
|
|
136
136
|
expect(await scheduledRevalidations(t)).toEqual([])
|
|
137
137
|
// The write itself still happened — silence is at the webhook, not the save.
|
|
138
|
+
// EC-616: a redirect is stored as a `records` row of kind `redirect` now.
|
|
138
139
|
expect(
|
|
139
|
-
await t.run(
|
|
140
|
+
await t.run(
|
|
141
|
+
async (ctx) =>
|
|
142
|
+
(await ctx.db.query('records').collect()).filter((row) => row.kind === 'redirect').length,
|
|
143
|
+
),
|
|
140
144
|
).toBe(1)
|
|
141
145
|
})
|
|
142
146
|
})
|
package/convex/redirects.ts
CHANGED
|
@@ -1,92 +1,133 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import { internalMutationGeneric } from 'convex/server'
|
|
2
|
+
import type { MutationBuilder } from 'convex/server'
|
|
3
|
+
import {
|
|
4
|
+
PersistenceValidationError,
|
|
5
|
+
recordColumnsOf,
|
|
6
|
+
recordSlots,
|
|
7
|
+
redirectRecordKind,
|
|
8
|
+
resolveRecordQuery,
|
|
9
|
+
resolveRecordSave,
|
|
10
|
+
} from '@elytracms/core/persistence'
|
|
11
|
+
import type { RedirectEntry } from '@elytracms/core/persistence'
|
|
12
|
+
import type { DataModel, MutationCtx, QueryCtx } from './functions'
|
|
13
|
+
import { defineRecordFunctions, findRecordDoc, listRecordWire } from './record-functions'
|
|
14
|
+
import type { LegacyRecord, LegacyRecordSource } from './record-functions'
|
|
15
|
+
import { recordEntryFromWire } from '@elytracms/core/persistence'
|
|
9
16
|
|
|
10
17
|
/**
|
|
11
|
-
* Editorial redirects persistence (EC-303)
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
18
|
+
* Editorial redirects persistence (EC-303) — since EC-616 the redirect RECORD KIND over
|
|
19
|
+
* the generic record store (`redirectRecordKind`, one `records` row per redirect). The
|
|
20
|
+
* functions keep their names and wire (`redirects:save/get/list/remove`, `redirectId` +
|
|
21
|
+
* the columns flat), so the studio's adapter and every consumer's
|
|
22
|
+
* `export * from '@elytracms/studio/convex/redirects'` line are unchanged. Content-gated
|
|
23
|
+
* (a viewer reads, an editor writes), NOT admin-only — redirects are content.
|
|
24
|
+
*
|
|
25
|
+
* ## The move from the `redirects` table (stored data — KON and NK hold real redirects)
|
|
26
|
+
*
|
|
27
|
+
* The EC-587 precedent, with no gap:
|
|
28
|
+
*
|
|
29
|
+
* - **writes go to `records`** (kind `redirect`); nothing writes the legacy table again;
|
|
30
|
+
* - **reads union the legacy rows no record shadows** — the TOLERANT READ below, marked:
|
|
31
|
+
* a deployment with these functions pushed and the migration NOT yet run serves the
|
|
32
|
+
* same redirects it served before (`list`, `get`, the delivery snapshot);
|
|
33
|
+
* - a save of a legacy-only redirect lifts it (record from its columns, legacy row
|
|
34
|
+
* deleted, one transaction); a remove deletes both;
|
|
35
|
+
* - **`redirects:migrateLegacy`** moves every legacy row in one idempotent internal
|
|
36
|
+
* mutation and reports counts — run per deployment (both consumers + the prod harness:
|
|
37
|
+
* owner-run); the legacy table and this tolerant read are dropped together once it is
|
|
38
|
+
* reported run everywhere (a gated item, like EC-587's alt column).
|
|
16
39
|
*/
|
|
17
40
|
|
|
18
|
-
|
|
19
|
-
* Create or modify a redirect (EC-577). The ONE write rule (`resolveRecordWrite`,
|
|
20
|
-
* shared with the in-memory adapter) runs here, inside the transaction: a column
|
|
21
|
-
* absent from the args keeps its stored value — `permanent`/`enabled` included,
|
|
22
|
-
* whose defaults (the record schema's `.default(true)`) apply only when this save
|
|
23
|
-
* creates the redirect — `null` clears the optional `meta` bag, a value sets. ONE
|
|
24
|
-
* `db.patch` or ONE `db.insert`, so the EC-510 revalidation trigger fires once.
|
|
25
|
-
*/
|
|
26
|
-
export const save = mutation({
|
|
27
|
-
args: {
|
|
28
|
-
redirectId: v.string(),
|
|
29
|
-
from: v.optional(v.string()),
|
|
30
|
-
to: v.optional(v.string()),
|
|
31
|
-
permanent: v.optional(v.boolean()),
|
|
32
|
-
enabled: v.optional(v.boolean()),
|
|
33
|
-
// Schema-driven additive-field bag (EC-311 Tier 2): present replaces the bag,
|
|
34
|
-
// `null` clears it, absent keeps it.
|
|
35
|
-
meta: v.optional(v.any()),
|
|
36
|
-
createdAt: v.string(),
|
|
37
|
-
updatedAt: v.string(),
|
|
38
|
-
},
|
|
39
|
-
handler: async (ctx, { redirectId, createdAt, updatedAt, ...write }) => {
|
|
40
|
-
await requireCapability(ctx, 'content.write')
|
|
41
|
-
const existing = await ctx.db
|
|
42
|
-
.query('redirects')
|
|
43
|
-
.withIndex('by_redirect', (q) => q.eq('redirectId', redirectId))
|
|
44
|
-
.first()
|
|
45
|
-
const resolved = resolveRecordWrite(redirectRowSchema, existing ?? undefined, write)
|
|
46
|
-
if (!resolved.ok) {
|
|
47
|
-
throw new ConvexError({
|
|
48
|
-
code: 'validation',
|
|
49
|
-
message: rejectedWriteMessage('redirect', redirectId, resolved.issues),
|
|
50
|
-
issues: resolved.issues,
|
|
51
|
-
})
|
|
52
|
-
}
|
|
53
|
-
if (existing) {
|
|
54
|
-
await ctx.db.patch(existing._id, { ...(resolved.patch as Partial<RedirectDoc>), updatedAt })
|
|
55
|
-
return ctx.db.get(existing._id)
|
|
56
|
-
}
|
|
57
|
-
const row: WithoutSystemFields<RedirectDoc> = { ...resolved.row, redirectId, createdAt, updatedAt }
|
|
58
|
-
const id = await ctx.db.insert('redirects', row)
|
|
59
|
-
return ctx.db.get(id)
|
|
60
|
-
},
|
|
61
|
-
})
|
|
41
|
+
type LegacyRedirectDoc = DataModel['redirects']['document']
|
|
62
42
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
await
|
|
67
|
-
return ctx.db
|
|
43
|
+
/** TOLERANT READ (marked) — the legacy `redirects` table as the kind's legacy source. */
|
|
44
|
+
const legacyRedirects: LegacyRecordSource = {
|
|
45
|
+
find: async (ctx, recordId) => {
|
|
46
|
+
const row = await ctx.db
|
|
68
47
|
.query('redirects')
|
|
69
|
-
.withIndex('by_redirect', (q) => q.eq('redirectId',
|
|
48
|
+
.withIndex('by_redirect', (q) => q.eq('redirectId', recordId))
|
|
70
49
|
.first()
|
|
50
|
+
return row ? legacyRecord(row) : null
|
|
71
51
|
},
|
|
72
|
-
|
|
52
|
+
list: async (ctx) => (await ctx.db.query('redirects').withIndex('by_redirect').collect()).map(legacyRecord),
|
|
53
|
+
remove: async (ctx, record) => {
|
|
54
|
+
await ctx.db.delete(record.handle as LegacyRedirectDoc['_id'])
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function legacyRecord(row: LegacyRedirectDoc): LegacyRecord {
|
|
59
|
+
return {
|
|
60
|
+
recordId: row.redirectId,
|
|
61
|
+
columns: recordColumnsOf(redirectRecordKind, row),
|
|
62
|
+
createdAt: row.createdAt,
|
|
63
|
+
updatedAt: row.updatedAt,
|
|
64
|
+
handle: row._id,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
73
67
|
|
|
74
|
-
export const list =
|
|
68
|
+
export const { save, get, list, remove } = defineRecordFunctions(redirectRecordKind, { legacy: legacyRedirects })
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Every stored redirect of this deployment (records + the tolerant legacy read), ordered
|
|
72
|
+
* by `from` — the ONE server-side reader; the delivery snapshot projects it through
|
|
73
|
+
* `liveRedirectRecords` (EC-590 / EC-627: one enabled-redirect reader).
|
|
74
|
+
*/
|
|
75
|
+
export async function loadRedirectEntries(ctx: QueryCtx, projectId: string): Promise<RedirectEntry[]> {
|
|
76
|
+
const rows = await listRecordWire(ctx, redirectRecordKind, resolveRecordQuery(redirectRecordKind), legacyRedirects)
|
|
77
|
+
return rows.map((row) => recordEntryFromWire(redirectRecordKind, row, projectId))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* EC-616 — move every legacy `redirects` row into `records` (kind `redirect`), the
|
|
82
|
+
* redirect move's migration. Per row: a record of the same id already exists (a save
|
|
83
|
+
* lifted it, or an earlier run) → the legacy row is dropped as shadowed; otherwise the
|
|
84
|
+
* row's columns are written as a record CREATE through the one rule (`resolveRecordSave`
|
|
85
|
+
* — a legacy row the current schema refuses is REPORTED and left where it is, still
|
|
86
|
+
* served by the tolerant read, never dropped silently) and the legacy row deleted.
|
|
87
|
+
* Idempotent: a second run scans nothing.
|
|
88
|
+
*
|
|
89
|
+
* Internal: an admin runs it from the CLI, once per deployment —
|
|
90
|
+
* `npx convex run redirects:migrateLegacy` (no args). Built on the RAW internal-mutation
|
|
91
|
+
* builder, deliberately outside the write-chokepoint triggers: the served redirect set is
|
|
92
|
+
* identical before and after by construction (the tolerant read), so the move needs no
|
|
93
|
+
* revalidation — through the triggers it would schedule two sweeps per row (an insert
|
|
94
|
+
* into `records`, a delete from `redirects`). One transaction: Convex bounds it at 8192
|
|
95
|
+
* written rows, far above any editorial redirect set (KON's import holds 192).
|
|
96
|
+
*/
|
|
97
|
+
export const migrateLegacy = (internalMutationGeneric as MutationBuilder<DataModel, 'internal'>)({
|
|
75
98
|
args: {},
|
|
76
99
|
handler: async (ctx) => {
|
|
77
|
-
await
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
100
|
+
const legacy = await ctx.db.query('redirects').withIndex('by_redirect').collect()
|
|
101
|
+
const moved: string[] = []
|
|
102
|
+
const shadowed: string[] = []
|
|
103
|
+
const refused: Array<{ redirectId: string; message: string }> = []
|
|
104
|
+
for (const row of legacy) {
|
|
105
|
+
const record = legacyRecord(row)
|
|
106
|
+
if (await findRecordDoc(ctx as unknown as MutationCtx, redirectRecordKind, record.recordId)) {
|
|
107
|
+
await ctx.db.delete(row._id)
|
|
108
|
+
shadowed.push(record.recordId)
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
let columns: Record<string, unknown>
|
|
112
|
+
try {
|
|
113
|
+
columns = resolveRecordSave(redirectRecordKind, record.recordId, undefined, record.columns)
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (!(error instanceof PersistenceValidationError)) throw error
|
|
116
|
+
refused.push({ redirectId: record.recordId, message: error.message })
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
const slots = recordSlots(redirectRecordKind, columns, record.createdAt)
|
|
120
|
+
await ctx.db.insert('records', {
|
|
121
|
+
kind: redirectRecordKind.kind,
|
|
122
|
+
recordId: record.recordId,
|
|
123
|
+
data: columns,
|
|
124
|
+
at: slots.at,
|
|
125
|
+
createdAt: record.createdAt,
|
|
126
|
+
updatedAt: record.updatedAt,
|
|
127
|
+
})
|
|
128
|
+
await ctx.db.delete(row._id)
|
|
129
|
+
moved.push(record.recordId)
|
|
130
|
+
}
|
|
131
|
+
return { scanned: legacy.length, moved: moved.length, shadowed: shadowed.length, refused, movedIds: moved }
|
|
91
132
|
},
|
|
92
133
|
})
|