canopycms 0.0.68-int.98 → 0.1.0
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/CHANGELOG.md +2131 -0
- package/README.md +47 -63
- package/dist/ai/generate.d.ts +5 -0
- package/dist/ai/generate.js +244 -115
- package/dist/ai/index.d.ts +1 -1
- package/dist/ai/json-to-markdown.d.ts +21 -1
- package/dist/ai/json-to-markdown.js +120 -57
- package/dist/ai/references.d.ts +72 -0
- package/dist/ai/references.js +223 -0
- package/dist/ai/resolve-branch.js +9 -2
- package/dist/ai/to-plain-text.d.ts +17 -8
- package/dist/ai/to-plain-text.js +18 -15
- package/dist/ai/types.d.ts +84 -3
- package/dist/api/admin-branch-health.d.ts +24 -1
- package/dist/api/admin-branch-health.js +52 -20
- package/dist/api/admin.d.ts +23 -5
- package/dist/api/admin.js +36 -1
- package/dist/api/assets.js +88 -78
- package/dist/api/branch-create-window.d.ts +12 -0
- package/dist/api/branch-create-window.js +13 -0
- package/dist/api/branch-merge.d.ts +1 -1
- package/dist/api/branch-merge.js +1 -1
- package/dist/api/branch-review.d.ts +3 -3
- package/dist/api/branch-review.js +16 -7
- package/dist/api/branch-status.d.ts +16 -6
- package/dist/api/branch-status.js +57 -12
- package/dist/api/branch-withdraw.d.ts +2 -2
- package/dist/api/branch-withdraw.js +8 -3
- package/dist/api/branch.d.ts +25 -12
- package/dist/api/branch.js +164 -103
- package/dist/api/client.d.ts +78 -58
- package/dist/api/client.js +150 -117
- package/dist/api/comments.d.ts +12 -3
- package/dist/api/comments.js +118 -9
- package/dist/api/content.d.ts +9 -9
- package/dist/api/content.js +131 -22
- package/dist/api/entries-constants.d.ts +14 -3
- package/dist/api/entries-constants.js +14 -3
- package/dist/api/entries.d.ts +40 -2
- package/dist/api/entries.js +102 -6
- package/dist/api/github-sync.d.ts +19 -0
- package/dist/api/github-sync.js +50 -3
- package/dist/api/groups.d.ts +4 -4
- package/dist/api/groups.js +2 -1
- package/dist/api/guards.js +23 -3
- package/dist/api/index.d.ts +3 -1
- package/dist/api/index.js +1 -0
- package/dist/api/permissions.d.ts +21 -2
- package/dist/api/permissions.js +50 -3
- package/dist/api/request-deadline.d.ts +59 -0
- package/dist/api/request-deadline.js +73 -0
- package/dist/api/resolve-references.js +8 -7
- package/dist/api/route-builder.d.ts +14 -0
- package/dist/api/route-builder.js +14 -1
- package/dist/api/schema.d.ts +11 -9
- package/dist/api/schema.js +2 -0
- package/dist/api/settings-helpers.d.ts +5 -1
- package/dist/api/settings-helpers.js +10 -2
- package/dist/api/types.d.ts +18 -0
- package/dist/api/users-constants.d.ts +6 -0
- package/dist/api/users-constants.js +6 -0
- package/dist/assets/asset-url.d.ts +28 -16
- package/dist/assets/asset-url.js +51 -25
- package/dist/assets/finalize.js +3 -0
- package/dist/assets/index.d.ts +1 -1
- package/dist/assets/materialize.d.ts +153 -0
- package/dist/assets/materialize.js +402 -0
- package/dist/assets/store-local.d.ts +19 -5
- package/dist/assets/store-local.js +69 -10
- package/dist/assets/store-s3.d.ts +38 -7
- package/dist/assets/store-s3.js +128 -22
- package/dist/assets/transform-directives.d.ts +52 -13
- package/dist/assets/transform-directives.js +87 -27
- package/dist/assets/transform.d.ts +3 -3
- package/dist/assets/transform.js +13 -8
- package/dist/assets/types.d.ts +31 -4
- package/dist/auth/cache.d.ts +1 -1
- package/dist/auth/cache.js +1 -1
- package/dist/auth/caching-auth-plugin.d.ts +2 -0
- package/dist/auth/caching-auth-plugin.js +12 -2
- package/dist/auth/file-based-auth-cache.d.ts +8 -0
- package/dist/auth/file-based-auth-cache.js +18 -8
- package/dist/auth/person-name.d.ts +5 -0
- package/dist/auth/person-name.js +8 -0
- package/dist/auth/plugin.d.ts +10 -1
- package/dist/auth/types.d.ts +2 -0
- package/dist/auth/user-metadata-lookup.d.ts +21 -0
- package/dist/auth/user-metadata-lookup.js +116 -0
- package/dist/authorization/branch.d.ts +1 -1
- package/dist/authorization/branch.js +12 -13
- package/dist/authorization/groups/schema.d.ts +4 -4
- package/dist/authorization/helpers.d.ts +8 -0
- package/dist/authorization/helpers.js +8 -0
- package/dist/authorization/index.d.ts +1 -1
- package/dist/authorization/index.js +1 -1
- package/dist/authorization/protected-branch.d.ts +5 -4
- package/dist/authorization/protected-branch.js +7 -5
- package/dist/branch-health.d.ts +35 -8
- package/dist/branch-health.js +54 -18
- package/dist/branch-metadata-error.d.ts +27 -0
- package/dist/branch-metadata-error.js +23 -0
- package/dist/branch-metadata-file.d.ts +205 -24
- package/dist/branch-metadata-file.js +126 -25
- package/dist/branch-metadata.d.ts +45 -6
- package/dist/branch-metadata.js +142 -50
- package/dist/branch-provisioning.d.ts +167 -0
- package/dist/branch-provisioning.js +501 -0
- package/dist/branch-registry.d.ts +8 -0
- package/dist/branch-registry.js +61 -9
- package/dist/branch-schema-cache.d.ts +29 -12
- package/dist/branch-schema-cache.js +64 -21
- package/dist/branch-sparse.d.ts +23 -0
- package/dist/branch-sparse.js +118 -0
- package/dist/branch-workspace.d.ts +42 -3
- package/dist/branch-workspace.js +239 -100
- package/dist/build/asset-refs.d.ts +94 -0
- package/dist/build/asset-refs.js +267 -0
- package/dist/build/generate-ai-content.js +5 -0
- package/dist/build/inline-flight.d.ts +37 -0
- package/dist/build/inline-flight.js +142 -0
- package/dist/cli/asset-refs.d.ts +52 -0
- package/dist/cli/asset-refs.js +194 -0
- package/dist/cli/cli.d.ts +15 -1
- package/dist/cli/cli.js +8543 -3193
- package/dist/cli/configured-asset-store.d.ts +12 -0
- package/dist/cli/configured-asset-store.js +46 -0
- package/dist/cli/generate-ai-content.js +4244 -1072
- package/dist/cli/init.js +217 -126
- package/dist/cli/project-detect.d.ts +2 -0
- package/dist/cli/project-detect.js +2 -0
- package/dist/cli/template-files/Dockerfile.cms.template +13 -5
- package/dist/cli/template-files/cms-stack.ts.template +16 -13
- package/dist/client.d.ts +1 -2
- package/dist/client.js +1 -2
- package/dist/comment-store.d.ts +25 -0
- package/dist/comment-store.js +43 -8
- package/dist/config/flatten.js +2 -0
- package/dist/config/helpers.js +2 -2
- package/dist/config/index.d.ts +1 -1
- package/dist/config/schemas/config.d.ts +12 -29
- package/dist/config/schemas/config.js +9 -12
- package/dist/config/schemas/field.d.ts +3 -0
- package/dist/config/schemas/field.js +58 -4
- package/dist/config/schemas/media.d.ts +4 -13
- package/dist/config/schemas/media.js +5 -8
- package/dist/config/schemas/url.d.ts +2 -20
- package/dist/config/schemas/url.js +2 -25
- package/dist/config/types.d.ts +116 -22
- package/dist/config/types.js +3 -9
- package/dist/config/validation.d.ts +25 -4
- package/dist/config/validation.js +116 -3
- package/dist/content-id-index.js +2 -2
- package/dist/content-index-generation.d.ts +1 -1
- package/dist/content-index-generation.js +1 -1
- package/dist/content-listing.d.ts +10 -1
- package/dist/content-listing.js +22 -5
- package/dist/content-reader.d.ts +9 -0
- package/dist/content-reader.js +16 -11
- package/dist/content-store.d.ts +75 -12
- package/dist/content-store.js +249 -86
- package/dist/content-tree.js +2 -2
- package/dist/context.js +5 -2
- package/dist/dangling-reference-log.d.ts +17 -0
- package/dist/dangling-reference-log.js +29 -0
- package/dist/dev-content-watcher.d.ts +1 -1
- package/dist/dev-content-watcher.js +1 -1
- package/dist/editor/BranchManager.d.ts +23 -7
- package/dist/editor/BranchManager.js +119 -54
- package/dist/editor/CanopyEditor.d.ts +1 -1
- package/dist/editor/CanopyEditor.js +1 -2
- package/dist/editor/CommentsPanel.d.ts +14 -0
- package/dist/editor/CommentsPanel.js +68 -43
- package/dist/editor/Editor.d.ts +11 -5
- package/dist/editor/Editor.js +891 -218
- package/dist/editor/EditorAuthGate.d.ts +7 -1
- package/dist/editor/EditorAuthGate.js +41 -9
- package/dist/editor/EntryNavigator.d.ts +22 -0
- package/dist/editor/EntryNavigator.js +353 -105
- package/dist/editor/FormHeader.d.ts +25 -0
- package/dist/editor/FormHeader.js +82 -0
- package/dist/editor/FormRenderer.d.ts +27 -3
- package/dist/editor/FormRenderer.js +171 -76
- package/dist/editor/PreviewFrame.d.ts +40 -0
- package/dist/editor/PreviewFrame.js +215 -0
- package/dist/editor/PreviewPane.d.ts +47 -0
- package/dist/editor/PreviewPane.js +147 -0
- package/dist/editor/PreviewToolbarTools.d.ts +22 -0
- package/dist/editor/PreviewToolbarTools.js +73 -0
- package/dist/editor/admin/SystemHealthPanel.d.ts +2 -1
- package/dist/editor/admin/SystemHealthPanel.js +163 -33
- package/dist/editor/admin/useSystemHealth.d.ts +18 -0
- package/dist/editor/admin/useSystemHealth.js +70 -1
- package/dist/editor/branch-status.d.ts +17 -0
- package/dist/editor/branch-status.js +29 -0
- package/dist/editor/canopy-path.d.ts +9 -6
- package/dist/editor/canopy-path.js +8 -11
- package/dist/editor/client-reference-resolver.d.ts +43 -16
- package/dist/editor/client-reference-resolver.js +155 -64
- package/dist/editor/comments/BranchComments.d.ts +1 -1
- package/dist/editor/comments/BranchComments.js +2 -2
- package/dist/editor/comments/CloseRefusedNotice.d.ts +4 -0
- package/dist/editor/comments/CloseRefusedNotice.js +7 -0
- package/dist/editor/comments/CommentThreadsPopover.d.ts +47 -0
- package/dist/editor/comments/CommentThreadsPopover.js +274 -0
- package/dist/editor/comments/CommentWriteAlert.d.ts +11 -0
- package/dist/editor/comments/CommentWriteAlert.js +13 -0
- package/dist/editor/comments/EntryComments.d.ts +10 -4
- package/dist/editor/comments/EntryComments.js +14 -17
- package/dist/editor/comments/FieldWrapper.d.ts +18 -4
- package/dist/editor/comments/FieldWrapper.js +62 -28
- package/dist/editor/comments/InlineCommentThread.d.ts +9 -2
- package/dist/editor/comments/InlineCommentThread.js +11 -47
- package/dist/editor/comments/ThreadCarousel.d.ts +1 -1
- package/dist/editor/comments/ThreadCarousel.js +18 -21
- package/dist/editor/comments/ThreadReplyControls.d.ts +28 -0
- package/dist/editor/comments/ThreadReplyControls.js +92 -0
- package/dist/editor/comments/field-comment-context.d.ts +14 -0
- package/dist/editor/comments/field-comment-context.js +14 -0
- package/dist/editor/comments/form-comments.d.ts +48 -0
- package/dist/editor/comments/form-comments.js +178 -0
- package/dist/editor/comments/item-anchor-copy.d.ts +6 -0
- package/dist/editor/comments/item-anchor-copy.js +6 -0
- package/dist/editor/comments/move-focus-if-unmoved.d.ts +6 -0
- package/dist/editor/comments/move-focus-if-unmoved.js +11 -0
- package/dist/editor/comments/popover-placement.d.ts +18 -0
- package/dist/editor/comments/popover-placement.js +58 -0
- package/dist/editor/comments/use-tracked-writes.d.ts +16 -0
- package/dist/editor/comments/use-tracked-writes.js +53 -0
- package/dist/editor/components/BranchAccessList.d.ts +15 -0
- package/dist/editor/components/BranchAccessList.js +52 -0
- package/dist/editor/components/BranchLockBanner.d.ts +22 -0
- package/dist/editor/components/BranchLockBanner.js +39 -0
- package/dist/editor/components/BranchPill.d.ts +32 -0
- package/dist/editor/components/BranchPill.js +33 -0
- package/dist/editor/components/BranchesDrawer.d.ts +11 -0
- package/dist/editor/components/BranchesDrawer.js +5 -0
- package/dist/editor/components/ConfirmDeleteModal.d.ts +6 -1
- package/dist/editor/components/ConfirmDeleteModal.js +3 -2
- package/dist/editor/components/DestructiveMenuItem.d.ts +7 -0
- package/dist/editor/components/DestructiveMenuItem.js +8 -0
- package/dist/editor/components/EditorCrashScreen.d.ts +17 -0
- package/dist/editor/components/EditorCrashScreen.js +22 -0
- package/dist/editor/components/EditorErrorBoundary.d.ts +38 -0
- package/dist/editor/components/EditorErrorBoundary.js +40 -0
- package/dist/editor/components/EditorHeader.d.ts +56 -106
- package/dist/editor/components/EditorHeader.js +130 -131
- package/dist/editor/components/EditorRail.d.ts +49 -0
- package/dist/editor/components/EditorRail.js +65 -0
- package/dist/editor/components/EntryBreadcrumb.d.ts +33 -0
- package/dist/editor/components/EntryBreadcrumb.js +56 -0
- package/dist/editor/components/EntryCreateModal.d.ts +12 -7
- package/dist/editor/components/EntryCreateModal.js +51 -57
- package/dist/editor/components/NoEditPermissionNotice.d.ts +6 -5
- package/dist/editor/components/NoEditPermissionNotice.js +5 -4
- package/dist/editor/components/QuickOpenPalette.d.ts +22 -0
- package/dist/editor/components/QuickOpenPalette.js +66 -0
- package/dist/editor/components/ReadOnlyDraftNotice.d.ts +11 -0
- package/dist/editor/components/ReadOnlyDraftNotice.js +12 -0
- package/dist/editor/components/ReasonedMenuItem.d.ts +15 -0
- package/dist/editor/components/ReasonedMenuItem.js +15 -0
- package/dist/editor/components/ReferencedByList.d.ts +11 -0
- package/dist/editor/components/ReferencedByList.js +28 -0
- package/dist/editor/components/RelativeTime.d.ts +10 -0
- package/dist/editor/components/RelativeTime.js +10 -0
- package/dist/editor/components/RenameEntryModal.d.ts +9 -1
- package/dist/editor/components/RenameEntryModal.js +19 -31
- package/dist/editor/components/RetryToastMessage.d.ts +9 -0
- package/dist/editor/components/RetryToastMessage.js +6 -0
- package/dist/editor/components/SaveAllResult.d.ts +25 -0
- package/dist/editor/components/SaveAllResult.js +86 -0
- package/dist/editor/components/SaveSplitButton.d.ts +30 -0
- package/dist/editor/components/SaveSplitButton.js +108 -0
- package/dist/editor/components/StagedChangesDrawer.d.ts +39 -0
- package/dist/editor/components/StagedChangesDrawer.js +92 -0
- package/dist/editor/components/UnavailableEntryNotice.d.ts +6 -0
- package/dist/editor/components/UnavailableEntryNotice.js +9 -0
- package/dist/editor/components/UnavailableTypeMessage.d.ts +6 -0
- package/dist/editor/components/UnavailableTypeMessage.js +9 -0
- package/dist/editor/components/UndoToastMessage.d.ts +18 -0
- package/dist/editor/components/UndoToastMessage.js +10 -0
- package/dist/editor/components/UserBadge.d.ts +2 -5
- package/dist/editor/components/UserBadge.js +62 -33
- package/dist/editor/components/ValidationSummary.d.ts +14 -0
- package/dist/editor/components/ValidationSummary.js +24 -0
- package/dist/editor/components/index.d.ts +2 -2
- package/dist/editor/components/index.js +2 -2
- package/dist/editor/context/ApiClientContext.d.ts +8 -2
- package/dist/editor/context/ApiClientContext.js +19 -8
- package/dist/editor/context/AssetContext.d.ts +13 -35
- package/dist/editor/context/AssetContext.js +12 -31
- package/dist/editor/context/EditorStateContext.d.ts +4 -4
- package/dist/editor/context/EditorStateContext.js +4 -4
- package/dist/editor/context/SWRProvider.d.ts +3 -3
- package/dist/editor/context/SiteMdxAllowContext.d.ts +4 -0
- package/dist/editor/context/SiteMdxAllowContext.js +7 -0
- package/dist/editor/context/index.d.ts +2 -1
- package/dist/editor/context/index.js +2 -1
- package/dist/editor/contrast.d.ts +19 -0
- package/dist/editor/contrast.js +41 -0
- package/dist/editor/copy.d.ts +18 -0
- package/dist/editor/copy.js +18 -0
- package/dist/editor/editor-config.js +16 -0
- package/dist/editor/editor-utils.d.ts +34 -8
- package/dist/editor/editor-utils.js +42 -20
- package/dist/editor/entry-search.d.ts +20 -0
- package/dist/editor/entry-search.js +34 -0
- package/dist/editor/field-breadcrumb.d.ts +12 -0
- package/dist/editor/field-breadcrumb.js +85 -0
- package/dist/editor/field-props.d.ts +86 -0
- package/dist/editor/field-props.js +27 -0
- package/dist/editor/field-target.d.ts +30 -0
- package/dist/editor/field-target.js +77 -0
- package/dist/editor/fields/BlockField.d.ts +12 -0
- package/dist/editor/fields/BlockField.js +89 -73
- package/dist/editor/fields/CodeField.d.ts +2 -0
- package/dist/editor/fields/CodeField.js +6 -2
- package/dist/editor/fields/DateTimeField.d.ts +4 -29
- package/dist/editor/fields/DateTimeField.js +79 -41
- package/dist/editor/fields/ExpandAllButton.d.ts +6 -0
- package/dist/editor/fields/ExpandAllButton.js +4 -0
- package/dist/editor/fields/FieldCrashFallback.d.ts +17 -0
- package/dist/editor/fields/FieldCrashFallback.js +30 -0
- package/dist/editor/fields/FieldLabel.d.ts +37 -0
- package/dist/editor/fields/FieldLabel.js +19 -0
- package/dist/editor/fields/ImageField.d.ts +4 -0
- package/dist/editor/fields/ImageField.js +29 -14
- package/dist/editor/fields/InlineGroupField.js +4 -3
- package/dist/editor/fields/ItemList.d.ts +42 -0
- package/dist/editor/fields/ItemList.js +130 -0
- package/dist/editor/fields/MarkdownField.d.ts +32 -0
- package/dist/editor/fields/MarkdownField.js +223 -46
- package/dist/editor/fields/MdxImageDialog.js +1 -1
- package/dist/editor/fields/NumberField.d.ts +2 -0
- package/dist/editor/fields/NumberField.js +8 -5
- package/dist/editor/fields/NumberListField.d.ts +2 -0
- package/dist/editor/fields/NumberListField.js +19 -16
- package/dist/editor/fields/ObjectField.d.ts +2 -7
- package/dist/editor/fields/ObjectField.js +15 -5
- package/dist/editor/fields/ObjectListField.d.ts +29 -0
- package/dist/editor/fields/ObjectListField.js +57 -0
- package/dist/editor/fields/ReferenceField.d.ts +2 -0
- package/dist/editor/fields/ReferenceField.js +34 -9
- package/dist/editor/fields/SelectField.d.ts +2 -0
- package/dist/editor/fields/SelectField.js +11 -2
- package/dist/editor/fields/StringListField.d.ts +4 -1
- package/dist/editor/fields/StringListField.js +22 -8
- package/dist/editor/fields/TextField.d.ts +2 -0
- package/dist/editor/fields/TextField.js +6 -2
- package/dist/editor/fields/ToggleField.d.ts +2 -0
- package/dist/editor/fields/ToggleField.js +8 -3
- package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +1 -1
- package/dist/editor/fields/entry-link/InsertEntryLink.js +1 -1
- package/dist/editor/fields/form-expansion.d.ts +9 -0
- package/dist/editor/fields/form-expansion.js +2 -0
- package/dist/editor/fields/item-expansion.d.ts +15 -0
- package/dist/editor/fields/item-expansion.js +60 -0
- package/dist/editor/fields/list-item-keys.d.ts +8 -0
- package/dist/editor/fields/list-item-keys.js +50 -0
- package/dist/editor/fields/list-item-title.d.ts +20 -0
- package/dist/editor/fields/list-item-title.js +85 -0
- package/dist/editor/fields/markdown-export-options.d.ts +2 -0
- package/dist/editor/fields/markdown-export-options.js +8 -0
- package/dist/editor/fields/markdown-fidelity-visitors.d.ts +11 -0
- package/dist/editor/fields/markdown-fidelity-visitors.js +160 -0
- package/dist/editor/fields/mdx-jsx-support.d.ts +5 -1
- package/dist/editor/fields/mdx-jsx-support.js +157 -69
- package/dist/editor/fields/rich-text-failures.d.ts +5 -0
- package/dist/editor/fields/rich-text-failures.js +26 -0
- package/dist/editor/fields/toolbar-wrap.d.ts +7 -0
- package/dist/editor/fields/toolbar-wrap.js +35 -0
- package/dist/editor/form-outline.d.ts +13 -0
- package/dist/editor/form-outline.js +70 -0
- package/dist/editor/group-manager/GroupCard.d.ts +4 -1
- package/dist/editor/group-manager/GroupCard.js +12 -3
- package/dist/editor/group-manager/GroupForm.js +1 -1
- package/dist/editor/group-manager/InternalGroupsTab.d.ts +2 -1
- package/dist/editor/group-manager/InternalGroupsTab.js +4 -4
- package/dist/editor/group-manager/MemberList.d.ts +2 -1
- package/dist/editor/group-manager/MemberList.js +15 -4
- package/dist/editor/group-manager/hooks/useGroupState.d.ts +5 -0
- package/dist/editor/group-manager/hooks/useGroupState.js +30 -7
- package/dist/editor/group-manager/index.js +34 -5
- package/dist/editor/group-manager/types.d.ts +5 -2
- package/dist/editor/hooks/create-branch-request.d.ts +20 -0
- package/dist/editor/hooks/create-branch-request.js +54 -0
- package/dist/editor/hooks/save-all.d.ts +68 -0
- package/dist/editor/hooks/save-all.js +61 -0
- package/dist/editor/hooks/useBranchActions.d.ts +11 -1
- package/dist/editor/hooks/useBranchActions.js +42 -19
- package/dist/editor/hooks/useBranchManager.d.ts +21 -0
- package/dist/editor/hooks/useBranchManager.js +321 -125
- package/dist/editor/hooks/useBranchesData.d.ts +8 -4
- package/dist/editor/hooks/useBranchesData.js +9 -5
- package/dist/editor/hooks/useCommentSystem.d.ts +14 -2
- package/dist/editor/hooks/useCommentSystem.js +79 -55
- package/dist/editor/hooks/useCommentsData.d.ts +11 -7
- package/dist/editor/hooks/useCommentsData.js +20 -19
- package/dist/editor/hooks/useDraftManager.d.ts +77 -3
- package/dist/editor/hooks/useDraftManager.js +887 -94
- package/dist/editor/hooks/useEditorLayout.d.ts +53 -7
- package/dist/editor/hooks/useEditorLayout.js +98 -20
- package/dist/editor/hooks/useEntriesData.d.ts +2 -1
- package/dist/editor/hooks/useEntriesData.js +18 -4
- package/dist/editor/hooks/useEntryManager.d.ts +41 -2
- package/dist/editor/hooks/useEntryManager.js +227 -41
- package/dist/editor/hooks/useGroupManager.js +13 -16
- package/dist/editor/hooks/usePermissionManager.js +14 -6
- package/dist/editor/hooks/usePreviewMarks.d.ts +21 -0
- package/dist/editor/hooks/usePreviewMarks.js +33 -0
- package/dist/editor/hooks/useReferenceResolution.d.ts +10 -3
- package/dist/editor/hooks/useReferenceResolution.js +34 -177
- package/dist/editor/hooks/useSchemaManager.d.ts +25 -6
- package/dist/editor/hooks/useSchemaManager.js +97 -39
- package/dist/editor/hooks/useUserMetadata.d.ts +14 -4
- package/dist/editor/hooks/useUserMetadata.js +76 -47
- package/dist/editor/hooks/user-metadata-batcher.d.ts +8 -0
- package/dist/editor/hooks/user-metadata-batcher.js +33 -0
- package/dist/editor/media/AssetCard.d.ts +6 -7
- package/dist/editor/media/AssetCard.js +24 -31
- package/dist/editor/media/AssetListRow.d.ts +16 -0
- package/dist/editor/media/AssetListRow.js +41 -0
- package/dist/editor/media/CropStep.js +6 -1
- package/dist/editor/media/MediaLibrary.d.ts +4 -3
- package/dist/editor/media/MediaLibrary.js +11 -4
- package/dist/editor/media/MediaLibraryBody.d.ts +3 -3
- package/dist/editor/media/MediaLibraryBody.js +195 -80
- package/dist/editor/media/MediaLibrarySkeleton.d.ts +6 -0
- package/dist/editor/media/MediaLibrarySkeleton.js +14 -0
- package/dist/editor/media/crop-math.d.ts +3 -4
- package/dist/editor/media/crop-math.js +11 -22
- package/dist/editor/media/editor-image-src.d.ts +8 -0
- package/dist/editor/media/editor-image-src.js +10 -0
- package/dist/editor/media/format-bytes.d.ts +2 -0
- package/dist/editor/media/format-bytes.js +23 -0
- package/dist/editor/media/media-library-prefs.d.ts +23 -0
- package/dist/editor/media/media-library-prefs.js +49 -0
- package/dist/editor/media/media-library-view.d.ts +34 -0
- package/dist/editor/media/media-library-view.js +40 -0
- package/dist/editor/media/useAssetLibrary.d.ts +30 -0
- package/dist/editor/media/useAssetLibrary.js +126 -0
- package/dist/editor/media/useStableAssetOrder.d.ts +11 -0
- package/dist/editor/media/useStableAssetOrder.js +37 -0
- package/dist/editor/media/useThumbnailFailure.d.ts +6 -0
- package/dist/editor/media/useThumbnailFailure.js +16 -0
- package/dist/editor/permission-manager/GroupSelector.js +2 -2
- package/dist/editor/permission-manager/PermissionEditor.d.ts +1 -1
- package/dist/editor/permission-manager/PermissionEditor.js +15 -4
- package/dist/editor/permission-manager/PermissionLevelBadge.js +1 -1
- package/dist/editor/permission-manager/PermissionTree.d.ts +1 -1
- package/dist/editor/permission-manager/UserSelector.d.ts +1 -1
- package/dist/editor/permission-manager/UserSelector.js +3 -3
- package/dist/editor/permission-manager/index.js +12 -3
- package/dist/editor/permission-manager/types.d.ts +5 -2
- package/dist/editor/platform.d.ts +3 -0
- package/dist/editor/platform.js +9 -0
- package/dist/editor/preview-asset-base.d.ts +9 -0
- package/dist/editor/preview-asset-base.js +17 -0
- package/dist/editor/preview-bridge.d.ts +51 -27
- package/dist/editor/preview-bridge.js +103 -144
- package/dist/editor/preview-marks.d.ts +15 -0
- package/dist/editor/preview-marks.js +74 -0
- package/dist/editor/preview-path.d.ts +12 -0
- package/dist/editor/preview-path.js +45 -1
- package/dist/editor/preview-scale.d.ts +27 -0
- package/dist/editor/preview-scale.js +34 -0
- package/dist/editor/raw-asset-base.d.ts +5 -0
- package/dist/editor/raw-asset-base.js +14 -0
- package/dist/editor/relative-time.d.ts +4 -4
- package/dist/editor/relative-time.js +16 -8
- package/dist/editor/schema-editor/CollectionEditor.d.ts +20 -3
- package/dist/editor/schema-editor/CollectionEditor.js +361 -62
- package/dist/editor/schema-editor/EntryTypeEditor.d.ts +5 -1
- package/dist/editor/schema-editor/EntryTypeEditor.js +11 -6
- package/dist/editor/schema-editor/collection-draft.d.ts +22 -0
- package/dist/editor/schema-editor/collection-draft.js +40 -0
- package/dist/editor/schema-editor/index.d.ts +1 -1
- package/dist/editor/schema-label.d.ts +10 -0
- package/dist/editor/schema-label.js +17 -0
- package/dist/editor/settings-conflict.d.ts +5 -0
- package/dist/editor/settings-conflict.js +8 -0
- package/dist/editor/shell/ActivityRail.d.ts +37 -0
- package/dist/editor/shell/ActivityRail.js +91 -0
- package/dist/editor/shell/EditorShell.d.ts +17 -0
- package/dist/editor/shell/EditorShell.js +31 -0
- package/dist/editor/shell/PaneLayout.d.ts +28 -0
- package/dist/editor/shell/PaneLayout.js +116 -0
- package/dist/editor/shell/SidePanelHost.d.ts +35 -0
- package/dist/editor/shell/SidePanelHost.js +184 -0
- package/dist/editor/shell/TopBar.d.ts +12 -0
- package/dist/editor/shell/TopBar.js +14 -0
- package/dist/editor/shell/host-chrome.d.ts +16 -0
- package/dist/editor/shell/host-chrome.js +69 -0
- package/dist/editor/slug.d.ts +16 -0
- package/dist/editor/slug.js +43 -0
- package/dist/editor/theme.d.ts +2 -1
- package/dist/editor/theme.js +314 -13
- package/dist/editor/unanswered.d.ts +17 -0
- package/dist/editor/unanswered.js +26 -0
- package/dist/editor/unavailable-entry-type.d.ts +20 -0
- package/dist/editor/unavailable-entry-type.js +28 -0
- package/dist/editor/undo-removal.d.ts +57 -0
- package/dist/editor/undo-removal.js +163 -0
- package/dist/editor/user-display.d.ts +4 -0
- package/dist/editor/user-display.js +5 -0
- package/dist/editor/utils/confirm-modal.d.ts +8 -0
- package/dist/editor/utils/confirm-modal.js +12 -0
- package/dist/editor/utils/editor-errors.d.ts +19 -0
- package/dist/editor/utils/editor-errors.js +32 -0
- package/dist/editor/utils/focus-return.d.ts +56 -0
- package/dist/editor/utils/focus-return.js +106 -0
- package/dist/entry-link-resolver.d.ts +11 -1
- package/dist/entry-link-resolver.js +65 -5
- package/dist/entry-schema-registry.d.ts +3 -3
- package/dist/entry-schema-registry.js +7 -4
- package/dist/entry-schema.d.ts +49 -3
- package/dist/entry-schema.js +18 -0
- package/dist/git-manager.d.ts +172 -10
- package/dist/git-manager.js +449 -107
- package/dist/github-service.d.ts +39 -2
- package/dist/github-service.js +101 -7
- package/dist/http/handler.js +74 -9
- package/dist/http/types.d.ts +2 -0
- package/dist/http/worker-not-ready.d.ts +9 -4
- package/dist/http/worker-not-ready.js +59 -6
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/item-keys.d.ts +46 -0
- package/dist/item-keys.js +235 -0
- package/dist/operating-mode/editor-mode-check.d.ts +15 -0
- package/dist/operating-mode/editor-mode-check.js +24 -0
- package/dist/operating-mode/mode-env.d.ts +13 -14
- package/dist/operating-mode/mode-env.js +13 -14
- package/dist/operating-mode/types.d.ts +2 -2
- package/dist/paths/branch.d.ts +0 -1
- package/dist/paths/branch.js +1 -8
- package/dist/paths/index.d.ts +1 -1
- package/dist/paths/index.js +1 -1
- package/dist/paths/validation.js +0 -2
- package/dist/preview.d.ts +8 -0
- package/dist/preview.js +7 -0
- package/dist/reference-resolver.js +1 -0
- package/dist/resolve-canopy-user.d.ts +1 -1
- package/dist/resolve-canopy-user.js +1 -1
- package/dist/schema/meta-loader.d.ts +18 -8
- package/dist/schema/meta-loader.js +42 -9
- package/dist/schema/registry-fingerprint.d.ts +14 -0
- package/dist/schema/registry-fingerprint.js +63 -0
- package/dist/schema/resolver.d.ts +7 -4
- package/dist/schema/resolver.js +7 -6
- package/dist/schema/schema-store.d.ts +7 -10
- package/dist/schema/schema-store.js +13 -11
- package/dist/schema/schema-unavailable-error.d.ts +11 -0
- package/dist/schema/schema-unavailable-error.js +15 -0
- package/dist/schema/types.d.ts +22 -0
- package/dist/schema-registry-record.d.ts +35 -0
- package/dist/schema-registry-record.js +134 -0
- package/dist/server.d.ts +79 -9
- package/dist/server.js +62 -8
- package/dist/services.d.ts +33 -5
- package/dist/services.js +155 -37
- package/dist/settings-workspace.d.ts +23 -3
- package/dist/settings-workspace.js +159 -45
- package/dist/stamp-on-publish.d.ts +36 -0
- package/dist/stamp-on-publish.js +71 -0
- package/dist/static/index.d.ts +58 -5
- package/dist/static/index.js +133 -3
- package/dist/static/seo.d.ts +5 -5
- package/dist/static/seo.js +5 -5
- package/dist/submission-attribution.d.ts +8 -0
- package/dist/submission-attribution.js +22 -0
- package/dist/system-users.d.ts +12 -0
- package/dist/system-users.js +12 -0
- package/dist/task-queue/cms-task-queue.d.ts +1 -1
- package/dist/task-queue/cms-task-queue.js +1 -1
- package/dist/task-queue/index.d.ts +1 -1
- package/dist/task-queue/index.js +1 -1
- package/dist/task-queue/task-queue.d.ts +6 -0
- package/dist/task-queue/task-queue.js +29 -0
- package/dist/task-queue/worker-status.d.ts +35 -9
- package/dist/task-queue/worker-status.js +84 -17
- package/dist/types.d.ts +90 -9
- package/dist/url-collision.js +3 -3
- package/dist/url-exclusivity-fixtures.d.ts +8 -1
- package/dist/url-exclusivity-fixtures.js +11 -1
- package/dist/user.js +2 -1
- package/dist/utils/atomic-write.js +2 -1
- package/dist/utils/base-branch.d.ts +13 -0
- package/dist/utils/base-branch.js +23 -0
- package/dist/utils/content-serialize.d.ts +15 -7
- package/dist/utils/content-serialize.js +353 -51
- package/dist/utils/content-write-lock.d.ts +6 -1
- package/dist/utils/content-write-lock.js +21 -6
- package/dist/utils/debug.d.ts +1 -1
- package/dist/utils/debug.js +5 -3
- package/dist/utils/error.js +13 -2
- package/dist/utils/escape-text.d.ts +7 -0
- package/dist/utils/escape-text.js +27 -0
- package/dist/utils/fs.d.ts +12 -4
- package/dist/utils/fs.js +18 -5
- package/dist/utils/git.d.ts +21 -1
- package/dist/utils/git.js +63 -8
- package/dist/utils/json-source-splice.d.ts +14 -0
- package/dist/utils/json-source-splice.js +120 -0
- package/dist/utils/markdown-body-splice.d.ts +39 -0
- package/dist/utils/markdown-body-splice.js +421 -0
- package/dist/utils/occ-json-write.js +18 -12
- package/dist/utils/provision-log.d.ts +25 -0
- package/dist/utils/provision-log.js +49 -0
- package/dist/utils/provisioning-lock.d.ts +32 -1
- package/dist/utils/provisioning-lock.js +78 -32
- package/dist/utils/sanitize-href.d.ts +9 -23
- package/dist/utils/sanitize-href.js +14 -28
- package/dist/utils/title-field.d.ts +10 -0
- package/dist/utils/title-field.js +47 -0
- package/dist/utils/yaml-source-splice.d.ts +58 -0
- package/dist/utils/yaml-source-splice.js +515 -0
- package/dist/validation/block-structural-keys.d.ts +1 -1
- package/dist/validation/block-structural-keys.js +1 -1
- package/dist/validation/deletion-checker.d.ts +21 -29
- package/dist/validation/deletion-checker.js +41 -117
- package/dist/validation/entry-validator.d.ts +6 -10
- package/dist/validation/entry-validator.js +19 -26
- package/dist/validation/field-traversal.d.ts +22 -0
- package/dist/validation/field-traversal.js +39 -8
- package/dist/validation/markdown-safety.d.ts +68 -0
- package/dist/validation/markdown-safety.js +596 -0
- package/dist/validation/mdx-allowlist.d.ts +55 -0
- package/dist/validation/mdx-allowlist.js +183 -0
- package/dist/validation/reference-validator.d.ts +2 -0
- package/dist/validation/reference-validator.js +3 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/branch-policy.d.ts +124 -0
- package/dist/worker/branch-policy.js +243 -0
- package/dist/worker/bundle-header.d.ts +117 -0
- package/dist/worker/bundle-header.js +253 -0
- package/dist/worker/cms-worker.d.ts +236 -142
- package/dist/worker/cms-worker.js +772 -285
- package/dist/worker/gateway-egress.d.ts +43 -0
- package/dist/worker/gateway-egress.js +209 -0
- package/dist/worker/gateway-instance.d.ts +42 -0
- package/dist/worker/gateway-instance.js +129 -0
- package/dist/worker/git-process.d.ts +81 -0
- package/dist/worker/git-process.js +234 -0
- package/dist/worker/git-sync.d.ts +34 -17
- package/dist/worker/git-sync.js +394 -131
- package/dist/worker/github-auth.d.ts +9 -9
- package/dist/worker/github-auth.js +13 -15
- package/dist/worker/github-gateway-http.d.ts +62 -0
- package/dist/worker/github-gateway-http.js +977 -0
- package/dist/worker/github-gateway-wire.d.ts +590 -0
- package/dist/worker/github-gateway-wire.js +488 -0
- package/dist/worker/github-gateway.d.ts +238 -0
- package/dist/worker/github-gateway.js +726 -0
- package/dist/worker/github-mirror-upkeep.d.ts +75 -0
- package/dist/worker/github-mirror-upkeep.js +266 -0
- package/dist/worker/github-mirror.d.ts +364 -0
- package/dist/worker/github-mirror.js +1030 -0
- package/dist/worker/github-transfer.d.ts +56 -0
- package/dist/worker/github-transfer.js +444 -0
- package/dist/worker/history-rewrite.d.ts +4 -5
- package/dist/worker/history-rewrite.js +12 -17
- package/dist/worker/provisioned-workspace.d.ts +5 -5
- package/dist/worker/provisioned-workspace.js +5 -5
- package/dist/worker/rebase.d.ts +1 -1
- package/dist/worker/rebase.js +58 -54
- package/dist/worker/remote-git-maintenance.d.ts +3 -0
- package/dist/worker/remote-git-maintenance.js +12 -0
- package/dist/worker/schema-gate.d.ts +49 -0
- package/dist/worker/schema-gate.js +226 -0
- package/dist/worker/self-checks.d.ts +107 -0
- package/dist/worker/self-checks.js +358 -0
- package/dist/worker/shared-repo-git.d.ts +96 -0
- package/dist/worker/shared-repo-git.js +459 -0
- package/dist/worker/sparse-cone.d.ts +20 -0
- package/dist/worker/sparse-cone.js +136 -0
- package/dist/worker/stale-mirror.d.ts +82 -0
- package/dist/worker/stale-mirror.js +130 -0
- package/dist/worker/task-runner.d.ts +5 -3
- package/dist/worker/task-runner.js +287 -186
- package/dist/worker/worker-context.d.ts +36 -37
- package/package.json +35 -7
- package/dist/editor/EditorPanes.d.ts +0 -10
- package/dist/editor/EditorPanes.js +0 -98
- package/dist/editor/components/EditorFooter.d.ts +0 -11
- package/dist/editor/components/EditorFooter.js +0 -9
- package/dist/editor/components/EditorSidebar.d.ts +0 -67
- package/dist/editor/components/EditorSidebar.js +0 -22
package/dist/content-store.js
CHANGED
|
@@ -17,21 +17,27 @@ import matter from 'gray-matter';
|
|
|
17
17
|
import { parse as yamlParse } from 'yaml';
|
|
18
18
|
import { atomicWriteFile } from './utils/atomic-write.js';
|
|
19
19
|
import { serializeFrontmatter, serializeYaml } from './utils/content-serialize.js';
|
|
20
|
+
import { serializeJson } from './utils/json-source-splice.js';
|
|
20
21
|
import { withLock } from './utils/async-mutex.js';
|
|
21
|
-
import { ContentWriteLockBusyError, DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS, withContentWriteLock, } from './utils/content-write-lock.js';
|
|
22
|
+
import { assertContentWriteLockHeld, ContentWriteLockBusyError, DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS, withContentWriteLock, } from './utils/content-write-lock.js';
|
|
22
23
|
import { findBodyFieldName } from './utils/body-field.js';
|
|
23
|
-
import {
|
|
24
|
+
import { withItemKeys } from './item-keys.js';
|
|
25
|
+
import { buildMissingReference, buildResolvedReference, buildRestrictedReference, } from './entry-schema.js';
|
|
26
|
+
import { warnDanglingReference } from './dangling-reference-log.js';
|
|
27
|
+
import { canopyLogError } from './utils/logger.js';
|
|
28
|
+
import { SchemaUnavailableError } from './schema/schema-unavailable-error.js';
|
|
24
29
|
import { resolveEntryTitle } from './utils/title-field.js';
|
|
25
30
|
import { computeEntryUrl } from './utils/entry-url.js';
|
|
26
31
|
import { findUrlPathClaimant } from './url-collision.js';
|
|
27
32
|
import { ContentIdIndex, extractIdFromFilename, extractSlugFromFilename, extractEntryTypeFromFilename, resolveCollectionPath, } from './content-id-index.js';
|
|
28
33
|
import { registerContentIndexForInvalidation } from './content-index-registry.js';
|
|
34
|
+
import { isPlainRecord } from './validation/field-traversal.js';
|
|
29
35
|
import { bumpContentIndexGeneration, readContentIndexGeneration } from './content-index-generation.js';
|
|
30
36
|
import { generateId } from './id.js';
|
|
31
37
|
import { isNodeError } from './utils/error.js';
|
|
32
|
-
import { filePathExists,
|
|
38
|
+
import { filePathExists, readFileWithMtimeIfExists } from './utils/fs.js';
|
|
33
39
|
import { asRecord, getFormatExtension } from './utils/format.js';
|
|
34
|
-
import { entryLogicalPath, normalizeFilesystemPath, parseSlug, } from './paths/index.js';
|
|
40
|
+
import { entryLogicalPath, normalizeFilesystemPath, parseSlug, validateAndNormalizePath, } from './paths/index.js';
|
|
35
41
|
/**
|
|
36
42
|
* Acquire `keys` in canonical (sorted) order before running `fn`, ruling out AB-BA
|
|
37
43
|
* deadlocks between callers with overlapping key sets. Only renameEntry() needs two
|
|
@@ -60,6 +66,22 @@ export class ContentConflictError extends Error {
|
|
|
60
66
|
this.name = 'ContentConflictError';
|
|
61
67
|
}
|
|
62
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Thrown when a write carrying a numeric `expectedVersion` finds no file: the entry was deleted
|
|
71
|
+
* or moved since that version was read. A missing file is a change since the read, so the write
|
|
72
|
+
* is refused rather than re-creating the entry; re-creating it is a create (`expectedVersion:
|
|
73
|
+
* null`) the caller makes deliberately.
|
|
74
|
+
*
|
|
75
|
+
* A `ContentConflictError` subclass so every 409 mapping keeps working; the distinct type lets
|
|
76
|
+
* the API tell the editor the entry is gone, where "reload to see the latest version" has
|
|
77
|
+
* nothing to reload.
|
|
78
|
+
*/
|
|
79
|
+
export class EntryGoneError extends ContentConflictError {
|
|
80
|
+
constructor() {
|
|
81
|
+
super('This entry was deleted or moved since it was read');
|
|
82
|
+
this.name = 'EntryGoneError';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
63
85
|
/**
|
|
64
86
|
* [SYNC-C1] Thrown when a mutation could not take the branch's cross-host
|
|
65
87
|
* content-write lock within its bounded wait (utils/content-write-lock.ts) --
|
|
@@ -71,8 +93,11 @@ export class ContentConflictError extends Error {
|
|
|
71
93
|
* instead of the misleading generic "modified by another editor".
|
|
72
94
|
*/
|
|
73
95
|
export class BranchSyncingError extends ContentConflictError {
|
|
74
|
-
constructor(message
|
|
96
|
+
constructor(message,
|
|
97
|
+
/** As on `ContentWriteLockBusyError`: `'unknown'` means the write ran and may have landed. */
|
|
98
|
+
outcome = 'not-run') {
|
|
75
99
|
super(message);
|
|
100
|
+
this.outcome = outcome;
|
|
76
101
|
this.name = 'BranchSyncingError';
|
|
77
102
|
}
|
|
78
103
|
}
|
|
@@ -94,15 +119,13 @@ export class DuplicateContentIdError extends ContentConflictError {
|
|
|
94
119
|
constructor(contentId, paths) {
|
|
95
120
|
const sorted = Array.from(new Set(paths)).sort();
|
|
96
121
|
super(
|
|
97
|
-
// Names
|
|
98
|
-
//
|
|
99
|
-
// duplicate list (SystemHealthPanel) is the diagnosis half only. The repair
|
|
100
|
-
// UI is tracked in .claude/future-tasks/duplicate-content-id-repair-ui.md.
|
|
122
|
+
// Names where an administrator fixes it, not the endpoint: the editor
|
|
123
|
+
// reading this cannot run an admin action.
|
|
101
124
|
`Content ID ${contentId} is on more than one file (${sorted
|
|
102
125
|
.map((p) => `"${p}"`)
|
|
103
126
|
.join(' and ')}), so this save was refused rather than risk overwriting or ` +
|
|
104
|
-
`deleting the wrong one. An administrator
|
|
105
|
-
`
|
|
127
|
+
`deleting the wrong one. An administrator can fix this in System health, on the ` +
|
|
128
|
+
`Branches tab; until then this entry cannot be saved.`);
|
|
106
129
|
this.name = 'DuplicateContentIdError';
|
|
107
130
|
this.contentId = contentId;
|
|
108
131
|
this.paths = sorted;
|
|
@@ -151,6 +174,54 @@ function validateSlug(slug) {
|
|
|
151
174
|
throw new ContentStoreError('Slugs cannot contain backslashes. Use nested collections instead.', 'VALIDATION');
|
|
152
175
|
}
|
|
153
176
|
}
|
|
177
|
+
const isEmptyFieldValue = (value) => value === undefined || value === null || value === '';
|
|
178
|
+
/**
|
|
179
|
+
* A parsed entry document's top-level record. Anything but a plain object throws, so a fill never
|
|
180
|
+
* replaces an array or scalar root; YAML reads an empty document as null.
|
|
181
|
+
*/
|
|
182
|
+
function entryRecord(parsed, emptyIsRecord) {
|
|
183
|
+
if (isPlainRecord(parsed))
|
|
184
|
+
return parsed;
|
|
185
|
+
if (emptyIsRecord && (parsed === null || parsed === undefined))
|
|
186
|
+
return {};
|
|
187
|
+
throw new Error('the file does not hold an object at its top level');
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* `raw` with top-level `field` set to the string `value`, through the same source-preserving
|
|
191
|
+
* serialisers `write()` uses, or undefined when `raw` already holds a non-empty value there.
|
|
192
|
+
* Throws when the result would not read back as that string.
|
|
193
|
+
*/
|
|
194
|
+
function fillEmptyTopLevelField(raw, format, field, value) {
|
|
195
|
+
if (format === 'json' || format === 'yaml') {
|
|
196
|
+
const parse = (text) => format === 'json' ? entryRecord(JSON.parse(text), false) : entryRecord(yamlParse(text), true);
|
|
197
|
+
const data = parse(raw);
|
|
198
|
+
if (!isEmptyFieldValue(data[field]))
|
|
199
|
+
return undefined;
|
|
200
|
+
const next = { ...data, [field]: value };
|
|
201
|
+
const content = format === 'json' ? serializeJson(next, raw) : serializeYaml(next, raw);
|
|
202
|
+
if (parse(content)[field] !== value)
|
|
203
|
+
throw new Error(`"${field}" does not read back as written`);
|
|
204
|
+
return content;
|
|
205
|
+
}
|
|
206
|
+
const frontmatter = (text) => entryRecord(matter(text, {}).data, true);
|
|
207
|
+
const fill = (from, fieldValue) => {
|
|
208
|
+
const parsed = matter(from, {});
|
|
209
|
+
const data = { ...entryRecord(parsed.data, true), [field]: fieldValue };
|
|
210
|
+
return serializeFrontmatter(parsed.content, data, from, format);
|
|
211
|
+
};
|
|
212
|
+
const readsBack = (content) => frontmatter(content)[field] === value;
|
|
213
|
+
if (!isEmptyFieldValue(frontmatter(raw)[field]))
|
|
214
|
+
return undefined;
|
|
215
|
+
const content = fill(raw, value);
|
|
216
|
+
if (readsBack(content))
|
|
217
|
+
return content;
|
|
218
|
+
// A key the serialiser inserts is written plain, and gray-matter reads a plain timestamp as a
|
|
219
|
+
// Date. Inserted as '' it is written quoted, and replacing it keeps that quoting.
|
|
220
|
+
const quoted = fill(fill(raw, ''), value);
|
|
221
|
+
if (readsBack(quoted))
|
|
222
|
+
return quoted;
|
|
223
|
+
throw new Error(`"${field}" does not read back as written`);
|
|
224
|
+
}
|
|
154
225
|
const DEFAULT_INDEX_FRESHNESS_INTERVAL_MS = 1000;
|
|
155
226
|
/**
|
|
156
227
|
* At most one suspicious-lookup forced rescan per store per this window, so
|
|
@@ -159,6 +230,8 @@ const DEFAULT_INDEX_FRESHNESS_INTERVAL_MS = 1000;
|
|
|
159
230
|
const FORCED_REFRESH_MIN_INTERVAL_MS = 5000;
|
|
160
231
|
/** Internal sentinel: a lookup result that suggests this store's index is stale. */
|
|
161
232
|
const STALE_LOOKUP = Symbol('stale-index-lookup');
|
|
233
|
+
/** A resolver's own `MissingReference`; a full target never carries `unavailable`. */
|
|
234
|
+
const isMissingReference = (value) => value.unavailable === true && value.reason === 'missing';
|
|
162
235
|
/** Create a cache for one batch of reference resolution. See {@link ReferenceResolveCache}. */
|
|
163
236
|
export const createReferenceResolveCache = () => new Map();
|
|
164
237
|
export class ContentStore {
|
|
@@ -208,7 +281,7 @@ export class ContentStore {
|
|
|
208
281
|
// Translate at the boundary, after the bounded wait -- never inside the
|
|
209
282
|
// acquire loop, which would disable its ELOCKED retry predicate.
|
|
210
283
|
if (err instanceof ContentWriteLockBusyError)
|
|
211
|
-
throw new BranchSyncingError(err.message);
|
|
284
|
+
throw new BranchSyncingError(err.message, err.outcome);
|
|
212
285
|
throw err;
|
|
213
286
|
}
|
|
214
287
|
}
|
|
@@ -604,6 +677,8 @@ export class ContentStore {
|
|
|
604
677
|
absolutePath: resolved,
|
|
605
678
|
relativePath: path.relative(this.root, resolved),
|
|
606
679
|
logicalPath: entryLogicalPath(schemaItem.logicalPath, safeSlug),
|
|
680
|
+
collectionPath: schemaItem.logicalPath,
|
|
681
|
+
slug: safeSlug,
|
|
607
682
|
id,
|
|
608
683
|
entryTypeName: finalEntryTypeName,
|
|
609
684
|
existed,
|
|
@@ -640,7 +715,11 @@ export class ContentStore {
|
|
|
640
715
|
*/
|
|
641
716
|
async read(collectionPath, slug = '', options = {}) {
|
|
642
717
|
const schemaItem = this.assertSchemaItem(collectionPath);
|
|
643
|
-
const { absolutePath, relativePath, entryTypeName: resolvedEntryTypeName, } = await this.buildPaths(schemaItem, slug);
|
|
718
|
+
const { absolutePath, relativePath, existed, entryTypeName: resolvedEntryTypeName, } = await this.buildPaths(schemaItem, slug);
|
|
719
|
+
// A missing entry stays a not-found below, whatever its collection's default type.
|
|
720
|
+
if (existed && !options.allowUnavailableEntryType) {
|
|
721
|
+
this.assertEntryTypeAvailable(schemaItem, resolvedEntryTypeName);
|
|
722
|
+
}
|
|
644
723
|
// stat BEFORE readFile: a conservative version token can only produce false-positive
|
|
645
724
|
// conflicts. A write landing between stat and readFile gives the client newer content with
|
|
646
725
|
// an older token, so their next save 409s (safe); stat-after or a parallel stat+read risks
|
|
@@ -707,8 +786,14 @@ export class ContentStore {
|
|
|
707
786
|
absolutePath,
|
|
708
787
|
};
|
|
709
788
|
}
|
|
789
|
+
// Keys an item saved without one by its content, before references resolve into it, as the
|
|
790
|
+
// listing and the write path do (`item-keys.ts`).
|
|
791
|
+
doc.data = withItemKeys(fields, doc.data, 'content');
|
|
710
792
|
if (options.resolveReferences !== false) {
|
|
711
|
-
doc.data = await this.resolveReferencesInData(doc.data, fields, undefined, options.referenceAccess
|
|
793
|
+
doc.data = await this.resolveReferencesInData(doc.data, fields, undefined, options.referenceAccess, {
|
|
794
|
+
entry: slug ? entryLogicalPath(schemaItem.logicalPath, slug) : schemaItem.logicalPath,
|
|
795
|
+
prefix: '',
|
|
796
|
+
});
|
|
712
797
|
}
|
|
713
798
|
doc.version = stat.mtimeMs;
|
|
714
799
|
return doc;
|
|
@@ -747,6 +832,8 @@ export class ContentStore {
|
|
|
747
832
|
// concurrent renameEntry() completes, so it must be re-resolved as ground truth once the
|
|
748
833
|
// lock is held (see entryLockKey()). The format check above still runs unlocked.
|
|
749
834
|
const prePass = await this.buildPaths(schemaItem, slug, { entryTypeName, existingId });
|
|
835
|
+
// The type the entry is written as: its on-disk one when it exists, else the requested/default.
|
|
836
|
+
this.assertEntryTypeAvailable(schemaItem, prePass.entryTypeName);
|
|
750
837
|
let lockKey = this.entryLockKey(schemaItem, slug, prePass);
|
|
751
838
|
// Reclassification loop: the world can change between the pre-pass and lock acquisition (a
|
|
752
839
|
// concurrent renameEntry() freeing this slug flips existing->new; a concurrent create
|
|
@@ -814,6 +901,33 @@ export class ContentStore {
|
|
|
814
901
|
}
|
|
815
902
|
}
|
|
816
903
|
}
|
|
904
|
+
// The target as it is now, from one open: its mtime is the OCC version checked below,
|
|
905
|
+
// and its bytes are what the comment-preserving serialisation further down re-applies
|
|
906
|
+
// the payload onto, so the bytes kept are the bytes OCC validated. One open rather
|
|
907
|
+
// than a path `stat` plus a later read because opening revalidates against the NFS
|
|
908
|
+
// server, where a `stat` can answer from this host's cache for a file another host has
|
|
909
|
+
// since replaced or deleted (utils/fs.ts, docs/concurrency.md). Inside withLock(lockKey) and
|
|
910
|
+
// withContentWriteExclusion ([SYNC-C1]), so no writer or rebase moves it after this.
|
|
911
|
+
const current = await readFileWithMtimeIfExists(absolutePath);
|
|
912
|
+
// OCC: undefined means no opinion (skip entirely, a blind write). A number means
|
|
913
|
+
// "must match this mtime", and a missing file fails it too: the entry was deleted or
|
|
914
|
+
// moved since the read, and writing would re-create it (`EntryGoneError`). `null`
|
|
915
|
+
// means "must NOT exist yet" — the create-intent guard, without which a create
|
|
916
|
+
// request against a slug that already has content falls through to an ordinary
|
|
917
|
+
// blind overwrite. This is the authoritative check, so it holds even if a caller's
|
|
918
|
+
// own pre-write existence check (e.g. the API layer's `documentExists`) went stale
|
|
919
|
+
// under concurrency. It runs before the create-only guards below and before `mkdir`,
|
|
920
|
+
// so a refused update neither re-creates a deleted collection directory nor reports
|
|
921
|
+
// a create-only failure for a write that was never a create.
|
|
922
|
+
if (input.expectedVersion === null && current) {
|
|
923
|
+
throw new ContentConflictError('An entry with this slug already exists');
|
|
924
|
+
}
|
|
925
|
+
if (typeof input.expectedVersion === 'number') {
|
|
926
|
+
if (!current)
|
|
927
|
+
throw new EntryGoneError();
|
|
928
|
+
if (current.mtimeMs !== input.expectedVersion)
|
|
929
|
+
throw new ContentConflictError();
|
|
930
|
+
}
|
|
817
931
|
// [URL] Contested-URL guard, create only: an ordinary save cannot contest a URL it
|
|
818
932
|
// already holds, and blocking it would trap the author in an entry they can no
|
|
819
933
|
// longer edit.
|
|
@@ -831,7 +945,7 @@ export class ContentStore {
|
|
|
831
945
|
// Serialization comes from `withContentWriteExclusion` (the branch-wide, cross-host
|
|
832
946
|
// lock wrapping this whole loop), NOT the per-entry lock: the two halves of a
|
|
833
947
|
// contested pair live in DIFFERENT collections and take different entry-lock keys.
|
|
834
|
-
if (!
|
|
948
|
+
if (!current) {
|
|
835
949
|
const chosenSlug = extractSlugFromFilename(path.basename(absolutePath));
|
|
836
950
|
// [SLUG] Routability guard, create only. `validateSlug` (in buildPaths) is the
|
|
837
951
|
// path-traversal check and rejects only `/` and `\`, and has to stay that weak
|
|
@@ -857,36 +971,6 @@ export class ContentStore {
|
|
|
857
971
|
await this.assertUrlPathAvailable(path.dirname(absolutePath), chosenSlug);
|
|
858
972
|
}
|
|
859
973
|
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
|
|
860
|
-
// OCC: undefined means no opinion (skip entirely, a blind write). A number means
|
|
861
|
-
// "must match this mtime" (stale-write rejection). `null` means "must NOT exist
|
|
862
|
-
// yet" — the create-intent guard, without which a create request against a slug
|
|
863
|
-
// that already has content falls through to an ordinary blind overwrite. This is
|
|
864
|
-
// the authoritative check: it runs inside the per-entry lock against a fresh stat,
|
|
865
|
-
// so it holds even if a caller's own pre-write existence check (e.g. the API
|
|
866
|
-
// layer's `documentExists`) went stale under concurrency.
|
|
867
|
-
if (input.expectedVersion !== undefined) {
|
|
868
|
-
try {
|
|
869
|
-
const existing = await fs.stat(absolutePath);
|
|
870
|
-
if (input.expectedVersion === null) {
|
|
871
|
-
throw new ContentConflictError('An entry with this slug already exists');
|
|
872
|
-
}
|
|
873
|
-
if (existing.mtimeMs !== input.expectedVersion) {
|
|
874
|
-
throw new ContentConflictError();
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
catch (err) {
|
|
878
|
-
if (err instanceof ContentConflictError)
|
|
879
|
-
throw err;
|
|
880
|
-
if (isNodeError(err) && err.code === 'ENOENT') {
|
|
881
|
-
// No file yet: for a numeric expectedVersion this is the "first write,
|
|
882
|
-
// skip the version check" back-compat behavior; for null it is create-only
|
|
883
|
-
// correctly seeing no collision. Either way, proceed with the write.
|
|
884
|
-
}
|
|
885
|
-
else {
|
|
886
|
-
throw err;
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
974
|
// Existence guard (cross-process): the caller asserts this entry exists
|
|
891
975
|
// (existingId), so a missing file at the path about to be written means this store's
|
|
892
976
|
// index may be stale -- another process may have renamed the entry. Recreating a
|
|
@@ -895,8 +979,9 @@ export class ContentStore {
|
|
|
895
979
|
// this host: if the ID's actual on-disk location differs from what the index
|
|
896
980
|
// believes, fail with a conflict so the caller reloads fresh state. (An intentional
|
|
897
981
|
// slug-change save passes: index and directory agree on the entry's current,
|
|
898
|
-
// old-slug path.
|
|
899
|
-
//
|
|
982
|
+
// old-slug path. An external delete passes here, since the ID is nowhere on disk; a
|
|
983
|
+
// write carrying a version has already been refused by the OCC check above, so only
|
|
984
|
+
// a blind write re-creates it.)
|
|
900
985
|
//
|
|
901
986
|
// indexedRelPath reads the LIVE index synchronously — NOT idIndex(), which would
|
|
902
987
|
// run a full rescan while holding the entry lock if invalidateIndex() fired since
|
|
@@ -922,32 +1007,31 @@ export class ContentStore {
|
|
|
922
1007
|
}
|
|
923
1008
|
// Comment preservation: re-serialise onto the file's OWN parsed document rather
|
|
924
1009
|
// than a fresh one, so nodes the payload did not change -- and their attached
|
|
925
|
-
// comments -- survive the write. JSON has no
|
|
926
|
-
// utils/content-serialize.ts
|
|
1010
|
+
// comments -- survive the write. JSON has no comments; it reads the file to keep
|
|
1011
|
+
// untouched lines' formatting. See utils/content-serialize.ts and
|
|
1012
|
+
// utils/json-source-splice.ts.
|
|
927
1013
|
//
|
|
928
|
-
// This makes the write a genuine read-modify-write of the content file
|
|
929
|
-
//
|
|
930
|
-
//
|
|
931
|
-
// the bytes read here are the bytes that OCC check validated, and no rebase can be
|
|
932
|
-
// running against this branch. Do not hoist it out of the critical section.
|
|
1014
|
+
// This makes the write a genuine read-modify-write of the content file. The bytes are
|
|
1015
|
+
// `current`'s, read in the same open the OCC check took its version from, inside
|
|
1016
|
+
// both locks; do not replace them with a read outside the critical section.
|
|
933
1017
|
//
|
|
934
|
-
// INVARIANT, and the one case that does NOT preserve comments:
|
|
1018
|
+
// INVARIANT, and the one case that does NOT preserve comments: `current` is the path
|
|
935
1019
|
// being WRITTEN. A relocating write -- the ID resolves to a different relativePath,
|
|
936
1020
|
// so the block below unlinks `staleOldAbsPath` -- finds nothing at the new path and
|
|
937
1021
|
// falls back to a plain stringify, losing the old file's comments. Unreachable
|
|
938
1022
|
// today: the editor renames through renameEntry(), which link()s the bytes across
|
|
939
1023
|
// intact, and no caller passes `existingId`. If a relocating write becomes
|
|
940
|
-
// reachable, read the ID's current path
|
|
941
|
-
const existingRaw =
|
|
1024
|
+
// reachable, read the ID's current path instead of `absolutePath`.
|
|
1025
|
+
const existingRaw = current?.content;
|
|
942
1026
|
let content;
|
|
943
1027
|
if (input.format === 'json') {
|
|
944
|
-
content =
|
|
1028
|
+
content = serializeJson(input.data ?? {}, existingRaw);
|
|
945
1029
|
}
|
|
946
1030
|
else if (input.format === 'yaml') {
|
|
947
1031
|
content = serializeYaml(input.data ?? {}, existingRaw);
|
|
948
1032
|
}
|
|
949
1033
|
else {
|
|
950
|
-
content = serializeFrontmatter(input.body, input.data ?? {}, existingRaw);
|
|
1034
|
+
content = serializeFrontmatter(input.body, input.data ?? {}, existingRaw, input.format);
|
|
951
1035
|
}
|
|
952
1036
|
await atomicWriteFile(absolutePath, content);
|
|
953
1037
|
// Update the ID index after a successful write. Look up and mutate the LIVE index
|
|
@@ -1015,6 +1099,34 @@ export class ContentStore {
|
|
|
1015
1099
|
throw new ContentConflictError();
|
|
1016
1100
|
});
|
|
1017
1101
|
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Set top-level `field` of the entry file at `physicalPath` to the string `value` when the
|
|
1104
|
+
* file holds it empty (absent, null or ''), keeping every other byte. Resolves to the file's
|
|
1105
|
+
* version (the mtime `read()` reports) before and after the write, or undefined when it left
|
|
1106
|
+
* the file alone.
|
|
1107
|
+
*
|
|
1108
|
+
* PRECONDITION: the caller already holds this branch's content-write lock, which makes the
|
|
1109
|
+
* read and the write one critical section; a call outside it throws. This takes no lock
|
|
1110
|
+
* itself, since a nested acquisition fails busy and is why nothing holding that lock may call
|
|
1111
|
+
* `write()` (docs/concurrency.md, "Lock acquisition order").
|
|
1112
|
+
*/
|
|
1113
|
+
async fillEmptyFieldWhileHoldingWriteLock(physicalPath, format, field, value) {
|
|
1114
|
+
assertContentWriteLockHeld(this.root);
|
|
1115
|
+
const checked = validateAndNormalizePath(this.root, path.join(this.root, physicalPath));
|
|
1116
|
+
if (!checked.valid || !checked.normalizedPath) {
|
|
1117
|
+
throw new ContentStoreError(checked.error ?? 'Path traversal detected', 'VALIDATION');
|
|
1118
|
+
}
|
|
1119
|
+
const absolutePath = path.join(this.root, checked.normalizedPath);
|
|
1120
|
+
const current = await readFileWithMtimeIfExists(absolutePath);
|
|
1121
|
+
if (!current)
|
|
1122
|
+
throw new EntryGoneError();
|
|
1123
|
+
const content = fillEmptyTopLevelField(current.content, format, field, value);
|
|
1124
|
+
if (content === undefined)
|
|
1125
|
+
return undefined;
|
|
1126
|
+
await atomicWriteFile(absolutePath, content);
|
|
1127
|
+
await this.recordOwnMutation(this._idIndex);
|
|
1128
|
+
return { previousVersion: current.mtimeMs, version: (await fs.stat(absolutePath)).mtimeMs };
|
|
1129
|
+
}
|
|
1018
1130
|
/**
|
|
1019
1131
|
* Read an entry by its ID (UUID).
|
|
1020
1132
|
* Returns null if the ID doesn't exist or points to a collection.
|
|
@@ -1124,6 +1236,8 @@ export class ContentStore {
|
|
|
1124
1236
|
// Pre-pass: classify existing-vs-already-gone by directory scan (local ground truth), not
|
|
1125
1237
|
// this._idIndex, which can be stale in exactly the way this locking scheme guards against.
|
|
1126
1238
|
const prePass = await this.buildPaths(collection, slug);
|
|
1239
|
+
if (prePass.existed)
|
|
1240
|
+
this.assertEntryTypeAvailable(collection, prePass.entryTypeName);
|
|
1127
1241
|
if (!prePass.existed) {
|
|
1128
1242
|
// Nothing on disk for this slug -- no shared resource to lock on, and fs.unlink on the
|
|
1129
1243
|
// freshly generated (necessarily nonexistent) path throws ENOENT, as callers expect.
|
|
@@ -1206,6 +1320,7 @@ export class ContentStore {
|
|
|
1206
1320
|
if (!prePass.existed) {
|
|
1207
1321
|
throw new ContentStoreError(`Entry not found: ${currentSlug}`, 'NOT_FOUND');
|
|
1208
1322
|
}
|
|
1323
|
+
this.assertEntryTypeAvailable(collection, prePass.entryTypeName);
|
|
1209
1324
|
let sourceLockKey = this.entryLockKey(collection, currentSlug, prePass);
|
|
1210
1325
|
let sourceId = prePass.id;
|
|
1211
1326
|
// Also lock the destination create-key. Content ID is rename-invariant, so the SOURCE side
|
|
@@ -1389,40 +1504,56 @@ export class ContentStore {
|
|
|
1389
1504
|
* unmemoized behavior.
|
|
1390
1505
|
*
|
|
1391
1506
|
* `access` applies to every target exactly as in `read()`; see {@link ReferenceTargetAccess}.
|
|
1507
|
+
* `origin`, the referring entry's logical path, names it in the dangling-reference warning.
|
|
1392
1508
|
*/
|
|
1393
|
-
async resolveReferences(data, fields, cache, access) {
|
|
1394
|
-
return this.resolveReferencesInData(data, fields, cache, access);
|
|
1509
|
+
async resolveReferences(data, fields, cache, access, origin) {
|
|
1510
|
+
return this.resolveReferencesInData(data, fields, cache, access, { entry: origin, prefix: '' });
|
|
1395
1511
|
}
|
|
1396
1512
|
/**
|
|
1397
|
-
* Resolve one reference id the way a reference field resolves it, for
|
|
1398
|
-
* live-preview endpoint (api/resolve-references.ts)
|
|
1399
|
-
*
|
|
1513
|
+
* Resolve one reference id the way a reference field resolves it, for callers with ids but no
|
|
1514
|
+
* field to walk: the editor's live-preview endpoint (api/resolve-references.ts) and the AI
|
|
1515
|
+
* export (ai/references.ts). An id
|
|
1516
|
+
* naming no readable entry resolves to a `MissingReference`, as in `read()`. No body is
|
|
1517
|
+
* embedded, since there is no field to ask for one.
|
|
1400
1518
|
*/
|
|
1401
1519
|
async resolveReferenceTarget(id, access) {
|
|
1402
1520
|
return this.resolveSingleReference(id, await this.idIndex(), false, undefined, access);
|
|
1403
1521
|
}
|
|
1404
|
-
|
|
1522
|
+
/**
|
|
1523
|
+
* `trail` names where `data` sits, for the dangling-reference warning: the referring entry and
|
|
1524
|
+
* the field path so far, in `traverseFields`' format (`blocks[2].author`, `reviewers[1]`).
|
|
1525
|
+
*/
|
|
1526
|
+
async resolveReferencesInData(data, fields, cache, access, trail) {
|
|
1405
1527
|
const resolved = { ...data };
|
|
1406
1528
|
const idIndex = await this.idIndex();
|
|
1529
|
+
const at = (name) => (trail.prefix ? `${trail.prefix}.${name}` : name);
|
|
1530
|
+
const resolveAt = async (id, includeBody, fieldPath) => {
|
|
1531
|
+
const value = await this.resolveSingleReference(id, idIndex, includeBody, cache, access);
|
|
1532
|
+
if (isMissingReference(value)) {
|
|
1533
|
+
warnDanglingReference({ root: this.root, entry: trail.entry, path: fieldPath, id });
|
|
1534
|
+
}
|
|
1535
|
+
return value;
|
|
1536
|
+
};
|
|
1407
1537
|
for (const field of fields) {
|
|
1408
1538
|
// Inline groups are transparent — recurse into their children at the same data level
|
|
1409
1539
|
if (field.type === 'group') {
|
|
1410
|
-
const groupResolved = await this.resolveReferencesInData(resolved, field.fields, cache, access);
|
|
1540
|
+
const groupResolved = await this.resolveReferencesInData(resolved, field.fields, cache, access, trail);
|
|
1411
1541
|
Object.assign(resolved, groupResolved);
|
|
1412
1542
|
continue;
|
|
1413
1543
|
}
|
|
1414
1544
|
const value = data[field.name];
|
|
1545
|
+
const fieldPath = at(field.name);
|
|
1415
1546
|
if (field.type === 'reference') {
|
|
1416
1547
|
// Whether this reference EMBEDS its target (wants the target's body) or merely LINKS
|
|
1417
1548
|
// to it is a property of the field, declared once in the schema -- not of the call,
|
|
1418
1549
|
// which routinely contains both kinds at once. See ReferenceFieldConfig.includeBody.
|
|
1419
1550
|
const includeBody = field.includeBody === true;
|
|
1420
1551
|
if (typeof value === 'string' && value) {
|
|
1421
|
-
resolved[field.name] = await
|
|
1552
|
+
resolved[field.name] = await resolveAt(value, includeBody, fieldPath);
|
|
1422
1553
|
}
|
|
1423
1554
|
else if (field.list && Array.isArray(value)) {
|
|
1424
|
-
resolved[field.name] = await Promise.all(value.map((id) => typeof id === 'string'
|
|
1425
|
-
?
|
|
1555
|
+
resolved[field.name] = await Promise.all(value.map((id, index) => typeof id === 'string' && id
|
|
1556
|
+
? resolveAt(id, includeBody, `${fieldPath}[${index}]`)
|
|
1426
1557
|
: null));
|
|
1427
1558
|
}
|
|
1428
1559
|
}
|
|
@@ -1431,26 +1562,30 @@ export class ContentStore {
|
|
|
1431
1562
|
if (!objectField.fields)
|
|
1432
1563
|
continue;
|
|
1433
1564
|
if (objectField.list && Array.isArray(value)) {
|
|
1434
|
-
resolved[field.name] = await Promise.all(value.map((item) =>
|
|
1435
|
-
? this.resolveReferencesInData(item, objectField.fields, cache, access
|
|
1565
|
+
resolved[field.name] = await Promise.all(value.map((item, index) => isPlainRecord(item)
|
|
1566
|
+
? this.resolveReferencesInData(item, objectField.fields, cache, access, {
|
|
1567
|
+
entry: trail.entry,
|
|
1568
|
+
prefix: `${fieldPath}[${index}]`,
|
|
1569
|
+
})
|
|
1436
1570
|
: item));
|
|
1437
1571
|
}
|
|
1438
|
-
else if (
|
|
1439
|
-
resolved[field.name] = await this.resolveReferencesInData(value, objectField.fields, cache, access);
|
|
1572
|
+
else if (isPlainRecord(value)) {
|
|
1573
|
+
resolved[field.name] = await this.resolveReferencesInData(value, objectField.fields, cache, access, { entry: trail.entry, prefix: fieldPath });
|
|
1440
1574
|
}
|
|
1441
1575
|
}
|
|
1442
1576
|
else if (field.type === 'block' && Array.isArray(value)) {
|
|
1443
1577
|
const blockField = field;
|
|
1444
|
-
resolved[field.name] = await Promise.all(value.map(async (block) => {
|
|
1445
|
-
|
|
1446
|
-
|
|
1578
|
+
resolved[field.name] = await Promise.all(value.map(async (block, index) => {
|
|
1579
|
+
// An item or value that is not a record (null, an array, a scalar) passes through
|
|
1580
|
+
// untouched: it has no named fields, and reshaping it here would be saved to the file.
|
|
1581
|
+
if (!isPlainRecord(block) || !isPlainRecord(block.value))
|
|
1447
1582
|
return block;
|
|
1448
|
-
const template = blockField.templates.find((t) => t.name ===
|
|
1583
|
+
const template = blockField.templates.find((t) => t.name === block.template);
|
|
1449
1584
|
if (!template)
|
|
1450
1585
|
return block;
|
|
1451
1586
|
return {
|
|
1452
|
-
...
|
|
1453
|
-
value: await this.resolveReferencesInData(
|
|
1587
|
+
...block,
|
|
1588
|
+
value: await this.resolveReferencesInData(block.value, template.fields, cache, access, { entry: trail.entry, prefix: `${fieldPath}[${index}]` }),
|
|
1454
1589
|
};
|
|
1455
1590
|
}));
|
|
1456
1591
|
}
|
|
@@ -1471,7 +1606,7 @@ export class ContentStore {
|
|
|
1471
1606
|
* `data` from a process-global content-keyed cache, so `resolveSingleReferenceOnce`'s
|
|
1472
1607
|
* top-level spread severs exactly one level and NESTED frontmatter objects alias across
|
|
1473
1608
|
* occurrences, calls and requests. Pre-existing `read()` behavior, deliberately out of scope
|
|
1474
|
-
* here; see
|
|
1609
|
+
* here; see safeinsights/canopycms#708.
|
|
1475
1610
|
*
|
|
1476
1611
|
* Measured (2000 occurrences of one target, local disk, the friendliest case for NOT
|
|
1477
1612
|
* caching): cloning a snippet-sized target is ~63x cheaper than re-reading and parsing it, a
|
|
@@ -1499,10 +1634,11 @@ export class ContentStore {
|
|
|
1499
1634
|
// The cached promise always has this handler attached, so it is never an unhandled
|
|
1500
1635
|
// rejection; entry data is plain parsed JSON/YAML/frontmatter and so always cloneable (a
|
|
1501
1636
|
// `!!binary` Buffer is the sole shape not preserved exactly: it clones to a Uint8Array).
|
|
1502
|
-
return pending.then((resolved) =>
|
|
1637
|
+
return pending.then((resolved) => structuredClone(resolved));
|
|
1503
1638
|
}
|
|
1504
1639
|
/**
|
|
1505
|
-
*
|
|
1640
|
+
* The target's data with its reference metadata, a `RestrictedReference` for a denied target,
|
|
1641
|
+
* or a `MissingReference` when the id names no readable entry.
|
|
1506
1642
|
*
|
|
1507
1643
|
* A suspicious result (ID missing from the index, or an index hit whose file is gone)
|
|
1508
1644
|
* triggers one forced index refresh and a retry — self-healing for mutations by other
|
|
@@ -1523,7 +1659,32 @@ export class ContentStore {
|
|
|
1523
1659
|
// dedupes in-flight builds), so every miss in a Promise.all batch heals.
|
|
1524
1660
|
await this.refreshIndexForSuspiciousLookup();
|
|
1525
1661
|
const second = await this.resolveSingleReferenceOnce(id, await this.idIndex(), includeBody, access);
|
|
1526
|
-
return second === STALE_LOOKUP ?
|
|
1662
|
+
return second === STALE_LOOKUP ? buildMissingReference(id) : second;
|
|
1663
|
+
}
|
|
1664
|
+
/**
|
|
1665
|
+
* Throw {@link SchemaUnavailableError} when an entry exists at `slug` and is of an
|
|
1666
|
+
* `unavailable` entry type, for a handler to refuse before any other check answers first.
|
|
1667
|
+
*/
|
|
1668
|
+
async assertEntryAvailable(collectionPath, slug = '') {
|
|
1669
|
+
const schemaItem = this.assertSchemaItem(collectionPath);
|
|
1670
|
+
const { existed, entryTypeName } = await this.buildPaths(schemaItem, slug);
|
|
1671
|
+
if (existed)
|
|
1672
|
+
this.assertEntryTypeAvailable(schemaItem, entryTypeName);
|
|
1673
|
+
}
|
|
1674
|
+
/**
|
|
1675
|
+
* Throw {@link SchemaUnavailableError} when the entry type an entry is read or written as --
|
|
1676
|
+
* `entryTypeName`, else the collection's default -- is `unavailable`. A name the collection
|
|
1677
|
+
* does not declare is never charged to the default type, which it was not written as.
|
|
1678
|
+
*/
|
|
1679
|
+
assertEntryTypeAvailable(schemaItem, entryTypeName) {
|
|
1680
|
+
const entryType = schemaItem.type === 'entry-type'
|
|
1681
|
+
? schemaItem
|
|
1682
|
+
: entryTypeName
|
|
1683
|
+
? schemaItem.entries?.find((e) => e.name === entryTypeName)
|
|
1684
|
+
: getDefaultEntryType(schemaItem.entries);
|
|
1685
|
+
if (entryType?.unavailable) {
|
|
1686
|
+
throw new SchemaUnavailableError(entryType.name, entryType.unavailable);
|
|
1687
|
+
}
|
|
1527
1688
|
}
|
|
1528
1689
|
/**
|
|
1529
1690
|
* The entry type an entry file was written as, by its filename's type token, falling back to
|
|
@@ -1543,11 +1704,12 @@ export class ContentStore {
|
|
|
1543
1704
|
if (!location)
|
|
1544
1705
|
return STALE_LOOKUP;
|
|
1545
1706
|
if (location.type !== 'entry' || !location.collection || !location.slug) {
|
|
1546
|
-
return
|
|
1707
|
+
return buildMissingReference(id);
|
|
1547
1708
|
}
|
|
1548
1709
|
// Read the referenced entry WITHOUT resolving its references (prevent infinite loops)
|
|
1549
1710
|
const doc = await this.read(location.collection, location.slug, {
|
|
1550
1711
|
resolveReferences: false,
|
|
1712
|
+
allowUnavailableEntryType: true,
|
|
1551
1713
|
});
|
|
1552
1714
|
// `urlPath` is what makes a resolved reference linkable without a second listing pass to
|
|
1553
1715
|
// build a contentId -> url table. Deliberately `computeEntryUrl` (utils/entry-url.ts), the
|
|
@@ -1585,8 +1747,9 @@ export class ContentStore {
|
|
|
1585
1747
|
// rename/delete this store hasn't observed yet.
|
|
1586
1748
|
if (isNodeError(error) && error.code === 'ENOENT')
|
|
1587
1749
|
return STALE_LOOKUP;
|
|
1588
|
-
|
|
1589
|
-
|
|
1750
|
+
// Still the id, so a save of the referring entry keeps it.
|
|
1751
|
+
canopyLogError(`Failed to resolve reference ${id}:`, error);
|
|
1752
|
+
return buildMissingReference(id);
|
|
1590
1753
|
}
|
|
1591
1754
|
}
|
|
1592
1755
|
}
|
package/dist/content-tree.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* metadata); display concerns like labels are left to the adopter's `extract` callback.
|
|
7
7
|
*/
|
|
8
8
|
import { isIndexSlug } from './utils/entry-url.js';
|
|
9
|
-
import { createReferenceResolver, listCollectionEntries, resolveCollectionItemReferences, sortByOrder, } from './content-listing.js';
|
|
9
|
+
import { createReferenceResolver, listCollectionEntries, withoutUnavailableEntries, resolveCollectionItemReferences, sortByOrder, } from './content-listing.js';
|
|
10
10
|
/** Group flat schema items by parentPath for O(1) child lookup. */
|
|
11
11
|
const groupByParent = (flat) => {
|
|
12
12
|
const map = new Map();
|
|
@@ -73,7 +73,7 @@ export async function buildContentTree(branchRoot, flatSchema, contentRootName,
|
|
|
73
73
|
? createReferenceResolver(branchRoot, flatSchema, contentRootName, visibility)
|
|
74
74
|
: null;
|
|
75
75
|
const listVisibleEntries = async (collection) => {
|
|
76
|
-
const entries = await listCollectionEntries(branchRoot, collection);
|
|
76
|
+
const entries = withoutUnavailableEntries(await listCollectionEntries(branchRoot, collection), collection);
|
|
77
77
|
const visible = shouldInclude ? entries.filter((e) => shouldInclude(e.logicalPath)) : entries;
|
|
78
78
|
// Resolved here rather than at the two node-building sites so BOTH inherit it — the same
|
|
79
79
|
// reason the ACL filter lives here. A denied entry is filtered above and never resolved.
|
package/dist/context.js
CHANGED
|
@@ -7,6 +7,8 @@ import { loadBranchContext, loadOrCreateBranchContext } from './branch-workspace
|
|
|
7
7
|
import { buildContentTree as buildContentTreeImpl, } from './content-tree.js';
|
|
8
8
|
import { listEntries as listEntriesImpl, } from './content-listing.js';
|
|
9
9
|
import { createDebugLogger } from './utils/debug.js';
|
|
10
|
+
import { baseBranchOf } from './utils/base-branch.js';
|
|
11
|
+
import { CONTEXT_USER_ID } from './system-users.js';
|
|
10
12
|
const log = createDebugLogger({ prefix: 'Context' });
|
|
11
13
|
/** True when a ContentStoreError indicates a path/entry wasn't found (expected during candidate probing). */
|
|
12
14
|
function isLookupFailure(err) {
|
|
@@ -69,10 +71,11 @@ export function createCanopyContext(options) {
|
|
|
69
71
|
// defaultActiveBranch is explicit). Same contract as the HTTP API handler —
|
|
70
72
|
// switching branches mid-session updates what getCanopy() serves.
|
|
71
73
|
await services.refreshActiveBranch();
|
|
74
|
+
await services.resolvePendingBaseBranch();
|
|
72
75
|
const user = await getUser();
|
|
73
76
|
// Read once, so every read and listing in this request agrees on it even if
|
|
74
77
|
// a concurrent request's refreshActiveBranch() replaces services.config.
|
|
75
|
-
const activeBranch = services.config.defaultActiveBranch ?? services.config
|
|
78
|
+
const activeBranch = services.config.defaultActiveBranch ?? baseBranchOf(services.config);
|
|
76
79
|
// No ACL enforcement at build time, on static deployments, or for the
|
|
77
80
|
// synthetic admin. For the last this is load-bearing, not an optimization:
|
|
78
81
|
// that user has unconditional access anyway (path checks bypass entirely for
|
|
@@ -112,7 +115,7 @@ export function createCanopyContext(options) {
|
|
|
112
115
|
config: services.config,
|
|
113
116
|
branchName: activeBranch,
|
|
114
117
|
mode: services.config.mode,
|
|
115
|
-
createdBy:
|
|
118
|
+
createdBy: CONTEXT_USER_ID,
|
|
116
119
|
remoteUrl: services.config.defaultRemoteUrl,
|
|
117
120
|
});
|
|
118
121
|
}
|