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
package/dist/content-id-index.js
CHANGED
|
@@ -28,106 +28,46 @@ import { canopyLogWarn } from './utils/logger.js';
|
|
|
28
28
|
/** Logical path representing entries stored at the branch root (no parent collection). Rare in practice. */
|
|
29
29
|
const EMPTY_LOGICAL_PATH = '';
|
|
30
30
|
/**
|
|
31
|
-
* Strips embedded
|
|
32
|
-
*
|
|
33
|
-
* This is a module-private helper for internal path conversion only.
|
|
31
|
+
* Strips the embedded ID from each physical segment to produce a logical path:
|
|
32
|
+
* "content/posts.a1b2c3d4e5f6" → "content/posts".
|
|
34
33
|
*/
|
|
35
34
|
function toLogicalCollectionPath(physicalPath) {
|
|
36
35
|
if (physicalPath === '.')
|
|
37
36
|
return EMPTY_LOGICAL_PATH;
|
|
38
|
-
// extractSlugFromFilename strips the ID (and extension) from each segment
|
|
39
37
|
return physicalPath
|
|
40
38
|
.split('/')
|
|
41
39
|
.map((seg) => extractSlugFromFilename(seg))
|
|
42
40
|
.join('/');
|
|
43
41
|
}
|
|
44
42
|
/**
|
|
45
|
-
*
|
|
43
|
+
* Bidirectional ContentId <-> path index over `{type}.{slug}.{12-char-id}.{ext}`
|
|
44
|
+
* files and `{slug}.{12-char-id}/` directories, scanned lazily on first access
|
|
45
|
+
* into per-process memory. Filenames on disk are the source of truth; the class
|
|
46
|
+
* is not thread-safe and takes no locks. Staleness is bounded as the module
|
|
47
|
+
* header describes, plus ContentStore's suspicious-lookup rebuild.
|
|
46
48
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
49
|
+
* A duplicate embedded ID always predates the scan: concurrent creates each
|
|
50
|
+
* generate a unique ID, and ContentStore.write() refuses to recreate an entry
|
|
51
|
+
* file whose ID the directory listing places at a different slug. A crash does
|
|
52
|
+
* produce one — renameEntry()'s `fs.link()` then `fs.unlink()` is not atomic.
|
|
50
53
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
54
|
+
* `buildFromFilenames()` quarantines such a collision rather than throwing,
|
|
55
|
+
* which would propagate out of every caller and brick read-by-id, reference
|
|
56
|
+
* resolution, listing and every write on the branch. The winner is the
|
|
57
|
+
* lexicographically-SMALLEST relativePath, compared as strings and never by
|
|
58
|
+
* visit order, so hosts scanning in different `readdir()` orders converge on
|
|
59
|
+
* the same one. Losers leave `idToLocation`/`pathToId`/`byCollection` entirely
|
|
60
|
+
* and are recorded in {@link getDuplicateIds} for branch-health.ts and the
|
|
61
|
+
* repair action in api/admin-branch-health.ts: degraded, not dead.
|
|
53
62
|
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* - Filenames on the filesystem are the source of truth
|
|
63
|
-
* - Each process discovers the same filenames when building its index
|
|
64
|
-
* - Write operations that change filenames are atomic (rename is atomic)
|
|
65
|
-
* - Read operations always reflect current filesystem state after index rebuild
|
|
66
|
-
*
|
|
67
|
-
* **How staleness is bounded:**
|
|
68
|
-
* - Same process: mutation sites invalidate registered ContentStores directly
|
|
69
|
-
* (content-index-registry.ts); the next access rebuilds.
|
|
70
|
-
* - Other processes: mutation sites bump an on-disk generation marker
|
|
71
|
-
* (content-index-generation.ts) that ContentStore probes on access, so
|
|
72
|
-
* stale indexes converge within the probe interval plus NFS caching delay.
|
|
73
|
-
* - Residual windows self-heal via ContentStore's suspicious-lookup rebuild
|
|
74
|
-
* (ID miss or index hit whose file is gone).
|
|
75
|
-
*
|
|
76
|
-
* **Race condition handling:**
|
|
77
|
-
* - Multiple processes creating entries simultaneously: each generates a unique ID,
|
|
78
|
-
* so this cannot itself manufacture a duplicate-ID pair.
|
|
79
|
-
* - ContentStore.write() refuses to recreate an entry file whose ID the
|
|
80
|
-
* directory listing places at a different slug (existence guard), so stale
|
|
81
|
-
* indexes cannot manufacture duplicate-ID files either. Together these two
|
|
82
|
-
* guarantees mean buildFromFilenames() can only ever encounter a duplicate
|
|
83
|
-
* ID that ALREADY existed on disk before the scan started (see below).
|
|
84
|
-
*
|
|
85
|
-
* ## Duplicate-ID quarantine (not fail-fast)
|
|
86
|
-
*
|
|
87
|
-
* A duplicate embedded ID can still land on disk from an external crash --
|
|
88
|
-
* e.g. ContentStore.renameEntry()'s `fs.link()` then `fs.unlink()` is not
|
|
89
|
-
* atomic, and a crash between the two steps leaves two filenames sharing one
|
|
90
|
-
* ID. Earlier versions of this class threw on the first such collision,
|
|
91
|
-
* which propagated out of every caller and (since ContentStore's rebuild has
|
|
92
|
-
* no catch around it) permanently bricked read-by-id, reference resolution,
|
|
93
|
-
* collection listing, and every write on the branch -- one bad pair took the
|
|
94
|
-
* whole branch down.
|
|
95
|
-
*
|
|
96
|
-
* `buildFromFilenames()` now quarantines a collision instead of throwing:
|
|
97
|
-
* the two (or more) candidates sharing an ID are resolved to a single
|
|
98
|
-
* deterministic winner -- the lexicographically-**smallest** relativePath,
|
|
99
|
-
* chosen purely by comparing the candidate strings, never by which one the
|
|
100
|
-
* scan happened to visit first. This is what makes the choice agree across
|
|
101
|
-
* hosts: two Lambdas (or a Lambda and the EC2 worker) scanning the same
|
|
102
|
-
* directory can observe entries in different `readdir()` orders, but the
|
|
103
|
-
* MIN of a fixed set of strings does not depend on visitation order, so
|
|
104
|
-
* every host converges on the same winner. The loser(s) are excluded from
|
|
105
|
-
* `idToLocation`/`pathToId`/`byCollection` entirely (as if not indexed) and
|
|
106
|
-
* recorded in {@link getDuplicateIds} for admin-facing health reporting
|
|
107
|
-
* (branch-health.ts) and repair (api/admin-branch-health.ts). A branch with a
|
|
108
|
-
* quarantined duplicate is degraded (that one ID pair is invisible to
|
|
109
|
-
* ID-based lookups) but not dead.
|
|
110
|
-
*
|
|
111
|
-
* ### What quarantine does NOT promise
|
|
112
|
-
*
|
|
113
|
-
* Quarantining is an INDEX decision: this scan never touches the filesystem,
|
|
114
|
-
* so nothing here moves or deletes the dropped file. It does not follow that
|
|
115
|
-
* the dropped file is inert until an admin repairs it, and an earlier version
|
|
116
|
-
* of this comment wrongly claimed exactly that ("left untouched on disk until
|
|
117
|
-
* a human or the repair action moves them"). Slugs are resolved by directory
|
|
118
|
-
* scan (`ContentStore.buildPaths()`), which knows nothing about this
|
|
119
|
-
* quarantine, so the dropped file stays fully addressable by
|
|
120
|
-
* collection+slug: a stale editor tab can still save, delete or rename it.
|
|
121
|
-
*
|
|
122
|
-
* The dangerous case was the save. `ContentStore.write()`'s index-repair step
|
|
123
|
-
* reads "the index says this ID lives somewhere else" as "the slug changed"
|
|
124
|
-
* and unlinks that other file -- which, with a duplicate, is a DIFFERENT
|
|
125
|
-
* document (the kept one). It silently deleted the winner while reporting
|
|
126
|
-
* success. `write()` now refuses such a save with `DuplicateContentIdError`
|
|
127
|
-
* instead (see its "[F1] duplicate-ID guard"). `delete()`/`renameEntry()`
|
|
128
|
-
* were never affected: they only ever touch the file the caller addressed,
|
|
129
|
-
* and are deliberately still allowed, so removing or moving the dropped file
|
|
130
|
-
* by hand remains possible without an admin.
|
|
63
|
+
* Quarantine is an INDEX decision only: this scan never touches the filesystem,
|
|
64
|
+
* and slugs resolve by directory scan (`ContentStore.buildPaths()`), which
|
|
65
|
+
* knows nothing about it, so the dropped file stays addressable by
|
|
66
|
+
* collection+slug. Hence `write()` refuses such a save with
|
|
67
|
+
* `DuplicateContentIdError` — its index-repair step reads "this ID lives
|
|
68
|
+
* elsewhere" as "the slug changed" and would unlink the kept file, a different
|
|
69
|
+
* document. `delete()`/`renameEntry()` stay allowed, each touching only the
|
|
70
|
+
* file the caller addressed, so the dropped file can be removed by hand.
|
|
131
71
|
*/
|
|
132
72
|
export class ContentIdIndex {
|
|
133
73
|
constructor(root) {
|
|
@@ -135,18 +75,16 @@ export class ContentIdIndex {
|
|
|
135
75
|
this.pathToId = new Map();
|
|
136
76
|
this.byCollection = new Map();
|
|
137
77
|
/**
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* populated by add()/remove()/updatePath(), which still throw on collision
|
|
141
|
-
* -- see those methods' doc comments.
|
|
78
|
+
* Quarantined (losing) paths per duplicated id, populated by
|
|
79
|
+
* buildFromFilenames only: add()/remove()/updatePath() throw on collision.
|
|
142
80
|
*/
|
|
143
81
|
this.duplicateIds = new Map();
|
|
144
82
|
this.root = path.resolve(root);
|
|
145
83
|
}
|
|
146
84
|
/**
|
|
147
|
-
* Build index by scanning filenames recursively. Duplicate embedded IDs
|
|
148
|
-
* are quarantined, not thrown on
|
|
149
|
-
*
|
|
85
|
+
* Build the index by scanning filenames recursively. Duplicate embedded IDs
|
|
86
|
+
* are quarantined, not thrown on — see the class doc and
|
|
87
|
+
* {@link getDuplicateIds}.
|
|
150
88
|
*/
|
|
151
89
|
async buildFromFilenames(startPath = '') {
|
|
152
90
|
await this.scanDirectory(startPath);
|
|
@@ -162,7 +100,7 @@ export class ContentIdIndex {
|
|
|
162
100
|
this.byCollection.get(location.collection).add(id);
|
|
163
101
|
}
|
|
164
102
|
}
|
|
165
|
-
/** Remove a location from all three indexes
|
|
103
|
+
/** Remove a location from all three indexes, when an earlier duplicate displaces it. */
|
|
166
104
|
evictLocation(location) {
|
|
167
105
|
this.idToLocation.delete(location.id);
|
|
168
106
|
this.pathToId.delete(location.relativePath);
|
|
@@ -175,7 +113,7 @@ export class ContentIdIndex {
|
|
|
175
113
|
}
|
|
176
114
|
}
|
|
177
115
|
}
|
|
178
|
-
/** Record a quarantined duplicate path for
|
|
116
|
+
/** Record a quarantined duplicate path for health reporting (see getDuplicateIds). */
|
|
179
117
|
recordDuplicate(id, droppedPath) {
|
|
180
118
|
let dropped = this.duplicateIds.get(id);
|
|
181
119
|
if (!dropped) {
|
|
@@ -189,7 +127,6 @@ export class ContentIdIndex {
|
|
|
189
127
|
try {
|
|
190
128
|
const entries = await fs.readdir(absoluteDir, { withFileTypes: true });
|
|
191
129
|
for (const entry of entries) {
|
|
192
|
-
// Skip hidden files and directories (including _ids_)
|
|
193
130
|
if (entry.name.startsWith('.') || entry.name === '_ids_') {
|
|
194
131
|
continue;
|
|
195
132
|
}
|
|
@@ -201,11 +138,8 @@ export class ContentIdIndex {
|
|
|
201
138
|
type: entry.isDirectory() ? 'collection' : 'entry',
|
|
202
139
|
relativePath: fullRelativePath, // filesystem path with embedded IDs
|
|
203
140
|
};
|
|
204
|
-
// Extract slug and collection for entries
|
|
205
141
|
if (!entry.isDirectory()) {
|
|
206
142
|
const slug = extractSlugFromFilename(entry.name);
|
|
207
|
-
// Convert physical collection path to logical by stripping embedded IDs from each segment
|
|
208
|
-
// e.g., "content/posts.a1b2c3d4e5f6" → "content/posts"
|
|
209
143
|
const physicalCollection = path.dirname(fullRelativePath);
|
|
210
144
|
const collectionPath = toLogicalCollectionPath(physicalCollection);
|
|
211
145
|
location.slug = slug;
|
|
@@ -213,11 +147,9 @@ export class ContentIdIndex {
|
|
|
213
147
|
}
|
|
214
148
|
const existing = this.idToLocation.get(id);
|
|
215
149
|
if (existing) {
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
// comparing the two strings so every host scanning the same
|
|
220
|
-
// directory picks the same winner regardless of readdir order.
|
|
150
|
+
// Quarantine, don't throw (class doc). The winner is the
|
|
151
|
+
// lexicographically-smaller relativePath, so every host picks the
|
|
152
|
+
// same one whatever order readdir handed them to it in.
|
|
221
153
|
const newWins = fullRelativePath < existing.relativePath;
|
|
222
154
|
const kept = newWins ? fullRelativePath : existing.relativePath;
|
|
223
155
|
const dropped = newWins ? existing.relativePath : fullRelativePath;
|
|
@@ -229,10 +161,6 @@ export class ContentIdIndex {
|
|
|
229
161
|
else {
|
|
230
162
|
this.recordDuplicate(id, fullRelativePath);
|
|
231
163
|
}
|
|
232
|
-
// Written for an operator who has never seen this code: name
|
|
233
|
-
// both files, say explicitly that the dropped one still exists
|
|
234
|
-
// (quarantine, not deletion), and point at the concrete recovery
|
|
235
|
-
// action rather than an internal method name.
|
|
236
164
|
canopyLogWarn(`[ContentIdIndex] Duplicate content ID ${id}: "${kept}" and "${dropped}" both ` +
|
|
237
165
|
`embed this ID. Keeping "${kept}" for ID-based lookups (reads, references, ` +
|
|
238
166
|
`listings); "${dropped}" is excluded from those lookups for now but has NOT ` +
|
|
@@ -246,7 +174,6 @@ export class ContentIdIndex {
|
|
|
246
174
|
this.insertLocation(id, location);
|
|
247
175
|
}
|
|
248
176
|
}
|
|
249
|
-
// Recurse into directories
|
|
250
177
|
if (entry.isDirectory()) {
|
|
251
178
|
await this.scanDirectory(fullRelativePath);
|
|
252
179
|
}
|
|
@@ -260,11 +187,9 @@ export class ContentIdIndex {
|
|
|
260
187
|
}
|
|
261
188
|
}
|
|
262
189
|
/**
|
|
263
|
-
* IDs
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
* and by the repair-content-duplicates admin action
|
|
267
|
-
* (api/admin-branch-health.ts).
|
|
190
|
+
* IDs the last buildFromFilenames scan found on more than one file; empty
|
|
191
|
+
* otherwise. Consumed by branch-health.ts's admin scan and by the
|
|
192
|
+
* repair-content-duplicates action (api/admin-branch-health.ts).
|
|
268
193
|
*/
|
|
269
194
|
getDuplicateIds() {
|
|
270
195
|
const result = [];
|
|
@@ -276,22 +201,18 @@ export class ContentIdIndex {
|
|
|
276
201
|
return result;
|
|
277
202
|
}
|
|
278
203
|
/**
|
|
279
|
-
* The quarantine record for ONE id, or null when that id is not
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
* guard) and must not pay for materializing the whole list.
|
|
204
|
+
* The quarantine record for ONE id, or null when that id is not duplicated.
|
|
205
|
+
* The O(1) counterpart to {@link getDuplicateIds}, for ContentStore.write()'s
|
|
206
|
+
* duplicate-ID guard: it asks on every save whose resolved path disagrees
|
|
207
|
+
* with the index, so it must not pay to materialize the whole list.
|
|
284
208
|
*/
|
|
285
209
|
getDuplicateFor(id) {
|
|
286
210
|
const dropped = this.duplicateIds.get(id);
|
|
287
211
|
if (!dropped)
|
|
288
212
|
return null;
|
|
289
213
|
const kept = this.idToLocation.get(id);
|
|
290
|
-
// Defensive
|
|
291
|
-
//
|
|
292
|
-
// before, on the winner-displaced branch, or already present, on the
|
|
293
|
-
// loser branch), so a winner always exists once a duplicate is
|
|
294
|
-
// recorded.
|
|
214
|
+
// Defensive, unreachable: recordDuplicate() always runs alongside an
|
|
215
|
+
// insertLocation() for the same id, so a winner exists once one is recorded.
|
|
295
216
|
if (!kept)
|
|
296
217
|
return null;
|
|
297
218
|
return {
|
|
@@ -300,33 +221,19 @@ export class ContentIdIndex {
|
|
|
300
221
|
droppedPaths: Array.from(dropped).sort(),
|
|
301
222
|
};
|
|
302
223
|
}
|
|
303
|
-
/**
|
|
304
|
-
* Forward lookup: ID → location (O(1))
|
|
305
|
-
*/
|
|
224
|
+
/** Forward lookup: ID → location (O(1)). */
|
|
306
225
|
findById(id) {
|
|
307
226
|
return this.idToLocation.get(id) || null;
|
|
308
227
|
}
|
|
309
|
-
/**
|
|
310
|
-
* Reverse lookup: path → ID (O(1))
|
|
311
|
-
*/
|
|
228
|
+
/** Reverse lookup: path → ID (O(1)). */
|
|
312
229
|
findByPath(relativePath) {
|
|
313
230
|
return this.pathToId.get(relativePath) || null;
|
|
314
231
|
}
|
|
315
|
-
/**
|
|
316
|
-
* Get all ID locations in the index.
|
|
317
|
-
* Useful for validation and checking references.
|
|
318
|
-
*/
|
|
232
|
+
/** Every ID location in the index, for validation and reference checking. */
|
|
319
233
|
getAllLocations() {
|
|
320
234
|
return Array.from(this.idToLocation.values());
|
|
321
235
|
}
|
|
322
|
-
/**
|
|
323
|
-
* Get all entries in a collection by collection path.
|
|
324
|
-
*
|
|
325
|
-
* Performance: O(1) + O(m) where m is the number of entries in the collection.
|
|
326
|
-
*
|
|
327
|
-
* @param collectionPath - The collection path (e.g., "content/posts")
|
|
328
|
-
* @returns Array of IdLocation objects for entries in the collection
|
|
329
|
-
*/
|
|
236
|
+
/** Entries in one collection: O(1) + O(entries in it). */
|
|
330
237
|
getEntriesInCollection(collectionPath) {
|
|
331
238
|
const idSet = this.byCollection.get(collectionPath);
|
|
332
239
|
if (!idSet) {
|
|
@@ -342,14 +249,9 @@ export class ContentIdIndex {
|
|
|
342
249
|
return locations;
|
|
343
250
|
}
|
|
344
251
|
/**
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
* Performance: O(k * m) where k is number of matching collections and m is avg entries per collection.
|
|
350
|
-
*
|
|
351
|
-
* @param collectionPath - The root collection path to search
|
|
352
|
-
* @returns Array of IdLocation objects for entries in the collection tree
|
|
252
|
+
* Entries in a collection and every subcollection: "content/docs" returns
|
|
253
|
+
* "content/docs", "content/docs/api", and so on. O(matching collections ×
|
|
254
|
+
* entries each).
|
|
353
255
|
*/
|
|
354
256
|
getEntriesInCollectionTree(collectionPath) {
|
|
355
257
|
const locations = [];
|
|
@@ -372,7 +274,6 @@ export class ContentIdIndex {
|
|
|
372
274
|
*
|
|
373
275
|
* Performance: O(n) where n is total number of entries.
|
|
374
276
|
*
|
|
375
|
-
* @returns Array of all IdLocation objects that are entries
|
|
376
277
|
*/
|
|
377
278
|
getAllEntryLocations() {
|
|
378
279
|
const locations = [];
|
|
@@ -387,16 +288,14 @@ export class ContentIdIndex {
|
|
|
387
288
|
return locations;
|
|
388
289
|
}
|
|
389
290
|
/**
|
|
390
|
-
* Add
|
|
391
|
-
*
|
|
392
|
-
* must already exist on disk (created by ContentStore).
|
|
291
|
+
* Add an entry or collection. In-memory only: the file with the embedded ID
|
|
292
|
+
* must already exist on disk, created by ContentStore. Throws on collision.
|
|
393
293
|
*/
|
|
394
294
|
add(location) {
|
|
395
295
|
const id = extractIdFromFilename(path.basename(location.relativePath));
|
|
396
296
|
if (!id) {
|
|
397
297
|
throw new Error(`Cannot add location without ID in filename: ${location.relativePath}`);
|
|
398
298
|
}
|
|
399
|
-
// Collision detection
|
|
400
299
|
if (this.idToLocation.has(id)) {
|
|
401
300
|
const existing = this.idToLocation.get(id);
|
|
402
301
|
throw new Error(`ID collision detected: ${id}\n` +
|
|
@@ -409,7 +308,6 @@ export class ContentIdIndex {
|
|
|
409
308
|
};
|
|
410
309
|
this.idToLocation.set(id, fullLocation);
|
|
411
310
|
this.pathToId.set(location.relativePath, id);
|
|
412
|
-
// Add to collection index if it's an entry
|
|
413
311
|
if (fullLocation.type === 'entry' && fullLocation.collection) {
|
|
414
312
|
if (!this.byCollection.has(fullLocation.collection)) {
|
|
415
313
|
this.byCollection.set(fullLocation.collection, new Set());
|
|
@@ -418,19 +316,18 @@ export class ContentIdIndex {
|
|
|
418
316
|
}
|
|
419
317
|
}
|
|
420
318
|
/**
|
|
421
|
-
* Remove an entry or collection
|
|
422
|
-
*
|
|
319
|
+
* Remove an entry or collection by ID. In-memory only: the caller deletes the
|
|
320
|
+
* file separately.
|
|
423
321
|
*/
|
|
424
322
|
remove(id) {
|
|
425
323
|
const location = this.idToLocation.get(id);
|
|
426
324
|
if (!location)
|
|
427
325
|
return;
|
|
428
|
-
// Remove from collection index if it's an entry
|
|
429
326
|
if (location.type === 'entry' && location.collection) {
|
|
430
327
|
const idSet = this.byCollection.get(location.collection);
|
|
431
328
|
if (idSet) {
|
|
432
329
|
idSet.delete(id);
|
|
433
|
-
//
|
|
330
|
+
// Drop empty Sets so the collection map cannot grow without bound.
|
|
434
331
|
if (idSet.size === 0) {
|
|
435
332
|
this.byCollection.delete(location.collection);
|
|
436
333
|
}
|
|
@@ -440,27 +337,22 @@ export class ContentIdIndex {
|
|
|
440
337
|
this.pathToId.delete(location.relativePath);
|
|
441
338
|
}
|
|
442
339
|
/**
|
|
443
|
-
*
|
|
444
|
-
*
|
|
340
|
+
* Repoint an existing ID at a new path; keeps the index in step with a
|
|
341
|
+
* rename or move. Throws when the ID is unknown.
|
|
445
342
|
*/
|
|
446
343
|
updatePath(id, newRelativePath) {
|
|
447
344
|
const location = this.idToLocation.get(id);
|
|
448
345
|
if (!location) {
|
|
449
346
|
throw new Error(`Cannot update path for unknown ID: ${id}`);
|
|
450
347
|
}
|
|
451
|
-
// Remove old path mapping
|
|
452
348
|
this.pathToId.delete(location.relativePath);
|
|
453
|
-
// Update location
|
|
454
349
|
location.relativePath = newRelativePath;
|
|
455
|
-
// Update slug and collection for entries
|
|
456
350
|
if (location.type === 'entry') {
|
|
457
351
|
const oldCollection = location.collection;
|
|
458
352
|
location.slug = extractSlugFromFilename(path.basename(newRelativePath));
|
|
459
353
|
const physicalCollection = path.dirname(newRelativePath);
|
|
460
354
|
location.collection = toLogicalCollectionPath(physicalCollection);
|
|
461
|
-
// Update collection index if collection changed
|
|
462
355
|
if (oldCollection !== location.collection) {
|
|
463
|
-
// Remove from old collection
|
|
464
356
|
if (oldCollection) {
|
|
465
357
|
const oldSet = this.byCollection.get(oldCollection);
|
|
466
358
|
if (oldSet) {
|
|
@@ -470,7 +362,6 @@ export class ContentIdIndex {
|
|
|
470
362
|
}
|
|
471
363
|
}
|
|
472
364
|
}
|
|
473
|
-
// Add to new collection
|
|
474
365
|
if (location.collection) {
|
|
475
366
|
if (!this.byCollection.has(location.collection)) {
|
|
476
367
|
this.byCollection.set(location.collection, new Set());
|
|
@@ -479,39 +370,28 @@ export class ContentIdIndex {
|
|
|
479
370
|
}
|
|
480
371
|
}
|
|
481
372
|
}
|
|
482
|
-
// Add new path mapping
|
|
483
373
|
this.pathToId.set(newRelativePath, id);
|
|
484
374
|
}
|
|
485
375
|
}
|
|
486
376
|
/**
|
|
487
|
-
*
|
|
488
|
-
* Returns null if filename doesn't contain an ID or is a metadata file.
|
|
489
|
-
*
|
|
490
|
-
* Pattern:
|
|
491
|
-
* - Collection entry files: type.slug.id.ext → ID is parts[parts.length - 2] (e.g., "post.dune.a1b2c3d4e5f6.json")
|
|
492
|
-
* - Collection directories: slug.id → ID is parts[1] (e.g., "posts.a1b2c3d4e5f6")
|
|
493
|
-
* - Metadata files: .collection.json, .gitignore, etc. → null
|
|
377
|
+
* The ID embedded in a filename, or null when there is none.
|
|
494
378
|
*
|
|
495
|
-
*
|
|
496
|
-
*
|
|
497
|
-
*
|
|
498
|
-
* -
|
|
379
|
+
* - entry file `type.slug.id.ext` → the second-to-last part; a dotted slug
|
|
380
|
+
* ("post.my.page.a1b2c3d4e5f6.json") still resolves, since only position
|
|
381
|
+
* from the end matters
|
|
382
|
+
* - collection directory `slug.id` → the last part
|
|
383
|
+
* - anything dot-prefixed → null, metadata, even `.hidden.a1b2c3d4e5f6.json`
|
|
499
384
|
*/
|
|
500
385
|
export function extractIdFromFilename(filename) {
|
|
501
|
-
// Skip metadata files (no IDs) - anything starting with dot is metadata
|
|
502
|
-
// This includes .collection.json, .gitignore, and even .hidden.id.json
|
|
503
386
|
if (filename.startsWith('.')) {
|
|
504
387
|
return null;
|
|
505
388
|
}
|
|
506
389
|
const parts = filename.split('.');
|
|
507
|
-
// Files: type.slug.id.ext → need at least 3 parts
|
|
508
|
-
// The ID is always the second-to-last part before the extension
|
|
509
390
|
if (parts.length >= 3) {
|
|
510
391
|
const candidate = parts[parts.length - 2];
|
|
511
392
|
if (isValidId(candidate))
|
|
512
393
|
return candidate;
|
|
513
394
|
}
|
|
514
|
-
// Directories: slug.id → exactly 2 parts (slug and ID, no extension)
|
|
515
395
|
if (parts.length === 2) {
|
|
516
396
|
const candidate = parts[parts.length - 1];
|
|
517
397
|
if (isValidId(candidate))
|
|
@@ -520,16 +400,9 @@ export function extractIdFromFilename(filename) {
|
|
|
520
400
|
return null;
|
|
521
401
|
}
|
|
522
402
|
/**
|
|
523
|
-
* Resolve a logical collection path to
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
* Example:
|
|
527
|
-
* Input: resolveCollectionPath(root, "content/docs/api")
|
|
528
|
-
* Output: "/abs/path/to/content/docs.bChqT78gcaLd/api.meiuwxTSo7UN"
|
|
529
|
-
*
|
|
530
|
-
* @param root - Absolute path to the workspace root
|
|
531
|
-
* @param logicalPath - Logical path from schema (e.g., "content/docs/api")
|
|
532
|
-
* @returns Absolute filesystem path with embedded IDs, or null if path doesn't exist
|
|
403
|
+
* Resolve a logical collection path to the filesystem path with embedded IDs,
|
|
404
|
+
* one segment at a time so nested collections work: "content/docs/api" →
|
|
405
|
+
* "<root>/content/docs.bChqT78gcaLd/api.meiuwxTSo7UN". Null if it is not there.
|
|
533
406
|
*/
|
|
534
407
|
export async function resolveCollectionPath(root, logicalPath) {
|
|
535
408
|
// Dynamic import: keep Node built-ins out of browser/edge bundles
|
|
@@ -543,7 +416,6 @@ export async function resolveCollectionPath(root, logicalPath) {
|
|
|
543
416
|
const matchingDir = entries.find((entry) => {
|
|
544
417
|
if (!entry.isDirectory())
|
|
545
418
|
return false;
|
|
546
|
-
// Extract logical name from directory (strips embedded ID)
|
|
547
419
|
const logicalName = extractSlugFromFilename(entry.name);
|
|
548
420
|
return logicalName === segment.toLowerCase();
|
|
549
421
|
});
|
|
@@ -551,7 +423,6 @@ export async function resolveCollectionPath(root, logicalPath) {
|
|
|
551
423
|
currentPath = path.join(currentPath, matchingDir.name);
|
|
552
424
|
}
|
|
553
425
|
else {
|
|
554
|
-
// Directory not found - might not exist yet
|
|
555
426
|
return null;
|
|
556
427
|
}
|
|
557
428
|
}
|
|
@@ -564,22 +435,14 @@ export async function resolveCollectionPath(root, logicalPath) {
|
|
|
564
435
|
return currentPath;
|
|
565
436
|
}
|
|
566
437
|
/**
|
|
567
|
-
*
|
|
568
|
-
*
|
|
569
|
-
*
|
|
570
|
-
* Examples:
|
|
571
|
-
* - "post.my-slug.a1b2c3d4e5f6.json" → "post"
|
|
572
|
-
* - "article.test.a1b2c3d4e5f6.md" → "article"
|
|
573
|
-
* - "posts.a1b2c3d4e5f6" → null (directory, not an entry file)
|
|
574
|
-
*
|
|
575
|
-
* @param filename - The filename to parse
|
|
576
|
-
* @returns Entry type name or null if not a valid entry file
|
|
438
|
+
* The entry type of an entry file: the first part of `type.slug.id.ext`, so
|
|
439
|
+
* "post.my-slug.a1b2c3d4e5f6.json" → "post". Null for anything else, including
|
|
440
|
+
* a collection directory ("posts.a1b2c3d4e5f6").
|
|
577
441
|
*/
|
|
578
442
|
export function extractEntryTypeFromFilename(filename) {
|
|
579
443
|
if (filename.startsWith('.'))
|
|
580
444
|
return null;
|
|
581
445
|
const parts = filename.split('.');
|
|
582
|
-
// Need at least 4 parts for type.slug.id.ext
|
|
583
446
|
if (parts.length >= 4) {
|
|
584
447
|
const possibleId = parts[parts.length - 2];
|
|
585
448
|
if (isValidId(possibleId)) {
|
|
@@ -589,50 +452,36 @@ export function extractEntryTypeFromFilename(filename) {
|
|
|
589
452
|
return null;
|
|
590
453
|
}
|
|
591
454
|
/**
|
|
592
|
-
*
|
|
593
|
-
*
|
|
594
|
-
*
|
|
595
|
-
*
|
|
596
|
-
*
|
|
597
|
-
* For collection entry files (4+ parts), automatically strips the first part (type) to extract just the slug.
|
|
598
|
-
*
|
|
599
|
-
* Examples:
|
|
600
|
-
* - "post.my-slug.a1b2c3d4e5f6.json" → "my-slug" (4 parts)
|
|
601
|
-
* - "post.my.page.a1b2c3d4e5f6.json" → "my.page" (dotted slug, 5 parts)
|
|
602
|
-
* - "posts.a1b2c3d4e5f6" → "posts" (directory, 2 parts)
|
|
455
|
+
* The slug in a filename, lowercased: everything between the type and the ID in
|
|
456
|
+
* `type.slug.id.ext`, or the part before the ID in a `slug.id` directory. A
|
|
457
|
+
* dotted slug survives ("post.my.page.a1b2c3d4e5f6.json" → "my.page"). Falls
|
|
458
|
+
* back to the extensionless filename when there is no ID.
|
|
603
459
|
*
|
|
604
|
-
*
|
|
605
|
-
*
|
|
606
|
-
* If provided and matches first part, strips it from slug
|
|
460
|
+
* `entryTypeName`, when it matches the first part, strips that part explicitly
|
|
461
|
+
* instead of relying on the 4-part auto-detection.
|
|
607
462
|
*/
|
|
608
463
|
export function extractSlugFromFilename(filename, entryTypeName) {
|
|
609
464
|
const parts = filename.split('.');
|
|
610
|
-
// Files: type.slug.id.ext (at least 3 parts)
|
|
611
465
|
if (parts.length >= 3) {
|
|
612
466
|
const possibleId = parts[parts.length - 2];
|
|
613
467
|
if (isValidId(possibleId)) {
|
|
614
|
-
// Get all parts before ID (excluding extension)
|
|
615
468
|
let slugParts = parts.slice(0, parts.length - 2);
|
|
616
|
-
// If entryTypeName is provided and matches the first part, strip it
|
|
617
469
|
if (entryTypeName && slugParts.length > 1 && slugParts[0] === entryTypeName) {
|
|
618
470
|
slugParts = slugParts.slice(1);
|
|
619
471
|
}
|
|
620
|
-
// Auto-
|
|
472
|
+
// Auto-detected: 4+ parts is type.slug.id.ext, so the first part is the type.
|
|
621
473
|
else if (parts.length >= 4 && slugParts.length > 1) {
|
|
622
|
-
// Strip first part (the type) to get just the slug
|
|
623
474
|
slugParts = slugParts.slice(1);
|
|
624
475
|
}
|
|
625
476
|
return slugParts.join('.').toLowerCase();
|
|
626
477
|
}
|
|
627
478
|
}
|
|
628
|
-
// Directories: slug.id (exactly 2 parts)
|
|
629
479
|
if (parts.length === 2) {
|
|
630
480
|
const possibleId = parts[parts.length - 1];
|
|
631
481
|
if (isValidId(possibleId)) {
|
|
632
482
|
return parts[0].toLowerCase();
|
|
633
483
|
}
|
|
634
484
|
}
|
|
635
|
-
// No ID found, remove extension and return filename without extension
|
|
636
485
|
if (parts.length > 1) {
|
|
637
486
|
return parts.slice(0, -1).join('.').toLowerCase();
|
|
638
487
|
}
|