canopycms 0.0.0 → 0.0.2
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/dist/auth/plugin.d.ts +8 -0
- package/dist/auth/plugin.d.ts.map +1 -1
- package/dist/build-mode.d.ts +15 -5
- package/dist/build-mode.d.ts.map +1 -1
- package/dist/build-mode.js +18 -8
- package/dist/build-mode.js.map +1 -1
- package/dist/cli/init.d.ts +2 -2
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +37 -36
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/template-files/ai-config.ts.template +21 -0
- package/dist/cli/template-files/ai-route.ts.template +10 -0
- package/dist/cli/template-files/canopy.ts.template +24 -0
- package/dist/cli/templates.d.ts +5 -1
- package/dist/cli/templates.d.ts.map +1 -1
- package/dist/cli/templates.js +9 -2
- package/dist/cli/templates.js.map +1 -1
- package/dist/config/schemas/config.d.ts +4 -0
- package/dist/config/schemas/config.d.ts.map +1 -1
- package/dist/config/schemas/config.js +2 -0
- package/dist/config/schemas/config.js.map +1 -1
- package/dist/config/types.d.ts +5 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/content-reader.js +2 -2
- package/dist/content-reader.js.map +1 -1
- package/dist/context.js +5 -5
- package/dist/context.js.map +1 -1
- package/dist/operating-mode/client-unsafe-strategy.d.ts.map +1 -1
- package/dist/operating-mode/client-unsafe-strategy.js +15 -18
- package/dist/operating-mode/client-unsafe-strategy.js.map +1 -1
- package/dist/operating-mode/types.d.ts +8 -0
- package/dist/operating-mode/types.d.ts.map +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -0
- package/dist/server.js.map +1 -1
- package/package.json +5 -4
- package/src/cli/init.ts +43 -38
- package/dist/__integration__/fixtures/content-seeds.d.ts +0 -43
- package/dist/__integration__/fixtures/content-seeds.d.ts.map +0 -1
- package/dist/__integration__/fixtures/content-seeds.js +0 -99
- package/dist/__integration__/fixtures/content-seeds.js.map +0 -1
- package/dist/__integration__/fixtures/schemas.d.ts +0 -12
- package/dist/__integration__/fixtures/schemas.d.ts.map +0 -1
- package/dist/__integration__/fixtures/schemas.js +0 -65
- package/dist/__integration__/fixtures/schemas.js.map +0 -1
- package/dist/__integration__/test-utils/api-client.d.ts +0 -123
- package/dist/__integration__/test-utils/api-client.d.ts.map +0 -1
- package/dist/__integration__/test-utils/api-client.js +0 -118
- package/dist/__integration__/test-utils/api-client.js.map +0 -1
- package/dist/__integration__/test-utils/multi-user.d.ts +0 -25
- package/dist/__integration__/test-utils/multi-user.d.ts.map +0 -1
- package/dist/__integration__/test-utils/multi-user.js +0 -105
- package/dist/__integration__/test-utils/multi-user.js.map +0 -1
- package/dist/__integration__/test-utils/test-workspace.d.ts +0 -25
- package/dist/__integration__/test-utils/test-workspace.d.ts.map +0 -1
- package/dist/__integration__/test-utils/test-workspace.js +0 -102
- package/dist/__integration__/test-utils/test-workspace.js.map +0 -1
- package/dist/editor/BranchManager.stories.d.ts +0 -8
- package/dist/editor/BranchManager.stories.d.ts.map +0 -1
- package/dist/editor/BranchManager.stories.js +0 -74
- package/dist/editor/BranchManager.stories.js.map +0 -1
- package/dist/editor/CanopyEditor.stories.d.ts +0 -7
- package/dist/editor/CanopyEditor.stories.d.ts.map +0 -1
- package/dist/editor/CanopyEditor.stories.js +0 -99
- package/dist/editor/CanopyEditor.stories.js.map +0 -1
- package/dist/editor/CommentsPanel.stories.d.ts +0 -10
- package/dist/editor/CommentsPanel.stories.d.ts.map +0 -1
- package/dist/editor/CommentsPanel.stories.js +0 -175
- package/dist/editor/CommentsPanel.stories.js.map +0 -1
- package/dist/editor/Editor.stories.d.ts +0 -7
- package/dist/editor/Editor.stories.d.ts.map +0 -1
- package/dist/editor/Editor.stories.js +0 -95
- package/dist/editor/Editor.stories.js.map +0 -1
- package/dist/editor/EditorPanes.stories.d.ts +0 -7
- package/dist/editor/EditorPanes.stories.d.ts.map +0 -1
- package/dist/editor/EditorPanes.stories.js +0 -116
- package/dist/editor/EditorPanes.stories.js.map +0 -1
- package/dist/editor/EntryNavigator.stories.d.ts +0 -8
- package/dist/editor/EntryNavigator.stories.d.ts.map +0 -1
- package/dist/editor/EntryNavigator.stories.js +0 -42
- package/dist/editor/EntryNavigator.stories.js.map +0 -1
- package/dist/editor/FormRenderer.stories.d.ts +0 -7
- package/dist/editor/FormRenderer.stories.d.ts.map +0 -1
- package/dist/editor/FormRenderer.stories.js +0 -115
- package/dist/editor/FormRenderer.stories.js.map +0 -1
- package/dist/editor/GroupManager.stories.d.ts +0 -19
- package/dist/editor/GroupManager.stories.d.ts.map +0 -1
- package/dist/editor/GroupManager.stories.js +0 -265
- package/dist/editor/GroupManager.stories.js.map +0 -1
- package/dist/editor/PermissionManager.stories.d.ts +0 -20
- package/dist/editor/PermissionManager.stories.d.ts.map +0 -1
- package/dist/editor/PermissionManager.stories.js +0 -506
- package/dist/editor/PermissionManager.stories.js.map +0 -1
- package/dist/editor/comments/FieldWrapper.stories.d.ts +0 -10
- package/dist/editor/comments/FieldWrapper.stories.d.ts.map +0 -1
- package/dist/editor/comments/FieldWrapper.stories.js +0 -173
- package/dist/editor/comments/FieldWrapper.stories.js.map +0 -1
- package/dist/editor/fields/BlockField.stories.d.ts +0 -7
- package/dist/editor/fields/BlockField.stories.d.ts.map +0 -1
- package/dist/editor/fields/BlockField.stories.js +0 -50
- package/dist/editor/fields/BlockField.stories.js.map +0 -1
- package/dist/editor/fields/fields.stories.d.ts +0 -8
- package/dist/editor/fields/fields.stories.d.ts.map +0 -1
- package/dist/editor/fields/fields.stories.js +0 -34
- package/dist/editor/fields/fields.stories.js.map +0 -1
- package/dist/test-utils/api-test-helpers.d.ts +0 -238
- package/dist/test-utils/api-test-helpers.d.ts.map +0 -1
- package/dist/test-utils/api-test-helpers.js +0 -347
- package/dist/test-utils/api-test-helpers.js.map +0 -1
- package/dist/test-utils/console-spy.d.ts +0 -56
- package/dist/test-utils/console-spy.d.ts.map +0 -1
- package/dist/test-utils/console-spy.js +0 -81
- package/dist/test-utils/console-spy.js.map +0 -1
- package/dist/test-utils/git-helpers.d.ts +0 -21
- package/dist/test-utils/git-helpers.d.ts.map +0 -1
- package/dist/test-utils/git-helpers.js +0 -23
- package/dist/test-utils/git-helpers.js.map +0 -1
- package/dist/test-utils/index.d.ts +0 -5
- package/dist/test-utils/index.d.ts.map +0 -1
- package/dist/test-utils/index.js +0 -4
- package/dist/test-utils/index.js.map +0 -1
- package/src/__integration__/errors/invalid-content.test.ts +0 -238
- package/src/__integration__/errors/permission-denied.test.ts +0 -220
- package/src/__integration__/fixtures/content-seeds.ts +0 -105
- package/src/__integration__/fixtures/schemas.ts +0 -67
- package/src/__integration__/initialization/prod-sim-init.test.ts +0 -139
- package/src/__integration__/permissions/path-permissions.test.ts +0 -314
- package/src/__integration__/permissions/role-permissions.test.ts +0 -354
- package/src/__integration__/permissions/settings-branch-isolation.test.ts +0 -317
- package/src/__integration__/settings/groups-api.test.ts +0 -403
- package/src/__integration__/test-utils/api-client.ts +0 -167
- package/src/__integration__/test-utils/multi-user.ts +0 -129
- package/src/__integration__/test-utils/test-workspace.ts +0 -130
- package/src/__integration__/user/user-context.test.ts +0 -174
- package/src/__integration__/validation/input-validation.test.ts +0 -166
- package/src/__integration__/workflows/api-editing-workflow.test.ts +0 -244
- package/src/__integration__/workflows/conflict-resolution.test.ts +0 -259
- package/src/__integration__/workflows/editing-workflow.test.ts +0 -205
- package/src/__integration__/workflows/review-workflow.test.ts +0 -260
- package/src/ai/__tests__/build.integration.test.ts +0 -224
- package/src/ai/__tests__/generate.integration.test.ts +0 -495
- package/src/ai/__tests__/handler.integration.test.ts +0 -212
- package/src/ai/__tests__/json-to-markdown.test.ts +0 -553
- package/src/ai/generate.ts +0 -410
- package/src/ai/handler.ts +0 -123
- package/src/ai/index.ts +0 -26
- package/src/ai/json-to-markdown.ts +0 -424
- package/src/ai/resolve-branch.ts +0 -34
- package/src/ai/types.ts +0 -160
- package/src/api/AGENTS.md +0 -81
- package/src/api/__test__/mock-client.ts +0 -404
- package/src/api/assets.test.ts +0 -140
- package/src/api/assets.ts +0 -154
- package/src/api/branch-merge.test.ts +0 -163
- package/src/api/branch-merge.ts +0 -113
- package/src/api/branch-review.test.ts +0 -297
- package/src/api/branch-review.ts +0 -136
- package/src/api/branch-status.test.ts +0 -85
- package/src/api/branch-status.ts +0 -153
- package/src/api/branch-withdraw.test.ts +0 -146
- package/src/api/branch-withdraw.ts +0 -81
- package/src/api/branch-workflow.integration.test.ts +0 -578
- package/src/api/branch.test.ts +0 -620
- package/src/api/branch.ts +0 -492
- package/src/api/client.test.ts +0 -349
- package/src/api/client.ts +0 -506
- package/src/api/comments.test.ts +0 -285
- package/src/api/comments.ts +0 -210
- package/src/api/content.test.ts +0 -345
- package/src/api/content.ts +0 -454
- package/src/api/entries.test.ts +0 -1339
- package/src/api/entries.ts +0 -650
- package/src/api/github-sync.ts +0 -144
- package/src/api/groups.test.ts +0 -1013
- package/src/api/groups.ts +0 -375
- package/src/api/guards.test.ts +0 -533
- package/src/api/guards.ts +0 -271
- package/src/api/index.ts +0 -87
- package/src/api/permissions.test.ts +0 -766
- package/src/api/permissions.ts +0 -334
- package/src/api/reference-options.ts +0 -118
- package/src/api/resolve-references.ts +0 -107
- package/src/api/route-builder.ts +0 -289
- package/src/api/schema.test.ts +0 -840
- package/src/api/schema.ts +0 -936
- package/src/api/security.test.ts +0 -233
- package/src/api/settings-helpers.ts +0 -84
- package/src/api/types.ts +0 -40
- package/src/api/user.test.ts +0 -127
- package/src/api/user.ts +0 -42
- package/src/api/validators.test.ts +0 -275
- package/src/api/validators.ts +0 -176
- package/src/asset-store.test.ts +0 -37
- package/src/asset-store.ts +0 -110
- package/src/auth/cache.ts +0 -7
- package/src/auth/caching-auth-plugin.test.ts +0 -154
- package/src/auth/caching-auth-plugin.ts +0 -109
- package/src/auth/context-helpers.ts +0 -75
- package/src/auth/file-based-auth-cache.test.ts +0 -257
- package/src/auth/file-based-auth-cache.ts +0 -279
- package/src/auth/index.ts +0 -12
- package/src/auth/plugin.ts +0 -51
- package/src/auth/types.ts +0 -38
- package/src/authorization/__tests__/branch.test.ts +0 -260
- package/src/authorization/__tests__/content.test.ts +0 -142
- package/src/authorization/__tests__/path.test.ts +0 -133
- package/src/authorization/__tests__/permissions-loader.test.ts +0 -200
- package/src/authorization/branch.ts +0 -94
- package/src/authorization/content.ts +0 -93
- package/src/authorization/groups/index.ts +0 -11
- package/src/authorization/groups/loader.ts +0 -127
- package/src/authorization/groups/schema.ts +0 -48
- package/src/authorization/helpers.ts +0 -48
- package/src/authorization/index.ts +0 -84
- package/src/authorization/path.ts +0 -112
- package/src/authorization/permissions/index.ts +0 -11
- package/src/authorization/permissions/loader.ts +0 -116
- package/src/authorization/permissions/schema.ts +0 -66
- package/src/authorization/test-utils.ts +0 -15
- package/src/authorization/types.ts +0 -66
- package/src/authorization/validation.test.ts +0 -100
- package/src/authorization/validation.ts +0 -62
- package/src/branch-metadata.test.ts +0 -168
- package/src/branch-metadata.ts +0 -166
- package/src/branch-registry.test.ts +0 -248
- package/src/branch-registry.ts +0 -152
- package/src/branch-schema-cache.test.ts +0 -275
- package/src/branch-schema-cache.ts +0 -189
- package/src/branch-workspace.test.ts +0 -183
- package/src/branch-workspace.ts +0 -124
- package/src/build/generate-ai-content.ts +0 -78
- package/src/build/index.ts +0 -8
- package/src/build-mode.ts +0 -27
- package/src/cli/generate-ai-content.ts +0 -100
- package/src/cli/init.test.ts +0 -240
- package/src/cli/templates/canopy.ts.template +0 -55
- package/src/cli/templates.ts +0 -47
- package/src/client.ts +0 -12
- package/src/comment-store.test.ts +0 -442
- package/src/comment-store.ts +0 -301
- package/src/config/__tests__/config.test.ts +0 -513
- package/src/config/flatten.ts +0 -174
- package/src/config/helpers.ts +0 -167
- package/src/config/index.ts +0 -86
- package/src/config/schemas/collection.ts +0 -67
- package/src/config/schemas/config.ts +0 -77
- package/src/config/schemas/field.ts +0 -108
- package/src/config/schemas/media.ts +0 -27
- package/src/config/schemas/permissions.ts +0 -21
- package/src/config/types.ts +0 -321
- package/src/config/validation.ts +0 -70
- package/src/config-test.ts +0 -65
- package/src/config.ts +0 -11
- package/src/content-id-index.test.ts +0 -512
- package/src/content-id-index.ts +0 -479
- package/src/content-reader.test.ts +0 -478
- package/src/content-reader.ts +0 -214
- package/src/content-store.test.ts +0 -1126
- package/src/content-store.ts +0 -793
- package/src/context.ts +0 -111
- package/src/editor/BranchManager.stories.tsx +0 -80
- package/src/editor/BranchManager.test.tsx +0 -324
- package/src/editor/BranchManager.tsx +0 -461
- package/src/editor/CanopyEditor.stories.tsx +0 -128
- package/src/editor/CanopyEditor.test.tsx +0 -81
- package/src/editor/CanopyEditor.tsx +0 -73
- package/src/editor/CanopyEditorPage.test.tsx +0 -59
- package/src/editor/CanopyEditorPage.tsx +0 -25
- package/src/editor/CommentsPanel.stories.tsx +0 -184
- package/src/editor/CommentsPanel.tsx +0 -338
- package/src/editor/Editor.integration.test.tsx +0 -227
- package/src/editor/Editor.stories.tsx +0 -119
- package/src/editor/Editor.tsx +0 -1221
- package/src/editor/EditorPanes.stories.tsx +0 -256
- package/src/editor/EditorPanes.test.tsx +0 -77
- package/src/editor/EditorPanes.tsx +0 -180
- package/src/editor/EntryNavigator.stories.tsx +0 -65
- package/src/editor/EntryNavigator.test.tsx +0 -598
- package/src/editor/EntryNavigator.tsx +0 -665
- package/src/editor/FormRenderer.stories.tsx +0 -212
- package/src/editor/FormRenderer.test.tsx +0 -194
- package/src/editor/FormRenderer.tsx +0 -432
- package/src/editor/GroupManager.stories.tsx +0 -301
- package/src/editor/GroupManager.test.tsx +0 -682
- package/src/editor/GroupManager.tsx +0 -9
- package/src/editor/PermissionManager.stories.tsx +0 -539
- package/src/editor/PermissionManager.test.tsx +0 -864
- package/src/editor/PermissionManager.tsx +0 -12
- package/src/editor/canopy-path.test.ts +0 -23
- package/src/editor/canopy-path.ts +0 -52
- package/src/editor/client-reference-resolver.ts +0 -118
- package/src/editor/comments/BranchComments.tsx +0 -93
- package/src/editor/comments/EntryComments.tsx +0 -94
- package/src/editor/comments/FieldWrapper.stories.tsx +0 -210
- package/src/editor/comments/FieldWrapper.tsx +0 -129
- package/src/editor/comments/InlineCommentThread.test.tsx +0 -384
- package/src/editor/comments/InlineCommentThread.tsx +0 -246
- package/src/editor/comments/ThreadCarousel.test.tsx +0 -393
- package/src/editor/comments/ThreadCarousel.tsx +0 -525
- package/src/editor/components/ConfirmDeleteModal.tsx +0 -49
- package/src/editor/components/EditorContext.tsx +0 -49
- package/src/editor/components/EditorFooter.tsx +0 -47
- package/src/editor/components/EditorHeader.tsx +0 -492
- package/src/editor/components/EditorSidebar.tsx +0 -193
- package/src/editor/components/EntryCreateModal.tsx +0 -193
- package/src/editor/components/RenameEntryModal.tsx +0 -152
- package/src/editor/components/UserBadge.test.tsx +0 -274
- package/src/editor/components/UserBadge.tsx +0 -240
- package/src/editor/components/index.ts +0 -6
- package/src/editor/context/ApiClientContext.tsx +0 -56
- package/src/editor/context/EditorStateContext.tsx +0 -221
- package/src/editor/context/index.ts +0 -40
- package/src/editor/editor-config.test.ts +0 -385
- package/src/editor/editor-config.ts +0 -94
- package/src/editor/editor-utils.test.ts +0 -772
- package/src/editor/editor-utils.ts +0 -303
- package/src/editor/env.ts +0 -4
- package/src/editor/fields/BlockField.stories.tsx +0 -79
- package/src/editor/fields/BlockField.tsx +0 -267
- package/src/editor/fields/CodeField.tsx +0 -41
- package/src/editor/fields/MarkdownField.tsx +0 -205
- package/src/editor/fields/ObjectField.tsx +0 -71
- package/src/editor/fields/ReferenceField.tsx +0 -138
- package/src/editor/fields/SelectField.tsx +0 -76
- package/src/editor/fields/TextField.tsx +0 -35
- package/src/editor/fields/ToggleField.tsx +0 -37
- package/src/editor/fields/fields.stories.tsx +0 -40
- package/src/editor/group-manager/ExternalGroupsTab.tsx +0 -114
- package/src/editor/group-manager/GroupCard.tsx +0 -102
- package/src/editor/group-manager/GroupForm.tsx +0 -66
- package/src/editor/group-manager/InternalGroupsTab.tsx +0 -147
- package/src/editor/group-manager/MemberList.tsx +0 -184
- package/src/editor/group-manager/hooks/useExternalGroupSearch.ts +0 -63
- package/src/editor/group-manager/hooks/useGroupState.ts +0 -134
- package/src/editor/group-manager/hooks/useUserSearch.ts +0 -84
- package/src/editor/group-manager/index.tsx +0 -210
- package/src/editor/group-manager/types.ts +0 -28
- package/src/editor/hooks/README.md +0 -26
- package/src/editor/hooks/__test__/test-utils.tsx +0 -183
- package/src/editor/hooks/index.ts +0 -23
- package/src/editor/hooks/useBranchActions.test.tsx +0 -267
- package/src/editor/hooks/useBranchActions.tsx +0 -121
- package/src/editor/hooks/useBranchManager.test.tsx +0 -391
- package/src/editor/hooks/useBranchManager.tsx +0 -326
- package/src/editor/hooks/useCommentSystem.test.ts +0 -615
- package/src/editor/hooks/useCommentSystem.ts +0 -347
- package/src/editor/hooks/useDraftManager.test.ts +0 -375
- package/src/editor/hooks/useDraftManager.ts +0 -259
- package/src/editor/hooks/useEditorLayout.test.ts +0 -147
- package/src/editor/hooks/useEditorLayout.ts +0 -67
- package/src/editor/hooks/useEntryManager.test.ts +0 -588
- package/src/editor/hooks/useEntryManager.ts +0 -387
- package/src/editor/hooks/useGroupManager.test.ts +0 -277
- package/src/editor/hooks/useGroupManager.ts +0 -139
- package/src/editor/hooks/usePermissionManager.test.ts +0 -211
- package/src/editor/hooks/usePermissionManager.ts +0 -113
- package/src/editor/hooks/useReferenceResolution.ts +0 -248
- package/src/editor/hooks/useSchemaManager.test.ts +0 -370
- package/src/editor/hooks/useSchemaManager.ts +0 -310
- package/src/editor/hooks/useUserContext.tsx +0 -57
- package/src/editor/hooks/useUserMetadata.test.ts +0 -191
- package/src/editor/hooks/useUserMetadata.ts +0 -71
- package/src/editor/permission-manager/GroupSelector.tsx +0 -73
- package/src/editor/permission-manager/PermissionEditor.tsx +0 -321
- package/src/editor/permission-manager/PermissionLevelBadge.tsx +0 -53
- package/src/editor/permission-manager/PermissionTree.tsx +0 -237
- package/src/editor/permission-manager/UserSelector.tsx +0 -95
- package/src/editor/permission-manager/constants.tsx +0 -18
- package/src/editor/permission-manager/hooks/useGroupsAndUsers.ts +0 -153
- package/src/editor/permission-manager/hooks/usePermissionTree.ts +0 -200
- package/src/editor/permission-manager/index.tsx +0 -294
- package/src/editor/permission-manager/types.ts +0 -58
- package/src/editor/permission-manager/utils.ts +0 -179
- package/src/editor/preview-bridge.test.tsx +0 -50
- package/src/editor/preview-bridge.tsx +0 -294
- package/src/editor/schema-editor/CollectionEditor.test.tsx +0 -238
- package/src/editor/schema-editor/CollectionEditor.tsx +0 -520
- package/src/editor/schema-editor/EntryTypeEditor.test.tsx +0 -215
- package/src/editor/schema-editor/EntryTypeEditor.tsx +0 -367
- package/src/editor/schema-editor/index.ts +0 -19
- package/src/editor/setup-test-dom.ts +0 -10
- package/src/editor/test-setup.ts +0 -33
- package/src/editor/theme.tsx +0 -119
- package/src/editor/utils/env.ts +0 -39
- package/src/entry-schema-registry.test.ts +0 -281
- package/src/entry-schema-registry.ts +0 -121
- package/src/entry-schema.ts +0 -84
- package/src/git-manager.test.ts +0 -552
- package/src/git-manager.ts +0 -667
- package/src/github-service.test.ts +0 -312
- package/src/github-service.ts +0 -295
- package/src/http/handler.test.ts +0 -275
- package/src/http/handler.ts +0 -280
- package/src/http/index.ts +0 -11
- package/src/http/router.ts +0 -164
- package/src/http/types.ts +0 -44
- package/src/id.test.ts +0 -48
- package/src/id.ts +0 -22
- package/src/index.ts +0 -26
- package/src/operating-mode/__tests__/strategies.test.ts +0 -511
- package/src/operating-mode/client-safe-strategy.ts +0 -184
- package/src/operating-mode/client-unsafe-strategy.ts +0 -303
- package/src/operating-mode/client.ts +0 -13
- package/src/operating-mode/index.ts +0 -34
- package/src/operating-mode/types.ts +0 -186
- package/src/paths/__tests__/branch.test.ts +0 -53
- package/src/paths/__tests__/normalize.test.ts +0 -141
- package/src/paths/__tests__/resolve.test.ts +0 -207
- package/src/paths/__tests__/validation.test.ts +0 -61
- package/src/paths/branch.ts +0 -115
- package/src/paths/index.ts +0 -73
- package/src/paths/normalize-server.ts +0 -40
- package/src/paths/normalize.ts +0 -107
- package/src/paths/resolve.ts +0 -61
- package/src/paths/test-utils.ts +0 -37
- package/src/paths/types.ts +0 -68
- package/src/paths/validation.test.ts +0 -480
- package/src/paths/validation.ts +0 -391
- package/src/reference-resolver.test.ts +0 -107
- package/src/reference-resolver.ts +0 -157
- package/src/schema/index.ts +0 -29
- package/src/schema/meta-loader.ts +0 -366
- package/src/schema/resolver.ts +0 -83
- package/src/schema/schema-store-types.ts +0 -56
- package/src/schema/schema-store.test.ts +0 -816
- package/src/schema/schema-store.ts +0 -795
- package/src/schema/types.ts +0 -33
- package/src/schema-meta-loader.test.ts +0 -447
- package/src/server.ts +0 -15
- package/src/services.test.ts +0 -559
- package/src/services.ts +0 -373
- package/src/settings-branch-utils.ts +0 -53
- package/src/settings-workspace.ts +0 -156
- package/src/task-queue/README.md +0 -144
- package/src/task-queue/index.ts +0 -14
- package/src/task-queue/task-queue.test.ts +0 -524
- package/src/task-queue/task-queue.ts +0 -514
- package/src/task-queue/types.ts +0 -41
- package/src/test-utils/api-test-helpers.ts +0 -445
- package/src/test-utils/console-spy.test.ts +0 -14
- package/src/test-utils/console-spy.ts +0 -125
- package/src/test-utils/git-helpers.ts +0 -31
- package/src/test-utils/index.ts +0 -4
- package/src/types.ts +0 -54
- package/src/user.ts +0 -118
- package/src/utils/debug.test.ts +0 -114
- package/src/utils/debug.ts +0 -127
- package/src/utils/error.test.ts +0 -92
- package/src/utils/error.ts +0 -83
- package/src/utils/format.ts +0 -12
- package/src/validation/__tests__/field-traversal.test.ts +0 -263
- package/src/validation/deletion-checker.ts +0 -234
- package/src/validation/field-traversal.ts +0 -146
- package/src/validation/reference-validator.ts +0 -168
- package/src/worker/cms-worker-rebase.test.ts +0 -473
- package/src/worker/cms-worker.ts +0 -777
- package/src/worker/integration.test.ts +0 -289
- package/src/worker/task-queue-config.ts +0 -25
- package/src/worker/task-queue.test.ts +0 -452
- package/src/worker/task-queue.ts +0 -58
- /package/{src/cli/templates → dist/cli/template-files}/Dockerfile.cms.template +0 -0
- /package/{src/cli/templates → dist/cli/template-files}/canopycms.config.ts.template +0 -0
- /package/{src/cli/templates → dist/cli/template-files}/deploy-cms.yml.template +0 -0
- /package/{src/cli/templates → dist/cli/template-files}/edit-page.tsx.template +0 -0
- /package/{src/cli/templates → dist/cli/template-files}/route.ts.template +0 -0
- /package/{src/cli/templates → dist/cli/template-files}/schemas.ts.template +0 -0
|
@@ -1,795 +0,0 @@
|
|
|
1
|
-
/**
|
|
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
|
-
* All mutations are branch-specific (like content edits).
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { promises as fs } from 'node:fs'
|
|
13
|
-
import path from 'node:path'
|
|
14
|
-
import { z } from 'zod'
|
|
15
|
-
|
|
16
|
-
import type { ContentFormat } from '../config'
|
|
17
|
-
import type { EntrySchemaRegistry } from './types'
|
|
18
|
-
import { resolveCollectionPath } from '../content-id-index'
|
|
19
|
-
import { generateId, isValidId } from '../id'
|
|
20
|
-
import { createLogicalPath, validateAndNormalizePath } from '../paths'
|
|
21
|
-
import type { LogicalPath, ContentId } from '../paths/types'
|
|
22
|
-
import type { CanopyServices } from '../services'
|
|
23
|
-
|
|
24
|
-
// Re-export types from client-safe module
|
|
25
|
-
export type {
|
|
26
|
-
CreateCollectionInput,
|
|
27
|
-
CreateEntryTypeInput,
|
|
28
|
-
UpdateCollectionInput,
|
|
29
|
-
UpdateEntryTypeInput,
|
|
30
|
-
} from './schema-store-types'
|
|
31
|
-
|
|
32
|
-
// Import types for internal use
|
|
33
|
-
import type {
|
|
34
|
-
CreateCollectionInput,
|
|
35
|
-
CreateEntryTypeInput,
|
|
36
|
-
UpdateCollectionInput,
|
|
37
|
-
UpdateEntryTypeInput,
|
|
38
|
-
} from './schema-store-types'
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Raw collection meta as stored in .collection.json
|
|
42
|
-
*/
|
|
43
|
-
interface CollectionMetaFile {
|
|
44
|
-
name: string
|
|
45
|
-
label?: string
|
|
46
|
-
entries?: Array<{
|
|
47
|
-
name: string
|
|
48
|
-
label?: string
|
|
49
|
-
format: ContentFormat
|
|
50
|
-
schema: string
|
|
51
|
-
default?: boolean
|
|
52
|
-
maxItems?: number
|
|
53
|
-
}>
|
|
54
|
-
order?: string[]
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Raw root collection meta as stored in content/.collection.json
|
|
59
|
-
*/
|
|
60
|
-
interface RootCollectionMetaFile {
|
|
61
|
-
label?: string
|
|
62
|
-
entries?: Array<{
|
|
63
|
-
name: string
|
|
64
|
-
label?: string
|
|
65
|
-
format: ContentFormat
|
|
66
|
-
schema: string
|
|
67
|
-
default?: boolean
|
|
68
|
-
maxItems?: number
|
|
69
|
-
}>
|
|
70
|
-
order?: string[]
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ============================================================================
|
|
74
|
-
// Zod Schemas for Validation
|
|
75
|
-
// ============================================================================
|
|
76
|
-
|
|
77
|
-
/** Max length for names and slugs (filesystem path safety) */
|
|
78
|
-
const MAX_NAME_LENGTH = 64
|
|
79
|
-
/** Max length for labels */
|
|
80
|
-
const MAX_LABEL_LENGTH = 128
|
|
81
|
-
|
|
82
|
-
const entryTypeInputSchema = z.object({
|
|
83
|
-
name: z.string().min(1).max(MAX_NAME_LENGTH),
|
|
84
|
-
label: z.string().max(MAX_LABEL_LENGTH).optional(),
|
|
85
|
-
format: z.enum(['md', 'mdx', 'json']),
|
|
86
|
-
schema: z.string().min(1),
|
|
87
|
-
default: z.boolean().optional(),
|
|
88
|
-
maxItems: z.number().int().positive().optional(),
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
const createCollectionInputSchema = z.object({
|
|
92
|
-
name: z.string().min(1).max(MAX_NAME_LENGTH),
|
|
93
|
-
label: z.string().max(MAX_LABEL_LENGTH).optional(),
|
|
94
|
-
parentPath: z.string().optional(),
|
|
95
|
-
entries: z.array(entryTypeInputSchema).min(1, 'Collection must have at least one entry type'),
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
const updateCollectionInputSchema = z.object({
|
|
99
|
-
name: z.string().min(1).max(MAX_NAME_LENGTH).optional(),
|
|
100
|
-
label: z.string().max(MAX_LABEL_LENGTH).optional(),
|
|
101
|
-
slug: z.string().min(1).max(MAX_NAME_LENGTH).optional(), // Directory name (e.g., "posts" in "posts.{id}/")
|
|
102
|
-
order: z.array(z.string()).optional(),
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
const updateEntryTypeInputSchema = z.object({
|
|
106
|
-
label: z.string().max(MAX_LABEL_LENGTH).optional(),
|
|
107
|
-
format: z.enum(['md', 'mdx', 'json']).optional(),
|
|
108
|
-
schema: z.string().min(1).optional(),
|
|
109
|
-
default: z.boolean().optional(),
|
|
110
|
-
maxItems: z.number().int().positive().optional(),
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
// ============================================================================
|
|
114
|
-
// SchemaOps Class
|
|
115
|
-
// ============================================================================
|
|
116
|
-
|
|
117
|
-
export class SchemaOps {
|
|
118
|
-
constructor(
|
|
119
|
-
private readonly contentRoot: string,
|
|
120
|
-
private readonly entrySchemaRegistry: EntrySchemaRegistry,
|
|
121
|
-
private readonly services?: CanopyServices,
|
|
122
|
-
) {}
|
|
123
|
-
|
|
124
|
-
// --------------------------------------------------------------------------
|
|
125
|
-
// Cache Invalidation
|
|
126
|
-
// --------------------------------------------------------------------------
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Invalidate schema cache for this branch after mutations.
|
|
130
|
-
* This marks the cache as stale so the next schema load will regenerate it.
|
|
131
|
-
*/
|
|
132
|
-
private async invalidateSchemaCache(): Promise<void> {
|
|
133
|
-
if (this.services) {
|
|
134
|
-
// Get branchRoot from contentRoot (parent directory)
|
|
135
|
-
const branchRoot = path.dirname(this.contentRoot)
|
|
136
|
-
await this.services.branchSchemaCache.invalidate(branchRoot)
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// --------------------------------------------------------------------------
|
|
141
|
-
// Validation Helpers
|
|
142
|
-
// --------------------------------------------------------------------------
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Validate that a schema reference exists in the registry
|
|
146
|
-
*/
|
|
147
|
-
validateSchemaReference(schemaKey: string): boolean {
|
|
148
|
-
return schemaKey in this.entrySchemaRegistry
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Validate all schema references in entry types
|
|
153
|
-
*/
|
|
154
|
-
private validateEntryTypeSchemas(entryTypes: CreateEntryTypeInput[]): {
|
|
155
|
-
valid: boolean
|
|
156
|
-
error?: string
|
|
157
|
-
} {
|
|
158
|
-
for (const entryType of entryTypes) {
|
|
159
|
-
if (!this.validateSchemaReference(entryType.schema)) {
|
|
160
|
-
const available = Object.keys(this.entrySchemaRegistry).join(', ')
|
|
161
|
-
return {
|
|
162
|
-
valid: false,
|
|
163
|
-
error: `Schema reference "${entryType.schema}" not found. Available: ${available}`,
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
return { valid: true }
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Validate path to prevent traversal attacks
|
|
172
|
-
*/
|
|
173
|
-
private validatePath(targetPath: string): {
|
|
174
|
-
valid: boolean
|
|
175
|
-
normalizedPath?: string
|
|
176
|
-
error?: string
|
|
177
|
-
} {
|
|
178
|
-
const result = validateAndNormalizePath(this.contentRoot, targetPath)
|
|
179
|
-
if (!result.valid) {
|
|
180
|
-
return { valid: false, error: result.error || 'Invalid path' }
|
|
181
|
-
}
|
|
182
|
-
return { valid: true, normalizedPath: result.normalizedPath }
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// --------------------------------------------------------------------------
|
|
186
|
-
// Read Operations
|
|
187
|
-
// --------------------------------------------------------------------------
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Read a collection's .collection.json file
|
|
191
|
-
*/
|
|
192
|
-
async readCollectionMeta(collectionPath: LogicalPath): Promise<CollectionMetaFile | null> {
|
|
193
|
-
// Resolve logical path to physical path with embedded IDs
|
|
194
|
-
const physicalPath = await resolveCollectionPath(this.contentRoot, collectionPath)
|
|
195
|
-
if (!physicalPath) {
|
|
196
|
-
return null
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const metaPath = path.join(physicalPath, '.collection.json')
|
|
200
|
-
try {
|
|
201
|
-
const content = await fs.readFile(metaPath, 'utf-8')
|
|
202
|
-
return JSON.parse(content) as CollectionMetaFile
|
|
203
|
-
} catch (err) {
|
|
204
|
-
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
205
|
-
return null
|
|
206
|
-
}
|
|
207
|
-
throw err
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Read root collection meta (content/.collection.json)
|
|
213
|
-
*/
|
|
214
|
-
async readRootCollectionMeta(): Promise<RootCollectionMetaFile | null> {
|
|
215
|
-
const metaPath = path.join(this.contentRoot, '.collection.json')
|
|
216
|
-
try {
|
|
217
|
-
const content = await fs.readFile(metaPath, 'utf-8')
|
|
218
|
-
return JSON.parse(content) as RootCollectionMetaFile
|
|
219
|
-
} catch (err) {
|
|
220
|
-
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
221
|
-
return null
|
|
222
|
-
}
|
|
223
|
-
throw err
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Check if a collection is empty (has no content files or child collections)
|
|
229
|
-
*/
|
|
230
|
-
async isCollectionEmpty(collectionPath: LogicalPath): Promise<boolean> {
|
|
231
|
-
const physicalPath = await resolveCollectionPath(this.contentRoot, collectionPath)
|
|
232
|
-
if (!physicalPath) {
|
|
233
|
-
// Collection doesn't exist, consider it empty
|
|
234
|
-
return true
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
try {
|
|
238
|
-
const entries = await fs.readdir(physicalPath, { withFileTypes: true })
|
|
239
|
-
for (const entry of entries) {
|
|
240
|
-
// Content files mean not empty
|
|
241
|
-
if (entry.isFile() && entry.name !== '.collection.json') {
|
|
242
|
-
return false
|
|
243
|
-
}
|
|
244
|
-
// Child collection directories mean not empty
|
|
245
|
-
if (entry.isDirectory()) {
|
|
246
|
-
try {
|
|
247
|
-
await fs.access(path.join(physicalPath, entry.name, '.collection.json'))
|
|
248
|
-
return false
|
|
249
|
-
} catch {
|
|
250
|
-
// Not a collection directory, ignore
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
return true
|
|
255
|
-
} catch (err) {
|
|
256
|
-
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
257
|
-
return true
|
|
258
|
-
}
|
|
259
|
-
throw err
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// --------------------------------------------------------------------------
|
|
264
|
-
// Write Operations
|
|
265
|
-
// --------------------------------------------------------------------------
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Write a collection's .collection.json file
|
|
269
|
-
*/
|
|
270
|
-
private async writeCollectionMeta(physicalPath: string, meta: CollectionMetaFile): Promise<void> {
|
|
271
|
-
const metaPath = path.join(physicalPath, '.collection.json')
|
|
272
|
-
const content = JSON.stringify(meta, null, 2) + '\n'
|
|
273
|
-
await fs.writeFile(metaPath, content, 'utf-8')
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Write root collection meta
|
|
278
|
-
*/
|
|
279
|
-
private async writeRootCollectionMeta(meta: RootCollectionMetaFile): Promise<void> {
|
|
280
|
-
const metaPath = path.join(this.contentRoot, '.collection.json')
|
|
281
|
-
const content = JSON.stringify(meta, null, 2) + '\n'
|
|
282
|
-
await fs.writeFile(metaPath, content, 'utf-8')
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// --------------------------------------------------------------------------
|
|
286
|
-
// Collection Operations
|
|
287
|
-
// --------------------------------------------------------------------------
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Create a new collection
|
|
291
|
-
*/
|
|
292
|
-
async createCollection(
|
|
293
|
-
input: CreateCollectionInput,
|
|
294
|
-
): Promise<{ collectionPath: LogicalPath; contentId: ContentId }> {
|
|
295
|
-
// Validate input
|
|
296
|
-
const parseResult = createCollectionInputSchema.safeParse(input)
|
|
297
|
-
if (!parseResult.success) {
|
|
298
|
-
throw new Error(`Invalid input: ${parseResult.error.message}`)
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Validate schema references
|
|
302
|
-
const schemaValidation = this.validateEntryTypeSchemas(input.entries)
|
|
303
|
-
if (!schemaValidation.valid) {
|
|
304
|
-
throw new Error(schemaValidation.error)
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Determine parent directory
|
|
308
|
-
let parentPhysicalPath: string
|
|
309
|
-
if (input.parentPath) {
|
|
310
|
-
const resolved = await resolveCollectionPath(this.contentRoot, input.parentPath)
|
|
311
|
-
if (!resolved) {
|
|
312
|
-
throw new Error(`Parent collection not found: ${input.parentPath}`)
|
|
313
|
-
}
|
|
314
|
-
parentPhysicalPath = resolved
|
|
315
|
-
} else {
|
|
316
|
-
parentPhysicalPath = this.contentRoot
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// Generate embedded ID for new collection
|
|
320
|
-
const contentId = generateId()
|
|
321
|
-
const dirName = `${input.name}.${contentId}`
|
|
322
|
-
const physicalPath = path.join(parentPhysicalPath, dirName)
|
|
323
|
-
|
|
324
|
-
// Create directory
|
|
325
|
-
await fs.mkdir(physicalPath, { recursive: true })
|
|
326
|
-
|
|
327
|
-
// Build collection meta with empty order array (required for ordering support)
|
|
328
|
-
const meta: CollectionMetaFile = {
|
|
329
|
-
name: input.name,
|
|
330
|
-
label: input.label,
|
|
331
|
-
entries: input.entries.map((et) => ({
|
|
332
|
-
name: et.name,
|
|
333
|
-
label: et.label,
|
|
334
|
-
format: et.format,
|
|
335
|
-
schema: et.schema,
|
|
336
|
-
default: et.default,
|
|
337
|
-
maxItems: et.maxItems,
|
|
338
|
-
})),
|
|
339
|
-
order: [], // Initialize with empty order array
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// Write .collection.json
|
|
343
|
-
await this.writeCollectionMeta(physicalPath, meta)
|
|
344
|
-
|
|
345
|
-
// Add new collection's contentId to parent's order array
|
|
346
|
-
// For root-level collections (empty parentPath), we don't update parent order
|
|
347
|
-
const parentLogicalPath = input.parentPath
|
|
348
|
-
? createLogicalPath(input.parentPath)
|
|
349
|
-
: createLogicalPath('')
|
|
350
|
-
const parentMeta = input.parentPath ? await this.readCollectionMeta(parentLogicalPath) : null
|
|
351
|
-
if (parentMeta) {
|
|
352
|
-
// Initialize parent's order array if it doesn't exist
|
|
353
|
-
const existingOrder = parentMeta.order ?? []
|
|
354
|
-
parentMeta.order = [...existingOrder, contentId]
|
|
355
|
-
await this.writeCollectionMeta(parentPhysicalPath, parentMeta)
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// Build logical path
|
|
359
|
-
const logicalPath = input.parentPath
|
|
360
|
-
? createLogicalPath(`${input.parentPath}/${input.name}`)
|
|
361
|
-
: createLogicalPath(input.name)
|
|
362
|
-
|
|
363
|
-
// Invalidate schema cache after mutation
|
|
364
|
-
await this.invalidateSchemaCache()
|
|
365
|
-
|
|
366
|
-
return { collectionPath: logicalPath, contentId }
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* Update a collection's metadata
|
|
371
|
-
*/
|
|
372
|
-
async updateCollection(
|
|
373
|
-
collectionPath: LogicalPath,
|
|
374
|
-
updates: UpdateCollectionInput,
|
|
375
|
-
): Promise<void> {
|
|
376
|
-
// Validate input
|
|
377
|
-
const parseResult = updateCollectionInputSchema.safeParse(updates)
|
|
378
|
-
if (!parseResult.success) {
|
|
379
|
-
throw new Error(`Invalid input: ${parseResult.error.message}`)
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// Check if this is the root collection (path equals contentRoot basename, e.g., "content")
|
|
383
|
-
const contentRootName = path.basename(this.contentRoot)
|
|
384
|
-
if (collectionPath === contentRootName) {
|
|
385
|
-
// Update root collection meta
|
|
386
|
-
let meta = await this.readRootCollectionMeta()
|
|
387
|
-
if (!meta) {
|
|
388
|
-
meta = {}
|
|
389
|
-
}
|
|
390
|
-
// Root only supports label and order updates (no name)
|
|
391
|
-
if (updates.label !== undefined) {
|
|
392
|
-
meta.label = updates.label
|
|
393
|
-
}
|
|
394
|
-
if (updates.order !== undefined) {
|
|
395
|
-
meta.order = updates.order
|
|
396
|
-
}
|
|
397
|
-
await this.writeRootCollectionMeta(meta)
|
|
398
|
-
// Invalidate schema cache after mutation
|
|
399
|
-
await this.invalidateSchemaCache()
|
|
400
|
-
return
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// Strip contentRoot prefix to get relative path for regular collection
|
|
404
|
-
// E.g., "content/posts" -> "posts"
|
|
405
|
-
const relativePath = collectionPath.startsWith(`${contentRootName}/`)
|
|
406
|
-
? collectionPath.slice(contentRootName.length + 1)
|
|
407
|
-
: collectionPath
|
|
408
|
-
|
|
409
|
-
// Resolve path for regular collection
|
|
410
|
-
const physicalPath = await resolveCollectionPath(
|
|
411
|
-
this.contentRoot,
|
|
412
|
-
createLogicalPath(relativePath),
|
|
413
|
-
)
|
|
414
|
-
if (!physicalPath) {
|
|
415
|
-
throw new Error(`Collection not found: ${collectionPath}`)
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// Read existing meta
|
|
419
|
-
const meta = await this.readCollectionMeta(relativePath as LogicalPath)
|
|
420
|
-
if (!meta) {
|
|
421
|
-
throw new Error(`Collection meta not found: ${collectionPath}`)
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Handle slug change (directory rename) if provided
|
|
425
|
-
let finalPhysicalPath = physicalPath
|
|
426
|
-
if (updates.slug !== undefined) {
|
|
427
|
-
// Extract current slug and ID from physical path
|
|
428
|
-
// Format: /path/to/{slug}.{12-char-id}
|
|
429
|
-
const dirName = path.basename(physicalPath)
|
|
430
|
-
const parts = dirName.split('.')
|
|
431
|
-
|
|
432
|
-
if (parts.length !== 2 || !isValidId(parts[1])) {
|
|
433
|
-
throw new Error(`Invalid collection directory format: ${dirName}`)
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const currentSlug = parts[0]
|
|
437
|
-
const contentId = parts[1]
|
|
438
|
-
|
|
439
|
-
// Only rename if slug is actually different
|
|
440
|
-
if (updates.slug !== currentSlug) {
|
|
441
|
-
// Validate new slug (alphanumeric + hyphens, lowercase)
|
|
442
|
-
if (!/^[a-z][a-z0-9-]*$/.test(updates.slug)) {
|
|
443
|
-
throw new Error(
|
|
444
|
-
'Slug must start with a letter and contain only lowercase letters, numbers, and hyphens',
|
|
445
|
-
)
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// Build new path with new slug + same ID
|
|
449
|
-
const parentDir = path.dirname(physicalPath)
|
|
450
|
-
const newDirName = `${updates.slug}.${contentId}`
|
|
451
|
-
const newPhysicalPath = path.join(parentDir, newDirName)
|
|
452
|
-
|
|
453
|
-
// Check if any collection with this slug already exists
|
|
454
|
-
// Need to check for any directory matching {slug}.{any-id}
|
|
455
|
-
try {
|
|
456
|
-
const entries = await fs.readdir(parentDir, { withFileTypes: true })
|
|
457
|
-
for (const entry of entries) {
|
|
458
|
-
if (entry.isDirectory() && entry.name.startsWith(`${updates.slug}.`)) {
|
|
459
|
-
const parts = entry.name.split('.')
|
|
460
|
-
if (parts.length === 2 && isValidId(parts[1])) {
|
|
461
|
-
throw new Error(`Collection with slug "${updates.slug}" already exists`)
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
} catch (err) {
|
|
466
|
-
// Re-throw "already exists" errors
|
|
467
|
-
if ((err as Error).message.includes('already exists')) {
|
|
468
|
-
throw err
|
|
469
|
-
}
|
|
470
|
-
// Ignore other errors (e.g., ENOENT if parent dir doesn't exist somehow)
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// Atomically rename the directory
|
|
474
|
-
await fs.rename(physicalPath, newPhysicalPath)
|
|
475
|
-
finalPhysicalPath = newPhysicalPath
|
|
476
|
-
|
|
477
|
-
// Note: Content ID index will rebuild lazily on next access
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// Apply metadata updates
|
|
482
|
-
if (updates.name !== undefined) {
|
|
483
|
-
meta.name = updates.name
|
|
484
|
-
}
|
|
485
|
-
if (updates.label !== undefined) {
|
|
486
|
-
meta.label = updates.label
|
|
487
|
-
}
|
|
488
|
-
if (updates.order !== undefined) {
|
|
489
|
-
meta.order = updates.order
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
// Write back to the (potentially renamed) path
|
|
493
|
-
await this.writeCollectionMeta(finalPhysicalPath, meta)
|
|
494
|
-
|
|
495
|
-
// Invalidate schema cache after mutation
|
|
496
|
-
await this.invalidateSchemaCache()
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
/**
|
|
500
|
-
* Delete a collection (must be empty)
|
|
501
|
-
*/
|
|
502
|
-
async deleteCollection(collectionPath: LogicalPath): Promise<void> {
|
|
503
|
-
// Check if empty
|
|
504
|
-
const isEmpty = await this.isCollectionEmpty(collectionPath)
|
|
505
|
-
if (!isEmpty) {
|
|
506
|
-
throw new Error('Collection must be empty before deletion. Delete all entries first.')
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// Resolve path
|
|
510
|
-
const physicalPath = await resolveCollectionPath(this.contentRoot, collectionPath)
|
|
511
|
-
if (!physicalPath) {
|
|
512
|
-
throw new Error(`Collection not found: ${collectionPath}`)
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
// Delete the directory (including .collection.json)
|
|
516
|
-
await fs.rm(physicalPath, { recursive: true })
|
|
517
|
-
|
|
518
|
-
// Invalidate schema cache after mutation
|
|
519
|
-
await this.invalidateSchemaCache()
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
// --------------------------------------------------------------------------
|
|
523
|
-
// Entry Type Operations
|
|
524
|
-
// --------------------------------------------------------------------------
|
|
525
|
-
|
|
526
|
-
/**
|
|
527
|
-
* Add an entry type to a collection
|
|
528
|
-
*/
|
|
529
|
-
async addEntryType(collectionPath: LogicalPath, entryType: CreateEntryTypeInput): Promise<void> {
|
|
530
|
-
// Validate input
|
|
531
|
-
const parseResult = entryTypeInputSchema.safeParse(entryType)
|
|
532
|
-
if (!parseResult.success) {
|
|
533
|
-
throw new Error(`Invalid input: ${parseResult.error.message}`)
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// Validate schema reference
|
|
537
|
-
if (!this.validateSchemaReference(entryType.schema)) {
|
|
538
|
-
const available = Object.keys(this.entrySchemaRegistry).join(', ')
|
|
539
|
-
throw new Error(`Schema reference "${entryType.schema}" not found. Available: ${available}`)
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
// Resolve path
|
|
543
|
-
const physicalPath = await resolveCollectionPath(this.contentRoot, collectionPath)
|
|
544
|
-
if (!physicalPath) {
|
|
545
|
-
throw new Error(`Collection not found: ${collectionPath}`)
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
// Read existing meta
|
|
549
|
-
const meta = await this.readCollectionMeta(collectionPath)
|
|
550
|
-
if (!meta) {
|
|
551
|
-
throw new Error(`Collection meta not found: ${collectionPath}`)
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// Check for duplicate name
|
|
555
|
-
if (meta.entries?.some((et) => et.name === entryType.name)) {
|
|
556
|
-
throw new Error(`Entry type "${entryType.name}" already exists in this collection`)
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// Add entry type
|
|
560
|
-
meta.entries = meta.entries || []
|
|
561
|
-
meta.entries.push({
|
|
562
|
-
name: entryType.name,
|
|
563
|
-
label: entryType.label,
|
|
564
|
-
format: entryType.format,
|
|
565
|
-
schema: entryType.schema,
|
|
566
|
-
default: entryType.default,
|
|
567
|
-
maxItems: entryType.maxItems,
|
|
568
|
-
})
|
|
569
|
-
|
|
570
|
-
// Write back
|
|
571
|
-
await this.writeCollectionMeta(physicalPath, meta)
|
|
572
|
-
|
|
573
|
-
// Invalidate schema cache after mutation
|
|
574
|
-
await this.invalidateSchemaCache()
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
/**
|
|
578
|
-
* Update an entry type in a collection
|
|
579
|
-
*/
|
|
580
|
-
async updateEntryType(
|
|
581
|
-
collectionPath: LogicalPath,
|
|
582
|
-
entryTypeName: string,
|
|
583
|
-
updates: UpdateEntryTypeInput,
|
|
584
|
-
): Promise<void> {
|
|
585
|
-
// Validate input
|
|
586
|
-
const parseResult = updateEntryTypeInputSchema.safeParse(updates)
|
|
587
|
-
if (!parseResult.success) {
|
|
588
|
-
throw new Error(`Invalid input: ${parseResult.error.message}`)
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// Validate schema reference if provided
|
|
592
|
-
if (updates.schema && !this.validateSchemaReference(updates.schema)) {
|
|
593
|
-
const available = Object.keys(this.entrySchemaRegistry).join(', ')
|
|
594
|
-
throw new Error(`Schema reference "${updates.schema}" not found. Available: ${available}`)
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
// Resolve path
|
|
598
|
-
const physicalPath = await resolveCollectionPath(this.contentRoot, collectionPath)
|
|
599
|
-
if (!physicalPath) {
|
|
600
|
-
throw new Error(`Collection not found: ${collectionPath}`)
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
// Read existing meta
|
|
604
|
-
const meta = await this.readCollectionMeta(collectionPath)
|
|
605
|
-
if (!meta) {
|
|
606
|
-
throw new Error(`Collection meta not found: ${collectionPath}`)
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
// Find entry type
|
|
610
|
-
const entryType = meta.entries?.find((et) => et.name === entryTypeName)
|
|
611
|
-
if (!entryType) {
|
|
612
|
-
throw new Error(`Entry type "${entryTypeName}" not found in collection`)
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
// Apply updates
|
|
616
|
-
if (updates.label !== undefined) {
|
|
617
|
-
entryType.label = updates.label
|
|
618
|
-
}
|
|
619
|
-
if (updates.format !== undefined) {
|
|
620
|
-
entryType.format = updates.format
|
|
621
|
-
}
|
|
622
|
-
if (updates.schema !== undefined) {
|
|
623
|
-
entryType.schema = updates.schema
|
|
624
|
-
}
|
|
625
|
-
if (updates.default !== undefined) {
|
|
626
|
-
entryType.default = updates.default
|
|
627
|
-
}
|
|
628
|
-
if (updates.maxItems !== undefined) {
|
|
629
|
-
entryType.maxItems = updates.maxItems
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
// Write back
|
|
633
|
-
await this.writeCollectionMeta(physicalPath, meta)
|
|
634
|
-
|
|
635
|
-
// Invalidate schema cache after mutation
|
|
636
|
-
await this.invalidateSchemaCache()
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
/**
|
|
640
|
-
* Remove an entry type from a collection
|
|
641
|
-
*/
|
|
642
|
-
async removeEntryType(collectionPath: LogicalPath, entryTypeName: string): Promise<void> {
|
|
643
|
-
// Resolve path
|
|
644
|
-
const physicalPath = await resolveCollectionPath(this.contentRoot, collectionPath)
|
|
645
|
-
if (!physicalPath) {
|
|
646
|
-
throw new Error(`Collection not found: ${collectionPath}`)
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
// Read existing meta
|
|
650
|
-
const meta = await this.readCollectionMeta(collectionPath)
|
|
651
|
-
if (!meta) {
|
|
652
|
-
throw new Error(`Collection meta not found: ${collectionPath}`)
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
// Check entry type exists
|
|
656
|
-
const index = meta.entries?.findIndex((et) => et.name === entryTypeName) ?? -1
|
|
657
|
-
if (index === -1) {
|
|
658
|
-
throw new Error(`Entry type "${entryTypeName}" not found in collection`)
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
// Ensure at least one entry type remains
|
|
662
|
-
if (meta.entries!.length === 1) {
|
|
663
|
-
throw new Error(
|
|
664
|
-
'Cannot remove last entry type. Collection must have at least one entry type.',
|
|
665
|
-
)
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
// Check for entries still using this type
|
|
669
|
-
const usageCount = await this.countEntriesUsingType(collectionPath, entryTypeName)
|
|
670
|
-
if (usageCount > 0) {
|
|
671
|
-
throw new Error(
|
|
672
|
-
`Cannot remove entry type "${entryTypeName}": ${usageCount} ${usageCount === 1 ? 'entry still uses' : 'entries still use'} it. ` +
|
|
673
|
-
'Delete or migrate those entries first.',
|
|
674
|
-
)
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
// Remove entry type
|
|
678
|
-
meta.entries!.splice(index, 1)
|
|
679
|
-
|
|
680
|
-
// Write back
|
|
681
|
-
await this.writeCollectionMeta(physicalPath, meta)
|
|
682
|
-
|
|
683
|
-
// Invalidate schema cache after mutation
|
|
684
|
-
await this.invalidateSchemaCache()
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
// --------------------------------------------------------------------------
|
|
688
|
-
// Usage Counting
|
|
689
|
-
// --------------------------------------------------------------------------
|
|
690
|
-
|
|
691
|
-
/**
|
|
692
|
-
* Count the number of entries using a specific entry type in a collection.
|
|
693
|
-
* This is used to prevent breaking changes to entry types that have existing content.
|
|
694
|
-
*
|
|
695
|
-
* @param collectionPath - Logical path to the collection (e.g., "content/posts")
|
|
696
|
-
* @param entryTypeName - Name of the entry type to count
|
|
697
|
-
* @returns Number of entries using this entry type
|
|
698
|
-
*
|
|
699
|
-
* @example
|
|
700
|
-
* ```ts
|
|
701
|
-
* const count = await store.countEntriesUsingType('content/posts', 'post')
|
|
702
|
-
* if (count > 0) {
|
|
703
|
-
* // Cannot modify schema/format
|
|
704
|
-
* }
|
|
705
|
-
* ```
|
|
706
|
-
*/
|
|
707
|
-
async countEntriesUsingType(collectionPath: LogicalPath, entryTypeName: string): Promise<number> {
|
|
708
|
-
// Resolve collection physical path
|
|
709
|
-
const physicalPath = await resolveCollectionPath(this.contentRoot, collectionPath)
|
|
710
|
-
if (!physicalPath) {
|
|
711
|
-
// Collection doesn't exist yet - return 0
|
|
712
|
-
return 0
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
try {
|
|
716
|
-
// Read directory entries
|
|
717
|
-
const entries = await fs.readdir(physicalPath, { withFileTypes: true })
|
|
718
|
-
|
|
719
|
-
// Count files matching pattern: {entryTypeName}.{slug}.{id}.{ext}
|
|
720
|
-
let count = 0
|
|
721
|
-
for (const entry of entries) {
|
|
722
|
-
// Skip directories and hidden files
|
|
723
|
-
if (entry.isDirectory() || entry.name.startsWith('.')) {
|
|
724
|
-
continue
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
// Parse filename: type.slug.id.ext
|
|
728
|
-
const parts = entry.name.split('.')
|
|
729
|
-
|
|
730
|
-
// Need at least 4 parts: type, slug, id, ext
|
|
731
|
-
if (parts.length < 4) {
|
|
732
|
-
continue
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
// Check if first part matches entry type name
|
|
736
|
-
if (parts[0] !== entryTypeName) {
|
|
737
|
-
continue
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
// Check if second-to-last part is a valid 12-char ID
|
|
741
|
-
const candidateId = parts[parts.length - 2]
|
|
742
|
-
if (isValidId(candidateId)) {
|
|
743
|
-
count++
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
return count
|
|
748
|
-
} catch (err) {
|
|
749
|
-
// Directory might not exist yet
|
|
750
|
-
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
751
|
-
return 0
|
|
752
|
-
}
|
|
753
|
-
throw err
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
// --------------------------------------------------------------------------
|
|
758
|
-
// Order Operations
|
|
759
|
-
// --------------------------------------------------------------------------
|
|
760
|
-
|
|
761
|
-
/**
|
|
762
|
-
* Update the order of items in a collection
|
|
763
|
-
*/
|
|
764
|
-
async updateOrder(collectionPath: LogicalPath, order: string[]): Promise<void> {
|
|
765
|
-
// Check if this is the root collection (path equals contentRoot basename, e.g., "content")
|
|
766
|
-
const contentRootName = path.basename(this.contentRoot)
|
|
767
|
-
if (collectionPath === contentRootName) {
|
|
768
|
-
// Update root collection meta
|
|
769
|
-
let meta = await this.readRootCollectionMeta()
|
|
770
|
-
if (!meta) {
|
|
771
|
-
meta = {}
|
|
772
|
-
}
|
|
773
|
-
meta.order = order
|
|
774
|
-
await this.writeRootCollectionMeta(meta)
|
|
775
|
-
// Invalidate schema cache after mutation
|
|
776
|
-
await this.invalidateSchemaCache()
|
|
777
|
-
return
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
// Update regular collection (handles contentRoot prefix stripping internally)
|
|
781
|
-
// Note: updateCollection already invalidates cache, so no need to do it again
|
|
782
|
-
await this.updateCollection(collectionPath, { order })
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
// ============================================================================
|
|
787
|
-
// Exports
|
|
788
|
-
// ============================================================================
|
|
789
|
-
|
|
790
|
-
export {
|
|
791
|
-
createCollectionInputSchema,
|
|
792
|
-
updateCollectionInputSchema,
|
|
793
|
-
entryTypeInputSchema,
|
|
794
|
-
updateEntryTypeInputSchema,
|
|
795
|
-
}
|