canopycms 0.0.67-int.90 → 0.0.67
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/README.md +30 -3
- package/dist/ai/generate.js +1 -27
- package/dist/ai/handler.d.ts +0 -4
- package/dist/ai/handler.js +0 -11
- package/dist/ai/index.d.ts +1 -8
- package/dist/ai/index.js +1 -8
- package/dist/ai/json-to-markdown.js +15 -58
- package/dist/ai/resolve-branch.d.ts +2 -7
- package/dist/ai/resolve-branch.js +2 -8
- package/dist/ai/strip-mdx.d.ts +5 -13
- package/dist/ai/strip-mdx.js +5 -17
- package/dist/ai/to-plain-text.d.ts +10 -28
- package/dist/ai/to-plain-text.js +10 -28
- package/dist/ai/transform-components.d.ts +2 -4
- package/dist/ai/transform-components.js +8 -19
- package/dist/ai/types.d.ts +5 -10
- package/dist/api/admin-branch-health.d.ts +4 -12
- package/dist/api/admin-branch-health.js +26 -66
- package/dist/api/admin.d.ts +4 -12
- package/dist/api/admin.js +10 -26
- package/dist/api/assets.d.ts +0 -25
- package/dist/api/assets.js +50 -91
- package/dist/api/branch-merge.d.ts +1 -1
- package/dist/api/branch-merge.js +0 -2
- package/dist/api/branch-review.d.ts +0 -8
- package/dist/api/branch-review.js +0 -13
- package/dist/api/branch-status.d.ts +0 -3
- package/dist/api/branch-status.js +8 -33
- package/dist/api/branch-withdraw.d.ts +0 -1
- package/dist/api/branch-withdraw.js +6 -9
- package/dist/api/branch.d.ts +7 -12
- package/dist/api/branch.js +7 -44
- package/dist/api/client.d.ts +71 -224
- package/dist/api/client.js +82 -233
- package/dist/api/comments.d.ts +2 -4
- package/dist/api/comments.js +0 -17
- package/dist/api/content.d.ts +0 -6
- package/dist/api/content.js +66 -142
- package/dist/api/entries-constants.d.ts +5 -9
- package/dist/api/entries-constants.js +5 -9
- package/dist/api/entries.d.ts +8 -13
- package/dist/api/entries.js +20 -51
- package/dist/api/github-sync.d.ts +2 -4
- package/dist/api/github-sync.js +4 -8
- package/dist/api/groups.d.ts +3 -0
- package/dist/api/groups.js +8 -21
- package/dist/api/guards.d.ts +4 -16
- package/dist/api/guards.js +3 -22
- package/dist/api/index.d.ts +4 -4
- package/dist/api/permissions.d.ts +2 -0
- package/dist/api/permissions.js +18 -40
- package/dist/api/reference-options.d.ts +0 -1
- package/dist/api/reference-options.js +3 -11
- package/dist/api/request-body-hash.d.ts +12 -23
- package/dist/api/request-body-hash.js +12 -23
- package/dist/api/resolve-references.d.ts +0 -1
- package/dist/api/resolve-references.js +13 -28
- package/dist/api/route-builder.d.ts +3 -7
- package/dist/api/route-builder.js +1 -1
- package/dist/api/routes.d.ts +12 -0
- package/dist/api/routes.js +47 -0
- package/dist/api/schema.d.ts +23 -33
- package/dist/api/schema.js +20 -74
- package/dist/api/settings-helpers.js +4 -5
- package/dist/api/types.d.ts +0 -5
- package/dist/api/user.js +0 -5
- package/dist/api/validators.d.ts +14 -52
- package/dist/api/validators.js +14 -52
- package/dist/assets/asset-prefixes.d.ts +4 -8
- package/dist/assets/asset-prefixes.js +4 -8
- package/dist/assets/asset-src.d.ts +5 -22
- package/dist/assets/asset-src.js +5 -21
- package/dist/assets/factory.d.ts +1 -2
- package/dist/assets/factory.js +1 -2
- package/dist/assets/finalize.d.ts +3 -1
- package/dist/assets/finalize.js +3 -1
- package/dist/assets/index.d.ts +3 -3
- package/dist/assets/index.js +1 -1
- package/dist/assets/keys.d.ts +5 -3
- package/dist/assets/keys.js +6 -3
- package/dist/assets/pipeline.d.ts +3 -2
- package/dist/assets/pipeline.js +22 -56
- package/dist/assets/sharp-loader.d.ts +2 -10
- package/dist/assets/sharp-loader.js +2 -10
- package/dist/assets/store-local.d.ts +1 -2
- package/dist/assets/store-local.js +1 -2
- package/dist/assets/store-s3.d.ts +3 -4
- package/dist/assets/svg-sanitizer.d.ts +6 -14
- package/dist/assets/svg-sanitizer.js +6 -14
- package/dist/assets/transform-directives.d.ts +3 -4
- package/dist/assets/transform-directives.js +1 -1
- package/dist/assets/transform.d.ts +19 -30
- package/dist/assets/transform.js +18 -31
- package/dist/auth/cache.d.ts +2 -3
- package/dist/auth/cache.js +2 -3
- package/dist/auth/caching-auth-plugin.d.ts +11 -22
- package/dist/auth/caching-auth-plugin.js +12 -14
- package/dist/auth/context-helpers.d.ts +6 -20
- package/dist/auth/context-helpers.js +5 -17
- package/dist/auth/file-based-auth-cache.d.ts +10 -21
- package/dist/auth/file-based-auth-cache.js +23 -51
- package/dist/auth/plugin.d.ts +29 -61
- package/dist/auth/plugin.js +9 -13
- package/dist/auth/types.d.ts +11 -20
- package/dist/authorization/branch.d.ts +30 -49
- package/dist/authorization/branch.js +16 -42
- package/dist/authorization/content.d.ts +11 -40
- package/dist/authorization/content.js +13 -45
- package/dist/authorization/groups/index.d.ts +0 -3
- package/dist/authorization/groups/index.js +0 -3
- package/dist/authorization/groups/loader.d.ts +14 -28
- package/dist/authorization/groups/loader.js +14 -38
- package/dist/authorization/groups/schema.d.ts +0 -6
- package/dist/authorization/groups/schema.js +4 -7
- package/dist/authorization/helpers.d.ts +9 -28
- package/dist/authorization/helpers.js +9 -28
- package/dist/authorization/index.d.ts +3 -25
- package/dist/authorization/index.js +3 -35
- package/dist/authorization/path.d.ts +6 -13
- package/dist/authorization/path.js +8 -25
- package/dist/authorization/permissions/index.d.ts +0 -3
- package/dist/authorization/permissions/index.js +0 -3
- package/dist/authorization/permissions/loader.d.ts +14 -27
- package/dist/authorization/permissions/loader.js +14 -32
- package/dist/authorization/permissions/schema.d.ts +1 -6
- package/dist/authorization/permissions/schema.js +6 -13
- package/dist/authorization/protected-branch.d.ts +36 -65
- package/dist/authorization/protected-branch.js +29 -41
- package/dist/authorization/settings-file-store.d.ts +38 -81
- package/dist/authorization/settings-file-store.js +38 -79
- package/dist/authorization/types.d.ts +4 -26
- package/dist/authorization/types.js +0 -6
- package/dist/authorization/validation.d.ts +1 -23
- package/dist/authorization/validation.js +1 -24
- package/dist/branch-health.d.ts +39 -67
- package/dist/branch-health.js +27 -43
- package/dist/branch-metadata-file.d.ts +16 -32
- package/dist/branch-metadata-file.js +13 -26
- package/dist/branch-metadata.d.ts +49 -80
- package/dist/branch-metadata.js +57 -87
- package/dist/branch-registry.d.ts +26 -64
- package/dist/branch-registry.js +44 -93
- package/dist/branch-schema-cache.d.ts +34 -124
- package/dist/branch-schema-cache.js +49 -155
- package/dist/branch-workspace.d.ts +4 -4
- package/dist/branch-workspace.js +15 -19
- package/dist/build/generate-ai-content.d.ts +1 -0
- package/dist/build/generate-ai-content.js +12 -25
- package/dist/build-canopy.d.ts +17 -27
- package/dist/build-canopy.js +16 -23
- package/dist/build-mode.d.ts +25 -27
- package/dist/build-mode.js +25 -29
- package/dist/cli/cli.d.ts +15 -26
- package/dist/cli/cli.js +601 -978
- package/dist/cli/generate-ai-content.js +351 -628
- package/dist/cli/github-app-manifest.d.ts +84 -0
- package/dist/cli/github-app-manifest.js +161 -0
- package/dist/cli/init-github-app.d.ts +110 -444
- package/dist/cli/init-github-app.js +236 -773
- package/dist/cli/init.js +39 -80
- package/dist/cli/migrate.d.ts +13 -10
- package/dist/cli/migrate.js +22 -30
- package/dist/cli/project-detect.d.ts +17 -31
- package/dist/cli/project-detect.js +31 -42
- package/dist/cli/project-root.d.ts +4 -11
- package/dist/cli/project-root.js +4 -11
- package/dist/cli/prompt.d.ts +10 -0
- package/dist/cli/prompt.js +67 -0
- package/dist/cli/sync.d.ts +0 -8
- package/dist/cli/sync.js +8 -28
- package/dist/cli/template-files/middleware.ts.template +15 -29
- package/dist/cli/templates.d.ts +2 -12
- package/dist/cli/templates.js +4 -4
- package/dist/client.d.ts +1 -1
- package/dist/comment-store.d.ts +32 -53
- package/dist/comment-store.js +32 -52
- package/dist/config/flatten.d.ts +0 -3
- package/dist/config/flatten.js +2 -12
- package/dist/config/helpers.js +2 -2
- package/dist/config/index.d.ts +2 -2
- package/dist/config/index.js +1 -4
- package/dist/config/schemas/collection.d.ts +0 -8
- package/dist/config/schemas/collection.js +0 -10
- package/dist/config/schemas/config.d.ts +6 -78
- package/dist/config/schemas/config.js +38 -54
- package/dist/config/schemas/field.d.ts +2 -242
- package/dist/config/schemas/field.js +9 -20
- package/dist/config/schemas/media.js +1 -1
- package/dist/config/schemas/permissions.js +0 -2
- package/dist/config/schemas/url.d.ts +5 -6
- package/dist/config/schemas/url.js +5 -6
- package/dist/config/types.d.ts +45 -33
- package/dist/config/types.js +0 -1
- package/dist/config.d.ts +1 -8
- package/dist/config.js +1 -9
- package/dist/content-id-index.d.ts +76 -188
- package/dist/content-id-index.js +79 -230
- package/dist/content-index-generation.d.ts +27 -48
- package/dist/content-index-generation.js +35 -84
- package/dist/content-index-registry.d.ts +15 -24
- package/dist/content-index-registry.js +5 -9
- package/dist/content-listing.d.ts +64 -107
- package/dist/content-listing.js +68 -118
- package/dist/content-reader.d.ts +20 -29
- package/dist/content-reader.js +10 -18
- package/dist/content-store.d.ts +210 -336
- package/dist/content-store.js +413 -657
- package/dist/content-tree.d.ts +44 -86
- package/dist/content-tree.js +26 -54
- package/dist/context.d.ts +67 -94
- package/dist/context.js +53 -72
- package/dist/dev-content-watcher.d.ts +18 -18
- package/dist/dev-content-watcher.js +41 -44
- package/dist/editor/BranchManager.d.ts +3 -6
- package/dist/editor/BranchManager.js +24 -44
- package/dist/editor/CanopyEditor.js +5 -2
- package/dist/editor/CommentsPanel.js +0 -2
- package/dist/editor/Editor.js +19 -76
- package/dist/editor/EditorAuthGate.d.ts +27 -0
- package/dist/editor/EditorAuthGate.js +140 -0
- package/dist/editor/EntryNavigator.js +0 -20
- package/dist/editor/FormRenderer.d.ts +1 -2
- package/dist/editor/FormRenderer.js +8 -23
- package/dist/editor/admin/SystemHealthPanel.js +20 -37
- package/dist/editor/admin/useSystemHealth.d.ts +2 -3
- package/dist/editor/admin/useSystemHealth.js +10 -12
- package/dist/editor/client-reference-resolver.d.ts +2 -5
- package/dist/editor/client-reference-resolver.js +2 -19
- package/dist/editor/comments/BranchComments.js +0 -4
- package/dist/editor/comments/EntryComments.js +0 -4
- package/dist/editor/comments/FieldWrapper.js +0 -4
- package/dist/editor/comments/InlineCommentThread.js +0 -2
- package/dist/editor/comments/ThreadCarousel.d.ts +0 -58
- package/dist/editor/comments/ThreadCarousel.js +0 -12
- package/dist/editor/components/EditorFooter.d.ts +0 -8
- package/dist/editor/components/EditorFooter.js +0 -5
- package/dist/editor/components/EditorHeader.d.ts +0 -31
- package/dist/editor/components/EditorHeader.js +21 -71
- package/dist/editor/components/EditorSidebar.d.ts +0 -19
- package/dist/editor/components/EditorSidebar.js +0 -16
- package/dist/editor/components/EntryCreateModal.d.ts +4 -4
- package/dist/editor/components/EntryCreateModal.js +7 -22
- package/dist/editor/components/RenameEntryModal.js +0 -4
- package/dist/editor/components/UserBadge.js +0 -10
- package/dist/editor/components/index.js +0 -2
- package/dist/editor/context/ApiClientContext.d.ts +8 -7
- package/dist/editor/context/ApiClientContext.js +39 -11
- package/dist/editor/context/AssetContext.d.ts +7 -17
- package/dist/editor/context/AssetContext.js +7 -17
- package/dist/editor/context/EditorIdentityContext.d.ts +13 -0
- package/dist/editor/context/EditorIdentityContext.js +7 -0
- package/dist/editor/context/EditorStateContext.d.ts +10 -41
- package/dist/editor/context/EditorStateContext.js +7 -23
- package/dist/editor/context/SWRProvider.d.ts +3 -6
- package/dist/editor/context/index.d.ts +1 -20
- package/dist/editor/context/index.js +1 -20
- package/dist/editor/editor-utils.d.ts +8 -47
- package/dist/editor/editor-utils.js +19 -72
- package/dist/editor/fields/BlockField.d.ts +2 -2
- package/dist/editor/fields/BlockField.js +1 -2
- package/dist/editor/fields/CodeField.d.ts +0 -1
- package/dist/editor/fields/CodeField.js +0 -1
- package/dist/editor/fields/DateTimeField.d.ts +15 -22
- package/dist/editor/fields/DateTimeField.js +17 -26
- package/dist/editor/fields/ImageField.d.ts +2 -2
- package/dist/editor/fields/ImageField.js +8 -14
- package/dist/editor/fields/InlineGroupField.d.ts +0 -1
- package/dist/editor/fields/InlineGroupField.js +0 -1
- package/dist/editor/fields/MarkdownField.d.ts +0 -1
- package/dist/editor/fields/MarkdownField.js +6 -11
- package/dist/editor/fields/MdxImageDialog.d.ts +1 -1
- package/dist/editor/fields/MdxImageDialog.js +0 -1
- package/dist/editor/fields/NumberField.d.ts +9 -19
- package/dist/editor/fields/NumberField.js +11 -22
- package/dist/editor/fields/NumberListField.d.ts +7 -10
- package/dist/editor/fields/NumberListField.js +7 -10
- package/dist/editor/fields/ObjectField.d.ts +5 -8
- package/dist/editor/fields/ObjectField.js +0 -1
- package/dist/editor/fields/ReferenceField.d.ts +1 -1
- package/dist/editor/fields/ReferenceField.js +11 -16
- package/dist/editor/fields/SelectField.d.ts +2 -2
- package/dist/editor/fields/SelectField.js +0 -1
- package/dist/editor/fields/StringListField.d.ts +0 -4
- package/dist/editor/fields/StringListField.js +4 -9
- package/dist/editor/fields/TextField.d.ts +0 -1
- package/dist/editor/fields/TextField.js +0 -1
- package/dist/editor/fields/ToggleField.d.ts +0 -1
- package/dist/editor/fields/ToggleField.js +0 -1
- package/dist/editor/fields/entry-link/EntryLinkContext.js +2 -4
- package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +0 -3
- package/dist/editor/fields/entry-link/InsertEntryLink.js +0 -4
- package/dist/editor/group-manager/ExternalGroupsTab.d.ts +0 -3
- package/dist/editor/group-manager/ExternalGroupsTab.js +0 -1
- package/dist/editor/group-manager/GroupCard.d.ts +0 -3
- package/dist/editor/group-manager/GroupForm.d.ts +0 -3
- package/dist/editor/group-manager/InternalGroupsTab.d.ts +0 -3
- package/dist/editor/group-manager/InternalGroupsTab.js +0 -2
- package/dist/editor/group-manager/MemberList.d.ts +0 -3
- package/dist/editor/group-manager/hooks/useExternalGroupSearch.d.ts +0 -3
- package/dist/editor/group-manager/hooks/useExternalGroupSearch.js +0 -3
- package/dist/editor/group-manager/hooks/useGroupState.d.ts +0 -3
- package/dist/editor/group-manager/hooks/useGroupState.js +0 -5
- package/dist/editor/group-manager/hooks/useUserSearch.d.ts +0 -3
- package/dist/editor/group-manager/hooks/useUserSearch.js +0 -3
- package/dist/editor/group-manager/index.d.ts +0 -6
- package/dist/editor/group-manager/index.js +0 -10
- package/dist/editor/group-manager/types.d.ts +2 -5
- package/dist/editor/group-manager/types.js +0 -3
- package/dist/editor/hooks/index.d.ts +6 -11
- package/dist/editor/hooks/index.js +6 -12
- package/dist/editor/hooks/useBranchActions.d.ts +0 -14
- package/dist/editor/hooks/useBranchActions.js +0 -22
- package/dist/editor/hooks/useBranchManager.d.ts +2 -48
- package/dist/editor/hooks/useBranchManager.js +4 -64
- package/dist/editor/hooks/useBranchesData.d.ts +7 -6
- package/dist/editor/hooks/useBranchesData.js +7 -6
- package/dist/editor/hooks/useCommentSystem.d.ts +0 -49
- package/dist/editor/hooks/useCommentSystem.js +1 -36
- package/dist/editor/hooks/useDraftManager.d.ts +2 -35
- package/dist/editor/hooks/useDraftManager.js +23 -64
- package/dist/editor/hooks/useEditorLayout.d.ts +0 -20
- package/dist/editor/hooks/useEditorLayout.js +0 -23
- package/dist/editor/hooks/useEntriesData.d.ts +1 -0
- package/dist/editor/hooks/useEntriesData.js +2 -3
- package/dist/editor/hooks/useEntryLinkResolution.d.ts +2 -2
- package/dist/editor/hooks/useEntryLinkResolution.js +2 -2
- package/dist/editor/hooks/useEntryManager.d.ts +2 -27
- package/dist/editor/hooks/useEntryManager.js +29 -78
- package/dist/editor/hooks/useGroupManager.d.ts +0 -21
- package/dist/editor/hooks/useGroupManager.js +0 -21
- package/dist/editor/hooks/usePermissionManager.d.ts +0 -20
- package/dist/editor/hooks/usePermissionManager.js +0 -20
- package/dist/editor/hooks/useReferenceResolution.d.ts +2 -33
- package/dist/editor/hooks/useReferenceResolution.js +1 -48
- package/dist/editor/hooks/useSchemaManager.d.ts +8 -27
- package/dist/editor/hooks/useSchemaManager.js +0 -19
- package/dist/editor/hooks/useUserContext.d.ts +2 -9
- package/dist/editor/hooks/useUserContext.js +10 -10
- package/dist/editor/hooks/useUserMetadata.d.ts +0 -2
- package/dist/editor/hooks/useUserMetadata.js +0 -3
- package/dist/editor/media/AssetCard.d.ts +0 -1
- package/dist/editor/media/AssetCard.js +1 -2
- package/dist/editor/media/CropStep.d.ts +0 -1
- package/dist/editor/media/CropStep.js +0 -1
- package/dist/editor/media/MediaLibrary.d.ts +1 -2
- package/dist/editor/media/MediaLibrary.js +1 -2
- package/dist/editor/media/MediaLibraryBody.d.ts +0 -1
- package/dist/editor/media/MediaLibraryBody.js +0 -1
- package/dist/editor/media/crop-math.d.ts +1 -1
- package/dist/editor/media/crop-math.js +1 -1
- package/dist/editor/permission-manager/GroupSelector.d.ts +0 -3
- package/dist/editor/permission-manager/PermissionEditor.d.ts +0 -4
- package/dist/editor/permission-manager/PermissionEditor.js +0 -1
- package/dist/editor/permission-manager/PermissionLevelBadge.d.ts +0 -7
- package/dist/editor/permission-manager/PermissionLevelBadge.js +0 -4
- package/dist/editor/permission-manager/PermissionTree.d.ts +0 -4
- package/dist/editor/permission-manager/UserSelector.d.ts +0 -3
- package/dist/editor/permission-manager/constants.d.ts +0 -3
- package/dist/editor/permission-manager/hooks/useGroupsAndUsers.d.ts +0 -3
- package/dist/editor/permission-manager/hooks/useGroupsAndUsers.js +0 -10
- package/dist/editor/permission-manager/hooks/usePermissionTree.d.ts +0 -14
- package/dist/editor/permission-manager/hooks/usePermissionTree.js +0 -11
- package/dist/editor/permission-manager/index.d.ts +0 -7
- package/dist/editor/permission-manager/index.js +0 -16
- package/dist/editor/permission-manager/types.d.ts +2 -12
- package/dist/editor/permission-manager/types.js +0 -3
- package/dist/editor/permission-manager/utils.d.ts +0 -28
- package/dist/editor/permission-manager/utils.js +9 -23
- package/dist/editor/preview-bridge.d.ts +5 -1
- package/dist/editor/preview-bridge.js +4 -4
- package/dist/editor/schema-editor/CollectionEditor.d.ts +0 -5
- package/dist/editor/schema-editor/CollectionEditor.js +9 -31
- package/dist/editor/schema-editor/EntryTypeEditor.d.ts +0 -6
- package/dist/editor/schema-editor/EntryTypeEditor.js +3 -24
- package/dist/editor/schema-editor/index.d.ts +0 -7
- package/dist/editor/schema-editor/index.js +0 -7
- package/dist/editor/utils/env.d.ts +3 -18
- package/dist/editor/utils/env.js +6 -25
- package/dist/entry-link-resolver.d.ts +13 -35
- package/dist/entry-link-resolver.js +20 -53
- package/dist/entry-schema-registry.d.ts +20 -60
- package/dist/entry-schema-registry.js +20 -69
- package/dist/entry-schema.d.ts +118 -223
- package/dist/entry-schema.js +46 -91
- package/dist/git-manager.d.ts +101 -200
- package/dist/git-manager.js +211 -384
- package/dist/github-service.d.ts +34 -75
- package/dist/github-service.js +37 -91
- package/dist/http/handler.d.ts +6 -37
- package/dist/http/handler.js +65 -120
- package/dist/http/router.d.ts +15 -32
- package/dist/http/router.js +34 -99
- package/dist/http/types.d.ts +15 -40
- package/dist/http/types.js +3 -7
- package/dist/id.d.ts +4 -10
- package/dist/id.js +4 -10
- package/dist/index.js +6 -9
- package/dist/operating-mode/client-safe-strategy.d.ts +7 -14
- package/dist/operating-mode/client-safe-strategy.js +8 -28
- package/dist/operating-mode/client-unsafe-strategy.d.ts +6 -16
- package/dist/operating-mode/client-unsafe-strategy.js +9 -34
- package/dist/operating-mode/client.d.ts +3 -10
- package/dist/operating-mode/client.js +3 -8
- package/dist/operating-mode/deployment-name-fixtures.d.ts +14 -18
- package/dist/operating-mode/deployment-name-fixtures.js +14 -18
- package/dist/operating-mode/deployment-name.d.ts +19 -29
- package/dist/operating-mode/deployment-name.js +29 -47
- package/dist/operating-mode/index.d.ts +4 -25
- package/dist/operating-mode/index.js +9 -31
- package/dist/operating-mode/mode-env.d.ts +32 -54
- package/dist/operating-mode/mode-env.js +36 -58
- package/dist/operating-mode/types.d.ts +24 -72
- package/dist/operating-mode/types.js +1 -7
- package/dist/paths/branch-name.d.ts +30 -40
- package/dist/paths/branch-name.js +30 -40
- package/dist/paths/branch.d.ts +4 -18
- package/dist/paths/branch.js +6 -22
- package/dist/paths/index.d.ts +2 -13
- package/dist/paths/index.js +3 -23
- package/dist/paths/normalize-server.d.ts +3 -15
- package/dist/paths/normalize-server.js +3 -15
- package/dist/paths/normalize.d.ts +13 -33
- package/dist/paths/normalize.js +13 -33
- package/dist/paths/resolve.d.ts +4 -17
- package/dist/paths/resolve.js +4 -22
- package/dist/paths/types.d.ts +12 -34
- package/dist/paths/types.js +5 -7
- package/dist/paths/validation.d.ts +22 -119
- package/dist/paths/validation.js +30 -148
- package/dist/reference-resolver.d.ts +0 -13
- package/dist/reference-resolver.js +0 -18
- package/dist/resolve-canopy-user.d.ts +16 -18
- package/dist/resolve-canopy-user.js +17 -26
- package/dist/resource-generation.d.ts +16 -25
- package/dist/resource-generation.js +34 -97
- package/dist/schema/meta-loader.d.ts +2 -14
- package/dist/schema/meta-loader.js +15 -64
- package/dist/schema/resolver.d.ts +1 -4
- package/dist/schema/resolver.js +1 -6
- package/dist/schema/schema-store-types.d.ts +0 -12
- package/dist/schema/schema-store.d.ts +61 -138
- package/dist/schema/schema-store.js +85 -282
- package/dist/schema/types.d.ts +0 -4
- package/dist/server.d.ts +62 -93
- package/dist/server.js +63 -94
- package/dist/services.d.ts +23 -47
- package/dist/services.js +55 -98
- package/dist/settings-workspace.d.ts +17 -30
- package/dist/settings-workspace.js +51 -81
- package/dist/static/index.d.ts +26 -52
- package/dist/static/index.js +33 -67
- package/dist/{worker/task-queue.d.ts → task-queue/cms-task-queue.d.ts} +7 -9
- package/dist/task-queue/cms-task-queue.js +13 -0
- package/dist/task-queue/task-queue-config.d.ts +3 -0
- package/dist/{worker → task-queue}/task-queue-config.js +1 -6
- package/dist/task-queue/task-queue.d.ts +16 -22
- package/dist/task-queue/task-queue.js +20 -44
- package/dist/task-queue/types.d.ts +0 -1
- package/dist/task-queue/worker-status.d.ts +35 -0
- package/dist/task-queue/worker-status.js +40 -0
- package/dist/types.d.ts +40 -51
- package/dist/url-collision.d.ts +26 -40
- package/dist/url-collision.js +47 -63
- package/dist/url-exclusivity-fixtures.d.ts +15 -20
- package/dist/url-exclusivity-fixtures.js +14 -19
- package/dist/url-path-resolver.d.ts +8 -16
- package/dist/url-path-resolver.js +19 -29
- package/dist/user.d.ts +8 -21
- package/dist/user.js +10 -19
- package/dist/utils/async-mutex.js +4 -6
- package/dist/utils/atomic-write.d.ts +2 -11
- package/dist/utils/atomic-write.js +2 -11
- package/dist/utils/body-field.d.ts +8 -15
- package/dist/utils/body-field.js +8 -15
- package/dist/utils/content-serialize.d.ts +18 -24
- package/dist/utils/content-serialize.js +109 -160
- package/dist/utils/content-write-lock.d.ts +51 -106
- package/dist/utils/content-write-lock.js +66 -111
- package/dist/utils/debug.d.ts +7 -23
- package/dist/utils/debug.js +7 -23
- package/dist/utils/entry-url.d.ts +11 -23
- package/dist/utils/entry-url.js +11 -26
- package/dist/utils/error.d.ts +25 -90
- package/dist/utils/error.js +60 -147
- package/dist/utils/flatten-group-fields.d.ts +5 -7
- package/dist/utils/flatten-group-fields.js +5 -7
- package/dist/utils/format.d.ts +1 -5
- package/dist/utils/format.js +1 -5
- package/dist/utils/fs.d.ts +3 -4
- package/dist/utils/fs.js +3 -4
- package/dist/utils/git.d.ts +52 -83
- package/dist/utils/git.js +79 -125
- package/dist/utils/logger.d.ts +35 -50
- package/dist/utils/logger.js +35 -52
- package/dist/utils/provisioning-lock.d.ts +23 -35
- package/dist/utils/provisioning-lock.js +49 -69
- package/dist/utils/sanitize-href.d.ts +5 -10
- package/dist/utils/sanitize-href.js +8 -18
- package/dist/utils/title-field.d.ts +17 -20
- package/dist/utils/title-field.js +18 -27
- package/dist/utils/typed-filename.d.ts +20 -48
- package/dist/utils/typed-filename.js +25 -61
- package/dist/utils/url-prefix.d.ts +3 -3
- package/dist/utils/url-prefix.js +3 -3
- package/dist/validation/block-structural-keys.d.ts +16 -31
- package/dist/validation/block-structural-keys.js +19 -29
- package/dist/validation/deletion-checker.d.ts +0 -34
- package/dist/validation/deletion-checker.js +1 -43
- package/dist/validation/entry-link-validator.d.ts +6 -13
- package/dist/validation/entry-link-validator.js +4 -13
- package/dist/validation/entry-type-reference-validator.d.ts +13 -18
- package/dist/validation/entry-type-reference-validator.js +13 -18
- package/dist/validation/entry-validator.d.ts +13 -25
- package/dist/validation/entry-validator.js +15 -32
- package/dist/validation/field-traversal.d.ts +3 -30
- package/dist/validation/field-traversal.js +1 -32
- package/dist/validation/reference-validator.d.ts +5 -24
- package/dist/validation/reference-validator.js +5 -30
- package/dist/worker/cms-worker.d.ts +172 -232
- package/dist/worker/cms-worker.js +241 -333
- package/dist/worker/git-sync.d.ts +32 -133
- package/dist/worker/git-sync.js +129 -191
- package/dist/worker/github-auth.d.ts +13 -19
- package/dist/worker/github-auth.js +13 -19
- package/dist/worker/history-rewrite.d.ts +53 -66
- package/dist/worker/history-rewrite.js +54 -60
- package/dist/worker/log.d.ts +24 -41
- package/dist/worker/log.js +25 -42
- package/dist/worker/rebase.d.ts +20 -110
- package/dist/worker/rebase.js +279 -354
- package/dist/worker/task-runner.d.ts +32 -66
- package/dist/worker/task-runner.js +126 -162
- package/dist/worker/worker-context.d.ts +41 -81
- package/package.json +4 -4
- package/dist/cli/template-files/middleware-clerk.ts.template +0 -37
- package/dist/worker/task-queue-config.d.ts +0 -8
- package/dist/worker/task-queue.js +0 -19
- package/dist/worker/worker-status.d.ts +0 -47
- package/dist/worker/worker-status.js +0 -52
|
@@ -1,47 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Schema Store - handles reading and writing .collection.json files.
|
|
3
|
-
*
|
|
4
|
-
* This module provides CRUD operations for collection schema metadata:
|
|
5
|
-
* - Create/update/delete collections
|
|
6
|
-
* - Add/update/remove entry types
|
|
7
|
-
* - Update ordering of items within collections
|
|
8
|
-
*
|
|
9
3
|
* All mutations are branch-specific (like content edits).
|
|
10
4
|
*
|
|
11
5
|
* ## Concurrency
|
|
12
6
|
*
|
|
13
|
-
* `.collection.json` is read-modify-written by every mutator below
|
|
14
|
-
* mutated cross-host
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
7
|
+
* `.collection.json` is read-modify-written by every mutator below and
|
|
8
|
+
* mutated cross-host — two warm Lambdas, or a Lambda + the EC2 worker, on
|
|
9
|
+
* EFS can both read the same pre-mutation file and clobber each other's
|
|
10
|
+
* write. This deviates from the standard 3-layer recipe in
|
|
11
|
+
* docs/concurrency.md: no OCC `version`/`writeId` field goes into
|
|
12
|
+
* `.collection.json` itself, and no lockfile lives in the content tree —
|
|
13
|
+
* both are adopter-visible, git-committed files, so a rebase would make a
|
|
14
|
+
* version counter meaningless and a crash-leftover lockfile would land in a
|
|
15
|
+
* commit an adopter reviews.
|
|
20
16
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
17
|
+
* Protection is instead layer 1 ({@link withLock}) + layer 3
|
|
18
|
+
* ({@link withOccFileLock}) on one COARSE per-branch SURROGATE lock path
|
|
19
|
+
* OUTSIDE the content tree (`{branchRoot}/.canopy-meta/schema`, see
|
|
20
|
+
* `withSchemaLock`), covering every schema mutation on the branch including
|
|
21
|
+
* multi-file ones. Layer 2 (OCC read-back) is skipped, since there's no
|
|
22
|
+
* version field to check; layer 4 (generation marker) guards the schema
|
|
23
|
+
* CACHE separately, not this write path.
|
|
27
24
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* meta AND the parent's). Layer 2 (OCC read-back) is skipped entirely since
|
|
34
|
-
* there is no version field to check; layer 4 (generation marker) is used
|
|
35
|
-
* separately, for the schema CACHE, not for this write path.
|
|
36
|
-
*
|
|
37
|
-
* Accepted residual: `deleteBranch` does NOT take this lock before its
|
|
38
|
-
* recursive `rm` — an in-flight schema write can race a concurrent branch
|
|
39
|
-
* deletion (the write's `rm`/rename can hit ENOTEMPTY or land in a
|
|
40
|
-
* half-deleted tree). This mirrors the same residual documented on
|
|
41
|
-
* `BranchMetadataFileManager.save()` in branch-metadata.ts and is not closed
|
|
42
|
-
* here either; see `withSchemaLock`'s doc comment for the phantom-guard that
|
|
43
|
-
* IS in place for the common ordering (branch already gone before this call
|
|
44
|
-
* ever starts).
|
|
25
|
+
* Accepted residual: `deleteBranch` does not take this lock before its
|
|
26
|
+
* recursive `rm`, so an in-flight write can race a concurrent branch
|
|
27
|
+
* deletion (mirrors the residual on `BranchMetadataFileManager.save()` in
|
|
28
|
+
* branch-metadata.ts) — see `withSchemaLock`'s doc for the phantom-guard
|
|
29
|
+
* covering the common ordering instead.
|
|
45
30
|
*/
|
|
46
31
|
import { promises as fs } from 'node:fs';
|
|
47
32
|
import path from 'node:path';
|
|
@@ -57,12 +42,8 @@ import { isIndexSlug } from '../utils/entry-url.js';
|
|
|
57
42
|
import { invalidateBranchContentCaches } from '../content-index-generation.js';
|
|
58
43
|
import { generateId, isValidId } from '../id.js';
|
|
59
44
|
import { createLogicalPath, normalizeCollectionPath as stripContentRootPrefix, validateAndNormalizePath, } from '../paths/index.js';
|
|
60
|
-
// ============================================================================
|
|
61
|
-
// Zod Schemas for Validation
|
|
62
|
-
// ============================================================================
|
|
63
45
|
/** Max length for names and slugs (filesystem path safety) */
|
|
64
46
|
const MAX_NAME_LENGTH = 64;
|
|
65
|
-
/** Max length for labels */
|
|
66
47
|
const MAX_LABEL_LENGTH = 128;
|
|
67
48
|
/**
|
|
68
49
|
* Safe pattern for names and slugs that become filesystem path segments.
|
|
@@ -117,9 +98,6 @@ const updateEntryTypeInputSchema = z.object({
|
|
|
117
98
|
default: z.boolean().optional(),
|
|
118
99
|
maxItems: z.number().int().positive().optional(),
|
|
119
100
|
});
|
|
120
|
-
// ============================================================================
|
|
121
|
-
// SchemaOps Class
|
|
122
|
-
// ============================================================================
|
|
123
101
|
const log = createDebugLogger({ prefix: 'SchemaOps' });
|
|
124
102
|
/**
|
|
125
103
|
* Thrown when a schema mutation cannot proceed because the per-branch
|
|
@@ -154,43 +132,31 @@ export class SchemaOps {
|
|
|
154
132
|
.join('/');
|
|
155
133
|
this.schemaLockPath = path.join(this.branchRoot, '.canopy-meta', 'schema');
|
|
156
134
|
}
|
|
157
|
-
// --------------------------------------------------------------------------
|
|
158
|
-
// Schema Lock
|
|
159
|
-
// --------------------------------------------------------------------------
|
|
160
135
|
/**
|
|
161
136
|
* Serialize an entire read-modify-write schema mutation behind the coarse
|
|
162
|
-
* per-branch surrogate lock described in the module doc comment
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
* serialization.
|
|
168
|
-
* 2. {@link withOccFileLock} — server-enforced, cross-process/cross-host
|
|
169
|
-
* mutual exclusion (proper-lockfile, mkdir-based), immune to NFS client
|
|
170
|
-
* dentry/attribute caching.
|
|
137
|
+
* per-branch surrogate lock described in the module doc comment: layer 1
|
|
138
|
+
* ({@link withLock}, in-process FIFO mutex) wraps layer 3
|
|
139
|
+
* ({@link withOccFileLock}, cross-process/cross-host mkdir-based mutual
|
|
140
|
+
* exclusion, immune to NFS attribute caching), same structure as
|
|
141
|
+
* `BranchMetadataFileManager.save()` in branch-metadata.ts.
|
|
171
142
|
*
|
|
172
|
-
* NOT re-entrant
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
143
|
+
* NOT re-entrant — callers must never invoke this from inside a callback
|
|
144
|
+
* already running under it, or `withLock` deadlocks waiting on itself;
|
|
145
|
+
* that's why `updateOrderInner` calls `updateCollectionInner` directly
|
|
146
|
+
* instead of the public `updateCollection`.
|
|
176
147
|
*
|
|
177
148
|
* Phantom-resurrection guard: `branchRoot` can be removed by a concurrent
|
|
178
149
|
* `deleteBranch` between the caller resolving its BranchContext and this
|
|
179
|
-
* call
|
|
180
|
-
* `mkdir({recursive:true})`
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
* that passes this check can still race a `deleteBranch` `rm` that starts
|
|
187
|
-
* moments later and is still mid-flight when this call's write lands,
|
|
188
|
-
* resurrecting the tree. `deleteBranch` does not take this lock at all
|
|
189
|
-
* (see the module doc comment), so that race is not closed here either.
|
|
150
|
+
* call's own lock acquisition, which would otherwise let
|
|
151
|
+
* `withOccFileLock`'s `mkdir({recursive:true})` silently recreate
|
|
152
|
+
* `.canopy-meta/` in an otherwise-deleted tree. Checking BEFORE the lock
|
|
153
|
+
* fails fast instead. Residual (accepted, same shape as
|
|
154
|
+
* branch-metadata.ts's): a call that passes this check can still race a
|
|
155
|
+
* `deleteBranch` `rm` starting moments later — not closed here either,
|
|
156
|
+
* since `deleteBranch` never takes this lock (see the module doc comment).
|
|
190
157
|
*
|
|
191
158
|
* Translation happens ONLY at this boundary: inner code always sees the
|
|
192
|
-
* raw {@link OccWriteConflictError} bubble up
|
|
193
|
-
* re-translates it itself).
|
|
159
|
+
* raw {@link OccWriteConflictError} bubble up here untranslated.
|
|
194
160
|
*/
|
|
195
161
|
async withSchemaLock(fn) {
|
|
196
162
|
try {
|
|
@@ -212,12 +178,10 @@ export class SchemaOps {
|
|
|
212
178
|
throw err;
|
|
213
179
|
}
|
|
214
180
|
}
|
|
215
|
-
// --------------------------------------------------------------------------
|
|
216
|
-
// Cache Invalidation
|
|
217
|
-
// --------------------------------------------------------------------------
|
|
218
181
|
/**
|
|
219
182
|
* Invalidate schema cache for this branch after mutations, then eagerly
|
|
220
|
-
* re-resolve on THIS host.
|
|
183
|
+
* re-resolve on THIS host. Every mutator calls this after `withSchemaLock`
|
|
184
|
+
* has returned, never inside the critical section.
|
|
221
185
|
*
|
|
222
186
|
* The eager re-resolve is the durable-snapshot window-E mitigation (see
|
|
223
187
|
* BranchSchemaCache's class docs): the mutating host's own scan is
|
|
@@ -271,68 +235,38 @@ export class SchemaOps {
|
|
|
271
235
|
async invalidateContentIdIndexes() {
|
|
272
236
|
await invalidateBranchContentCaches(this.branchRoot);
|
|
273
237
|
}
|
|
274
|
-
// --------------------------------------------------------------------------
|
|
275
|
-
// Path Normalization
|
|
276
|
-
// --------------------------------------------------------------------------
|
|
277
238
|
/**
|
|
278
239
|
* The single normalisation boundary for logical collection paths entering
|
|
279
|
-
* this class. Every public method that accepts
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
* (`CollectionEditor.tsx` passes `editingCollection.logicalPath` as-is),
|
|
287
|
-
* but `resolveCollectionPath(this.contentRoot, ...)` treats its second
|
|
288
|
-
* argument as relative to `this.contentRoot` — which already embeds the
|
|
289
|
-
* content-root segment(s). A prefixed path therefore resolved one level
|
|
290
|
-
* too deep and was reported as not found. `updateCollectionInner` used to
|
|
291
|
-
* strip the prefix itself (the only mutator that did); that bespoke strip
|
|
292
|
-
* is gone now that every entry point normalises here instead.
|
|
240
|
+
* this class. Every public method that accepts one calls this exactly
|
|
241
|
+
* once, before touching the path: `flattenSchema` (branch-schema-cache.ts)
|
|
242
|
+
* produces content-root-prefixed logical paths (e.g. "content/posts") that
|
|
243
|
+
* the editor round-trips straight back into every mutator, but
|
|
244
|
+
* `resolveCollectionPath` treats its path argument as already relative to
|
|
245
|
+
* `this.contentRoot` — without stripping, a prefixed path resolves one
|
|
246
|
+
* level too deep and is reported as not found.
|
|
293
247
|
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
* no trailing segment): that has no trailing "/" to match, so it passes
|
|
302
|
-
* through unchanged and the `=== this.contentRootName` checks in
|
|
248
|
+
* Strips ONE leading `"{contentRootName}/"` if present — exact string
|
|
249
|
+
* match against the full, possibly multi-segment, `contentRootName`, never
|
|
250
|
+
* `path.basename()` (which would break a root like "cms/content") —
|
|
251
|
+
* otherwise returns the path unchanged, so both prefixed and unprefixed
|
|
252
|
+
* callers work. Leaves the bare root-collection sentinel
|
|
253
|
+
* (`collectionPath === this.contentRootName`) untouched: it has no
|
|
254
|
+
* trailing "/" to strip, so the `=== this.contentRootName` checks in
|
|
303
255
|
* `updateCollectionInner`/`updateOrderInner` keep working.
|
|
304
256
|
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
* collection — so a second call mutates a DIFFERENT collection rather than
|
|
312
|
-
* reporting not-found. Every entry point calls this exactly once today, so
|
|
313
|
-
* the ambiguity is unreachable; removing it needs the editor to send an
|
|
314
|
-
* unambiguously-scoped path, tracked in
|
|
315
|
-
* .claude/future-tasks/collection-path-content-root-ambiguity.md.
|
|
316
|
-
*
|
|
317
|
-
* A future public method that accepts a logical collection path must call
|
|
318
|
-
* this first, and only once, the same way every existing one does — that is
|
|
319
|
-
* the "safe by construction" contract this boundary is meant to provide.
|
|
257
|
+
* NOT idempotent — a sub-collection literally named after the content
|
|
258
|
+
* root can re-normalise to the ROOT collection on a second call, mutating
|
|
259
|
+
* the wrong one instead of reporting not-found. Every entry point calls
|
|
260
|
+
* this exactly once today, so the ambiguity is unreachable; see
|
|
261
|
+
* .claude/future-tasks/collection-path-content-root-ambiguity.md. A future
|
|
262
|
+
* method that accepts a logical collection path must do the same.
|
|
320
263
|
*/
|
|
321
264
|
normalizeCollectionPath(collectionPath) {
|
|
322
265
|
return createLogicalPath(stripContentRootPrefix(collectionPath, this.contentRootName));
|
|
323
266
|
}
|
|
324
|
-
// --------------------------------------------------------------------------
|
|
325
|
-
// Validation Helpers
|
|
326
|
-
// --------------------------------------------------------------------------
|
|
327
|
-
/**
|
|
328
|
-
* Validate that a schema reference exists in the registry
|
|
329
|
-
*/
|
|
330
267
|
validateSchemaReference(schemaKey) {
|
|
331
268
|
return schemaKey in this.entrySchemaRegistry;
|
|
332
269
|
}
|
|
333
|
-
/**
|
|
334
|
-
* Validate all schema references in entry types
|
|
335
|
-
*/
|
|
336
270
|
validateEntryTypeSchemas(entryTypes) {
|
|
337
271
|
for (const entryType of entryTypes) {
|
|
338
272
|
if (!this.validateSchemaReference(entryType.schema)) {
|
|
@@ -355,15 +289,8 @@ export class SchemaOps {
|
|
|
355
289
|
}
|
|
356
290
|
return { valid: true, normalizedPath: result.normalizedPath };
|
|
357
291
|
}
|
|
358
|
-
// --------------------------------------------------------------------------
|
|
359
|
-
// Read Operations
|
|
360
|
-
// --------------------------------------------------------------------------
|
|
361
|
-
/**
|
|
362
|
-
* Read a collection's .collection.json file
|
|
363
|
-
*/
|
|
364
292
|
async readCollectionMeta(collectionPath) {
|
|
365
293
|
const normalizedPath = this.normalizeCollectionPath(collectionPath);
|
|
366
|
-
// Resolve logical path to physical path with embedded IDs
|
|
367
294
|
const physicalPath = await resolveCollectionPath(this.contentRoot, normalizedPath);
|
|
368
295
|
if (!physicalPath) {
|
|
369
296
|
return null;
|
|
@@ -380,9 +307,6 @@ export class SchemaOps {
|
|
|
380
307
|
throw err;
|
|
381
308
|
}
|
|
382
309
|
}
|
|
383
|
-
/**
|
|
384
|
-
* Read root collection meta (content/.collection.json)
|
|
385
|
-
*/
|
|
386
310
|
async readRootCollectionMeta() {
|
|
387
311
|
const metaPath = path.join(this.contentRoot, '.collection.json');
|
|
388
312
|
try {
|
|
@@ -409,11 +333,9 @@ export class SchemaOps {
|
|
|
409
333
|
try {
|
|
410
334
|
const entries = await fs.readdir(physicalPath, { withFileTypes: true });
|
|
411
335
|
for (const entry of entries) {
|
|
412
|
-
// Content files mean not empty
|
|
413
336
|
if (entry.isFile() && entry.name !== '.collection.json') {
|
|
414
337
|
return false;
|
|
415
338
|
}
|
|
416
|
-
// Child collection directories mean not empty
|
|
417
339
|
if (entry.isDirectory()) {
|
|
418
340
|
try {
|
|
419
341
|
await fs.access(path.join(physicalPath, entry.name, '.collection.json'));
|
|
@@ -433,45 +355,27 @@ export class SchemaOps {
|
|
|
433
355
|
throw err;
|
|
434
356
|
}
|
|
435
357
|
}
|
|
436
|
-
// --------------------------------------------------------------------------
|
|
437
|
-
// Write Operations
|
|
438
|
-
//
|
|
439
358
|
// These are plain atomic writes with NO locking of their own: every caller
|
|
440
359
|
// reaches them from inside a public mutator's `withSchemaLock` critical
|
|
441
360
|
// section (see the module doc comment), which is what actually protects
|
|
442
361
|
// against concurrent writers. Locking here too would be redundant and
|
|
443
362
|
// would mislead a reader into thinking THIS is where the safety comes
|
|
444
363
|
// from.
|
|
445
|
-
// --------------------------------------------------------------------------
|
|
446
|
-
/**
|
|
447
|
-
* Write a collection's .collection.json file
|
|
448
|
-
*/
|
|
449
364
|
async writeCollectionMeta(physicalPath, meta) {
|
|
450
365
|
const metaPath = path.join(physicalPath, '.collection.json');
|
|
451
366
|
const content = JSON.stringify(meta, null, 2) + '\n';
|
|
452
367
|
await atomicWriteFile(metaPath, content);
|
|
453
368
|
}
|
|
454
|
-
/**
|
|
455
|
-
* Write root collection meta
|
|
456
|
-
*/
|
|
457
369
|
async writeRootCollectionMeta(meta) {
|
|
458
370
|
const metaPath = path.join(this.contentRoot, '.collection.json');
|
|
459
371
|
const content = JSON.stringify(meta, null, 2) + '\n';
|
|
460
372
|
await atomicWriteFile(metaPath, content);
|
|
461
373
|
}
|
|
462
|
-
// --------------------------------------------------------------------------
|
|
463
|
-
// Collection Operations
|
|
464
|
-
// --------------------------------------------------------------------------
|
|
465
|
-
/**
|
|
466
|
-
* Create a new collection
|
|
467
|
-
*/
|
|
468
374
|
async createCollection(input) {
|
|
469
|
-
// Validate input
|
|
470
375
|
const parseResult = createCollectionInputSchema.safeParse(input);
|
|
471
376
|
if (!parseResult.success) {
|
|
472
377
|
throw new Error(`Invalid input: ${parseResult.error.message}`);
|
|
473
378
|
}
|
|
474
|
-
// Validate schema references
|
|
475
379
|
const schemaValidation = this.validateEntryTypeSchemas(input.entries);
|
|
476
380
|
if (!schemaValidation.valid) {
|
|
477
381
|
throw new Error(schemaValidation.error);
|
|
@@ -485,12 +389,11 @@ export class SchemaOps {
|
|
|
485
389
|
? { ...input, parentPath: this.normalizeCollectionPath(input.parentPath) }
|
|
486
390
|
: input;
|
|
487
391
|
const result = await this.withSchemaLock(() => this.createCollectionInner(normalizedInput));
|
|
488
|
-
//
|
|
392
|
+
// Outside the lock -- see invalidateSchemaCache.
|
|
489
393
|
await this.invalidateSchemaCache();
|
|
490
394
|
return result;
|
|
491
395
|
}
|
|
492
396
|
async createCollectionInner(input) {
|
|
493
|
-
// Determine parent directory
|
|
494
397
|
let parentPhysicalPath;
|
|
495
398
|
if (input.parentPath) {
|
|
496
399
|
const resolved = await resolveCollectionPath(this.contentRoot, input.parentPath);
|
|
@@ -502,18 +405,16 @@ export class SchemaOps {
|
|
|
502
405
|
else {
|
|
503
406
|
parentPhysicalPath = this.contentRoot;
|
|
504
407
|
}
|
|
505
|
-
// Generate embedded ID for new collection
|
|
506
408
|
const contentId = generateId();
|
|
507
409
|
const dirName = `${input.name}.${contentId}`;
|
|
508
410
|
const physicalPath = path.join(parentPhysicalPath, dirName);
|
|
509
|
-
// Defense-in-depth
|
|
411
|
+
// Defense-in-depth: the name pattern above already prevents
|
|
510
412
|
// traversal, but independently assert the resolved path stays within the
|
|
511
413
|
// content root before any filesystem write.
|
|
512
414
|
const containment = this.validatePath(physicalPath);
|
|
513
415
|
if (!containment.valid) {
|
|
514
416
|
throw new Error(`Invalid collection path: ${containment.error}`);
|
|
515
417
|
}
|
|
516
|
-
// Create directory
|
|
517
418
|
await fs.mkdir(physicalPath, { recursive: true });
|
|
518
419
|
await this.invalidateContentIdIndexes();
|
|
519
420
|
// Build collection meta with empty order array (required for ordering support)
|
|
@@ -528,40 +429,32 @@ export class SchemaOps {
|
|
|
528
429
|
default: et.default,
|
|
529
430
|
maxItems: et.maxItems,
|
|
530
431
|
})),
|
|
531
|
-
order: [],
|
|
432
|
+
order: [],
|
|
532
433
|
};
|
|
533
|
-
// Write .collection.json
|
|
534
434
|
await this.writeCollectionMeta(physicalPath, meta);
|
|
535
|
-
// Add new collection's contentId to parent's order array
|
|
536
435
|
// For root-level collections (empty parentPath), we don't update parent order
|
|
537
436
|
const parentLogicalPath = input.parentPath
|
|
538
437
|
? createLogicalPath(input.parentPath)
|
|
539
438
|
: createLogicalPath('');
|
|
540
439
|
const parentMeta = input.parentPath ? await this.readCollectionMeta(parentLogicalPath) : null;
|
|
541
440
|
if (parentMeta) {
|
|
542
|
-
// Initialize parent's order array if it doesn't exist
|
|
543
441
|
const existingOrder = parentMeta.order ?? [];
|
|
544
442
|
parentMeta.order = [...existingOrder, contentId];
|
|
545
443
|
await this.writeCollectionMeta(parentPhysicalPath, parentMeta);
|
|
546
444
|
}
|
|
547
|
-
// Build logical path
|
|
548
445
|
const logicalPath = input.parentPath
|
|
549
446
|
? createLogicalPath(`${input.parentPath}/${input.name}`)
|
|
550
447
|
: createLogicalPath(input.name);
|
|
551
448
|
return { collectionPath: logicalPath, contentId };
|
|
552
449
|
}
|
|
553
|
-
/**
|
|
554
|
-
* Update a collection's metadata
|
|
555
|
-
*/
|
|
556
450
|
async updateCollection(collectionPath, updates) {
|
|
557
|
-
// Validate input
|
|
558
451
|
const parseResult = updateCollectionInputSchema.safeParse(updates);
|
|
559
452
|
if (!parseResult.success) {
|
|
560
453
|
throw new Error(`Invalid input: ${parseResult.error.message}`);
|
|
561
454
|
}
|
|
562
455
|
const normalizedPath = this.normalizeCollectionPath(collectionPath);
|
|
563
456
|
await this.withSchemaLock(() => this.updateCollectionInner(normalizedPath, updates));
|
|
564
|
-
//
|
|
457
|
+
// Outside the lock -- see invalidateSchemaCache.
|
|
565
458
|
await this.invalidateSchemaCache();
|
|
566
459
|
}
|
|
567
460
|
/**
|
|
@@ -577,10 +470,7 @@ export class SchemaOps {
|
|
|
577
470
|
* comment for the single boundary that owns that now.
|
|
578
471
|
*/
|
|
579
472
|
async updateCollectionInner(collectionPath, updates) {
|
|
580
|
-
// Check if this is the root collection (path equals the configured content
|
|
581
|
-
// root, e.g. "content" or "cms/content")
|
|
582
473
|
if (collectionPath === this.contentRootName) {
|
|
583
|
-
// Update root collection meta
|
|
584
474
|
let meta = await this.readRootCollectionMeta();
|
|
585
475
|
if (!meta) {
|
|
586
476
|
meta = {};
|
|
@@ -595,22 +485,16 @@ export class SchemaOps {
|
|
|
595
485
|
await this.writeRootCollectionMeta(meta);
|
|
596
486
|
return;
|
|
597
487
|
}
|
|
598
|
-
// Resolve path for regular collection. collectionPath is already
|
|
599
|
-
// normalized (no content-root prefix) by the caller — see this method's
|
|
600
|
-
// doc comment.
|
|
601
488
|
const physicalPath = await resolveCollectionPath(this.contentRoot, collectionPath);
|
|
602
489
|
if (!physicalPath) {
|
|
603
490
|
throw new Error(`Collection not found: ${collectionPath}`);
|
|
604
491
|
}
|
|
605
|
-
// Read existing meta
|
|
606
492
|
const meta = await this.readCollectionMeta(collectionPath);
|
|
607
493
|
if (!meta) {
|
|
608
494
|
throw new Error(`Collection meta not found: ${collectionPath}`);
|
|
609
495
|
}
|
|
610
|
-
// Handle slug change (directory rename) if provided
|
|
611
496
|
let finalPhysicalPath = physicalPath;
|
|
612
497
|
if (updates.slug !== undefined) {
|
|
613
|
-
// Extract current slug and ID from physical path
|
|
614
498
|
// Format: /path/to/{slug}.{12-char-id}
|
|
615
499
|
const dirName = path.basename(physicalPath);
|
|
616
500
|
const parts = dirName.split('.');
|
|
@@ -619,17 +503,13 @@ export class SchemaOps {
|
|
|
619
503
|
}
|
|
620
504
|
const currentSlug = parts[0];
|
|
621
505
|
const contentId = parts[1];
|
|
622
|
-
// Only rename if slug is actually different
|
|
623
506
|
if (updates.slug !== currentSlug) {
|
|
624
|
-
// Validate new slug (alphanumeric + hyphens, lowercase)
|
|
625
507
|
if (!SAFE_NAME_PATTERN.test(updates.slug)) {
|
|
626
508
|
throw new Error(`Slug ${SAFE_NAME_MESSAGE}`);
|
|
627
509
|
}
|
|
628
|
-
// Build new path with new slug + same ID
|
|
629
510
|
const parentDir = path.dirname(physicalPath);
|
|
630
511
|
const newDirName = `${updates.slug}.${contentId}`;
|
|
631
512
|
const newPhysicalPath = path.join(parentDir, newDirName);
|
|
632
|
-
// Check if any collection with this slug already exists
|
|
633
513
|
// Need to check for any directory matching {slug}.{any-id}
|
|
634
514
|
try {
|
|
635
515
|
const entries = await fs.readdir(parentDir, { withFileTypes: true });
|
|
@@ -643,34 +523,22 @@ export class SchemaOps {
|
|
|
643
523
|
}
|
|
644
524
|
}
|
|
645
525
|
catch (err) {
|
|
646
|
-
// Re-throw "already exists" errors
|
|
647
526
|
if (err.message.includes('already exists')) {
|
|
648
527
|
throw err;
|
|
649
528
|
}
|
|
650
529
|
// Ignore other errors (e.g., ENOENT if parent dir doesn't exist somehow)
|
|
651
530
|
}
|
|
652
|
-
//
|
|
653
|
-
//
|
|
654
|
-
//
|
|
655
|
-
//
|
|
656
|
-
//
|
|
657
|
-
//
|
|
658
|
-
//
|
|
659
|
-
// the index entry has anything to collide with.
|
|
660
|
-
//
|
|
661
|
-
// That holds only as far as the check above does, and it is narrower than it looks: it
|
|
662
|
-
// compares case-SENSITIVELY (so `guides` beside an existing `Guides.{id}` passes it) and
|
|
663
|
-
// it requires `parts.length === 2 && isValidId(parts[1])` (so a plain, ID-less `docs/`
|
|
664
|
-
// directory at the destination passes it too). Descendants CAN then contest.
|
|
665
|
-
// See .claude/future-tasks/collection-sibling-name-uniqueness.md, which tracks both the
|
|
666
|
-
// missing create-side check and this case hole.
|
|
531
|
+
// Contested-URL guard: renaming re-paths every entry beneath this collection, so its own
|
|
532
|
+
// index entry can end up sharing a URL with a same-slugged entry already in the parent
|
|
533
|
+
// (see url-collision.ts). Only the index entry is at risk — the sibling-name check above
|
|
534
|
+
// already rules out a `{slug}.{id}` directory at the destination — but that check is
|
|
535
|
+
// case-SENSITIVE and lets an ID-less `docs/`-style directory through, so a case or
|
|
536
|
+
// ID-less collision can still slip past it for entries deeper than the index; see
|
|
537
|
+
// .claude/future-tasks/collection-sibling-name-uniqueness.md.
|
|
667
538
|
if (await findIndexEntryIn(physicalPath)) {
|
|
668
|
-
// Renaming TO "index" collapses this collection onto
|
|
669
|
-
//
|
|
670
|
-
//
|
|
671
|
-
// entry, which collapses onto `<parentPath>` itself -- a different URL, not a collision.
|
|
672
|
-
// Skip the lookup in that case; see url-collision.ts's `findUrlPathClaimant` for the
|
|
673
|
-
// create-time version of this same false positive.
|
|
539
|
+
// Renaming TO "index" collapses this collection onto its OWN new path, not the
|
|
540
|
+
// parent's — see url-collision.ts's findUrlPathClaimant for the equivalent create-time
|
|
541
|
+
// check, which skips this same false positive the same way.
|
|
674
542
|
const conflicting = isIndexSlug(updates.slug)
|
|
675
543
|
? null
|
|
676
544
|
: await findEntryBySlugIn(parentDir, updates.slug);
|
|
@@ -688,7 +556,6 @@ export class SchemaOps {
|
|
|
688
556
|
await this.invalidateContentIdIndexes();
|
|
689
557
|
}
|
|
690
558
|
}
|
|
691
|
-
// Apply metadata updates
|
|
692
559
|
if (updates.name !== undefined) {
|
|
693
560
|
meta.name = updates.name;
|
|
694
561
|
}
|
|
@@ -707,7 +574,7 @@ export class SchemaOps {
|
|
|
707
574
|
async deleteCollection(collectionPath) {
|
|
708
575
|
const normalizedPath = this.normalizeCollectionPath(collectionPath);
|
|
709
576
|
await this.withSchemaLock(() => this.deleteCollectionInner(normalizedPath));
|
|
710
|
-
//
|
|
577
|
+
// Outside the lock -- see invalidateSchemaCache.
|
|
711
578
|
await this.invalidateSchemaCache();
|
|
712
579
|
}
|
|
713
580
|
async deleteCollectionInner(collectionPath) {
|
|
@@ -717,53 +584,39 @@ export class SchemaOps {
|
|
|
717
584
|
if (!isEmpty) {
|
|
718
585
|
throw new Error('Collection must be empty before deletion. Delete all entries first.');
|
|
719
586
|
}
|
|
720
|
-
// Resolve path
|
|
721
587
|
const physicalPath = await resolveCollectionPath(this.contentRoot, collectionPath);
|
|
722
588
|
if (!physicalPath) {
|
|
723
589
|
throw new Error(`Collection not found: ${collectionPath}`);
|
|
724
590
|
}
|
|
725
|
-
// Delete the directory (including .collection.json)
|
|
726
591
|
await fs.rm(physicalPath, { recursive: true });
|
|
727
592
|
await this.invalidateContentIdIndexes();
|
|
728
593
|
}
|
|
729
|
-
// --------------------------------------------------------------------------
|
|
730
|
-
// Entry Type Operations
|
|
731
|
-
// --------------------------------------------------------------------------
|
|
732
|
-
/**
|
|
733
|
-
* Add an entry type to a collection
|
|
734
|
-
*/
|
|
735
594
|
async addEntryType(collectionPath, entryType) {
|
|
736
|
-
// Validate input
|
|
737
595
|
const parseResult = entryTypeInputSchema.safeParse(entryType);
|
|
738
596
|
if (!parseResult.success) {
|
|
739
597
|
throw new Error(`Invalid input: ${parseResult.error.message}`);
|
|
740
598
|
}
|
|
741
|
-
// Validate schema reference
|
|
742
599
|
if (!this.validateSchemaReference(entryType.schema)) {
|
|
743
600
|
const available = Object.keys(this.entrySchemaRegistry).join(', ');
|
|
744
601
|
throw new Error(`Schema reference "${entryType.schema}" not found. Available: ${available}`);
|
|
745
602
|
}
|
|
746
603
|
const normalizedPath = this.normalizeCollectionPath(collectionPath);
|
|
747
604
|
await this.withSchemaLock(() => this.addEntryTypeInner(normalizedPath, entryType));
|
|
748
|
-
//
|
|
605
|
+
// Outside the lock -- see invalidateSchemaCache.
|
|
749
606
|
await this.invalidateSchemaCache();
|
|
750
607
|
}
|
|
751
608
|
async addEntryTypeInner(collectionPath, entryType) {
|
|
752
|
-
// Resolve path
|
|
753
609
|
const physicalPath = await resolveCollectionPath(this.contentRoot, collectionPath);
|
|
754
610
|
if (!physicalPath) {
|
|
755
611
|
throw new Error(`Collection not found: ${collectionPath}`);
|
|
756
612
|
}
|
|
757
|
-
// Read existing meta
|
|
758
613
|
const meta = await this.readCollectionMeta(collectionPath);
|
|
759
614
|
if (!meta) {
|
|
760
615
|
throw new Error(`Collection meta not found: ${collectionPath}`);
|
|
761
616
|
}
|
|
762
|
-
// Check for duplicate name
|
|
763
617
|
if (meta.entries?.some((et) => et.name === entryType.name)) {
|
|
764
618
|
throw new Error(`Entry type "${entryType.name}" already exists in this collection`);
|
|
765
619
|
}
|
|
766
|
-
// Add entry type
|
|
767
620
|
meta.entries = meta.entries || [];
|
|
768
621
|
meta.entries.push({
|
|
769
622
|
name: entryType.name,
|
|
@@ -773,35 +626,27 @@ export class SchemaOps {
|
|
|
773
626
|
default: entryType.default,
|
|
774
627
|
maxItems: entryType.maxItems,
|
|
775
628
|
});
|
|
776
|
-
// Write back
|
|
777
629
|
await this.writeCollectionMeta(physicalPath, meta);
|
|
778
630
|
}
|
|
779
|
-
/**
|
|
780
|
-
* Update an entry type in a collection
|
|
781
|
-
*/
|
|
782
631
|
async updateEntryType(collectionPath, entryTypeName, updates) {
|
|
783
|
-
// Validate input
|
|
784
632
|
const parseResult = updateEntryTypeInputSchema.safeParse(updates);
|
|
785
633
|
if (!parseResult.success) {
|
|
786
634
|
throw new Error(`Invalid input: ${parseResult.error.message}`);
|
|
787
635
|
}
|
|
788
|
-
// Validate schema reference if provided
|
|
789
636
|
if (updates.schema && !this.validateSchemaReference(updates.schema)) {
|
|
790
637
|
const available = Object.keys(this.entrySchemaRegistry).join(', ');
|
|
791
638
|
throw new Error(`Schema reference "${updates.schema}" not found. Available: ${available}`);
|
|
792
639
|
}
|
|
793
640
|
const normalizedPath = this.normalizeCollectionPath(collectionPath);
|
|
794
641
|
await this.withSchemaLock(() => this.updateEntryTypeInner(normalizedPath, entryTypeName, updates));
|
|
795
|
-
//
|
|
642
|
+
// Outside the lock -- see invalidateSchemaCache.
|
|
796
643
|
await this.invalidateSchemaCache();
|
|
797
644
|
}
|
|
798
645
|
async updateEntryTypeInner(collectionPath, entryTypeName, updates) {
|
|
799
|
-
// Breaking-change usage guard
|
|
800
|
-
//
|
|
801
|
-
//
|
|
802
|
-
//
|
|
803
|
-
// the write closes that window. Error message preserved exactly — the
|
|
804
|
-
// handler's catch surfaces it verbatim as a 400.
|
|
646
|
+
// Breaking-change usage guard: the usage count runs under the same lock
|
|
647
|
+
// as the write; counted outside it, a concurrent write could land an
|
|
648
|
+
// entry between the count and this write (TOCTOU). Error message
|
|
649
|
+
// preserved exactly — the handler's catch surfaces it verbatim as a 400.
|
|
805
650
|
const isBreakingChange = updates.format !== undefined || updates.schema !== undefined;
|
|
806
651
|
if (isBreakingChange) {
|
|
807
652
|
const usageCount = await this.countEntriesUsingType(collectionPath, entryTypeName);
|
|
@@ -810,22 +655,18 @@ export class SchemaOps {
|
|
|
810
655
|
throw new Error(`Cannot modify schema or format for entry type with existing ${entryWord}. ${usageCount} ${entryWord} currently use this type.`);
|
|
811
656
|
}
|
|
812
657
|
}
|
|
813
|
-
// Resolve path
|
|
814
658
|
const physicalPath = await resolveCollectionPath(this.contentRoot, collectionPath);
|
|
815
659
|
if (!physicalPath) {
|
|
816
660
|
throw new Error(`Collection not found: ${collectionPath}`);
|
|
817
661
|
}
|
|
818
|
-
// Read existing meta
|
|
819
662
|
const meta = await this.readCollectionMeta(collectionPath);
|
|
820
663
|
if (!meta) {
|
|
821
664
|
throw new Error(`Collection meta not found: ${collectionPath}`);
|
|
822
665
|
}
|
|
823
|
-
// Find entry type
|
|
824
666
|
const entryType = meta.entries?.find((et) => et.name === entryTypeName);
|
|
825
667
|
if (!entryType) {
|
|
826
668
|
throw new Error(`Entry type "${entryTypeName}" not found in collection`);
|
|
827
669
|
}
|
|
828
|
-
// Apply updates
|
|
829
670
|
if (updates.label !== undefined) {
|
|
830
671
|
entryType.label = updates.label;
|
|
831
672
|
}
|
|
@@ -841,55 +682,41 @@ export class SchemaOps {
|
|
|
841
682
|
if (updates.maxItems !== undefined) {
|
|
842
683
|
entryType.maxItems = updates.maxItems;
|
|
843
684
|
}
|
|
844
|
-
// Write back
|
|
845
685
|
await this.writeCollectionMeta(physicalPath, meta);
|
|
846
686
|
}
|
|
847
|
-
/**
|
|
848
|
-
* Remove an entry type from a collection
|
|
849
|
-
*/
|
|
850
687
|
async removeEntryType(collectionPath, entryTypeName) {
|
|
851
688
|
const normalizedPath = this.normalizeCollectionPath(collectionPath);
|
|
852
689
|
await this.withSchemaLock(() => this.removeEntryTypeInner(normalizedPath, entryTypeName));
|
|
853
|
-
//
|
|
690
|
+
// Outside the lock -- see invalidateSchemaCache.
|
|
854
691
|
await this.invalidateSchemaCache();
|
|
855
692
|
}
|
|
856
693
|
async removeEntryTypeInner(collectionPath, entryTypeName) {
|
|
857
|
-
// Resolve path
|
|
858
694
|
const physicalPath = await resolveCollectionPath(this.contentRoot, collectionPath);
|
|
859
695
|
if (!physicalPath) {
|
|
860
696
|
throw new Error(`Collection not found: ${collectionPath}`);
|
|
861
697
|
}
|
|
862
|
-
// Read existing meta
|
|
863
698
|
const meta = await this.readCollectionMeta(collectionPath);
|
|
864
699
|
if (!meta) {
|
|
865
700
|
throw new Error(`Collection meta not found: ${collectionPath}`);
|
|
866
701
|
}
|
|
867
|
-
// Check entry type exists
|
|
868
702
|
const index = meta.entries?.findIndex((et) => et.name === entryTypeName) ?? -1;
|
|
869
703
|
if (index === -1) {
|
|
870
704
|
throw new Error(`Entry type "${entryTypeName}" not found in collection`);
|
|
871
705
|
}
|
|
872
|
-
// Ensure at least one entry type remains
|
|
873
706
|
if (meta.entries.length === 1) {
|
|
874
707
|
throw new Error('Cannot remove last entry type. Collection must have at least one entry type.');
|
|
875
708
|
}
|
|
876
|
-
// Check for entries still using this type
|
|
877
709
|
const usageCount = await this.countEntriesUsingType(collectionPath, entryTypeName);
|
|
878
710
|
if (usageCount > 0) {
|
|
879
711
|
throw new Error(`Cannot remove entry type "${entryTypeName}": ${usageCount} ${usageCount === 1 ? 'entry still uses' : 'entries still use'} it. ` +
|
|
880
712
|
'Delete or migrate those entries first.');
|
|
881
713
|
}
|
|
882
|
-
// Remove entry type
|
|
883
714
|
meta.entries.splice(index, 1);
|
|
884
|
-
// Write back
|
|
885
715
|
await this.writeCollectionMeta(physicalPath, meta);
|
|
886
716
|
}
|
|
887
|
-
// --------------------------------------------------------------------------
|
|
888
|
-
// Usage Counting
|
|
889
|
-
// --------------------------------------------------------------------------
|
|
890
717
|
/**
|
|
891
718
|
* Count the number of entries using a specific entry type in a collection.
|
|
892
|
-
*
|
|
719
|
+
* Prevents breaking changes to entry types that have existing content.
|
|
893
720
|
*
|
|
894
721
|
* @param collectionPath - Logical path to the collection (e.g., "content/posts")
|
|
895
722
|
* @param entryTypeName - Name of the entry type to count
|
|
@@ -905,29 +732,22 @@ export class SchemaOps {
|
|
|
905
732
|
*/
|
|
906
733
|
async countEntriesUsingType(collectionPath, entryTypeName) {
|
|
907
734
|
const normalizedPath = this.normalizeCollectionPath(collectionPath);
|
|
908
|
-
// Resolve collection physical path
|
|
909
735
|
const physicalPath = await resolveCollectionPath(this.contentRoot, normalizedPath);
|
|
910
736
|
if (!physicalPath) {
|
|
911
|
-
// Collection doesn't exist yet - return 0
|
|
912
737
|
return 0;
|
|
913
738
|
}
|
|
914
739
|
try {
|
|
915
|
-
// Read directory entries
|
|
916
740
|
const entries = await fs.readdir(physicalPath, { withFileTypes: true });
|
|
917
741
|
// Count files matching pattern: {entryTypeName}.{slug}.{id}.{ext}
|
|
918
742
|
let count = 0;
|
|
919
743
|
for (const entry of entries) {
|
|
920
|
-
// Skip directories and hidden files
|
|
921
744
|
if (entry.isDirectory() || entry.name.startsWith('.')) {
|
|
922
745
|
continue;
|
|
923
746
|
}
|
|
924
|
-
// Parse filename: type.slug.id.ext
|
|
925
747
|
const parts = entry.name.split('.');
|
|
926
|
-
// Need at least 4 parts: type, slug, id, ext
|
|
927
748
|
if (parts.length < 4) {
|
|
928
749
|
continue;
|
|
929
750
|
}
|
|
930
|
-
// Check if first part matches entry type name
|
|
931
751
|
if (parts[0] !== entryTypeName) {
|
|
932
752
|
continue;
|
|
933
753
|
}
|
|
@@ -947,23 +767,14 @@ export class SchemaOps {
|
|
|
947
767
|
throw err;
|
|
948
768
|
}
|
|
949
769
|
}
|
|
950
|
-
// --------------------------------------------------------------------------
|
|
951
|
-
// Order Operations
|
|
952
|
-
// --------------------------------------------------------------------------
|
|
953
|
-
/**
|
|
954
|
-
* Update the order of items in a collection
|
|
955
|
-
*/
|
|
956
770
|
async updateOrder(collectionPath, order) {
|
|
957
771
|
const normalizedPath = this.normalizeCollectionPath(collectionPath);
|
|
958
772
|
await this.withSchemaLock(() => this.updateOrderInner(normalizedPath, order));
|
|
959
|
-
//
|
|
773
|
+
// Outside the lock -- see invalidateSchemaCache.
|
|
960
774
|
await this.invalidateSchemaCache();
|
|
961
775
|
}
|
|
962
776
|
async updateOrderInner(collectionPath, order) {
|
|
963
|
-
// Check if this is the root collection (path equals the configured content
|
|
964
|
-
// root, e.g. "content" or "cms/content")
|
|
965
777
|
if (collectionPath === this.contentRootName) {
|
|
966
|
-
// Update root collection meta
|
|
967
778
|
let meta = await this.readRootCollectionMeta();
|
|
968
779
|
if (!meta) {
|
|
969
780
|
meta = {};
|
|
@@ -972,16 +783,8 @@ export class SchemaOps {
|
|
|
972
783
|
await this.writeRootCollectionMeta(meta);
|
|
973
784
|
return;
|
|
974
785
|
}
|
|
975
|
-
//
|
|
976
|
-
// content-root prefix) by the public updateOrder above. Calls
|
|
977
|
-
// updateCollectionInner DIRECTLY, never the public updateCollection:
|
|
978
|
-
// we're already inside withSchemaLock's critical section here, and
|
|
979
|
-
// withLock is not re-entrant — going through updateCollection would call
|
|
980
|
-
// withSchemaLock again and deadlock waiting on the lock it itself holds.
|
|
786
|
+
// Inner, not updateCollection: withSchemaLock is not re-entrant (see its doc).
|
|
981
787
|
await this.updateCollectionInner(collectionPath, { order });
|
|
982
788
|
}
|
|
983
789
|
}
|
|
984
|
-
// ============================================================================
|
|
985
|
-
// Exports
|
|
986
|
-
// ============================================================================
|
|
987
790
|
export { createCollectionInputSchema, updateCollectionInputSchema, entryTypeInputSchema, updateEntryTypeInputSchema, };
|