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
package/src/api/entries.test.ts
DELETED
|
@@ -1,1339 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises'
|
|
2
|
-
import os from 'node:os'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
|
|
5
|
-
import { describe, expect, it, vi } from 'vitest'
|
|
6
|
-
|
|
7
|
-
import { defineCanopyTestConfig } from '../config-test'
|
|
8
|
-
import { flattenSchema } from '../config'
|
|
9
|
-
import { createCheckBranchAccess } from '../authorization'
|
|
10
|
-
import { createCheckContentAccess } from '../authorization'
|
|
11
|
-
import { unsafeAsPermissionPath } from '../authorization/test-utils'
|
|
12
|
-
import type { PathPermission } from '../config'
|
|
13
|
-
import { listEntries } from './entries'
|
|
14
|
-
import { createMockApiContext, createMockBranchContext } from '../test-utils'
|
|
15
|
-
import { loadCollectionMetaFiles, resolveCollectionReferences } from '../schema'
|
|
16
|
-
import { unsafeAsBranchName, unsafeAsLogicalPath } from '../paths/test-utils'
|
|
17
|
-
|
|
18
|
-
const tmpDir = async () => fs.mkdtemp(path.join(os.tmpdir(), 'canopycms-entries-'))
|
|
19
|
-
|
|
20
|
-
describe('listEntries', () => {
|
|
21
|
-
it('lists entries with access filtering and pagination', async () => {
|
|
22
|
-
const root = await tmpDir()
|
|
23
|
-
await fs.mkdir(path.join(root, 'content/posts'), { recursive: true })
|
|
24
|
-
await fs.writeFile(
|
|
25
|
-
path.join(root, 'content/posts/entry.first.abc123def456.json'),
|
|
26
|
-
JSON.stringify({ title: 'First Post' }),
|
|
27
|
-
'utf8',
|
|
28
|
-
)
|
|
29
|
-
await fs.writeFile(
|
|
30
|
-
path.join(root, 'content/posts/entry.hidden.xyz789abcDEF.json'),
|
|
31
|
-
JSON.stringify({ title: 'Hidden Post' }),
|
|
32
|
-
'utf8',
|
|
33
|
-
)
|
|
34
|
-
await fs.writeFile(
|
|
35
|
-
path.join(root, 'content/settings.abc123XYZ789.json'),
|
|
36
|
-
JSON.stringify({ siteName: 'CanopyCMS' }),
|
|
37
|
-
'utf8',
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
const schema = {
|
|
41
|
-
collections: [
|
|
42
|
-
{
|
|
43
|
-
name: 'posts',
|
|
44
|
-
path: 'posts',
|
|
45
|
-
entries: [
|
|
46
|
-
{
|
|
47
|
-
name: 'entry',
|
|
48
|
-
format: 'json' as const,
|
|
49
|
-
schema: [{ name: 'title', type: 'string' as const }],
|
|
50
|
-
},
|
|
51
|
-
],
|
|
52
|
-
},
|
|
53
|
-
],
|
|
54
|
-
} as const
|
|
55
|
-
|
|
56
|
-
const config = defineCanopyTestConfig({
|
|
57
|
-
defaultBranchAccess: 'allow',
|
|
58
|
-
schema,
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
// Mock loadPathPermissions to return rules that hide 'entry.hidden.xyz789abcDEF.json' from user 'u1'
|
|
62
|
-
// Use 'read' access restriction to actually hide the file from listing
|
|
63
|
-
const pathRules: PathPermission[] = [
|
|
64
|
-
{
|
|
65
|
-
path: unsafeAsPermissionPath('content/posts/entry.hidden.xyz789abcDEF.json'),
|
|
66
|
-
read: { allowedUsers: ['other'] },
|
|
67
|
-
},
|
|
68
|
-
]
|
|
69
|
-
const mockLoadPermissions = vi.fn().mockResolvedValue(pathRules)
|
|
70
|
-
|
|
71
|
-
const checkBranchAccess = createCheckBranchAccess('allow')
|
|
72
|
-
const checkContentAccess = createCheckContentAccess({
|
|
73
|
-
checkBranchAccess,
|
|
74
|
-
loadPathPermissions: mockLoadPermissions,
|
|
75
|
-
defaultPathAccess: 'allow',
|
|
76
|
-
mode: 'dev',
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
const ctx = createMockApiContext({
|
|
80
|
-
services: {
|
|
81
|
-
config,
|
|
82
|
-
checkBranchAccess,
|
|
83
|
-
checkContentAccess,
|
|
84
|
-
},
|
|
85
|
-
branchContext: {
|
|
86
|
-
...createMockBranchContext({
|
|
87
|
-
branchName: 'main',
|
|
88
|
-
baseRoot: root,
|
|
89
|
-
branchRoot: root,
|
|
90
|
-
createdBy: 'u1',
|
|
91
|
-
}),
|
|
92
|
-
flatSchema: flattenSchema(schema, config.contentRoot),
|
|
93
|
-
},
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
// Request limit=2 to get entries
|
|
97
|
-
const res = await listEntries.handler(
|
|
98
|
-
ctx,
|
|
99
|
-
{ user: { type: 'authenticated', userId: 'u1', groups: [] } },
|
|
100
|
-
{ branch: unsafeAsBranchName('main'), limit: 2 },
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
expect(res.ok).toBe(true)
|
|
104
|
-
// Should include posts but not hidden.json (restricted by permission)
|
|
105
|
-
expect(res.data?.entries.some((e) => e.slug === 'first')).toBe(true)
|
|
106
|
-
expect(res.data?.entries.some((e) => e.slug === 'hidden')).toBe(false)
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
it('returns 404 when branch is missing', async () => {
|
|
110
|
-
const ctx = createMockApiContext({ branchContext: null })
|
|
111
|
-
const res = await listEntries.handler(
|
|
112
|
-
ctx,
|
|
113
|
-
{ user: { type: 'authenticated', userId: 'u1', groups: [] } },
|
|
114
|
-
{ branch: unsafeAsBranchName('missing') },
|
|
115
|
-
)
|
|
116
|
-
expect(res.status).toBe(404)
|
|
117
|
-
expect(res.ok).toBe(false)
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
it('lists entries recursively from deeply nested collections', async () => {
|
|
121
|
-
const root = await tmpDir()
|
|
122
|
-
|
|
123
|
-
// Create 3-level nested structure with embedded IDs: docs/api/v2
|
|
124
|
-
// This mirrors the real example1 structure
|
|
125
|
-
const docsId = 'bChqT78gcaLd'
|
|
126
|
-
const apiId = 'meiuwxTSo7UN'
|
|
127
|
-
const v2Id = 'muwmyafM6mEJ'
|
|
128
|
-
|
|
129
|
-
await fs.mkdir(path.join(root, `content/docs.${docsId}`), {
|
|
130
|
-
recursive: true,
|
|
131
|
-
})
|
|
132
|
-
await fs.mkdir(path.join(root, `content/docs.${docsId}/api.${apiId}`), {
|
|
133
|
-
recursive: true,
|
|
134
|
-
})
|
|
135
|
-
await fs.mkdir(path.join(root, `content/docs.${docsId}/api.${apiId}/v2.${v2Id}`), {
|
|
136
|
-
recursive: true,
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
// Create entries at each level with embedded IDs in filenames
|
|
140
|
-
const overviewId = 'gnVmHnnMjWrD'
|
|
141
|
-
const introId = 'k396pBDVP8tC'
|
|
142
|
-
const authId = 'kmtzTh2k9Axq'
|
|
143
|
-
const usersId = 'ppqJw61uKkV5'
|
|
144
|
-
|
|
145
|
-
await fs.writeFile(
|
|
146
|
-
path.join(root, `content/docs.${docsId}/entry.overview.${overviewId}.json`),
|
|
147
|
-
JSON.stringify({ title: 'Overview' }),
|
|
148
|
-
'utf8',
|
|
149
|
-
)
|
|
150
|
-
await fs.writeFile(
|
|
151
|
-
path.join(root, `content/docs.${docsId}/api.${apiId}/entry.intro.${introId}.json`),
|
|
152
|
-
JSON.stringify({ title: 'API Introduction' }),
|
|
153
|
-
'utf8',
|
|
154
|
-
)
|
|
155
|
-
await fs.writeFile(
|
|
156
|
-
path.join(root, `content/docs.${docsId}/api.${apiId}/v2.${v2Id}/entry.auth.${authId}.json`),
|
|
157
|
-
JSON.stringify({ title: 'Authentication' }),
|
|
158
|
-
'utf8',
|
|
159
|
-
)
|
|
160
|
-
await fs.writeFile(
|
|
161
|
-
path.join(root, `content/docs.${docsId}/api.${apiId}/v2.${v2Id}/entry.users.${usersId}.json`),
|
|
162
|
-
JSON.stringify({ title: 'Users API' }),
|
|
163
|
-
'utf8',
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
const schema = {
|
|
167
|
-
collections: [
|
|
168
|
-
{
|
|
169
|
-
name: 'docs',
|
|
170
|
-
path: 'docs',
|
|
171
|
-
entries: [
|
|
172
|
-
{
|
|
173
|
-
name: 'entry',
|
|
174
|
-
format: 'json' as const,
|
|
175
|
-
schema: [{ name: 'title', type: 'string' as const }],
|
|
176
|
-
},
|
|
177
|
-
],
|
|
178
|
-
collections: [
|
|
179
|
-
{
|
|
180
|
-
name: 'api',
|
|
181
|
-
path: 'api',
|
|
182
|
-
entries: [
|
|
183
|
-
{
|
|
184
|
-
name: 'entry',
|
|
185
|
-
format: 'json' as const,
|
|
186
|
-
schema: [{ name: 'title', type: 'string' as const }],
|
|
187
|
-
},
|
|
188
|
-
],
|
|
189
|
-
collections: [
|
|
190
|
-
{
|
|
191
|
-
name: 'v2',
|
|
192
|
-
path: 'v2',
|
|
193
|
-
entries: [
|
|
194
|
-
{
|
|
195
|
-
name: 'entry',
|
|
196
|
-
format: 'json' as const,
|
|
197
|
-
schema: [{ name: 'title', type: 'string' as const }],
|
|
198
|
-
},
|
|
199
|
-
],
|
|
200
|
-
},
|
|
201
|
-
],
|
|
202
|
-
},
|
|
203
|
-
],
|
|
204
|
-
},
|
|
205
|
-
],
|
|
206
|
-
} as const
|
|
207
|
-
|
|
208
|
-
const config = defineCanopyTestConfig({
|
|
209
|
-
defaultBranchAccess: 'allow',
|
|
210
|
-
schema,
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
const checkBranchAccess = createCheckBranchAccess('allow')
|
|
214
|
-
const checkContentAccess = createCheckContentAccess({
|
|
215
|
-
checkBranchAccess,
|
|
216
|
-
loadPathPermissions: vi.fn().mockResolvedValue([]),
|
|
217
|
-
defaultPathAccess: 'allow',
|
|
218
|
-
mode: 'dev',
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
const ctx = createMockApiContext({
|
|
222
|
-
services: {
|
|
223
|
-
config,
|
|
224
|
-
checkBranchAccess,
|
|
225
|
-
checkContentAccess,
|
|
226
|
-
},
|
|
227
|
-
branchContext: {
|
|
228
|
-
...createMockBranchContext({
|
|
229
|
-
branchName: 'main',
|
|
230
|
-
baseRoot: root,
|
|
231
|
-
branchRoot: root,
|
|
232
|
-
createdBy: 'u1',
|
|
233
|
-
}),
|
|
234
|
-
flatSchema: flattenSchema(schema, config.contentRoot),
|
|
235
|
-
},
|
|
236
|
-
})
|
|
237
|
-
|
|
238
|
-
// Test 1: Without collection filter - lists entries from all collections (flat list)
|
|
239
|
-
const allEntriesRes = await listEntries.handler(
|
|
240
|
-
ctx,
|
|
241
|
-
{ user: { type: 'authenticated', userId: 'u1', groups: [] } },
|
|
242
|
-
{ branch: unsafeAsBranchName('main') },
|
|
243
|
-
)
|
|
244
|
-
|
|
245
|
-
expect(allEntriesRes.ok).toBe(true)
|
|
246
|
-
expect(allEntriesRes.data?.entries.length).toBe(4) // All entries from all collections
|
|
247
|
-
expect(allEntriesRes.data?.entries.some((e) => e.slug === 'overview')).toBe(true)
|
|
248
|
-
expect(allEntriesRes.data?.entries.some((e) => e.slug === 'intro')).toBe(true)
|
|
249
|
-
expect(allEntriesRes.data?.entries.some((e) => e.slug === 'auth')).toBe(true)
|
|
250
|
-
expect(allEntriesRes.data?.entries.some((e) => e.slug === 'users')).toBe(true)
|
|
251
|
-
|
|
252
|
-
// Test 2: With collection filter, non-recursive - only gets entries from 'content/docs' collection (no children)
|
|
253
|
-
const docsNonRecursiveRes = await listEntries.handler(
|
|
254
|
-
ctx,
|
|
255
|
-
{ user: { type: 'authenticated', userId: 'u1', groups: [] } },
|
|
256
|
-
{
|
|
257
|
-
branch: unsafeAsBranchName('main'),
|
|
258
|
-
collection: unsafeAsLogicalPath('content/docs'),
|
|
259
|
-
},
|
|
260
|
-
)
|
|
261
|
-
|
|
262
|
-
expect(docsNonRecursiveRes.ok).toBe(true)
|
|
263
|
-
expect(docsNonRecursiveRes.data?.entries.length).toBe(1) // Only 'overview' from docs
|
|
264
|
-
expect(docsNonRecursiveRes.data?.entries.some((e) => e.slug === 'overview')).toBe(true)
|
|
265
|
-
expect(docsNonRecursiveRes.data?.entries.some((e) => e.slug === 'intro')).toBe(false) // From child collection
|
|
266
|
-
expect(docsNonRecursiveRes.data?.entries.some((e) => e.slug === 'auth')).toBe(false) // From grandchild collection
|
|
267
|
-
|
|
268
|
-
// Test 3: With collection filter and recursive flag - gets entries from 'content/docs' and all children
|
|
269
|
-
const docsRecursiveRes = await listEntries.handler(
|
|
270
|
-
ctx,
|
|
271
|
-
{ user: { type: 'authenticated', userId: 'u1', groups: [] } },
|
|
272
|
-
{
|
|
273
|
-
branch: unsafeAsBranchName('main'),
|
|
274
|
-
collection: unsafeAsLogicalPath('content/docs'),
|
|
275
|
-
recursive: true,
|
|
276
|
-
},
|
|
277
|
-
)
|
|
278
|
-
|
|
279
|
-
expect(docsRecursiveRes.ok).toBe(true)
|
|
280
|
-
expect(docsRecursiveRes.data?.entries.length).toBe(4) // All entries from docs tree
|
|
281
|
-
expect(docsRecursiveRes.data?.entries.some((e) => e.slug === 'overview')).toBe(true)
|
|
282
|
-
expect(docsRecursiveRes.data?.entries.some((e) => e.slug === 'intro')).toBe(true)
|
|
283
|
-
expect(docsRecursiveRes.data?.entries.some((e) => e.slug === 'auth')).toBe(true)
|
|
284
|
-
expect(docsRecursiveRes.data?.entries.some((e) => e.slug === 'users')).toBe(true)
|
|
285
|
-
|
|
286
|
-
// Test 4: Nested collection with recursive - gets entries from 'content/docs/api' and its children
|
|
287
|
-
const apiRecursiveRes = await listEntries.handler(
|
|
288
|
-
ctx,
|
|
289
|
-
{ user: { type: 'authenticated', userId: 'u1', groups: [] } },
|
|
290
|
-
{
|
|
291
|
-
branch: unsafeAsBranchName('main'),
|
|
292
|
-
collection: unsafeAsLogicalPath('content/docs/api'),
|
|
293
|
-
recursive: true,
|
|
294
|
-
},
|
|
295
|
-
)
|
|
296
|
-
|
|
297
|
-
expect(apiRecursiveRes.ok).toBe(true)
|
|
298
|
-
expect(apiRecursiveRes.data?.entries.length).toBe(3) // intro, auth, users (not overview)
|
|
299
|
-
expect(apiRecursiveRes.data?.entries.some((e) => e.slug === 'overview')).toBe(false) // From parent
|
|
300
|
-
expect(apiRecursiveRes.data?.entries.some((e) => e.slug === 'intro')).toBe(true)
|
|
301
|
-
expect(apiRecursiveRes.data?.entries.some((e) => e.slug === 'auth')).toBe(true)
|
|
302
|
-
expect(apiRecursiveRes.data?.entries.some((e) => e.slug === 'users')).toBe(true)
|
|
303
|
-
|
|
304
|
-
// Verify collectionPath values match the nested structure
|
|
305
|
-
const authEntry = docsRecursiveRes.data?.entries.find((e) => e.slug === 'auth')
|
|
306
|
-
expect(authEntry?.collectionPath).toBe('content/docs/api/v2')
|
|
307
|
-
})
|
|
308
|
-
|
|
309
|
-
it('returns entries with schemas using new schema format', async () => {
|
|
310
|
-
const root = await tmpDir()
|
|
311
|
-
await fs.mkdir(path.join(root, 'content/pages'), { recursive: true })
|
|
312
|
-
await fs.writeFile(
|
|
313
|
-
path.join(root, 'content/pages/page.home.abc123XYZ789.json'),
|
|
314
|
-
JSON.stringify({ title: 'Home Page', tagline: 'Welcome' }),
|
|
315
|
-
'utf8',
|
|
316
|
-
)
|
|
317
|
-
|
|
318
|
-
const schema = {
|
|
319
|
-
collections: [
|
|
320
|
-
{
|
|
321
|
-
name: 'pages',
|
|
322
|
-
label: 'Pages',
|
|
323
|
-
path: 'pages',
|
|
324
|
-
entries: [
|
|
325
|
-
{
|
|
326
|
-
name: 'page',
|
|
327
|
-
format: 'json' as const,
|
|
328
|
-
schema: [
|
|
329
|
-
{ name: 'title', type: 'string' as const },
|
|
330
|
-
{ name: 'tagline', type: 'string' as const },
|
|
331
|
-
],
|
|
332
|
-
},
|
|
333
|
-
],
|
|
334
|
-
},
|
|
335
|
-
],
|
|
336
|
-
} as const
|
|
337
|
-
|
|
338
|
-
const config = defineCanopyTestConfig({
|
|
339
|
-
defaultBranchAccess: 'allow',
|
|
340
|
-
contentRoot: 'content',
|
|
341
|
-
schema,
|
|
342
|
-
})
|
|
343
|
-
|
|
344
|
-
const checkBranchAccess = createCheckBranchAccess('allow')
|
|
345
|
-
const checkContentAccess = createCheckContentAccess({
|
|
346
|
-
checkBranchAccess,
|
|
347
|
-
loadPathPermissions: async () => [],
|
|
348
|
-
defaultPathAccess: 'allow',
|
|
349
|
-
mode: 'dev',
|
|
350
|
-
})
|
|
351
|
-
|
|
352
|
-
const ctx = createMockApiContext({
|
|
353
|
-
services: {
|
|
354
|
-
config,
|
|
355
|
-
checkBranchAccess,
|
|
356
|
-
checkContentAccess,
|
|
357
|
-
},
|
|
358
|
-
branchContext: {
|
|
359
|
-
...createMockBranchContext({
|
|
360
|
-
branchName: 'main',
|
|
361
|
-
baseRoot: root,
|
|
362
|
-
branchRoot: root,
|
|
363
|
-
createdBy: 'u1',
|
|
364
|
-
}),
|
|
365
|
-
flatSchema: flattenSchema(schema, config.contentRoot),
|
|
366
|
-
},
|
|
367
|
-
})
|
|
368
|
-
|
|
369
|
-
const res = await listEntries.handler(
|
|
370
|
-
ctx,
|
|
371
|
-
{ user: { type: 'authenticated', userId: 'u1', groups: [] } },
|
|
372
|
-
{ branch: unsafeAsBranchName('main') },
|
|
373
|
-
)
|
|
374
|
-
|
|
375
|
-
expect(res.ok).toBe(true)
|
|
376
|
-
|
|
377
|
-
// Verify entry is returned
|
|
378
|
-
const homeEntry = res.data?.entries.find((e) => e.slug === 'home')
|
|
379
|
-
expect(homeEntry).toBeDefined()
|
|
380
|
-
expect(homeEntry?.collectionPath).toBe('content/pages')
|
|
381
|
-
|
|
382
|
-
// Collections are now fetched from schema API, not entries API
|
|
383
|
-
})
|
|
384
|
-
|
|
385
|
-
it('includes canEdit flag based on edit permissions', async () => {
|
|
386
|
-
const root = await tmpDir()
|
|
387
|
-
await fs.mkdir(path.join(root, 'content/posts'), { recursive: true })
|
|
388
|
-
await fs.writeFile(
|
|
389
|
-
path.join(root, 'content/posts/entry.public.abcDEFghj123.json'),
|
|
390
|
-
JSON.stringify({ title: 'Public Post' }),
|
|
391
|
-
'utf8',
|
|
392
|
-
)
|
|
393
|
-
await fs.writeFile(
|
|
394
|
-
path.join(root, 'content/posts/entry.readonly.defGHJkmn456.json'),
|
|
395
|
-
JSON.stringify({ title: 'Read-Only Post' }),
|
|
396
|
-
'utf8',
|
|
397
|
-
)
|
|
398
|
-
|
|
399
|
-
const schema = {
|
|
400
|
-
collections: [
|
|
401
|
-
{
|
|
402
|
-
name: 'posts',
|
|
403
|
-
path: 'posts',
|
|
404
|
-
entries: [
|
|
405
|
-
{
|
|
406
|
-
name: 'entry',
|
|
407
|
-
format: 'json' as const,
|
|
408
|
-
schema: [{ name: 'title', type: 'string' as const }],
|
|
409
|
-
},
|
|
410
|
-
],
|
|
411
|
-
},
|
|
412
|
-
],
|
|
413
|
-
} as const
|
|
414
|
-
|
|
415
|
-
const config = defineCanopyTestConfig({
|
|
416
|
-
defaultBranchAccess: 'allow',
|
|
417
|
-
schema,
|
|
418
|
-
})
|
|
419
|
-
|
|
420
|
-
// Mock loadPathPermissions: 'entry.readonly.defGHJkmn456.json' is read-only for user 'u1'
|
|
421
|
-
const pathRules: PathPermission[] = [
|
|
422
|
-
{
|
|
423
|
-
path: unsafeAsPermissionPath('content/posts/entry.readonly.defGHJkmn456.json'),
|
|
424
|
-
read: { allowedUsers: ['u1'] },
|
|
425
|
-
edit: { allowedUsers: ['admin'] }, // u1 cannot edit
|
|
426
|
-
},
|
|
427
|
-
]
|
|
428
|
-
const mockLoadPermissions = vi.fn().mockResolvedValue(pathRules)
|
|
429
|
-
|
|
430
|
-
const checkBranchAccess = createCheckBranchAccess('allow')
|
|
431
|
-
const checkContentAccess = createCheckContentAccess({
|
|
432
|
-
checkBranchAccess,
|
|
433
|
-
loadPathPermissions: mockLoadPermissions,
|
|
434
|
-
defaultPathAccess: 'allow',
|
|
435
|
-
mode: 'dev',
|
|
436
|
-
})
|
|
437
|
-
|
|
438
|
-
const ctx = createMockApiContext({
|
|
439
|
-
services: {
|
|
440
|
-
config,
|
|
441
|
-
checkBranchAccess,
|
|
442
|
-
checkContentAccess,
|
|
443
|
-
},
|
|
444
|
-
branchContext: {
|
|
445
|
-
...createMockBranchContext({
|
|
446
|
-
branchName: 'main',
|
|
447
|
-
baseRoot: root,
|
|
448
|
-
branchRoot: root,
|
|
449
|
-
createdBy: 'u1',
|
|
450
|
-
}),
|
|
451
|
-
flatSchema: flattenSchema(schema, config.contentRoot),
|
|
452
|
-
},
|
|
453
|
-
})
|
|
454
|
-
|
|
455
|
-
const res = await listEntries.handler(
|
|
456
|
-
ctx,
|
|
457
|
-
{ user: { type: 'authenticated', userId: 'u1', groups: [] } },
|
|
458
|
-
{ branch: unsafeAsBranchName('main') },
|
|
459
|
-
)
|
|
460
|
-
|
|
461
|
-
expect(res.ok).toBe(true)
|
|
462
|
-
expect(res.data?.entries).toHaveLength(2)
|
|
463
|
-
|
|
464
|
-
// Check canEdit flag on entries
|
|
465
|
-
const publicEntry = res.data?.entries.find((e) => e.slug === 'public')
|
|
466
|
-
const readonlyEntry = res.data?.entries.find((e) => e.slug === 'readonly')
|
|
467
|
-
|
|
468
|
-
expect(publicEntry?.canEdit).toBe(true) // u1 can edit public post (default allow)
|
|
469
|
-
expect(readonlyEntry?.canEdit).toBe(false) // u1 cannot edit readonly post (restricted to admin)
|
|
470
|
-
})
|
|
471
|
-
|
|
472
|
-
it('lists entries with embedded IDs in filenames', async () => {
|
|
473
|
-
const root = await tmpDir()
|
|
474
|
-
|
|
475
|
-
// Create content directory
|
|
476
|
-
await fs.mkdir(path.join(root, 'content'), { recursive: true })
|
|
477
|
-
|
|
478
|
-
// Create collection folder with ID (like authors.q52DCVPuH4ga)
|
|
479
|
-
const authorsId = 'q52DCVPuH4ga'
|
|
480
|
-
await fs.mkdir(path.join(root, `content/authors.${authorsId}`), {
|
|
481
|
-
recursive: true,
|
|
482
|
-
})
|
|
483
|
-
|
|
484
|
-
// Create .collection.json file (matching example1's approach)
|
|
485
|
-
await fs.writeFile(
|
|
486
|
-
path.join(root, `content/authors.${authorsId}/.collection.json`),
|
|
487
|
-
JSON.stringify({
|
|
488
|
-
name: 'authors',
|
|
489
|
-
label: 'Authors',
|
|
490
|
-
entries: [
|
|
491
|
-
{
|
|
492
|
-
name: 'author',
|
|
493
|
-
format: 'json',
|
|
494
|
-
schema: 'authorSchema',
|
|
495
|
-
},
|
|
496
|
-
],
|
|
497
|
-
order: [],
|
|
498
|
-
}),
|
|
499
|
-
'utf8',
|
|
500
|
-
)
|
|
501
|
-
|
|
502
|
-
// Create entry files with embedded IDs: {type}.{slug}.{id}.{ext}
|
|
503
|
-
const aliceId = '5NVkkrB1MJUv'
|
|
504
|
-
const bobId = 'jm6FYVAtJie8'
|
|
505
|
-
await fs.writeFile(
|
|
506
|
-
path.join(root, `content/authors.${authorsId}/author.alice.${aliceId}.json`),
|
|
507
|
-
JSON.stringify({ name: 'Alice', bio: 'Developer' }),
|
|
508
|
-
'utf8',
|
|
509
|
-
)
|
|
510
|
-
await fs.writeFile(
|
|
511
|
-
path.join(root, `content/authors.${authorsId}/author.bob.${bobId}.json`),
|
|
512
|
-
JSON.stringify({ name: 'Bob', bio: 'Designer' }),
|
|
513
|
-
'utf8',
|
|
514
|
-
)
|
|
515
|
-
|
|
516
|
-
// Load schema from .collection.json files (like services do)
|
|
517
|
-
const entrySchemaRegistry = {
|
|
518
|
-
authorSchema: [
|
|
519
|
-
{ name: 'name', type: 'string' },
|
|
520
|
-
{ name: 'bio', type: 'string' },
|
|
521
|
-
],
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
const metaFiles = await loadCollectionMetaFiles(path.join(root, 'content'))
|
|
525
|
-
const schema = resolveCollectionReferences(metaFiles, entrySchemaRegistry)
|
|
526
|
-
|
|
527
|
-
// Create config with the loaded schema
|
|
528
|
-
const config = defineCanopyTestConfig({
|
|
529
|
-
defaultBranchAccess: 'allow',
|
|
530
|
-
contentRoot: 'content',
|
|
531
|
-
schema,
|
|
532
|
-
})
|
|
533
|
-
|
|
534
|
-
const checkBranchAccess = createCheckBranchAccess('allow')
|
|
535
|
-
const checkContentAccess = createCheckContentAccess({
|
|
536
|
-
checkBranchAccess,
|
|
537
|
-
loadPathPermissions: vi.fn().mockResolvedValue([]),
|
|
538
|
-
defaultPathAccess: 'allow',
|
|
539
|
-
mode: 'dev',
|
|
540
|
-
})
|
|
541
|
-
|
|
542
|
-
const ctx = createMockApiContext({
|
|
543
|
-
services: {
|
|
544
|
-
config,
|
|
545
|
-
entrySchemaRegistry: entrySchemaRegistry,
|
|
546
|
-
checkBranchAccess,
|
|
547
|
-
checkContentAccess,
|
|
548
|
-
},
|
|
549
|
-
branchContext: {
|
|
550
|
-
...createMockBranchContext({
|
|
551
|
-
branchName: 'main',
|
|
552
|
-
baseRoot: root,
|
|
553
|
-
branchRoot: root,
|
|
554
|
-
createdBy: 'u1',
|
|
555
|
-
}),
|
|
556
|
-
flatSchema: flattenSchema(schema, config.contentRoot),
|
|
557
|
-
},
|
|
558
|
-
})
|
|
559
|
-
|
|
560
|
-
const res = await listEntries.handler(
|
|
561
|
-
ctx,
|
|
562
|
-
{ user: { type: 'authenticated', userId: 'u1', groups: [] } },
|
|
563
|
-
{ branch: unsafeAsBranchName('main') },
|
|
564
|
-
)
|
|
565
|
-
|
|
566
|
-
expect(res.ok).toBe(true)
|
|
567
|
-
expect(res.data?.entries).toHaveLength(2) // alice + bob
|
|
568
|
-
|
|
569
|
-
// Verify slugs are extracted correctly (without IDs) for collection entries
|
|
570
|
-
const aliceEntry = res.data?.entries.find((e) => e.slug === 'alice')
|
|
571
|
-
const bobEntry = res.data?.entries.find((e) => e.slug === 'bob')
|
|
572
|
-
|
|
573
|
-
expect(aliceEntry).toBeDefined()
|
|
574
|
-
expect(aliceEntry?.slug).toBe('alice')
|
|
575
|
-
expect(aliceEntry?.title).toBe('Alice')
|
|
576
|
-
expect(aliceEntry?.collectionPath).toBe('content/authors') // Logical path, no ID
|
|
577
|
-
|
|
578
|
-
expect(bobEntry).toBeDefined()
|
|
579
|
-
expect(bobEntry?.slug).toBe('bob')
|
|
580
|
-
expect(bobEntry?.title).toBe('Bob')
|
|
581
|
-
expect(bobEntry?.collectionPath).toBe('content/authors') // Logical path, no ID
|
|
582
|
-
})
|
|
583
|
-
|
|
584
|
-
it('lists root-level entry types with maxItems: 1', async () => {
|
|
585
|
-
const root = await tmpDir()
|
|
586
|
-
|
|
587
|
-
// Create content directory
|
|
588
|
-
await fs.mkdir(path.join(root, 'content'), { recursive: true })
|
|
589
|
-
|
|
590
|
-
// Create root .collection.json with entries (not singletons)
|
|
591
|
-
await fs.writeFile(
|
|
592
|
-
path.join(root, 'content/.collection.json'),
|
|
593
|
-
JSON.stringify({
|
|
594
|
-
entries: [
|
|
595
|
-
{
|
|
596
|
-
name: 'home',
|
|
597
|
-
label: 'Home',
|
|
598
|
-
format: 'json',
|
|
599
|
-
schema: 'homeSchema',
|
|
600
|
-
maxItems: 1,
|
|
601
|
-
},
|
|
602
|
-
{
|
|
603
|
-
name: 'settings',
|
|
604
|
-
label: 'Settings',
|
|
605
|
-
format: 'json',
|
|
606
|
-
schema: 'settingsSchema',
|
|
607
|
-
maxItems: 1,
|
|
608
|
-
},
|
|
609
|
-
],
|
|
610
|
-
order: [],
|
|
611
|
-
}),
|
|
612
|
-
'utf8',
|
|
613
|
-
)
|
|
614
|
-
|
|
615
|
-
// Create root-level entry files (pattern: {type}.{slug}.{id}.{ext})
|
|
616
|
-
const homeId = 'agfzDt2RLpSn'
|
|
617
|
-
const settingsId = 'Xp7qR2sL9mKn'
|
|
618
|
-
await fs.writeFile(
|
|
619
|
-
path.join(root, `content/home.home.${homeId}.json`),
|
|
620
|
-
JSON.stringify({ title: 'Welcome Home', hero: 'Hello World' }),
|
|
621
|
-
'utf8',
|
|
622
|
-
)
|
|
623
|
-
await fs.writeFile(
|
|
624
|
-
path.join(root, `content/settings.settings.${settingsId}.json`),
|
|
625
|
-
JSON.stringify({ siteName: 'My Site', theme: 'dark' }),
|
|
626
|
-
'utf8',
|
|
627
|
-
)
|
|
628
|
-
|
|
629
|
-
// Also create a collection to verify both work together
|
|
630
|
-
const postsId = '916jXZabYCxu'
|
|
631
|
-
await fs.mkdir(path.join(root, `content/posts.${postsId}`), {
|
|
632
|
-
recursive: true,
|
|
633
|
-
})
|
|
634
|
-
await fs.writeFile(
|
|
635
|
-
path.join(root, `content/posts.${postsId}/.collection.json`),
|
|
636
|
-
JSON.stringify({
|
|
637
|
-
name: 'posts',
|
|
638
|
-
label: 'Posts',
|
|
639
|
-
entries: [{ name: 'post', format: 'json', schema: 'postSchema' }],
|
|
640
|
-
order: [],
|
|
641
|
-
}),
|
|
642
|
-
'utf8',
|
|
643
|
-
)
|
|
644
|
-
await fs.writeFile(
|
|
645
|
-
path.join(root, `content/posts.${postsId}/post.first.abc123def456.json`),
|
|
646
|
-
JSON.stringify({ title: 'First Post' }),
|
|
647
|
-
'utf8',
|
|
648
|
-
)
|
|
649
|
-
|
|
650
|
-
// Load schema from .collection.json files
|
|
651
|
-
const entrySchemaRegistry = {
|
|
652
|
-
homeSchema: [
|
|
653
|
-
{ name: 'title', type: 'string' },
|
|
654
|
-
{ name: 'hero', type: 'string' },
|
|
655
|
-
],
|
|
656
|
-
settingsSchema: [
|
|
657
|
-
{ name: 'siteName', type: 'string' },
|
|
658
|
-
{ name: 'theme', type: 'string' },
|
|
659
|
-
],
|
|
660
|
-
postSchema: [{ name: 'title', type: 'string' }],
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
const metaFiles = await loadCollectionMetaFiles(path.join(root, 'content'))
|
|
664
|
-
const schema = resolveCollectionReferences(metaFiles, entrySchemaRegistry)
|
|
665
|
-
|
|
666
|
-
const config = defineCanopyTestConfig({
|
|
667
|
-
defaultBranchAccess: 'allow',
|
|
668
|
-
contentRoot: 'content',
|
|
669
|
-
schema,
|
|
670
|
-
})
|
|
671
|
-
|
|
672
|
-
const checkBranchAccess = createCheckBranchAccess('allow')
|
|
673
|
-
const checkContentAccess = createCheckContentAccess({
|
|
674
|
-
checkBranchAccess,
|
|
675
|
-
loadPathPermissions: vi.fn().mockResolvedValue([]),
|
|
676
|
-
defaultPathAccess: 'allow',
|
|
677
|
-
mode: 'dev',
|
|
678
|
-
})
|
|
679
|
-
|
|
680
|
-
const ctx = createMockApiContext({
|
|
681
|
-
services: {
|
|
682
|
-
config,
|
|
683
|
-
entrySchemaRegistry: entrySchemaRegistry,
|
|
684
|
-
checkBranchAccess,
|
|
685
|
-
checkContentAccess,
|
|
686
|
-
},
|
|
687
|
-
branchContext: {
|
|
688
|
-
...createMockBranchContext({
|
|
689
|
-
branchName: 'main',
|
|
690
|
-
baseRoot: root,
|
|
691
|
-
branchRoot: root,
|
|
692
|
-
createdBy: 'u1',
|
|
693
|
-
}),
|
|
694
|
-
flatSchema: flattenSchema(schema, config.contentRoot),
|
|
695
|
-
},
|
|
696
|
-
})
|
|
697
|
-
|
|
698
|
-
const res = await listEntries.handler(
|
|
699
|
-
ctx,
|
|
700
|
-
{ user: { type: 'authenticated', userId: 'u1', groups: [] } },
|
|
701
|
-
{ branch: unsafeAsBranchName('main') },
|
|
702
|
-
)
|
|
703
|
-
|
|
704
|
-
expect(res.ok).toBe(true)
|
|
705
|
-
// Should have 3 entries: home, settings (root-level) + first post (collection)
|
|
706
|
-
expect(res.data?.entries).toHaveLength(3)
|
|
707
|
-
|
|
708
|
-
// Check root-level entry types
|
|
709
|
-
const homeEntry = res.data?.entries.find(
|
|
710
|
-
(e) => e.slug === 'home' && e.collectionPath === 'content',
|
|
711
|
-
)
|
|
712
|
-
expect(homeEntry).toBeDefined()
|
|
713
|
-
expect(homeEntry?.slug).toBe('home') // Name acts as slug
|
|
714
|
-
expect(homeEntry?.title).toBe('Welcome Home')
|
|
715
|
-
expect(homeEntry?.collectionPath).toBe('content') // Parent path, not full path
|
|
716
|
-
expect(homeEntry?.entryType).toBe('home')
|
|
717
|
-
|
|
718
|
-
const settingsEntry = res.data?.entries.find(
|
|
719
|
-
(e) => e.slug === 'settings' && e.collectionPath === 'content',
|
|
720
|
-
)
|
|
721
|
-
expect(settingsEntry).toBeDefined()
|
|
722
|
-
expect(settingsEntry?.slug).toBe('settings') // Name acts as slug
|
|
723
|
-
expect(settingsEntry?.title).toBe('Settings') // Falls back to label since siteName isn't title
|
|
724
|
-
expect(settingsEntry?.collectionPath).toBe('content') // Parent path, not full path
|
|
725
|
-
expect(settingsEntry?.entryType).toBe('settings')
|
|
726
|
-
|
|
727
|
-
// Check collection entry still works
|
|
728
|
-
const postEntry = res.data?.entries.find((e) => e.slug === 'first')
|
|
729
|
-
expect(postEntry).toBeDefined()
|
|
730
|
-
expect(postEntry?.collectionPath).toBe('content/posts')
|
|
731
|
-
})
|
|
732
|
-
})
|
|
733
|
-
|
|
734
|
-
describe('sortEntriesByOrder', () => {
|
|
735
|
-
// Import the function for testing
|
|
736
|
-
// Since it's not exported, we'll test it indirectly through the list handler
|
|
737
|
-
// or we can add a describe block that tests ordering behavior
|
|
738
|
-
|
|
739
|
-
it('returns entries sorted by order array when order is provided', async () => {
|
|
740
|
-
const root = await tmpDir()
|
|
741
|
-
|
|
742
|
-
// Create collection folder with embedded ID
|
|
743
|
-
const postsId = 'q52DCVPuH4ga'
|
|
744
|
-
await fs.mkdir(path.join(root, `content/posts.${postsId}`), {
|
|
745
|
-
recursive: true,
|
|
746
|
-
})
|
|
747
|
-
|
|
748
|
-
// Create .collection.json with an order array
|
|
749
|
-
await fs.writeFile(
|
|
750
|
-
path.join(root, `content/posts.${postsId}/.collection.json`),
|
|
751
|
-
JSON.stringify({
|
|
752
|
-
name: 'posts',
|
|
753
|
-
label: 'Posts',
|
|
754
|
-
entries: [{ name: 'post', format: 'json', schema: 'postSchema' }],
|
|
755
|
-
order: ['ccc333def456', 'aaa111abc123', 'bbb222xyz789'], // Custom order
|
|
756
|
-
}),
|
|
757
|
-
'utf8',
|
|
758
|
-
)
|
|
759
|
-
|
|
760
|
-
// Create entries (alphabetically: aaa < bbb < ccc, but order says ccc, aaa, bbb)
|
|
761
|
-
await fs.writeFile(
|
|
762
|
-
path.join(root, `content/posts.${postsId}/post.alpha.aaa111abc123.json`),
|
|
763
|
-
JSON.stringify({ title: 'Alpha Post' }),
|
|
764
|
-
'utf8',
|
|
765
|
-
)
|
|
766
|
-
await fs.writeFile(
|
|
767
|
-
path.join(root, `content/posts.${postsId}/post.beta.bbb222xyz789.json`),
|
|
768
|
-
JSON.stringify({ title: 'Beta Post' }),
|
|
769
|
-
'utf8',
|
|
770
|
-
)
|
|
771
|
-
await fs.writeFile(
|
|
772
|
-
path.join(root, `content/posts.${postsId}/post.gamma.ccc333def456.json`),
|
|
773
|
-
JSON.stringify({ title: 'Gamma Post' }),
|
|
774
|
-
'utf8',
|
|
775
|
-
)
|
|
776
|
-
|
|
777
|
-
const entrySchemaRegistry = {
|
|
778
|
-
postSchema: [{ name: 'title', type: 'string' }],
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
const metaFiles = await loadCollectionMetaFiles(path.join(root, 'content'))
|
|
782
|
-
const schema = resolveCollectionReferences(metaFiles, entrySchemaRegistry)
|
|
783
|
-
|
|
784
|
-
const config = defineCanopyTestConfig({
|
|
785
|
-
defaultBranchAccess: 'allow',
|
|
786
|
-
contentRoot: 'content',
|
|
787
|
-
schema,
|
|
788
|
-
})
|
|
789
|
-
|
|
790
|
-
const checkBranchAccess = createCheckBranchAccess('allow')
|
|
791
|
-
const checkContentAccess = createCheckContentAccess({
|
|
792
|
-
checkBranchAccess,
|
|
793
|
-
loadPathPermissions: vi.fn().mockResolvedValue([]),
|
|
794
|
-
defaultPathAccess: 'allow',
|
|
795
|
-
mode: 'dev',
|
|
796
|
-
})
|
|
797
|
-
|
|
798
|
-
const ctx = createMockApiContext({
|
|
799
|
-
services: {
|
|
800
|
-
config,
|
|
801
|
-
entrySchemaRegistry: entrySchemaRegistry,
|
|
802
|
-
checkBranchAccess,
|
|
803
|
-
checkContentAccess,
|
|
804
|
-
},
|
|
805
|
-
branchContext: {
|
|
806
|
-
...createMockBranchContext({
|
|
807
|
-
branchName: 'main',
|
|
808
|
-
baseRoot: root,
|
|
809
|
-
branchRoot: root,
|
|
810
|
-
createdBy: 'u1',
|
|
811
|
-
}),
|
|
812
|
-
flatSchema: flattenSchema(schema, config.contentRoot),
|
|
813
|
-
},
|
|
814
|
-
})
|
|
815
|
-
|
|
816
|
-
const res = await listEntries.handler(
|
|
817
|
-
ctx,
|
|
818
|
-
{ user: { type: 'authenticated', userId: 'u1', groups: [] } },
|
|
819
|
-
{
|
|
820
|
-
branch: unsafeAsBranchName('main'),
|
|
821
|
-
collection: unsafeAsLogicalPath('content/posts'),
|
|
822
|
-
},
|
|
823
|
-
)
|
|
824
|
-
|
|
825
|
-
expect(res.ok).toBe(true)
|
|
826
|
-
expect(res.data?.entries).toHaveLength(3)
|
|
827
|
-
|
|
828
|
-
// Verify entries are sorted according to order array: gamma, alpha, beta
|
|
829
|
-
const slugs = res.data?.entries.map((e) => e.slug)
|
|
830
|
-
expect(slugs).toEqual(['gamma', 'alpha', 'beta'])
|
|
831
|
-
|
|
832
|
-
// Verify entries have contentId
|
|
833
|
-
const contentIds = res.data?.entries.map((e) => e.contentId)
|
|
834
|
-
expect(contentIds).toEqual(['ccc333def456', 'aaa111abc123', 'bbb222xyz789'])
|
|
835
|
-
})
|
|
836
|
-
|
|
837
|
-
it('puts unordered entries at the end alphabetically', async () => {
|
|
838
|
-
const root = await tmpDir()
|
|
839
|
-
|
|
840
|
-
const postsId = 'q52DCVPuH4ga'
|
|
841
|
-
await fs.mkdir(path.join(root, `content/posts.${postsId}`), {
|
|
842
|
-
recursive: true,
|
|
843
|
-
})
|
|
844
|
-
|
|
845
|
-
// Order only has one entry
|
|
846
|
-
await fs.writeFile(
|
|
847
|
-
path.join(root, `content/posts.${postsId}/.collection.json`),
|
|
848
|
-
JSON.stringify({
|
|
849
|
-
name: 'posts',
|
|
850
|
-
entries: [{ name: 'post', format: 'json', schema: 'postSchema' }],
|
|
851
|
-
order: ['bbb222xyz789'], // Only beta is in the order
|
|
852
|
-
}),
|
|
853
|
-
'utf8',
|
|
854
|
-
)
|
|
855
|
-
|
|
856
|
-
// Create entries
|
|
857
|
-
await fs.writeFile(
|
|
858
|
-
path.join(root, `content/posts.${postsId}/post.alpha.aaa111abc123.json`),
|
|
859
|
-
JSON.stringify({ title: 'Alpha' }),
|
|
860
|
-
'utf8',
|
|
861
|
-
)
|
|
862
|
-
await fs.writeFile(
|
|
863
|
-
path.join(root, `content/posts.${postsId}/post.beta.bbb222xyz789.json`),
|
|
864
|
-
JSON.stringify({ title: 'Beta' }),
|
|
865
|
-
'utf8',
|
|
866
|
-
)
|
|
867
|
-
await fs.writeFile(
|
|
868
|
-
path.join(root, `content/posts.${postsId}/post.gamma.ccc333def456.json`),
|
|
869
|
-
JSON.stringify({ title: 'Gamma' }),
|
|
870
|
-
'utf8',
|
|
871
|
-
)
|
|
872
|
-
|
|
873
|
-
const entrySchemaRegistry = {
|
|
874
|
-
postSchema: [{ name: 'title', type: 'string' }],
|
|
875
|
-
}
|
|
876
|
-
const metaFiles = await loadCollectionMetaFiles(path.join(root, 'content'))
|
|
877
|
-
const schema = resolveCollectionReferences(metaFiles, entrySchemaRegistry)
|
|
878
|
-
|
|
879
|
-
const config = defineCanopyTestConfig({
|
|
880
|
-
defaultBranchAccess: 'allow',
|
|
881
|
-
contentRoot: 'content',
|
|
882
|
-
schema,
|
|
883
|
-
})
|
|
884
|
-
|
|
885
|
-
const checkBranchAccess = createCheckBranchAccess('allow')
|
|
886
|
-
const checkContentAccess = createCheckContentAccess({
|
|
887
|
-
checkBranchAccess,
|
|
888
|
-
loadPathPermissions: vi.fn().mockResolvedValue([]),
|
|
889
|
-
defaultPathAccess: 'allow',
|
|
890
|
-
mode: 'dev',
|
|
891
|
-
})
|
|
892
|
-
|
|
893
|
-
const ctx = createMockApiContext({
|
|
894
|
-
services: {
|
|
895
|
-
config,
|
|
896
|
-
entrySchemaRegistry: entrySchemaRegistry,
|
|
897
|
-
checkBranchAccess,
|
|
898
|
-
checkContentAccess,
|
|
899
|
-
},
|
|
900
|
-
branchContext: {
|
|
901
|
-
...createMockBranchContext({
|
|
902
|
-
branchName: 'main',
|
|
903
|
-
baseRoot: root,
|
|
904
|
-
branchRoot: root,
|
|
905
|
-
createdBy: 'u1',
|
|
906
|
-
}),
|
|
907
|
-
flatSchema: flattenSchema(schema, config.contentRoot),
|
|
908
|
-
},
|
|
909
|
-
})
|
|
910
|
-
|
|
911
|
-
const res = await listEntries.handler(
|
|
912
|
-
ctx,
|
|
913
|
-
{ user: { type: 'authenticated', userId: 'u1', groups: [] } },
|
|
914
|
-
{
|
|
915
|
-
branch: unsafeAsBranchName('main'),
|
|
916
|
-
collection: unsafeAsLogicalPath('content/posts'),
|
|
917
|
-
},
|
|
918
|
-
)
|
|
919
|
-
|
|
920
|
-
expect(res.ok).toBe(true)
|
|
921
|
-
// Beta first (in order), then alpha and gamma alphabetically
|
|
922
|
-
const slugs = res.data?.entries.map((e) => e.slug)
|
|
923
|
-
expect(slugs).toEqual(['beta', 'alpha', 'gamma'])
|
|
924
|
-
})
|
|
925
|
-
})
|
|
926
|
-
|
|
927
|
-
describe('dynamic collection discovery', () => {
|
|
928
|
-
it('discovers collections from .collection.json files not in flatSchema', async () => {
|
|
929
|
-
const root = await tmpDir()
|
|
930
|
-
|
|
931
|
-
// Create initial collection folder
|
|
932
|
-
const docsId = 'bChqT78gcaLd'
|
|
933
|
-
await fs.mkdir(path.join(root, `content/docs.${docsId}`), {
|
|
934
|
-
recursive: true,
|
|
935
|
-
})
|
|
936
|
-
|
|
937
|
-
await fs.writeFile(
|
|
938
|
-
path.join(root, `content/docs.${docsId}/.collection.json`),
|
|
939
|
-
JSON.stringify({
|
|
940
|
-
name: 'docs',
|
|
941
|
-
label: 'Documentation',
|
|
942
|
-
entries: [{ name: 'doc', format: 'json', schema: 'docSchema' }],
|
|
943
|
-
order: [],
|
|
944
|
-
}),
|
|
945
|
-
'utf8',
|
|
946
|
-
)
|
|
947
|
-
|
|
948
|
-
// Create an entry in docs
|
|
949
|
-
await fs.writeFile(
|
|
950
|
-
path.join(root, `content/docs.${docsId}/doc.overview.gnVmHnnMjWrD.json`),
|
|
951
|
-
JSON.stringify({ title: 'Overview' }),
|
|
952
|
-
'utf8',
|
|
953
|
-
)
|
|
954
|
-
|
|
955
|
-
// Now simulate a dynamically created subcollection that is NOT in the flatSchema
|
|
956
|
-
// This is the scenario where a user creates a collection via the schema API
|
|
957
|
-
const innerId = '2XWmsdeEU2Li'
|
|
958
|
-
await fs.mkdir(path.join(root, `content/docs.${docsId}/inner.${innerId}`), {
|
|
959
|
-
recursive: true,
|
|
960
|
-
})
|
|
961
|
-
|
|
962
|
-
await fs.writeFile(
|
|
963
|
-
path.join(root, `content/docs.${docsId}/inner.${innerId}/.collection.json`),
|
|
964
|
-
JSON.stringify({
|
|
965
|
-
name: 'inner',
|
|
966
|
-
label: 'Inner Docs',
|
|
967
|
-
entries: [{ name: 'doc', format: 'json', schema: 'docSchema' }],
|
|
968
|
-
order: [],
|
|
969
|
-
}),
|
|
970
|
-
'utf8',
|
|
971
|
-
)
|
|
972
|
-
|
|
973
|
-
const entrySchemaRegistry = {
|
|
974
|
-
docSchema: [
|
|
975
|
-
{ name: 'title', type: 'string' },
|
|
976
|
-
{ name: 'body', type: 'markdown' },
|
|
977
|
-
],
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
// Only load the ORIGINAL schema (docs only, not inner)
|
|
981
|
-
// This simulates the flatSchema being cached at startup before "inner" was created
|
|
982
|
-
const originalSchema = {
|
|
983
|
-
collections: [
|
|
984
|
-
{
|
|
985
|
-
name: 'docs',
|
|
986
|
-
label: 'Documentation',
|
|
987
|
-
path: 'docs',
|
|
988
|
-
entries: [
|
|
989
|
-
{
|
|
990
|
-
name: 'doc',
|
|
991
|
-
format: 'json' as const,
|
|
992
|
-
schema: entrySchemaRegistry.docSchema,
|
|
993
|
-
},
|
|
994
|
-
],
|
|
995
|
-
},
|
|
996
|
-
],
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
const config = defineCanopyTestConfig({
|
|
1000
|
-
defaultBranchAccess: 'allow',
|
|
1001
|
-
contentRoot: 'content',
|
|
1002
|
-
schema: originalSchema,
|
|
1003
|
-
})
|
|
1004
|
-
|
|
1005
|
-
const checkBranchAccess = createCheckBranchAccess('allow')
|
|
1006
|
-
const checkContentAccess = createCheckContentAccess({
|
|
1007
|
-
checkBranchAccess,
|
|
1008
|
-
loadPathPermissions: vi.fn().mockResolvedValue([]),
|
|
1009
|
-
defaultPathAccess: 'allow',
|
|
1010
|
-
mode: 'dev',
|
|
1011
|
-
})
|
|
1012
|
-
|
|
1013
|
-
const ctx = createMockApiContext({
|
|
1014
|
-
services: {
|
|
1015
|
-
config,
|
|
1016
|
-
entrySchemaRegistry: entrySchemaRegistry,
|
|
1017
|
-
checkBranchAccess,
|
|
1018
|
-
checkContentAccess,
|
|
1019
|
-
},
|
|
1020
|
-
branchContext: {
|
|
1021
|
-
...createMockBranchContext({
|
|
1022
|
-
branchName: 'main',
|
|
1023
|
-
baseRoot: root,
|
|
1024
|
-
branchRoot: root,
|
|
1025
|
-
createdBy: 'u1',
|
|
1026
|
-
}),
|
|
1027
|
-
// flatSchema only knows about 'docs', NOT 'docs/inner'
|
|
1028
|
-
flatSchema: flattenSchema(originalSchema, config.contentRoot),
|
|
1029
|
-
},
|
|
1030
|
-
})
|
|
1031
|
-
|
|
1032
|
-
const res = await listEntries.handler(
|
|
1033
|
-
ctx,
|
|
1034
|
-
{ user: { type: 'authenticated', userId: 'u1', groups: [] } },
|
|
1035
|
-
{ branch: unsafeAsBranchName('main') },
|
|
1036
|
-
)
|
|
1037
|
-
|
|
1038
|
-
expect(res.ok).toBe(true)
|
|
1039
|
-
|
|
1040
|
-
// Dynamic collection discovery has moved to schema API
|
|
1041
|
-
// This test now only verifies entries API returns entries, not collections
|
|
1042
|
-
expect(res.data?.entries).toBeDefined()
|
|
1043
|
-
})
|
|
1044
|
-
})
|
|
1045
|
-
|
|
1046
|
-
describe('deleteEntry', () => {
|
|
1047
|
-
it('deletes an entry and returns success', async () => {
|
|
1048
|
-
const root = await tmpDir()
|
|
1049
|
-
|
|
1050
|
-
const postsId = 'q52DCVPuH4ga'
|
|
1051
|
-
await fs.mkdir(path.join(root, `content/posts.${postsId}`), {
|
|
1052
|
-
recursive: true,
|
|
1053
|
-
})
|
|
1054
|
-
|
|
1055
|
-
await fs.writeFile(
|
|
1056
|
-
path.join(root, `content/posts.${postsId}/.collection.json`),
|
|
1057
|
-
JSON.stringify({
|
|
1058
|
-
name: 'posts',
|
|
1059
|
-
entries: [{ name: 'post', format: 'json', schema: 'postSchema' }],
|
|
1060
|
-
order: [],
|
|
1061
|
-
}),
|
|
1062
|
-
'utf8',
|
|
1063
|
-
)
|
|
1064
|
-
|
|
1065
|
-
const entryId = 'abc123def456'
|
|
1066
|
-
await fs.writeFile(
|
|
1067
|
-
path.join(root, `content/posts.${postsId}/post.to-delete.${entryId}.json`),
|
|
1068
|
-
JSON.stringify({ title: 'Delete Me' }),
|
|
1069
|
-
'utf8',
|
|
1070
|
-
)
|
|
1071
|
-
|
|
1072
|
-
const entrySchemaRegistry = {
|
|
1073
|
-
postSchema: [{ name: 'title', type: 'string' }],
|
|
1074
|
-
}
|
|
1075
|
-
const metaFiles = await loadCollectionMetaFiles(path.join(root, 'content'))
|
|
1076
|
-
const schema = resolveCollectionReferences(metaFiles, entrySchemaRegistry)
|
|
1077
|
-
|
|
1078
|
-
const config = defineCanopyTestConfig({
|
|
1079
|
-
defaultBranchAccess: 'allow',
|
|
1080
|
-
contentRoot: 'content',
|
|
1081
|
-
schema,
|
|
1082
|
-
})
|
|
1083
|
-
|
|
1084
|
-
const checkBranchAccess = createCheckBranchAccess('allow')
|
|
1085
|
-
const checkContentAccess = createCheckContentAccess({
|
|
1086
|
-
checkBranchAccess,
|
|
1087
|
-
loadPathPermissions: vi.fn().mockResolvedValue([]),
|
|
1088
|
-
defaultPathAccess: 'allow',
|
|
1089
|
-
mode: 'dev',
|
|
1090
|
-
})
|
|
1091
|
-
|
|
1092
|
-
const ctx = createMockApiContext({
|
|
1093
|
-
services: {
|
|
1094
|
-
config,
|
|
1095
|
-
entrySchemaRegistry: entrySchemaRegistry,
|
|
1096
|
-
checkBranchAccess,
|
|
1097
|
-
checkContentAccess,
|
|
1098
|
-
},
|
|
1099
|
-
branchContext: {
|
|
1100
|
-
...createMockBranchContext({
|
|
1101
|
-
branchName: 'main',
|
|
1102
|
-
baseRoot: root,
|
|
1103
|
-
branchRoot: root,
|
|
1104
|
-
createdBy: 'u1',
|
|
1105
|
-
}),
|
|
1106
|
-
flatSchema: flattenSchema(schema, config.contentRoot),
|
|
1107
|
-
},
|
|
1108
|
-
})
|
|
1109
|
-
|
|
1110
|
-
// Import deleteEntry handler
|
|
1111
|
-
const { deleteEntry } = await import('./entries')
|
|
1112
|
-
|
|
1113
|
-
const res = await deleteEntry.handler(
|
|
1114
|
-
ctx,
|
|
1115
|
-
{ user: { type: 'authenticated', userId: 'u1', groups: [] } },
|
|
1116
|
-
{
|
|
1117
|
-
branch: unsafeAsBranchName('main'),
|
|
1118
|
-
entryPath: unsafeAsLogicalPath('content/posts/to-delete'),
|
|
1119
|
-
},
|
|
1120
|
-
)
|
|
1121
|
-
|
|
1122
|
-
expect(res.ok).toBe(true)
|
|
1123
|
-
expect(res.data?.deleted).toBe(true)
|
|
1124
|
-
|
|
1125
|
-
// Verify file was deleted
|
|
1126
|
-
const files = await fs.readdir(path.join(root, `content/posts.${postsId}`))
|
|
1127
|
-
expect(files.filter((f) => f.endsWith('.json') && f !== '.collection.json')).toHaveLength(0)
|
|
1128
|
-
})
|
|
1129
|
-
|
|
1130
|
-
it('returns 403 when user lacks edit permission', async () => {
|
|
1131
|
-
const root = await tmpDir()
|
|
1132
|
-
|
|
1133
|
-
const postsId = 'q52DCVPuH4ga'
|
|
1134
|
-
await fs.mkdir(path.join(root, `content/posts.${postsId}`), {
|
|
1135
|
-
recursive: true,
|
|
1136
|
-
})
|
|
1137
|
-
|
|
1138
|
-
await fs.writeFile(
|
|
1139
|
-
path.join(root, `content/posts.${postsId}/.collection.json`),
|
|
1140
|
-
JSON.stringify({
|
|
1141
|
-
name: 'posts',
|
|
1142
|
-
entries: [{ name: 'post', format: 'json', schema: 'postSchema' }],
|
|
1143
|
-
order: [],
|
|
1144
|
-
}),
|
|
1145
|
-
'utf8',
|
|
1146
|
-
)
|
|
1147
|
-
|
|
1148
|
-
await fs.writeFile(
|
|
1149
|
-
path.join(root, `content/posts.${postsId}/post.protected.abc123def456.json`),
|
|
1150
|
-
JSON.stringify({ title: 'Protected' }),
|
|
1151
|
-
'utf8',
|
|
1152
|
-
)
|
|
1153
|
-
|
|
1154
|
-
const entrySchemaRegistry = {
|
|
1155
|
-
postSchema: [{ name: 'title', type: 'string' }],
|
|
1156
|
-
}
|
|
1157
|
-
const metaFiles = await loadCollectionMetaFiles(path.join(root, 'content'))
|
|
1158
|
-
const schema = resolveCollectionReferences(metaFiles, entrySchemaRegistry)
|
|
1159
|
-
|
|
1160
|
-
const config = defineCanopyTestConfig({
|
|
1161
|
-
defaultBranchAccess: 'allow',
|
|
1162
|
-
contentRoot: 'content',
|
|
1163
|
-
schema,
|
|
1164
|
-
})
|
|
1165
|
-
|
|
1166
|
-
// Mock edit access denied
|
|
1167
|
-
const pathRules: PathPermission[] = [
|
|
1168
|
-
{
|
|
1169
|
-
path: unsafeAsPermissionPath(`content/posts.${postsId}/post.protected.abc123def456.json`),
|
|
1170
|
-
edit: { allowedUsers: ['admin'] },
|
|
1171
|
-
},
|
|
1172
|
-
]
|
|
1173
|
-
|
|
1174
|
-
const checkBranchAccess = createCheckBranchAccess('allow')
|
|
1175
|
-
const checkContentAccess = createCheckContentAccess({
|
|
1176
|
-
checkBranchAccess,
|
|
1177
|
-
loadPathPermissions: vi.fn().mockResolvedValue(pathRules),
|
|
1178
|
-
defaultPathAccess: 'allow',
|
|
1179
|
-
mode: 'dev',
|
|
1180
|
-
})
|
|
1181
|
-
|
|
1182
|
-
const ctx = createMockApiContext({
|
|
1183
|
-
services: {
|
|
1184
|
-
config,
|
|
1185
|
-
entrySchemaRegistry: entrySchemaRegistry,
|
|
1186
|
-
checkBranchAccess,
|
|
1187
|
-
checkContentAccess,
|
|
1188
|
-
},
|
|
1189
|
-
branchContext: {
|
|
1190
|
-
...createMockBranchContext({
|
|
1191
|
-
branchName: 'main',
|
|
1192
|
-
baseRoot: root,
|
|
1193
|
-
branchRoot: root,
|
|
1194
|
-
createdBy: 'u1',
|
|
1195
|
-
}),
|
|
1196
|
-
flatSchema: flattenSchema(schema, config.contentRoot),
|
|
1197
|
-
},
|
|
1198
|
-
})
|
|
1199
|
-
|
|
1200
|
-
const { deleteEntry } = await import('./entries')
|
|
1201
|
-
|
|
1202
|
-
const res = await deleteEntry.handler(
|
|
1203
|
-
ctx,
|
|
1204
|
-
{ user: { type: 'authenticated', userId: 'u1', groups: [] } },
|
|
1205
|
-
{
|
|
1206
|
-
branch: unsafeAsBranchName('main'),
|
|
1207
|
-
entryPath: unsafeAsLogicalPath('content/posts/protected'),
|
|
1208
|
-
},
|
|
1209
|
-
)
|
|
1210
|
-
|
|
1211
|
-
expect(res.ok).toBe(false)
|
|
1212
|
-
expect(res.status).toBe(403)
|
|
1213
|
-
expect(res.error).toContain('Edit permission required')
|
|
1214
|
-
})
|
|
1215
|
-
|
|
1216
|
-
it('returns 404 for non-existent entry', async () => {
|
|
1217
|
-
const root = await tmpDir()
|
|
1218
|
-
|
|
1219
|
-
const postsId = 'q52DCVPuH4ga'
|
|
1220
|
-
await fs.mkdir(path.join(root, `content/posts.${postsId}`), {
|
|
1221
|
-
recursive: true,
|
|
1222
|
-
})
|
|
1223
|
-
|
|
1224
|
-
await fs.writeFile(
|
|
1225
|
-
path.join(root, `content/posts.${postsId}/.collection.json`),
|
|
1226
|
-
JSON.stringify({
|
|
1227
|
-
name: 'posts',
|
|
1228
|
-
entries: [{ name: 'post', format: 'json', schema: 'postSchema' }],
|
|
1229
|
-
order: [],
|
|
1230
|
-
}),
|
|
1231
|
-
'utf8',
|
|
1232
|
-
)
|
|
1233
|
-
|
|
1234
|
-
const entrySchemaRegistry = {
|
|
1235
|
-
postSchema: [{ name: 'title', type: 'string' }],
|
|
1236
|
-
}
|
|
1237
|
-
const metaFiles = await loadCollectionMetaFiles(path.join(root, 'content'))
|
|
1238
|
-
const schema = resolveCollectionReferences(metaFiles, entrySchemaRegistry)
|
|
1239
|
-
|
|
1240
|
-
const config = defineCanopyTestConfig({
|
|
1241
|
-
defaultBranchAccess: 'allow',
|
|
1242
|
-
contentRoot: 'content',
|
|
1243
|
-
schema,
|
|
1244
|
-
})
|
|
1245
|
-
|
|
1246
|
-
const checkBranchAccess = createCheckBranchAccess('allow')
|
|
1247
|
-
const checkContentAccess = createCheckContentAccess({
|
|
1248
|
-
checkBranchAccess,
|
|
1249
|
-
loadPathPermissions: vi.fn().mockResolvedValue([]),
|
|
1250
|
-
defaultPathAccess: 'allow',
|
|
1251
|
-
mode: 'dev',
|
|
1252
|
-
})
|
|
1253
|
-
|
|
1254
|
-
const ctx = createMockApiContext({
|
|
1255
|
-
services: {
|
|
1256
|
-
config,
|
|
1257
|
-
entrySchemaRegistry: entrySchemaRegistry,
|
|
1258
|
-
checkBranchAccess,
|
|
1259
|
-
checkContentAccess,
|
|
1260
|
-
},
|
|
1261
|
-
branchContext: {
|
|
1262
|
-
...createMockBranchContext({
|
|
1263
|
-
branchName: 'main',
|
|
1264
|
-
baseRoot: root,
|
|
1265
|
-
branchRoot: root,
|
|
1266
|
-
createdBy: 'u1',
|
|
1267
|
-
}),
|
|
1268
|
-
flatSchema: flattenSchema(schema, config.contentRoot),
|
|
1269
|
-
},
|
|
1270
|
-
})
|
|
1271
|
-
|
|
1272
|
-
const { deleteEntry } = await import('./entries')
|
|
1273
|
-
|
|
1274
|
-
const res = await deleteEntry.handler(
|
|
1275
|
-
ctx,
|
|
1276
|
-
{ user: { type: 'authenticated', userId: 'u1', groups: [] } },
|
|
1277
|
-
{
|
|
1278
|
-
branch: unsafeAsBranchName('main'),
|
|
1279
|
-
entryPath: unsafeAsLogicalPath('content/posts/nonexistent'),
|
|
1280
|
-
},
|
|
1281
|
-
)
|
|
1282
|
-
|
|
1283
|
-
expect(res.ok).toBe(false)
|
|
1284
|
-
expect(res.status).toBe(404)
|
|
1285
|
-
})
|
|
1286
|
-
|
|
1287
|
-
it('returns 400 for invalid entry path format', async () => {
|
|
1288
|
-
const root = await tmpDir()
|
|
1289
|
-
await fs.mkdir(path.join(root, 'content'), { recursive: true })
|
|
1290
|
-
|
|
1291
|
-
const config = defineCanopyTestConfig({
|
|
1292
|
-
defaultBranchAccess: 'allow',
|
|
1293
|
-
contentRoot: 'content',
|
|
1294
|
-
schema: { collections: [] },
|
|
1295
|
-
})
|
|
1296
|
-
|
|
1297
|
-
const checkBranchAccess = createCheckBranchAccess('allow')
|
|
1298
|
-
const checkContentAccess = createCheckContentAccess({
|
|
1299
|
-
checkBranchAccess,
|
|
1300
|
-
loadPathPermissions: vi.fn().mockResolvedValue([]),
|
|
1301
|
-
defaultPathAccess: 'allow',
|
|
1302
|
-
mode: 'dev',
|
|
1303
|
-
})
|
|
1304
|
-
|
|
1305
|
-
const ctx = createMockApiContext({
|
|
1306
|
-
services: {
|
|
1307
|
-
config,
|
|
1308
|
-
entrySchemaRegistry: {},
|
|
1309
|
-
checkBranchAccess,
|
|
1310
|
-
checkContentAccess,
|
|
1311
|
-
},
|
|
1312
|
-
branchContext: {
|
|
1313
|
-
...createMockBranchContext({
|
|
1314
|
-
branchName: 'main',
|
|
1315
|
-
baseRoot: root,
|
|
1316
|
-
branchRoot: root,
|
|
1317
|
-
createdBy: 'u1',
|
|
1318
|
-
}),
|
|
1319
|
-
flatSchema: [],
|
|
1320
|
-
},
|
|
1321
|
-
})
|
|
1322
|
-
|
|
1323
|
-
const { deleteEntry } = await import('./entries')
|
|
1324
|
-
|
|
1325
|
-
// Path without slash is invalid
|
|
1326
|
-
const res = await deleteEntry.handler(
|
|
1327
|
-
ctx,
|
|
1328
|
-
{ user: { type: 'authenticated', userId: 'u1', groups: [] } },
|
|
1329
|
-
{
|
|
1330
|
-
branch: unsafeAsBranchName('main'),
|
|
1331
|
-
entryPath: unsafeAsLogicalPath('invalid-no-slash'),
|
|
1332
|
-
},
|
|
1333
|
-
)
|
|
1334
|
-
|
|
1335
|
-
expect(res.ok).toBe(false)
|
|
1336
|
-
expect(res.status).toBe(400)
|
|
1337
|
-
expect(res.error).toContain('Invalid entry path format')
|
|
1338
|
-
})
|
|
1339
|
-
})
|