@elytracms/studio 0.0.93 → 0.0.95
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 +2 -2
- package/convex/asset-meta-patch.test.ts +218 -0
- package/convex/asset-replace-delete-race.test.ts +127 -0
- package/convex/assets.ts +27 -2
- package/convex/auth.ts +5 -2
- package/convex/authority.ts +35 -11
- package/convex/contract-parity.test.ts +7 -0
- package/convex/delivery-redirects.test.ts +167 -0
- package/convex/delivery.ts +23 -10
- 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/rail-write-parity.test.ts +6 -6
- 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 +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 +28 -13
- 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-locale.d.ts +9 -0
- package/dist/components/assets/asset-locale.js +16 -0
- package/dist/components/assets/asset-locale.js.map +1 -0
- package/dist/components/assets/asset-meta-label.d.ts +12 -0
- package/dist/components/assets/asset-meta-label.js +15 -0
- package/dist/components/assets/asset-meta-label.js.map +1 -0
- package/dist/components/assets/asset-metadata.d.ts +69 -15
- package/dist/components/assets/asset-metadata.js +99 -11
- package/dist/components/assets/asset-metadata.js.map +1 -1
- package/dist/components/assets/asset-search.js +8 -2
- package/dist/components/assets/asset-search.js.map +1 -1
- package/dist/components/assets/asset-thumb.js +9 -5
- package/dist/components/assets/asset-thumb.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.d.ts +25 -11
- package/dist/components/assets/assets-collection-view-source.js +78 -26
- 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 +22 -9
- 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.d.ts +21 -0
- package/dist/components/builder/builder-store.js +24 -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-commit-boundary.d.ts +22 -0
- package/dist/components/builder/canvas-commit-boundary.js +27 -0
- package/dist/components/builder/canvas-commit-boundary.js.map +1 -0
- package/dist/components/builder/canvas-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-rebase.d.ts +77 -0
- package/dist/components/builder/canvas-rebase.js +169 -0
- package/dist/components/builder/canvas-rebase.js.map +1 -0
- package/dist/components/builder/canvas-render-context.d.ts +7 -4
- package/dist/components/builder/canvas-render-context.js +21 -13
- 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 +21 -3
- package/dist/components/builder/composition-field-editor.js +48 -30
- 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-factory.js +15 -3
- package/dist/components/builder/node-factory.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 +7 -5
- 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 +11 -7
- 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.d.ts +5 -2
- package/dist/components/cms/asset-field.js +36 -19
- package/dist/components/cms/asset-field.js.map +1 -1
- package/dist/components/cms/asset-picker.js +5 -3
- 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/canvas-commit.d.ts +56 -0
- package/dist/components/cms/canvas-commit.js +35 -0
- package/dist/components/cms/canvas-commit.js.map +1 -0
- 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 +30 -14
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/commit-policy.d.ts +6 -2
- package/dist/components/cms/commit-policy.js +1 -1
- package/dist/components/cms/commit-policy.js.map +1 -1
- package/dist/components/cms/detail-editor.d.ts +5 -2
- package/dist/components/cms/detail-editor.js +31 -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.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 +69 -5
- package/dist/components/cms/use-document-draft.js +118 -34
- 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 +7 -2
- package/dist/components/cms/use-live-url.js +15 -12
- 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 +11 -7
- 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 +13 -4
- 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 +151 -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/assets/upload.d.ts +25 -7
- package/dist/lib/assets/upload.js +30 -21
- package/dist/lib/assets/upload.js.map +1 -1
- package/dist/lib/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.d.ts +24 -6
- package/dist/lib/cms/published-documents.js +39 -6
- package/dist/lib/cms/published-documents.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +12 -99
- package/dist/lib/cms-views/view-model.js +22 -103
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/collection-view/collection-view-source.d.ts +13 -3
- 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 +11 -3
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +9 -1
- package/dist/lib/i18n/messages.en.js +16 -4
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/config-manifest-lookup.d.ts +1 -1
- package/dist/lib/operations/config-manifest-lookup.js +3 -3
- package/dist/lib/operations/config-manifest-lookup.js.map +1 -1
- package/dist/lib/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/deployment-readiness.js +14 -11
- package/dist/lib/persistence/deployment-readiness.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 +2 -1
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/query/index.d.ts +2 -2
- package/dist/lib/query/index.js +1 -1
- package/dist/lib/query/index.js.map +1 -1
- package/dist/lib/query/operations-query.d.ts +27 -1
- package/dist/lib/query/operations-query.js +13 -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/registry/index.js +2 -2
- package/dist/lib/registry/index.js.map +1 -1
- package/dist/lib/registry/preview.d.ts +10 -0
- package/dist/lib/registry/preview.js +13 -4
- package/dist/lib/registry/preview.js.map +1 -1
- package/dist/lib/routes/route-issues.js +6 -3
- package/dist/lib/routes/route-issues.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +6 -0
- package/dist/lib/workspace/index.js +8 -0
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/lib/workspace/workspace-source.js +6 -7
- package/dist/lib/workspace/workspace-source.js.map +1 -1
- package/dist/mount.d.ts +28 -0
- package/dist/mount.js +27 -0
- package/dist/mount.js.map +1 -0
- package/dist/previews/entry-node.d.ts +14 -0
- package/dist/previews/entry-node.js +22 -0
- package/dist/previews/entry-node.js.map +1 -0
- package/dist/previews/harness.js +2 -16
- package/dist/previews/harness.js.map +1 -1
- package/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
|
@@ -80,7 +80,7 @@ describe('EC-587 — assets:liftLegacyAlt moves the legacy column home, once', (
|
|
|
80
80
|
it('lifts the column into meta.alt (meta.alt wins when both), unsets it, and keeps every alt reading', async () => {
|
|
81
81
|
const t = await seededBackend()
|
|
82
82
|
const before = await storedRows(t)
|
|
83
|
-
expect(Object.fromEntries(Object.entries(before).map(([id, r]) => [id, assetAlt(r)]))).toEqual(READ_BEFORE)
|
|
83
|
+
expect(Object.fromEntries(Object.entries(before).map(([id, r]) => [id, assetAlt(r, null)]))).toEqual(READ_BEFORE)
|
|
84
84
|
|
|
85
85
|
const report = (await t.mutation(liftLegacyAlt, {})) as { scanned: number; lifted: string[] }
|
|
86
86
|
expect(report.scanned).toBe(4)
|
|
@@ -94,7 +94,7 @@ describe('EC-587 — assets:liftLegacyAlt moves the legacy column home, once', (
|
|
|
94
94
|
expect(measureMaxDepth(stored.meta).depth).toBeLessThanOrEqual(16)
|
|
95
95
|
}
|
|
96
96
|
// The lift changes no reading: every alt is what it was.
|
|
97
|
-
expect(Object.fromEntries(Object.entries(after).map(([id, r]) => [id, assetAlt(r)]))).toEqual(READ_BEFORE)
|
|
97
|
+
expect(Object.fromEntries(Object.entries(after).map(([id, r]) => [id, assetAlt(r, null)]))).toEqual(READ_BEFORE)
|
|
98
98
|
})
|
|
99
99
|
|
|
100
100
|
it('is idempotent: a second run lifts nothing and writes nothing', async () => {
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { convexTest } from 'convex-test'
|
|
4
|
+
import type { ConvexHttpClient } from 'convex/browser'
|
|
5
|
+
import type { FunctionReference } from 'convex/server'
|
|
6
|
+
import { defaultAssetCollection, measureMaxDepth } from '@elytracms/core/cms-core'
|
|
7
|
+
import { InMemoryBlobStorage, InMemoryPersistenceAdapter, PersistenceValidationError, assetAlt } from '@elytracms/core/persistence'
|
|
8
|
+
import type { AssetRepository } from '@elytracms/core/persistence'
|
|
9
|
+
import { ConvexPersistenceAdapter } from '../src/lib/persistence/convex-adapter'
|
|
10
|
+
import { runUpload } from '../src/lib/assets/upload'
|
|
11
|
+
import {
|
|
12
|
+
assetMetaDraft,
|
|
13
|
+
assetMetaDraftPatch,
|
|
14
|
+
setAssetMetaField,
|
|
15
|
+
updateAssetMetadata,
|
|
16
|
+
} from '../src/components/assets/asset-metadata'
|
|
17
|
+
import schema from './schema'
|
|
18
|
+
|
|
19
|
+
// convex-test fires a `runAfter(0)` on a REAL timer; fake timers keep the scheduled
|
|
20
|
+
// revalidation queued (its row written, the `webhooks:deliver` fetch never fired).
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
vi.useFakeTimers()
|
|
23
|
+
})
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
vi.useRealTimers()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* EC-634 — an asset's `meta` is written per key, inside the mutation, on BOTH adapters:
|
|
30
|
+
* two sessions (two adapters over ONE backend — two studio tabs on one deployment) whose
|
|
31
|
+
* dialog saves interleave keep both keys; a dialog save landing while a file replace is in
|
|
32
|
+
* flight survives the replace; a not-yet-migrated legacy `alt` column is lifted into the
|
|
33
|
+
* stored bag by the same transaction. convex-test serialises mutations, so this proves the
|
|
34
|
+
* per-key MERGE in `assets:upsert`, not Convex's OCC retry (which re-runs the same handler
|
|
35
|
+
* against the newer row — the merge is what makes that retry correct).
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
const modules = {
|
|
39
|
+
...import.meta.glob(['./**/*.ts', '!./**/*.test.ts', '!./_generated/**']),
|
|
40
|
+
'./_generated/api.ts': async () => ({}),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const PROJECT_ID = 'asset-meta-patch'
|
|
44
|
+
const SESSION_USER_ID = 'user_admin'
|
|
45
|
+
const STAMP = '2026-10-27T00:00:00.000Z'
|
|
46
|
+
|
|
47
|
+
async function convexBackend() {
|
|
48
|
+
const t = convexTest(schema, modules)
|
|
49
|
+
const now = new Date().toISOString()
|
|
50
|
+
await t.run(async (ctx) => {
|
|
51
|
+
await ctx.db.insert('members', { userId: SESSION_USER_ID, role: 'admin', createdAt: now, updatedAt: now })
|
|
52
|
+
})
|
|
53
|
+
const asAdmin = t.withIdentity({ subject: SESSION_USER_ID, issuer: 'https://convex.test' })
|
|
54
|
+
const session = () =>
|
|
55
|
+
new ConvexPersistenceAdapter(
|
|
56
|
+
{
|
|
57
|
+
query: (ref: FunctionReference<'query'>, args?: Record<string, unknown>) => asAdmin.query(ref, args ?? {}),
|
|
58
|
+
mutation: (ref: FunctionReference<'mutation'>, args?: Record<string, unknown>) =>
|
|
59
|
+
asAdmin.mutation(ref, args ?? {}),
|
|
60
|
+
} as unknown as ConvexHttpClient,
|
|
61
|
+
PROJECT_ID,
|
|
62
|
+
).assets
|
|
63
|
+
return { t, a: session(), b: session() }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function memoryBackend() {
|
|
67
|
+
const adapter = new InMemoryPersistenceAdapter()
|
|
68
|
+
return { a: adapter.assets, b: adapter.assets }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function seed(assets: AssetRepository, meta: Record<string, unknown>) {
|
|
72
|
+
await assets.upsertAsset(PROJECT_ID, {
|
|
73
|
+
id: 'hero',
|
|
74
|
+
filename: 'hero.jpg',
|
|
75
|
+
contentType: 'image/jpeg',
|
|
76
|
+
byteSize: 1,
|
|
77
|
+
storageKey: 'blob_old',
|
|
78
|
+
url: 'https://assets.test/old.jpg',
|
|
79
|
+
meta,
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function openDialog(assets: AssetRepository) {
|
|
84
|
+
const opened = assetMetaDraft(await assets.getAsset(PROJECT_ID, 'hero'))
|
|
85
|
+
let draft = opened
|
|
86
|
+
return {
|
|
87
|
+
edit: (key: string, value: unknown) => {
|
|
88
|
+
draft = setAssetMetaField(draft, { name: key }, null, value)
|
|
89
|
+
},
|
|
90
|
+
commit: () => updateAssetMetadata({ assets }, PROJECT_ID, 'hero', assetMetaDraftPatch(opened, draft)),
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Two dialogs open on one asset, each edits one key, the commits interleave. */
|
|
95
|
+
async function interleavedDialogs({ a, b }: { a: AssetRepository; b: AssetRepository }) {
|
|
96
|
+
await seed(a, { alt: 'old alt', credit: 'old credit' })
|
|
97
|
+
const dialogA = await openDialog(a)
|
|
98
|
+
const dialogB = await openDialog(b)
|
|
99
|
+
dialogA.edit('alt', 'A alt')
|
|
100
|
+
dialogB.edit('credit', 'B credit')
|
|
101
|
+
expect((await dialogA.commit()).ok).toBe(true)
|
|
102
|
+
expect((await dialogB.commit()).ok).toBe(true)
|
|
103
|
+
return (await b.getAsset(PROJECT_ID, 'hero')).meta
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const iptcJpeg = new Uint8Array(
|
|
107
|
+
readFileSync(new URL('../../core/src/cms-core/__fixtures__/embedded-metadata/iptc-xmp.jpg', import.meta.url)),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
/** A replace whose read is followed — before its write — by another session's dialog save. */
|
|
111
|
+
async function replaceRacingADialog({ a, b }: { a: AssetRepository; b: AssetRepository }) {
|
|
112
|
+
await seed(a, { alt: 'old alt', note: 'old note' })
|
|
113
|
+
const dialogB = await openDialog(b)
|
|
114
|
+
dialogB.edit('note', 'B note')
|
|
115
|
+
const racing: AssetRepository = {
|
|
116
|
+
// B's dialog commits at the worst moment: right before A's replace writes.
|
|
117
|
+
upsertAsset: async (projectId, input) => {
|
|
118
|
+
await dialogB.commit()
|
|
119
|
+
return a.upsertAsset(projectId, input)
|
|
120
|
+
},
|
|
121
|
+
getAsset: (projectId, id) => a.getAsset(projectId, id),
|
|
122
|
+
listAssets: (projectId) => a.listAssets(projectId),
|
|
123
|
+
removeAsset: (projectId, id) => a.removeAsset(projectId, id),
|
|
124
|
+
resolveMany: (projectId, ids) => a.resolveMany(projectId, ids),
|
|
125
|
+
findAsset: (projectId, id) => a.findAsset(projectId, id),
|
|
126
|
+
}
|
|
127
|
+
const result = await runUpload(
|
|
128
|
+
{
|
|
129
|
+
blobs: new InMemoryBlobStorage(),
|
|
130
|
+
assets: racing,
|
|
131
|
+
assetFields: defaultAssetCollection().fields,
|
|
132
|
+
detectDimensions: async () => ({ width: 8, height: 6 }),
|
|
133
|
+
generateVariants: async () => ({}),
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
projectId: PROJECT_ID,
|
|
137
|
+
file: {
|
|
138
|
+
name: 'new.jpg',
|
|
139
|
+
type: 'image/jpeg',
|
|
140
|
+
size: iptcJpeg.byteLength,
|
|
141
|
+
arrayBuffer: async () => iptcJpeg.slice().buffer as ArrayBuffer,
|
|
142
|
+
},
|
|
143
|
+
replace: { assetId: 'hero' },
|
|
144
|
+
},
|
|
145
|
+
)
|
|
146
|
+
expect(result.ok).toBe(true)
|
|
147
|
+
return (await a.getAsset(PROJECT_ID, 'hero')).meta
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
describe('EC-634 — asset meta is patched per key in the write, on both adapters', () => {
|
|
151
|
+
it('two sessions’ interleaved dialog saves of different keys keep both (the worst row)', async () => {
|
|
152
|
+
const expected = { alt: 'A alt', credit: 'B credit' }
|
|
153
|
+
expect(await interleavedDialogs(memoryBackend())).toEqual(expected)
|
|
154
|
+
expect(await interleavedDialogs(await convexBackend())).toEqual(expected)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('a dialog save landing just before a replace’s write survives the replace', async () => {
|
|
158
|
+
const memory = await replaceRacingADialog(memoryBackend())
|
|
159
|
+
const convex = await replaceRacingADialog(await convexBackend())
|
|
160
|
+
expect(convex).toEqual(memory)
|
|
161
|
+
expect(convex).toMatchObject({ alt: 'A harbour at dusk & boats', note: 'B note' })
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('a patch lifts a not-yet-migrated legacy alt column into the stored bag, in the same mutation', async () => {
|
|
165
|
+
const { t, a } = await convexBackend()
|
|
166
|
+
await t.run(async (ctx) => {
|
|
167
|
+
await ctx.db.insert('assets', {
|
|
168
|
+
assetId: 'legacy',
|
|
169
|
+
filename: 'l.jpg',
|
|
170
|
+
contentType: 'image/jpeg',
|
|
171
|
+
byteSize: 1,
|
|
172
|
+
storageKey: 'k_legacy',
|
|
173
|
+
alt: 'Column alt',
|
|
174
|
+
createdAt: STAMP,
|
|
175
|
+
updatedAt: STAMP,
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
// An edit of ANOTHER key: the alt it did not name moves home and is kept.
|
|
179
|
+
const kept = await a.upsertAsset(PROJECT_ID, { id: 'legacy', metaPatch: { credit: 'Ada' } })
|
|
180
|
+
expect(kept.meta).toEqual({ alt: 'Column alt', credit: 'Ada' })
|
|
181
|
+
expect(kept.alt).toBeUndefined()
|
|
182
|
+
expect(assetAlt(kept, null)).toBe('Column alt')
|
|
183
|
+
// A clear of the alt stays cleared — no column left to re-surface it (ASSETS D21).
|
|
184
|
+
const cleared = await a.upsertAsset(PROJECT_ID, { id: 'legacy', metaPatch: { alt: null } })
|
|
185
|
+
expect(assetAlt(cleared, null)).toBeNull()
|
|
186
|
+
const row = await t.run((ctx) => ctx.db.query('assets').filter((q) => q.eq(q.field('assetId'), 'legacy')).first())
|
|
187
|
+
expect(row?.alt).toBeUndefined()
|
|
188
|
+
expect(row?.meta).toEqual({ credit: 'Ada' })
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('EC-632 — per-locale buckets are stored by the real mutation, and the stored row measures its depth', async () => {
|
|
192
|
+
const { t, a, b } = await convexBackend()
|
|
193
|
+
await seed(a, { credit: 'Ada' })
|
|
194
|
+
// Two translators, two sessions, interleaved: both locales are kept.
|
|
195
|
+
await a.upsertAsset(PROJECT_ID, { id: 'hero', localizedMetaPatch: { de: { alt: 'Hafen' } } })
|
|
196
|
+
await b.upsertAsset(PROJECT_ID, {
|
|
197
|
+
id: 'hero',
|
|
198
|
+
localizedMetaPatch: { en: { alt: 'Harbour', disclosure: { shown: true, badge: { corner: 'tl', tone: 'dark' } } } },
|
|
199
|
+
})
|
|
200
|
+
const row = await t.run((ctx) => ctx.db.query('assets').filter((q) => q.eq(q.field('assetId'), 'hero')).first())
|
|
201
|
+
expect(row?.localizedMeta).toEqual({
|
|
202
|
+
de: { alt: 'Hafen' },
|
|
203
|
+
en: { alt: 'Harbour', disclosure: { shown: true, badge: { corner: 'tl', tone: 'dark' } } },
|
|
204
|
+
})
|
|
205
|
+
expect(row?.meta).toEqual({ credit: 'Ada' })
|
|
206
|
+
// The stored Convex document (system fields included): 5 of Convex's 16 levels.
|
|
207
|
+
expect(measureMaxDepth(row).depth).toBe(5)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('refuses meta and metaPatch together, through the rule (a PersistenceValidationError naming metaPatch)', async () => {
|
|
211
|
+
const { a } = await convexBackend()
|
|
212
|
+
await seed(a, { alt: 'x' })
|
|
213
|
+
await expect(
|
|
214
|
+
a.upsertAsset(PROJECT_ID, { id: 'hero', meta: { alt: 'y' }, metaPatch: { credit: 'z' } }),
|
|
215
|
+
).rejects.toBeInstanceOf(PersistenceValidationError)
|
|
216
|
+
expect((await a.getAsset(PROJECT_ID, 'hero')).meta).toEqual({ alt: 'x' })
|
|
217
|
+
})
|
|
218
|
+
})
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { convexTest } from 'convex-test'
|
|
3
|
+
import type { ConvexHttpClient } from 'convex/browser'
|
|
4
|
+
import type { FunctionReference } from 'convex/server'
|
|
5
|
+
import { InMemoryBlobStorage, InMemoryPersistenceAdapter } from '@elytracms/core/persistence'
|
|
6
|
+
import type { AssetRepository } from '@elytracms/core/persistence'
|
|
7
|
+
import { ConvexPersistenceAdapter } from '../src/lib/persistence/convex-adapter'
|
|
8
|
+
import { runUpload } from '../src/lib/assets/upload'
|
|
9
|
+
import schema from './schema'
|
|
10
|
+
|
|
11
|
+
// convex-test fires a `runAfter(0)` on a REAL timer; fake timers keep the scheduled
|
|
12
|
+
// revalidation queued (its row written, the `webhooks:deliver` fetch never fired).
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
vi.useFakeTimers()
|
|
15
|
+
})
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
vi.useRealTimers()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* EC-634 follow-on — a replace is an UPDATE of a stored asset, never a create. Session A
|
|
22
|
+
* replaces the file; session B deletes the asset in the window between A's check and A's
|
|
23
|
+
* write. The replace used to carry every column a create needs, so it silently RE-CREATED
|
|
24
|
+
* the asset B had deleted (a colleague's delete undone). Two sessions over ONE backend,
|
|
25
|
+
* in-memory AND Convex (the real `assets:upsert` over convex-test).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const modules = {
|
|
29
|
+
...import.meta.glob(['./**/*.ts', '!./**/*.test.ts', '!./_generated/**']),
|
|
30
|
+
'./_generated/api.ts': async () => ({}),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const PROJECT_ID = 'asset-replace-race'
|
|
34
|
+
const SESSION_USER_ID = 'user_admin'
|
|
35
|
+
|
|
36
|
+
async function convexSessions() {
|
|
37
|
+
const t = convexTest(schema, modules)
|
|
38
|
+
const now = new Date().toISOString()
|
|
39
|
+
await t.run(async (ctx) => {
|
|
40
|
+
await ctx.db.insert('members', { userId: SESSION_USER_ID, role: 'admin', createdAt: now, updatedAt: now })
|
|
41
|
+
})
|
|
42
|
+
const asAdmin = t.withIdentity({ subject: SESSION_USER_ID, issuer: 'https://convex.test' })
|
|
43
|
+
const session = () =>
|
|
44
|
+
new ConvexPersistenceAdapter(
|
|
45
|
+
{
|
|
46
|
+
query: (ref: FunctionReference<'query'>, args?: Record<string, unknown>) => asAdmin.query(ref, args ?? {}),
|
|
47
|
+
mutation: (ref: FunctionReference<'mutation'>, args?: Record<string, unknown>) =>
|
|
48
|
+
asAdmin.mutation(ref, args ?? {}),
|
|
49
|
+
} as unknown as ConvexHttpClient,
|
|
50
|
+
PROJECT_ID,
|
|
51
|
+
).assets
|
|
52
|
+
return { a: session(), b: session() }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function memorySessions() {
|
|
56
|
+
const adapter = new InMemoryPersistenceAdapter()
|
|
57
|
+
return { a: adapter.assets, b: adapter.assets }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A's replace, with B's delete landing right after A checked that the asset exists. */
|
|
61
|
+
async function replaceRacingADelete({ a, b }: { a: AssetRepository; b: AssetRepository }) {
|
|
62
|
+
await a.upsertAsset(PROJECT_ID, {
|
|
63
|
+
id: 'hero',
|
|
64
|
+
filename: 'old.jpg',
|
|
65
|
+
contentType: 'image/jpeg',
|
|
66
|
+
byteSize: 1,
|
|
67
|
+
storageKey: 'blob_old',
|
|
68
|
+
url: 'https://assets.test/old.jpg',
|
|
69
|
+
})
|
|
70
|
+
// B's delete lands immediately before A's write — after anything A checked or read.
|
|
71
|
+
const racing: AssetRepository = {
|
|
72
|
+
upsertAsset: async (projectId, input) => {
|
|
73
|
+
await b.removeAsset(projectId, 'hero')
|
|
74
|
+
return a.upsertAsset(projectId, input)
|
|
75
|
+
},
|
|
76
|
+
getAsset: (projectId, id) => a.getAsset(projectId, id),
|
|
77
|
+
listAssets: (projectId) => a.listAssets(projectId),
|
|
78
|
+
removeAsset: (projectId, id) => a.removeAsset(projectId, id),
|
|
79
|
+
resolveMany: (projectId, ids) => a.resolveMany(projectId, ids),
|
|
80
|
+
findAsset: (projectId, id) => a.findAsset(projectId, id),
|
|
81
|
+
}
|
|
82
|
+
const result = await runUpload(
|
|
83
|
+
{
|
|
84
|
+
blobs: new InMemoryBlobStorage(),
|
|
85
|
+
assets: racing,
|
|
86
|
+
detectDimensions: async () => ({ width: 8, height: 6 }),
|
|
87
|
+
generateVariants: async () => ({}),
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
projectId: PROJECT_ID,
|
|
91
|
+
file: { name: 'new.jpg', type: 'image/jpeg', size: 3, arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer },
|
|
92
|
+
replace: { assetId: 'hero' },
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
return { result, stored: await b.findAsset(PROJECT_ID, 'hero') }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
describe('EC-634 follow-on — a replace never re-creates an asset another session deleted', () => {
|
|
99
|
+
for (const [name, backend] of [
|
|
100
|
+
['in-memory', async () => memorySessions()],
|
|
101
|
+
['Convex', convexSessions],
|
|
102
|
+
] as const) {
|
|
103
|
+
it(`${name}: the replace is refused, visibly, and the delete stands`, async () => {
|
|
104
|
+
const { result, stored } = await replaceRacingADelete(await backend())
|
|
105
|
+
expect(stored).toBeUndefined()
|
|
106
|
+
expect(result.ok).toBe(false)
|
|
107
|
+
if (!result.ok) {
|
|
108
|
+
expect(result.failure.stage).toBe('confirm')
|
|
109
|
+
expect(result.failure.message).toContain('hero')
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
it('control: without the delete the replace lands (same id, the new file)', async () => {
|
|
115
|
+
const { a } = memorySessions()
|
|
116
|
+
await a.upsertAsset(PROJECT_ID, { id: 'hero', filename: 'old.jpg', contentType: 'image/jpeg', byteSize: 1 })
|
|
117
|
+
const result = await runUpload(
|
|
118
|
+
{ blobs: new InMemoryBlobStorage(), assets: a, detectDimensions: async () => undefined, generateVariants: async () => ({}) },
|
|
119
|
+
{
|
|
120
|
+
projectId: PROJECT_ID,
|
|
121
|
+
file: { name: 'new.jpg', type: 'image/jpeg', size: 3, arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer },
|
|
122
|
+
replace: { assetId: 'hero' },
|
|
123
|
+
},
|
|
124
|
+
)
|
|
125
|
+
expect(result.ok && result.asset).toMatchObject({ id: 'hero', filename: 'new.jpg' })
|
|
126
|
+
})
|
|
127
|
+
})
|
package/convex/assets.ts
CHANGED
|
@@ -25,8 +25,9 @@ const clearable = <T, F extends string>(validator: Validator<T, 'required', F>)
|
|
|
25
25
|
/**
|
|
26
26
|
* Write one asset (EC-577). The ONE write rule (`resolveAssetWrite`, shared with
|
|
27
27
|
* the in-memory adapter) runs here, inside the transaction: a column absent from
|
|
28
|
-
* the args keeps its stored value, `null` clears it, a value sets it —
|
|
29
|
-
* `alt` column can only be cleared (EC-587: an asset's alt is `meta.alt`)
|
|
28
|
+
* the args keeps its stored value, `null` clears it, a value sets it — the legacy
|
|
29
|
+
* `alt` column can only be cleared (EC-587: an asset's alt is `meta.alt`), and a
|
|
30
|
+
* `metaPatch` changes `meta` per key against the row read here (EC-634). An edit is
|
|
30
31
|
* ONE `db.patch` — the rule's store patch, where a cleared column is `undefined`,
|
|
31
32
|
* which `db.patch` removes — so the write-chokepoint triggers fire once. No stored
|
|
32
33
|
* asset → ONE `db.insert` of the complete row (the `defaults` fill what the write
|
|
@@ -65,6 +66,30 @@ export const upsert = mutation({
|
|
|
65
66
|
lqip: clearable(v.string()),
|
|
66
67
|
/** Schema-driven asset metadata bag (EC-301): alt, copyright, credit, …. `null` clears it. */
|
|
67
68
|
meta: v.optional(v.any()),
|
|
69
|
+
/**
|
|
70
|
+
* EC-634 — a per-key change to `meta` (a value sets the key, `null` removes it), applied
|
|
71
|
+
* by the rule to the row as stored HERE, in this transaction — so an editor's save never
|
|
72
|
+
* carries a stale copy of a key another session changed. Validated by the rule (an
|
|
73
|
+
* object of keys, never beside `meta`), as `meta` is.
|
|
74
|
+
*/
|
|
75
|
+
metaPatch: v.optional(v.any()),
|
|
76
|
+
/**
|
|
77
|
+
* EC-632 — the per-locale buckets of the asset collection's `localized: true` fields
|
|
78
|
+
* (`{ [locale]: { [field]: value } }`): `null` clears them, an object replaces them
|
|
79
|
+
* whole (a create, an import); an editor sends `localizedMetaPatch` instead.
|
|
80
|
+
*/
|
|
81
|
+
localizedMeta: v.optional(v.any()),
|
|
82
|
+
/**
|
|
83
|
+
* EC-632 — a per-locale, per-key change to those buckets, applied by the rule to the
|
|
84
|
+
* row as stored HERE (as `metaPatch` is to `meta`). Validated by the rule.
|
|
85
|
+
*/
|
|
86
|
+
localizedMetaPatch: v.optional(v.any()),
|
|
87
|
+
/**
|
|
88
|
+
* EC-634 follow-on — this write only UPDATES a stored asset (a file replace): with the
|
|
89
|
+
* row gone (deleted by another session) the rule refuses it here, so it never re-creates
|
|
90
|
+
* what a colleague deleted.
|
|
91
|
+
*/
|
|
92
|
+
mustExist: v.optional(v.boolean()),
|
|
68
93
|
/**
|
|
69
94
|
* What a CREATE fills in when the write leaves it absent (`assetCreateDefaults`
|
|
70
95
|
* in core). Sent by the adapter because only it knows the project — rows carry
|
package/convex/auth.ts
CHANGED
|
@@ -4,8 +4,9 @@ import { actionGeneric } from 'convex/server'
|
|
|
4
4
|
import type { AnyDataModel, GenericMutationCtx } from 'convex/server'
|
|
5
5
|
import { ConvexError, v } from 'convex/values'
|
|
6
6
|
import type { GenericId } from 'convex/values'
|
|
7
|
-
import { classifyInviteSignup, normalizeInviteEmail } from '@elytracms/core/persistence'
|
|
7
|
+
import { bootstrapRole, classifyInviteSignup, normalizeInviteEmail } from '@elytracms/core/persistence'
|
|
8
8
|
import type { InviteSignupDecision } from '@elytracms/core/persistence'
|
|
9
|
+
import { deployedRolePolicy } from './roles'
|
|
9
10
|
|
|
10
11
|
/** The provider list `convexAuth` accepts, derived from its own signature. */
|
|
11
12
|
type ConvexAuthProviders = Parameters<typeof convexAuth>[0]['providers']
|
|
@@ -210,7 +211,9 @@ export async function gateSessionCreation(
|
|
|
210
211
|
case 'bootstrap-admin':
|
|
211
212
|
await ctx.db.insert('members', {
|
|
212
213
|
userId: args.userId,
|
|
213
|
-
|
|
214
|
+
// EC-605: the deployed policy's strongest role (which the policy rules require
|
|
215
|
+
// to grant `project.administer`) — `admin` under the default policy.
|
|
216
|
+
role: bootstrapRole(deployedRolePolicy()),
|
|
214
217
|
createdAt: now,
|
|
215
218
|
updatedAt: now,
|
|
216
219
|
})
|
package/convex/authority.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { checkAuthorityFloor } from '@elytracms/core/operations'
|
|
2
2
|
import {
|
|
3
3
|
buildAuthorityAuditEntry,
|
|
4
|
-
|
|
4
|
+
isKnownRole,
|
|
5
|
+
policyRoleIds,
|
|
5
6
|
roleGrantsCapability,
|
|
6
7
|
} from '@elytracms/core/persistence'
|
|
7
8
|
import type { AuthorityAuditOutcome, ProjectRole } from '@elytracms/core/persistence'
|
|
8
9
|
import type { MutationCtx } from './functions'
|
|
9
10
|
import { memberRole, type GuardCtx } from './guard'
|
|
11
|
+
import { deployedRolePolicy } from './roles'
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* The engine integrity FLOOR at the Convex trust boundary (EC-240 / AD-18 tenet
|
|
@@ -16,9 +18,10 @@ import { memberRole, type GuardCtx } from './guard'
|
|
|
16
18
|
* invariants on authority-mutating handlers (`members:set`, `members:remove` —
|
|
17
19
|
* and therefore `users.save`/`users.remove`, which delegate to them):
|
|
18
20
|
*
|
|
19
|
-
* - **(a) assign only roles you hold** —
|
|
20
|
-
*
|
|
21
|
-
*
|
|
21
|
+
* - **(a) assign only roles you hold** — never fires under the default policy (the
|
|
22
|
+
* only administrator, `admin`, is the strongest role); under a declared policy
|
|
23
|
+
* with an administering role below the strongest (EC-605) it is what stops that
|
|
24
|
+
* role granting — or inviting someone into (`invites:create`) — a rank above its own.
|
|
22
25
|
* - **(b) the last admin can't be removed or demoted** — the hole this closes:
|
|
23
26
|
* before EC-240 the handlers did a blind upsert/delete, so an admin could
|
|
24
27
|
* remove the sole admin and lock everyone out (the empty-table bootstrap in
|
|
@@ -27,28 +30,32 @@ import { memberRole, type GuardCtx } from './guard'
|
|
|
27
30
|
* The decision logic is the ONE shared `checkAuthorityFloor` primitive that the
|
|
28
31
|
* engine resolver also calls — this file only supplies the primitives it reads
|
|
29
32
|
* from `ctx.db`, so client and server can never drift. Denials throw
|
|
30
|
-
* `unauthorized: …`, matching the guard convention.
|
|
33
|
+
* `unauthorized: …`, matching the guard convention. The policy is the deployment's
|
|
34
|
+
* (EC-605, `deployedRolePolicy`) — the one `requireCapability` granted by.
|
|
31
35
|
*/
|
|
32
36
|
export async function assertAuthorityFloor(
|
|
33
37
|
ctx: GuardCtx,
|
|
34
38
|
params: {
|
|
35
39
|
/** The caller (already `requireCapability('project.administer')`-checked). */
|
|
36
40
|
readonly callerId: string
|
|
37
|
-
/**
|
|
38
|
-
|
|
41
|
+
/**
|
|
42
|
+
* The member whose role/membership this op changes; absent for an invite (EC-605 —
|
|
43
|
+
* it grants a role to someone who is not a member yet, so only (a) can apply).
|
|
44
|
+
*/
|
|
45
|
+
readonly targetUserId?: string
|
|
39
46
|
/** Roles this op grants the target (empty for a pure removal). */
|
|
40
47
|
readonly rolesGranted: readonly ProjectRole[]
|
|
41
48
|
/** True when the op removes the membership entirely. */
|
|
42
49
|
readonly removesMembership: boolean
|
|
43
50
|
},
|
|
44
51
|
): Promise<void> {
|
|
52
|
+
const policy = deployedRolePolicy()
|
|
45
53
|
const callerRole = await memberRole(ctx, params.callerId)
|
|
46
|
-
const targetRole = await memberRole(ctx, params.targetUserId)
|
|
54
|
+
const targetRole = params.targetUserId !== undefined ? await memberRole(ctx, params.targetUserId) : null
|
|
47
55
|
const members = await ctx.db.query('members').collect()
|
|
48
56
|
// "admin" generalizes to "grants project.administer" (EC-240c) — the same
|
|
49
57
|
// policy the engine resolver uses, so client and server can never drift.
|
|
50
|
-
const isAdministrator = (role: string) =>
|
|
51
|
-
roleGrantsCapability(role, 'project.administer', defaultRolePolicy)
|
|
58
|
+
const isAdministrator = (role: string) => roleGrantsCapability(role, 'project.administer', policy)
|
|
52
59
|
const adminCount = members.filter((m) => isAdministrator(m.role)).length
|
|
53
60
|
|
|
54
61
|
const decision = checkAuthorityFloor(
|
|
@@ -59,11 +66,28 @@ export async function assertAuthorityFloor(
|
|
|
59
66
|
removesMembership: params.removesMembership,
|
|
60
67
|
adminCount,
|
|
61
68
|
},
|
|
62
|
-
|
|
69
|
+
policy,
|
|
63
70
|
)
|
|
64
71
|
if (!decision.allow) throw new Error(`unauthorized: ${decision.reason}`)
|
|
65
72
|
}
|
|
66
73
|
|
|
74
|
+
/**
|
|
75
|
+
* EC-605 — refuse a role the deployed policy does not declare, at the WRITE (a member's
|
|
76
|
+
* role, an invite's role), visibly: `invalid-role: …` naming the declared set. The
|
|
77
|
+
* studio validates against the project's config first; this is the trust boundary's own
|
|
78
|
+
* check, so a hand-crafted client or a drifted deployment can never store a role nobody
|
|
79
|
+
* can enforce (an unknown role grants nothing — it would fail closed on every read,
|
|
80
|
+
* silently).
|
|
81
|
+
*/
|
|
82
|
+
export function assertDeclaredRole(role: string): void {
|
|
83
|
+
const policy = deployedRolePolicy()
|
|
84
|
+
if (!isKnownRole(role, policy)) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`invalid-role: role "${role}" is not declared in this deployment's role policy (declared: ${policyRoleIds(policy).join(', ')}). Declare it in elytra.config.ts \`roles\` and re-provision (\`elytra init\`), or pick a declared role.`,
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
67
91
|
/**
|
|
68
92
|
* Append one {@link buildAuthorityAuditEntry} row to the `authorityAudit` table
|
|
69
93
|
* (EC-240 H-tail item 6). Called at the floor chokepoint (`members:set` /
|
|
@@ -4,6 +4,7 @@ import type { ConvexHttpClient } from 'convex/browser'
|
|
|
4
4
|
import type { FunctionReference } from 'convex/server'
|
|
5
5
|
import { fieldPath, measureMaxDepth, nodePropPath } from '@elytracms/core/cms-core'
|
|
6
6
|
import {
|
|
7
|
+
CONTRACT_RECORD_KINDS,
|
|
7
8
|
PERSISTENCE_CONTRACT_SCENARIOS,
|
|
8
9
|
publishCurrentDraft,
|
|
9
10
|
replaceAtCurrentRevision,
|
|
@@ -12,6 +13,7 @@ import type { ContractContext } from '@elytracms/core/persistence'
|
|
|
12
13
|
import { ConvexPersistenceAdapter } from '../src/lib/persistence/convex-adapter'
|
|
13
14
|
import type { ReactiveDocumentClient } from '../src/lib/persistence/convex-client'
|
|
14
15
|
import schema from './schema'
|
|
16
|
+
import { defineRecordFunctions } from './record-functions'
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* EC-454 — Convex-side contract parity, restored.
|
|
@@ -40,6 +42,11 @@ import schema from './schema'
|
|
|
40
42
|
const modules = {
|
|
41
43
|
...import.meta.glob(['./**/*.ts', '!./**/*.test.ts', '!./_generated/**']),
|
|
42
44
|
'./_generated/api.ts': async () => ({}),
|
|
45
|
+
// EC-616: the contract's FIXTURE record kinds, registered as test-only modules exactly as
|
|
46
|
+
// an addon's convex module registers its kind (one module per kind, named by the kind).
|
|
47
|
+
...Object.fromEntries(
|
|
48
|
+
CONTRACT_RECORD_KINDS.map((kind) => [`./${kind.module}.ts`, async () => defineRecordFunctions(kind)]),
|
|
49
|
+
),
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
/** The config-owned slug this deployment's records are stamped with (EC-179). */
|