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,1126 +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 } from 'vitest'
|
|
6
|
-
|
|
7
|
-
import { defineCanopyTestConfig } from './config-test'
|
|
8
|
-
import { flattenSchema } from './config'
|
|
9
|
-
import { ContentStore, ContentStoreError } from './content-store'
|
|
10
|
-
import { unsafeAsLogicalPath, unsafeAsEntrySlug } from './paths/test-utils'
|
|
11
|
-
|
|
12
|
-
const tmpDir = async () => fs.mkdtemp(path.join(os.tmpdir(), 'canopycms-'))
|
|
13
|
-
|
|
14
|
-
describe('ContentStore', () => {
|
|
15
|
-
it('writes and reads markdown content', async () => {
|
|
16
|
-
const root = await tmpDir()
|
|
17
|
-
const schema = {
|
|
18
|
-
collections: [
|
|
19
|
-
{
|
|
20
|
-
name: 'posts',
|
|
21
|
-
path: 'posts',
|
|
22
|
-
entries: [
|
|
23
|
-
{
|
|
24
|
-
name: 'post',
|
|
25
|
-
format: 'md' as const,
|
|
26
|
-
schema: [{ name: 'title', type: 'string' as const }],
|
|
27
|
-
},
|
|
28
|
-
],
|
|
29
|
-
},
|
|
30
|
-
],
|
|
31
|
-
} as const
|
|
32
|
-
|
|
33
|
-
const config = defineCanopyTestConfig({ schema })
|
|
34
|
-
const store = new ContentStore(root, flattenSchema(schema, config.contentRoot))
|
|
35
|
-
|
|
36
|
-
await store.write(unsafeAsLogicalPath('content/posts'), unsafeAsEntrySlug('hello-world'), {
|
|
37
|
-
format: 'md',
|
|
38
|
-
data: { title: 'Hello' },
|
|
39
|
-
body: 'Body text',
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
const doc = await store.read(
|
|
43
|
-
unsafeAsLogicalPath('content/posts'),
|
|
44
|
-
unsafeAsEntrySlug('hello-world'),
|
|
45
|
-
)
|
|
46
|
-
if (doc.format === 'json') throw new Error('expected markdown')
|
|
47
|
-
expect(doc.data.title).toBe('Hello')
|
|
48
|
-
expect(doc.body).toContain('Body text')
|
|
49
|
-
expect(doc.relativePath.endsWith('.md')).toBe(true)
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it('writes and reads mdx content with frontmatter', async () => {
|
|
53
|
-
const root = await tmpDir()
|
|
54
|
-
const schema = {
|
|
55
|
-
collections: [
|
|
56
|
-
{
|
|
57
|
-
name: 'pages',
|
|
58
|
-
path: 'pages',
|
|
59
|
-
entries: [
|
|
60
|
-
{
|
|
61
|
-
name: 'page',
|
|
62
|
-
format: 'mdx' as const,
|
|
63
|
-
schema: [{ name: 'title', type: 'string' as const }],
|
|
64
|
-
},
|
|
65
|
-
],
|
|
66
|
-
},
|
|
67
|
-
],
|
|
68
|
-
} as const
|
|
69
|
-
|
|
70
|
-
const config = defineCanopyTestConfig({ schema })
|
|
71
|
-
const store = new ContentStore(root, flattenSchema(schema, config.contentRoot))
|
|
72
|
-
|
|
73
|
-
await store.write(unsafeAsLogicalPath('content/pages'), unsafeAsEntrySlug('landing'), {
|
|
74
|
-
format: 'mdx',
|
|
75
|
-
data: { title: 'Landing' },
|
|
76
|
-
body: '<Hero title="Hi" />',
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
const doc = await store.read(unsafeAsLogicalPath('content/pages'), unsafeAsEntrySlug('landing'))
|
|
80
|
-
if (doc.format === 'json') throw new Error('expected mdx')
|
|
81
|
-
expect(doc.data.title).toBe('Landing')
|
|
82
|
-
expect(doc.body?.includes('<Hero')).toBe(true)
|
|
83
|
-
expect(doc.absolutePath.endsWith('.mdx')).toBe(true)
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
it('writes and reads json content', async () => {
|
|
87
|
-
const root = await tmpDir()
|
|
88
|
-
const schema = {
|
|
89
|
-
collections: [
|
|
90
|
-
{
|
|
91
|
-
name: 'settings',
|
|
92
|
-
path: 'config',
|
|
93
|
-
entries: [
|
|
94
|
-
{
|
|
95
|
-
name: 'setting',
|
|
96
|
-
format: 'json' as const,
|
|
97
|
-
schema: [{ name: 'siteName', type: 'string' as const }],
|
|
98
|
-
},
|
|
99
|
-
],
|
|
100
|
-
},
|
|
101
|
-
],
|
|
102
|
-
} as const
|
|
103
|
-
|
|
104
|
-
const config = defineCanopyTestConfig({ schema })
|
|
105
|
-
const store = new ContentStore(root, flattenSchema(schema, config.contentRoot))
|
|
106
|
-
|
|
107
|
-
await store.write(unsafeAsLogicalPath('content/config'), unsafeAsEntrySlug('site'), {
|
|
108
|
-
format: 'json',
|
|
109
|
-
data: { siteName: 'CanopyCMS' },
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
const doc = await store.read(unsafeAsLogicalPath('content/config'), unsafeAsEntrySlug('site'))
|
|
113
|
-
expect(doc.data.siteName).toBe('CanopyCMS')
|
|
114
|
-
expect(doc.relativePath.endsWith('.json')).toBe(true)
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
it('prevents path traversal outside root', async () => {
|
|
118
|
-
const root = await tmpDir()
|
|
119
|
-
const schema = {
|
|
120
|
-
collections: [
|
|
121
|
-
{
|
|
122
|
-
name: 'posts',
|
|
123
|
-
path: 'posts',
|
|
124
|
-
entries: [
|
|
125
|
-
{
|
|
126
|
-
name: 'post',
|
|
127
|
-
format: 'md' as const,
|
|
128
|
-
schema: [{ name: 'title', type: 'string' as const }],
|
|
129
|
-
},
|
|
130
|
-
],
|
|
131
|
-
},
|
|
132
|
-
],
|
|
133
|
-
} as const
|
|
134
|
-
|
|
135
|
-
const config = defineCanopyTestConfig({ schema })
|
|
136
|
-
const store = new ContentStore(root, flattenSchema(schema, config.contentRoot))
|
|
137
|
-
|
|
138
|
-
await expect(
|
|
139
|
-
store.write(unsafeAsLogicalPath('content/posts'), unsafeAsEntrySlug('../escape'), {
|
|
140
|
-
format: 'md',
|
|
141
|
-
data: { title: 'bad' },
|
|
142
|
-
body: 'x',
|
|
143
|
-
}),
|
|
144
|
-
).rejects.toBeInstanceOf(ContentStoreError)
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
it('reads and writes entry items with a slug', async () => {
|
|
148
|
-
const root = await tmpDir()
|
|
149
|
-
const schema = {
|
|
150
|
-
collections: [
|
|
151
|
-
{
|
|
152
|
-
name: 'settings',
|
|
153
|
-
path: 'settings',
|
|
154
|
-
entries: [
|
|
155
|
-
{
|
|
156
|
-
name: 'setting',
|
|
157
|
-
format: 'json' as const,
|
|
158
|
-
schema: [{ name: 'siteName', type: 'string' as const }],
|
|
159
|
-
},
|
|
160
|
-
],
|
|
161
|
-
},
|
|
162
|
-
],
|
|
163
|
-
} as const
|
|
164
|
-
|
|
165
|
-
const config = defineCanopyTestConfig({ schema })
|
|
166
|
-
const store = new ContentStore(root, flattenSchema(schema, config.contentRoot))
|
|
167
|
-
|
|
168
|
-
await store.write(unsafeAsLogicalPath('content/settings'), unsafeAsEntrySlug('site'), {
|
|
169
|
-
format: 'json',
|
|
170
|
-
data: { siteName: 'CanopyCMS' },
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
const doc = await store.read(unsafeAsLogicalPath('content/settings'), unsafeAsEntrySlug('site'))
|
|
174
|
-
expect(doc.format).toBe('json')
|
|
175
|
-
expect(doc.data.siteName).toBe('CanopyCMS')
|
|
176
|
-
// Pattern: {type}.{slug}.{id}.{ext}
|
|
177
|
-
expect(doc.relativePath).toMatch(/content\/settings\/setting\.site\.[a-zA-Z0-9]+\.json/)
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
it('rejects slugs with forward slashes', async () => {
|
|
181
|
-
const root = await tmpDir()
|
|
182
|
-
const schema = {
|
|
183
|
-
collections: [
|
|
184
|
-
{
|
|
185
|
-
name: 'posts',
|
|
186
|
-
path: 'posts',
|
|
187
|
-
entries: [
|
|
188
|
-
{
|
|
189
|
-
name: 'post',
|
|
190
|
-
format: 'md' as const,
|
|
191
|
-
schema: [{ name: 'title', type: 'string' as const }],
|
|
192
|
-
},
|
|
193
|
-
],
|
|
194
|
-
},
|
|
195
|
-
],
|
|
196
|
-
} as const
|
|
197
|
-
|
|
198
|
-
const config = defineCanopyTestConfig({ schema })
|
|
199
|
-
const store = new ContentStore(root, flattenSchema(schema, config.contentRoot))
|
|
200
|
-
|
|
201
|
-
await expect(
|
|
202
|
-
store.write(unsafeAsLogicalPath('content/posts'), unsafeAsEntrySlug('2024/hello'), {
|
|
203
|
-
format: 'md',
|
|
204
|
-
data: { title: 'Bad Slug' },
|
|
205
|
-
body: 'Content',
|
|
206
|
-
}),
|
|
207
|
-
).rejects.toThrow('Slugs cannot contain forward slashes')
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
it('rejects slugs with backslashes', async () => {
|
|
211
|
-
const root = await tmpDir()
|
|
212
|
-
const schema = {
|
|
213
|
-
collections: [
|
|
214
|
-
{
|
|
215
|
-
name: 'posts',
|
|
216
|
-
path: 'posts',
|
|
217
|
-
entries: [
|
|
218
|
-
{
|
|
219
|
-
name: 'post',
|
|
220
|
-
format: 'md' as const,
|
|
221
|
-
schema: [{ name: 'title', type: 'string' as const }],
|
|
222
|
-
},
|
|
223
|
-
],
|
|
224
|
-
},
|
|
225
|
-
],
|
|
226
|
-
} as const
|
|
227
|
-
|
|
228
|
-
const config = defineCanopyTestConfig({ schema })
|
|
229
|
-
const store = new ContentStore(root, flattenSchema(schema, config.contentRoot))
|
|
230
|
-
|
|
231
|
-
await expect(
|
|
232
|
-
store.write(unsafeAsLogicalPath('content/posts'), unsafeAsEntrySlug('bad\\slug'), {
|
|
233
|
-
format: 'md',
|
|
234
|
-
data: { title: 'Bad Slug' },
|
|
235
|
-
body: 'Content',
|
|
236
|
-
}),
|
|
237
|
-
).rejects.toThrow('Slugs cannot contain backslashes')
|
|
238
|
-
})
|
|
239
|
-
|
|
240
|
-
it('resolves paths using trivial algorithm: collection + slug', async () => {
|
|
241
|
-
const root = await tmpDir()
|
|
242
|
-
const schema = {
|
|
243
|
-
collections: [
|
|
244
|
-
{
|
|
245
|
-
name: 'posts',
|
|
246
|
-
path: 'posts',
|
|
247
|
-
entries: [
|
|
248
|
-
{
|
|
249
|
-
name: 'post',
|
|
250
|
-
format: 'md' as const,
|
|
251
|
-
schema: [{ name: 'title', type: 'string' as const }],
|
|
252
|
-
},
|
|
253
|
-
],
|
|
254
|
-
},
|
|
255
|
-
],
|
|
256
|
-
} as const
|
|
257
|
-
|
|
258
|
-
const config = defineCanopyTestConfig({ schema })
|
|
259
|
-
const store = new ContentStore(root, flattenSchema(schema, config.contentRoot))
|
|
260
|
-
|
|
261
|
-
// Path: content/posts/hello -> collection=content/posts, slug=hello
|
|
262
|
-
const result = store.resolvePath(['content', 'posts', 'hello'])
|
|
263
|
-
expect(result.schemaItem.logicalPath).toBe('content/posts')
|
|
264
|
-
expect(result.schemaItem.type).toBe('collection')
|
|
265
|
-
expect(result.slug).toBe('hello')
|
|
266
|
-
})
|
|
267
|
-
|
|
268
|
-
it('resolves paths for collection entries with slug', async () => {
|
|
269
|
-
const root = await tmpDir()
|
|
270
|
-
const schema = {
|
|
271
|
-
collections: [
|
|
272
|
-
{
|
|
273
|
-
name: 'settings',
|
|
274
|
-
path: 'settings',
|
|
275
|
-
entries: [
|
|
276
|
-
{
|
|
277
|
-
name: 'setting',
|
|
278
|
-
format: 'json' as const,
|
|
279
|
-
schema: [{ name: 'siteName', type: 'string' as const }],
|
|
280
|
-
},
|
|
281
|
-
],
|
|
282
|
-
},
|
|
283
|
-
],
|
|
284
|
-
} as const
|
|
285
|
-
|
|
286
|
-
const config = defineCanopyTestConfig({ schema })
|
|
287
|
-
const store = new ContentStore(root, flattenSchema(schema, config.contentRoot))
|
|
288
|
-
|
|
289
|
-
// Path: content/settings/site -> collection entry with slug
|
|
290
|
-
const result = store.resolvePath(['content', 'settings', 'site'])
|
|
291
|
-
expect(result.schemaItem.logicalPath).toBe('content/settings')
|
|
292
|
-
expect(result.schemaItem.type).toBe('collection')
|
|
293
|
-
expect(result.slug).toBe('site')
|
|
294
|
-
})
|
|
295
|
-
|
|
296
|
-
it('resolves nested collection paths', async () => {
|
|
297
|
-
const root = await tmpDir()
|
|
298
|
-
const schema = {
|
|
299
|
-
collections: [
|
|
300
|
-
{
|
|
301
|
-
name: 'docs',
|
|
302
|
-
path: 'docs',
|
|
303
|
-
entries: [
|
|
304
|
-
{
|
|
305
|
-
name: 'entry',
|
|
306
|
-
format: 'md' as const,
|
|
307
|
-
schema: [{ name: 'title', type: 'string' as const }],
|
|
308
|
-
},
|
|
309
|
-
],
|
|
310
|
-
collections: [
|
|
311
|
-
{
|
|
312
|
-
name: 'guides',
|
|
313
|
-
path: 'guides',
|
|
314
|
-
entries: [
|
|
315
|
-
{
|
|
316
|
-
name: 'entry',
|
|
317
|
-
format: 'md' as const,
|
|
318
|
-
schema: [{ name: 'title', type: 'string' as const }],
|
|
319
|
-
},
|
|
320
|
-
],
|
|
321
|
-
},
|
|
322
|
-
],
|
|
323
|
-
},
|
|
324
|
-
],
|
|
325
|
-
} as const
|
|
326
|
-
|
|
327
|
-
const config = defineCanopyTestConfig({ schema })
|
|
328
|
-
const store = new ContentStore(root, flattenSchema(schema, config.contentRoot))
|
|
329
|
-
|
|
330
|
-
// Path: content/docs/guides/getting-started
|
|
331
|
-
// -> collection=content/docs/guides, slug=getting-started
|
|
332
|
-
const result = store.resolvePath(['content', 'docs', 'guides', 'getting-started'])
|
|
333
|
-
expect(result.schemaItem.logicalPath).toBe('content/docs/guides')
|
|
334
|
-
expect(result.schemaItem.type).toBe('collection')
|
|
335
|
-
expect(result.slug).toBe('getting-started')
|
|
336
|
-
})
|
|
337
|
-
|
|
338
|
-
it('resolves 3-level nested collection paths', async () => {
|
|
339
|
-
const root = await tmpDir()
|
|
340
|
-
const schema = {
|
|
341
|
-
collections: [
|
|
342
|
-
{
|
|
343
|
-
name: 'docs',
|
|
344
|
-
path: 'docs',
|
|
345
|
-
entries: [
|
|
346
|
-
{
|
|
347
|
-
name: 'entry',
|
|
348
|
-
format: 'md' as const,
|
|
349
|
-
schema: [{ name: 'title', type: 'string' as const }],
|
|
350
|
-
},
|
|
351
|
-
],
|
|
352
|
-
collections: [
|
|
353
|
-
{
|
|
354
|
-
name: 'api',
|
|
355
|
-
path: 'api',
|
|
356
|
-
entries: [
|
|
357
|
-
{
|
|
358
|
-
name: 'entry',
|
|
359
|
-
format: 'md' as const,
|
|
360
|
-
schema: [{ name: 'title', type: 'string' as const }],
|
|
361
|
-
},
|
|
362
|
-
],
|
|
363
|
-
collections: [
|
|
364
|
-
{
|
|
365
|
-
name: 'v2',
|
|
366
|
-
path: 'v2',
|
|
367
|
-
entries: [
|
|
368
|
-
{
|
|
369
|
-
name: 'entry',
|
|
370
|
-
format: 'md' as const,
|
|
371
|
-
schema: [{ name: 'title', type: 'string' as const }],
|
|
372
|
-
},
|
|
373
|
-
],
|
|
374
|
-
},
|
|
375
|
-
],
|
|
376
|
-
},
|
|
377
|
-
],
|
|
378
|
-
},
|
|
379
|
-
],
|
|
380
|
-
} as const
|
|
381
|
-
|
|
382
|
-
const config = defineCanopyTestConfig({ schema })
|
|
383
|
-
const store = new ContentStore(root, flattenSchema(schema, config.contentRoot))
|
|
384
|
-
|
|
385
|
-
// Path: content/docs/api/v2/authentication
|
|
386
|
-
// -> collection=content/docs/api/v2, slug=authentication
|
|
387
|
-
const result = store.resolvePath(['content', 'docs', 'api', 'v2', 'authentication'])
|
|
388
|
-
expect(result.schemaItem.logicalPath).toBe('content/docs/api/v2')
|
|
389
|
-
expect(result.schemaItem.type).toBe('collection')
|
|
390
|
-
expect(result.slug).toBe('authentication')
|
|
391
|
-
})
|
|
392
|
-
|
|
393
|
-
it('resolves 4-level nested collection paths', async () => {
|
|
394
|
-
const root = await tmpDir()
|
|
395
|
-
const schema = {
|
|
396
|
-
collections: [
|
|
397
|
-
{
|
|
398
|
-
name: 'docs',
|
|
399
|
-
path: 'docs',
|
|
400
|
-
entries: [
|
|
401
|
-
{
|
|
402
|
-
name: 'entry',
|
|
403
|
-
format: 'md' as const,
|
|
404
|
-
schema: [{ name: 'title', type: 'string' as const }],
|
|
405
|
-
},
|
|
406
|
-
],
|
|
407
|
-
collections: [
|
|
408
|
-
{
|
|
409
|
-
name: 'api',
|
|
410
|
-
path: 'api',
|
|
411
|
-
entries: [
|
|
412
|
-
{
|
|
413
|
-
name: 'entry',
|
|
414
|
-
format: 'md' as const,
|
|
415
|
-
schema: [{ name: 'title', type: 'string' as const }],
|
|
416
|
-
},
|
|
417
|
-
],
|
|
418
|
-
collections: [
|
|
419
|
-
{
|
|
420
|
-
name: 'v2',
|
|
421
|
-
path: 'v2',
|
|
422
|
-
entries: [
|
|
423
|
-
{
|
|
424
|
-
name: 'entry',
|
|
425
|
-
format: 'md' as const,
|
|
426
|
-
schema: [{ name: 'title', type: 'string' as const }],
|
|
427
|
-
},
|
|
428
|
-
],
|
|
429
|
-
collections: [
|
|
430
|
-
{
|
|
431
|
-
name: 'endpoints',
|
|
432
|
-
path: 'endpoints',
|
|
433
|
-
entries: [
|
|
434
|
-
{
|
|
435
|
-
name: 'entry',
|
|
436
|
-
format: 'md' as const,
|
|
437
|
-
schema: [{ name: 'title', type: 'string' as const }],
|
|
438
|
-
},
|
|
439
|
-
],
|
|
440
|
-
},
|
|
441
|
-
],
|
|
442
|
-
},
|
|
443
|
-
],
|
|
444
|
-
},
|
|
445
|
-
],
|
|
446
|
-
},
|
|
447
|
-
],
|
|
448
|
-
} as const
|
|
449
|
-
|
|
450
|
-
const config = defineCanopyTestConfig({ schema })
|
|
451
|
-
const store = new ContentStore(root, flattenSchema(schema, config.contentRoot))
|
|
452
|
-
|
|
453
|
-
// Path: content/docs/api/v2/endpoints/users
|
|
454
|
-
// -> collection=content/docs/api/v2/endpoints, slug=users
|
|
455
|
-
const result = store.resolvePath(['content', 'docs', 'api', 'v2', 'endpoints', 'users'])
|
|
456
|
-
expect(result.schemaItem.logicalPath).toBe('content/docs/api/v2/endpoints')
|
|
457
|
-
expect(result.schemaItem.type).toBe('collection')
|
|
458
|
-
expect(result.slug).toBe('users')
|
|
459
|
-
})
|
|
460
|
-
|
|
461
|
-
it('writes and reads content in deeply nested collections', async () => {
|
|
462
|
-
const root = await tmpDir()
|
|
463
|
-
const schema = {
|
|
464
|
-
collections: [
|
|
465
|
-
{
|
|
466
|
-
name: 'docs',
|
|
467
|
-
path: 'docs',
|
|
468
|
-
entries: [
|
|
469
|
-
{
|
|
470
|
-
name: 'entry',
|
|
471
|
-
format: 'md' as const,
|
|
472
|
-
schema: [{ name: 'title', type: 'string' as const }],
|
|
473
|
-
},
|
|
474
|
-
],
|
|
475
|
-
collections: [
|
|
476
|
-
{
|
|
477
|
-
name: 'api',
|
|
478
|
-
path: 'api',
|
|
479
|
-
entries: [
|
|
480
|
-
{
|
|
481
|
-
name: 'entry',
|
|
482
|
-
format: 'md' as const,
|
|
483
|
-
schema: [{ name: 'title', type: 'string' as const }],
|
|
484
|
-
},
|
|
485
|
-
],
|
|
486
|
-
collections: [
|
|
487
|
-
{
|
|
488
|
-
name: 'v2',
|
|
489
|
-
path: 'v2',
|
|
490
|
-
entries: [
|
|
491
|
-
{
|
|
492
|
-
name: 'entry',
|
|
493
|
-
format: 'md' as const,
|
|
494
|
-
schema: [{ name: 'title', type: 'string' as const }],
|
|
495
|
-
},
|
|
496
|
-
],
|
|
497
|
-
},
|
|
498
|
-
],
|
|
499
|
-
},
|
|
500
|
-
],
|
|
501
|
-
},
|
|
502
|
-
],
|
|
503
|
-
} as const
|
|
504
|
-
|
|
505
|
-
const config = defineCanopyTestConfig({ schema })
|
|
506
|
-
const store = new ContentStore(root, flattenSchema(schema, config.contentRoot))
|
|
507
|
-
|
|
508
|
-
// Write to 3-level nested collection
|
|
509
|
-
await store.write(
|
|
510
|
-
unsafeAsLogicalPath('content/docs/api/v2'),
|
|
511
|
-
unsafeAsEntrySlug('authentication'),
|
|
512
|
-
{
|
|
513
|
-
format: 'md',
|
|
514
|
-
data: { title: 'Authentication Guide' },
|
|
515
|
-
body: '# Authentication\n\nHow to authenticate.',
|
|
516
|
-
},
|
|
517
|
-
)
|
|
518
|
-
|
|
519
|
-
// Read it back
|
|
520
|
-
const doc = await store.read(
|
|
521
|
-
unsafeAsLogicalPath('content/docs/api/v2'),
|
|
522
|
-
unsafeAsEntrySlug('authentication'),
|
|
523
|
-
)
|
|
524
|
-
if (doc.format === 'json') throw new Error('expected markdown')
|
|
525
|
-
expect(doc.data.title).toBe('Authentication Guide')
|
|
526
|
-
expect(doc.body).toContain('How to authenticate')
|
|
527
|
-
// Pattern: {type}.{slug}.{id}.{ext}
|
|
528
|
-
expect(doc.relativePath).toMatch(
|
|
529
|
-
/^content\/docs\/api\/v2\/entry\.authentication\.[a-zA-Z0-9]{12}\.md$/,
|
|
530
|
-
)
|
|
531
|
-
})
|
|
532
|
-
|
|
533
|
-
describe('renameEntry', () => {
|
|
534
|
-
it('renames an entry by changing its slug', async () => {
|
|
535
|
-
const root = await tmpDir()
|
|
536
|
-
const schema = {
|
|
537
|
-
collections: [
|
|
538
|
-
{
|
|
539
|
-
name: 'posts',
|
|
540
|
-
path: 'posts',
|
|
541
|
-
entries: [
|
|
542
|
-
{
|
|
543
|
-
name: 'post',
|
|
544
|
-
format: 'md' as const,
|
|
545
|
-
schema: [{ name: 'title', type: 'string' as const }],
|
|
546
|
-
},
|
|
547
|
-
],
|
|
548
|
-
},
|
|
549
|
-
],
|
|
550
|
-
} as const
|
|
551
|
-
|
|
552
|
-
const config = defineCanopyTestConfig({ schema })
|
|
553
|
-
const store = new ContentStore(root, flattenSchema(schema, config.contentRoot))
|
|
554
|
-
|
|
555
|
-
// Create an entry
|
|
556
|
-
await store.write(unsafeAsLogicalPath('content/posts'), unsafeAsEntrySlug('old-slug'), {
|
|
557
|
-
format: 'md',
|
|
558
|
-
data: { title: 'Test Post' },
|
|
559
|
-
body: 'Content here',
|
|
560
|
-
})
|
|
561
|
-
|
|
562
|
-
// Rename it
|
|
563
|
-
const result = await store.renameEntry(
|
|
564
|
-
unsafeAsLogicalPath('content/posts'),
|
|
565
|
-
unsafeAsEntrySlug('old-slug'),
|
|
566
|
-
unsafeAsEntrySlug('new-slug'),
|
|
567
|
-
)
|
|
568
|
-
|
|
569
|
-
// Verify new path is returned
|
|
570
|
-
expect(result.newPath).toBe('content/posts/new-slug')
|
|
571
|
-
|
|
572
|
-
// Verify old path doesn't exist anymore
|
|
573
|
-
await expect(
|
|
574
|
-
store.read(unsafeAsLogicalPath('content/posts'), unsafeAsEntrySlug('old-slug')),
|
|
575
|
-
).rejects.toThrow()
|
|
576
|
-
|
|
577
|
-
// Verify new path works
|
|
578
|
-
const doc = await store.read(
|
|
579
|
-
unsafeAsLogicalPath('content/posts'),
|
|
580
|
-
unsafeAsEntrySlug('new-slug'),
|
|
581
|
-
)
|
|
582
|
-
if (doc.format === 'json') throw new Error('expected markdown')
|
|
583
|
-
expect(doc.data.title).toBe('Test Post')
|
|
584
|
-
expect(doc.body).toContain('Content here')
|
|
585
|
-
})
|
|
586
|
-
|
|
587
|
-
it('throws when entry does not exist', async () => {
|
|
588
|
-
const root = await tmpDir()
|
|
589
|
-
const schema = {
|
|
590
|
-
collections: [
|
|
591
|
-
{
|
|
592
|
-
name: 'posts',
|
|
593
|
-
path: 'posts',
|
|
594
|
-
entries: [{ name: 'post', format: 'json' as const, schema: [] }],
|
|
595
|
-
},
|
|
596
|
-
],
|
|
597
|
-
} as const
|
|
598
|
-
|
|
599
|
-
const config = defineCanopyTestConfig({ schema })
|
|
600
|
-
const store = new ContentStore(root, flattenSchema(schema, config.contentRoot))
|
|
601
|
-
|
|
602
|
-
await expect(
|
|
603
|
-
store.renameEntry(
|
|
604
|
-
unsafeAsLogicalPath('content/posts'),
|
|
605
|
-
unsafeAsEntrySlug('nonexistent'),
|
|
606
|
-
unsafeAsEntrySlug('new-slug'),
|
|
607
|
-
),
|
|
608
|
-
).rejects.toThrow('Entry not found: nonexistent')
|
|
609
|
-
})
|
|
610
|
-
|
|
611
|
-
it('throws when new slug already exists', async () => {
|
|
612
|
-
const root = await tmpDir()
|
|
613
|
-
const schema = {
|
|
614
|
-
collections: [
|
|
615
|
-
{
|
|
616
|
-
name: 'posts',
|
|
617
|
-
path: 'posts',
|
|
618
|
-
entries: [{ name: 'post', format: 'json' as const, schema: [] }],
|
|
619
|
-
},
|
|
620
|
-
],
|
|
621
|
-
} as const
|
|
622
|
-
|
|
623
|
-
const config = defineCanopyTestConfig({ schema })
|
|
624
|
-
const store = new ContentStore(root, flattenSchema(schema, config.contentRoot))
|
|
625
|
-
|
|
626
|
-
// Create two entries
|
|
627
|
-
await store.write(unsafeAsLogicalPath('content/posts'), unsafeAsEntrySlug('first-post'), {
|
|
628
|
-
format: 'json',
|
|
629
|
-
data: { title: 'First' },
|
|
630
|
-
})
|
|
631
|
-
await store.write(unsafeAsLogicalPath('content/posts'), unsafeAsEntrySlug('second-post'), {
|
|
632
|
-
format: 'json',
|
|
633
|
-
data: { title: 'Second' },
|
|
634
|
-
})
|
|
635
|
-
|
|
636
|
-
// Try to rename first-post to second-post (conflict)
|
|
637
|
-
await expect(
|
|
638
|
-
store.renameEntry(
|
|
639
|
-
unsafeAsLogicalPath('content/posts'),
|
|
640
|
-
unsafeAsEntrySlug('first-post'),
|
|
641
|
-
unsafeAsEntrySlug('second-post'),
|
|
642
|
-
),
|
|
643
|
-
).rejects.toThrow('already exists')
|
|
644
|
-
})
|
|
645
|
-
|
|
646
|
-
it('validates slug format', async () => {
|
|
647
|
-
const root = await tmpDir()
|
|
648
|
-
const schema = {
|
|
649
|
-
collections: [
|
|
650
|
-
{
|
|
651
|
-
name: 'posts',
|
|
652
|
-
path: 'posts',
|
|
653
|
-
entries: [{ name: 'post', format: 'json' as const, schema: [] }],
|
|
654
|
-
},
|
|
655
|
-
],
|
|
656
|
-
} as const
|
|
657
|
-
|
|
658
|
-
const config = defineCanopyTestConfig({ schema })
|
|
659
|
-
const store = new ContentStore(root, flattenSchema(schema, config.contentRoot))
|
|
660
|
-
|
|
661
|
-
// Create an entry
|
|
662
|
-
await store.write(unsafeAsLogicalPath('content/posts'), unsafeAsEntrySlug('test-post'), {
|
|
663
|
-
format: 'json',
|
|
664
|
-
data: { title: 'Test' },
|
|
665
|
-
})
|
|
666
|
-
|
|
667
|
-
// Try invalid slug with slash
|
|
668
|
-
await expect(
|
|
669
|
-
store.renameEntry(
|
|
670
|
-
unsafeAsLogicalPath('content/posts'),
|
|
671
|
-
unsafeAsEntrySlug('test-post'),
|
|
672
|
-
unsafeAsEntrySlug('invalid/slug'),
|
|
673
|
-
),
|
|
674
|
-
).rejects.toThrow('cannot contain forward slashes')
|
|
675
|
-
|
|
676
|
-
// Try invalid slug with uppercase
|
|
677
|
-
await expect(
|
|
678
|
-
store.renameEntry(
|
|
679
|
-
unsafeAsLogicalPath('content/posts'),
|
|
680
|
-
unsafeAsEntrySlug('test-post'),
|
|
681
|
-
unsafeAsEntrySlug('Invalid-Slug'),
|
|
682
|
-
),
|
|
683
|
-
).rejects.toThrow('must start with a letter or number')
|
|
684
|
-
})
|
|
685
|
-
|
|
686
|
-
it('handles no-op when slug is unchanged', async () => {
|
|
687
|
-
const root = await tmpDir()
|
|
688
|
-
const schema = {
|
|
689
|
-
collections: [
|
|
690
|
-
{
|
|
691
|
-
name: 'posts',
|
|
692
|
-
path: 'posts',
|
|
693
|
-
entries: [{ name: 'post', format: 'json' as const, schema: [] }],
|
|
694
|
-
},
|
|
695
|
-
],
|
|
696
|
-
} as const
|
|
697
|
-
|
|
698
|
-
const config = defineCanopyTestConfig({ schema })
|
|
699
|
-
const store = new ContentStore(root, flattenSchema(schema, config.contentRoot))
|
|
700
|
-
|
|
701
|
-
// Create an entry
|
|
702
|
-
await store.write(unsafeAsLogicalPath('content/posts'), unsafeAsEntrySlug('same-slug'), {
|
|
703
|
-
format: 'json',
|
|
704
|
-
data: { title: 'Test' },
|
|
705
|
-
})
|
|
706
|
-
|
|
707
|
-
// Rename to same slug (no-op)
|
|
708
|
-
const result = await store.renameEntry(
|
|
709
|
-
unsafeAsLogicalPath('content/posts'),
|
|
710
|
-
unsafeAsEntrySlug('same-slug'),
|
|
711
|
-
unsafeAsEntrySlug('same-slug'),
|
|
712
|
-
)
|
|
713
|
-
|
|
714
|
-
// Should return the same path
|
|
715
|
-
expect(result.newPath).toBe('content/posts/same-slug')
|
|
716
|
-
|
|
717
|
-
// Entry should still be readable
|
|
718
|
-
const doc = await store.read(
|
|
719
|
-
unsafeAsLogicalPath('content/posts'),
|
|
720
|
-
unsafeAsEntrySlug('same-slug'),
|
|
721
|
-
)
|
|
722
|
-
expect(doc.format).toBe('json')
|
|
723
|
-
if (doc.format === 'json') {
|
|
724
|
-
expect(doc.data.title).toBe('Test')
|
|
725
|
-
}
|
|
726
|
-
})
|
|
727
|
-
|
|
728
|
-
it('preserves content ID through rename', async () => {
|
|
729
|
-
const root = await tmpDir()
|
|
730
|
-
const schema = {
|
|
731
|
-
collections: [
|
|
732
|
-
{
|
|
733
|
-
name: 'posts',
|
|
734
|
-
path: 'posts',
|
|
735
|
-
entries: [{ name: 'post', format: 'json' as const, schema: [] }],
|
|
736
|
-
},
|
|
737
|
-
],
|
|
738
|
-
} as const
|
|
739
|
-
|
|
740
|
-
const config = defineCanopyTestConfig({ schema })
|
|
741
|
-
const store = new ContentStore(root, flattenSchema(schema, config.contentRoot))
|
|
742
|
-
|
|
743
|
-
// Create an entry
|
|
744
|
-
await store.write(unsafeAsLogicalPath('content/posts'), unsafeAsEntrySlug('original'), {
|
|
745
|
-
format: 'json',
|
|
746
|
-
data: { title: 'Test' },
|
|
747
|
-
})
|
|
748
|
-
|
|
749
|
-
// Get the content ID before rename
|
|
750
|
-
const idBefore = await store.getIdForEntry(
|
|
751
|
-
unsafeAsLogicalPath('content/posts'),
|
|
752
|
-
unsafeAsEntrySlug('original'),
|
|
753
|
-
)
|
|
754
|
-
|
|
755
|
-
// Rename the entry
|
|
756
|
-
await store.renameEntry(
|
|
757
|
-
unsafeAsLogicalPath('content/posts'),
|
|
758
|
-
unsafeAsEntrySlug('original'),
|
|
759
|
-
unsafeAsEntrySlug('renamed'),
|
|
760
|
-
)
|
|
761
|
-
|
|
762
|
-
// Get the content ID after rename
|
|
763
|
-
const idAfter = await store.getIdForEntry(
|
|
764
|
-
unsafeAsLogicalPath('content/posts'),
|
|
765
|
-
unsafeAsEntrySlug('renamed'),
|
|
766
|
-
)
|
|
767
|
-
|
|
768
|
-
// IDs should match (preserved through rename)
|
|
769
|
-
expect(idBefore).toBe(idAfter)
|
|
770
|
-
expect(idBefore).toBeTruthy()
|
|
771
|
-
})
|
|
772
|
-
})
|
|
773
|
-
|
|
774
|
-
describe('multiple entry types', () => {
|
|
775
|
-
it('creates entries with specified entry type', async () => {
|
|
776
|
-
const root = await tmpDir()
|
|
777
|
-
const schema = {
|
|
778
|
-
collections: [
|
|
779
|
-
{
|
|
780
|
-
name: 'content',
|
|
781
|
-
path: 'content',
|
|
782
|
-
entries: [
|
|
783
|
-
{
|
|
784
|
-
name: 'post',
|
|
785
|
-
format: 'mdx' as const,
|
|
786
|
-
schema: [],
|
|
787
|
-
default: true,
|
|
788
|
-
},
|
|
789
|
-
{ name: 'article', format: 'md' as const, schema: [] },
|
|
790
|
-
{ name: 'note', format: 'json' as const, schema: [] },
|
|
791
|
-
],
|
|
792
|
-
},
|
|
793
|
-
],
|
|
794
|
-
} as const
|
|
795
|
-
|
|
796
|
-
const config = defineCanopyTestConfig({ schema })
|
|
797
|
-
const store = new ContentStore(root, flattenSchema(schema, config.contentRoot))
|
|
798
|
-
|
|
799
|
-
// Create entries of different types
|
|
800
|
-
const post = await store.write(
|
|
801
|
-
unsafeAsLogicalPath('content/content'),
|
|
802
|
-
unsafeAsEntrySlug('my-post'),
|
|
803
|
-
{ format: 'mdx', data: {}, body: 'Post content' },
|
|
804
|
-
'post',
|
|
805
|
-
)
|
|
806
|
-
const article = await store.write(
|
|
807
|
-
unsafeAsLogicalPath('content/content'),
|
|
808
|
-
unsafeAsEntrySlug('my-article'),
|
|
809
|
-
{ format: 'md', data: {}, body: 'Article content' },
|
|
810
|
-
'article',
|
|
811
|
-
)
|
|
812
|
-
const note = await store.write(
|
|
813
|
-
unsafeAsLogicalPath('content/content'),
|
|
814
|
-
unsafeAsEntrySlug('my-note'),
|
|
815
|
-
{ format: 'json', data: { text: 'Note' } },
|
|
816
|
-
'note',
|
|
817
|
-
)
|
|
818
|
-
|
|
819
|
-
// Verify filenames include correct entry type (check the returned paths)
|
|
820
|
-
const postFile = path.basename(post.relativePath)
|
|
821
|
-
const articleFile = path.basename(article.relativePath)
|
|
822
|
-
const noteFile = path.basename(note.relativePath)
|
|
823
|
-
|
|
824
|
-
expect(postFile.startsWith('post.my-post.')).toBe(true)
|
|
825
|
-
expect(postFile.endsWith('.mdx')).toBe(true)
|
|
826
|
-
expect(articleFile.startsWith('article.my-article.')).toBe(true)
|
|
827
|
-
expect(articleFile.endsWith('.md')).toBe(true)
|
|
828
|
-
expect(noteFile.startsWith('note.my-note.')).toBe(true)
|
|
829
|
-
expect(noteFile.endsWith('.json')).toBe(true)
|
|
830
|
-
})
|
|
831
|
-
|
|
832
|
-
it('throws error for invalid entry type', async () => {
|
|
833
|
-
const root = await tmpDir()
|
|
834
|
-
const schema = {
|
|
835
|
-
collections: [
|
|
836
|
-
{
|
|
837
|
-
name: 'posts',
|
|
838
|
-
path: 'posts',
|
|
839
|
-
entries: [{ name: 'post', format: 'mdx' as const, schema: [] }],
|
|
840
|
-
},
|
|
841
|
-
],
|
|
842
|
-
} as const
|
|
843
|
-
|
|
844
|
-
const config = defineCanopyTestConfig({ schema })
|
|
845
|
-
const store = new ContentStore(root, flattenSchema(schema, config.contentRoot))
|
|
846
|
-
|
|
847
|
-
await expect(
|
|
848
|
-
store.write(
|
|
849
|
-
unsafeAsLogicalPath('content/posts'),
|
|
850
|
-
unsafeAsEntrySlug('test'),
|
|
851
|
-
{ format: 'mdx', data: {}, body: '' },
|
|
852
|
-
'invalid-type',
|
|
853
|
-
),
|
|
854
|
-
).rejects.toThrow("Entry type 'invalid-type' not found in collection")
|
|
855
|
-
})
|
|
856
|
-
|
|
857
|
-
it('uses default entry type when not specified', async () => {
|
|
858
|
-
const root = await tmpDir()
|
|
859
|
-
const schema = {
|
|
860
|
-
collections: [
|
|
861
|
-
{
|
|
862
|
-
name: 'docs',
|
|
863
|
-
path: 'docs',
|
|
864
|
-
entries: [
|
|
865
|
-
{ name: 'guide', format: 'md' as const, schema: [] },
|
|
866
|
-
{
|
|
867
|
-
name: 'tutorial',
|
|
868
|
-
format: 'mdx' as const,
|
|
869
|
-
schema: [],
|
|
870
|
-
default: true,
|
|
871
|
-
},
|
|
872
|
-
],
|
|
873
|
-
},
|
|
874
|
-
],
|
|
875
|
-
} as const
|
|
876
|
-
|
|
877
|
-
const config = defineCanopyTestConfig({ schema })
|
|
878
|
-
const store = new ContentStore(root, flattenSchema(schema, config.contentRoot))
|
|
879
|
-
|
|
880
|
-
// Write without specifying entry type - should use default (tutorial)
|
|
881
|
-
const doc = await store.write(
|
|
882
|
-
unsafeAsLogicalPath('content/docs'),
|
|
883
|
-
unsafeAsEntrySlug('my-doc'),
|
|
884
|
-
{ format: 'mdx', data: {}, body: 'Content' },
|
|
885
|
-
)
|
|
886
|
-
|
|
887
|
-
const tutorialFile = path.basename(doc.relativePath)
|
|
888
|
-
expect(tutorialFile.startsWith('tutorial.my-doc.')).toBe(true)
|
|
889
|
-
expect(tutorialFile.endsWith('.mdx')).toBe(true)
|
|
890
|
-
})
|
|
891
|
-
|
|
892
|
-
it('preserves entry type for existing entries (immutable after creation)', async () => {
|
|
893
|
-
const root = await tmpDir()
|
|
894
|
-
const schema = {
|
|
895
|
-
collections: [
|
|
896
|
-
{
|
|
897
|
-
name: 'content',
|
|
898
|
-
path: 'content',
|
|
899
|
-
entries: [
|
|
900
|
-
{ name: 'post', format: 'mdx' as const, schema: [] },
|
|
901
|
-
{ name: 'article', format: 'md' as const, schema: [] },
|
|
902
|
-
],
|
|
903
|
-
},
|
|
904
|
-
],
|
|
905
|
-
} as const
|
|
906
|
-
|
|
907
|
-
const config = defineCanopyTestConfig({ schema })
|
|
908
|
-
const store = new ContentStore(root, flattenSchema(schema, config.contentRoot))
|
|
909
|
-
|
|
910
|
-
// Create an entry with entry type "post"
|
|
911
|
-
const created = await store.write(
|
|
912
|
-
unsafeAsLogicalPath('content/content'),
|
|
913
|
-
unsafeAsEntrySlug('my-content'),
|
|
914
|
-
{ format: 'mdx', data: {}, body: 'Original' },
|
|
915
|
-
'post',
|
|
916
|
-
)
|
|
917
|
-
const createdFile = path.basename(created.relativePath)
|
|
918
|
-
expect(createdFile.startsWith('post.my-content.')).toBe(true)
|
|
919
|
-
|
|
920
|
-
// Update the same entry WITHOUT specifying entry type
|
|
921
|
-
// The entry type should be automatically preserved from the existing file
|
|
922
|
-
const updated = await store.write(
|
|
923
|
-
unsafeAsLogicalPath('content/content'),
|
|
924
|
-
unsafeAsEntrySlug('my-content'),
|
|
925
|
-
{ format: 'mdx', data: {}, body: 'Updated' },
|
|
926
|
-
)
|
|
927
|
-
const updatedFile = path.basename(updated.relativePath)
|
|
928
|
-
|
|
929
|
-
// Entry type should still be "post" (preserved from existing file)
|
|
930
|
-
expect(updatedFile.startsWith('post.my-content.')).toBe(true)
|
|
931
|
-
expect(updatedFile).toBe(createdFile) // Filename should be exactly the same
|
|
932
|
-
|
|
933
|
-
// Verify the content was updated
|
|
934
|
-
const read = await store.read(
|
|
935
|
-
unsafeAsLogicalPath('content/content'),
|
|
936
|
-
unsafeAsEntrySlug('my-content'),
|
|
937
|
-
)
|
|
938
|
-
if (read.format === 'json') throw new Error('Expected mdx')
|
|
939
|
-
expect(read.body.trim()).toBe('Updated')
|
|
940
|
-
|
|
941
|
-
// Also verify that even if we specify a different entry type, it gets ignored (preserved)
|
|
942
|
-
const updated2 = await store.write(
|
|
943
|
-
unsafeAsLogicalPath('content/content'),
|
|
944
|
-
unsafeAsEntrySlug('my-content'),
|
|
945
|
-
{ format: 'mdx', data: {}, body: 'Updated again' },
|
|
946
|
-
'post',
|
|
947
|
-
)
|
|
948
|
-
const updated2File = path.basename(updated2.relativePath)
|
|
949
|
-
expect(updated2File).toBe(createdFile) // Still the same filename
|
|
950
|
-
})
|
|
951
|
-
})
|
|
952
|
-
|
|
953
|
-
describe('entry-type path delegation', () => {
|
|
954
|
-
// When buildPaths receives an entry-type schema item (e.g., from
|
|
955
|
-
// store.read('content/home', '')), it delegates to the parent collection.
|
|
956
|
-
// The API layer doesn't trigger this path (resolvePath returns collections
|
|
957
|
-
// directly), but direct ContentStore usage can.
|
|
958
|
-
|
|
959
|
-
it('writes and reads via entry-type logical path', async () => {
|
|
960
|
-
const root = await tmpDir()
|
|
961
|
-
const schema = {
|
|
962
|
-
entries: [
|
|
963
|
-
{
|
|
964
|
-
name: 'home',
|
|
965
|
-
format: 'json' as const,
|
|
966
|
-
schema: [{ name: 'hero', type: 'string' as const }],
|
|
967
|
-
maxItems: 1,
|
|
968
|
-
},
|
|
969
|
-
{
|
|
970
|
-
name: 'settings',
|
|
971
|
-
format: 'json' as const,
|
|
972
|
-
schema: [{ name: 'siteName', type: 'string' as const }],
|
|
973
|
-
},
|
|
974
|
-
],
|
|
975
|
-
} as const
|
|
976
|
-
|
|
977
|
-
const config = defineCanopyTestConfig({ schema })
|
|
978
|
-
const store = new ContentStore(root, flattenSchema(schema, config.contentRoot))
|
|
979
|
-
|
|
980
|
-
// Write using the entry-type path (content/home) with empty slug
|
|
981
|
-
await store.write(unsafeAsLogicalPath('content/home'), unsafeAsEntrySlug(''), {
|
|
982
|
-
format: 'json',
|
|
983
|
-
data: { hero: 'Welcome' },
|
|
984
|
-
})
|
|
985
|
-
|
|
986
|
-
// Read it back via the same entry-type path
|
|
987
|
-
const doc = await store.read(unsafeAsLogicalPath('content/home'), unsafeAsEntrySlug(''))
|
|
988
|
-
expect(doc.format).toBe('json')
|
|
989
|
-
expect(doc.data.hero).toBe('Welcome')
|
|
990
|
-
|
|
991
|
-
// Verify 4-part filename: home.home.{id}.json
|
|
992
|
-
expect(doc.relativePath).toMatch(/^content\/home\.home\.[a-zA-Z0-9]{12}\.json$/)
|
|
993
|
-
})
|
|
994
|
-
|
|
995
|
-
it('uses provided slug instead of entry type name', async () => {
|
|
996
|
-
const root = await tmpDir()
|
|
997
|
-
const schema = {
|
|
998
|
-
entries: [
|
|
999
|
-
{
|
|
1000
|
-
name: 'page',
|
|
1001
|
-
format: 'json' as const,
|
|
1002
|
-
schema: [{ name: 'title', type: 'string' as const }],
|
|
1003
|
-
},
|
|
1004
|
-
],
|
|
1005
|
-
} as const
|
|
1006
|
-
|
|
1007
|
-
const config = defineCanopyTestConfig({ schema })
|
|
1008
|
-
const store = new ContentStore(root, flattenSchema(schema, config.contentRoot))
|
|
1009
|
-
|
|
1010
|
-
// Write with an explicit slug different from entry type name
|
|
1011
|
-
await store.write(unsafeAsLogicalPath('content/page'), unsafeAsEntrySlug('about'), {
|
|
1012
|
-
format: 'json',
|
|
1013
|
-
data: { title: 'About Us' },
|
|
1014
|
-
})
|
|
1015
|
-
|
|
1016
|
-
const doc = await store.read(unsafeAsLogicalPath('content/page'), unsafeAsEntrySlug('about'))
|
|
1017
|
-
expect(doc.data.title).toBe('About Us')
|
|
1018
|
-
|
|
1019
|
-
// Verify filename: page.about.{id}.json (type from entry type, slug from arg)
|
|
1020
|
-
expect(doc.relativePath).toMatch(/^content\/page\.about\.[a-zA-Z0-9]{12}\.json$/)
|
|
1021
|
-
})
|
|
1022
|
-
|
|
1023
|
-
it('uses correct format and fields from entry-type schema', async () => {
|
|
1024
|
-
const root = await tmpDir()
|
|
1025
|
-
const schema = {
|
|
1026
|
-
entries: [
|
|
1027
|
-
{
|
|
1028
|
-
name: 'post',
|
|
1029
|
-
format: 'md' as const,
|
|
1030
|
-
schema: [{ name: 'title', type: 'string' as const }],
|
|
1031
|
-
},
|
|
1032
|
-
],
|
|
1033
|
-
} as const
|
|
1034
|
-
|
|
1035
|
-
const config = defineCanopyTestConfig({ schema })
|
|
1036
|
-
const store = new ContentStore(root, flattenSchema(schema, config.contentRoot))
|
|
1037
|
-
|
|
1038
|
-
await store.write(unsafeAsLogicalPath('content/post'), unsafeAsEntrySlug('hello'), {
|
|
1039
|
-
format: 'md',
|
|
1040
|
-
data: { title: 'Hello' },
|
|
1041
|
-
body: 'World',
|
|
1042
|
-
})
|
|
1043
|
-
|
|
1044
|
-
const doc = await store.read(unsafeAsLogicalPath('content/post'), unsafeAsEntrySlug('hello'))
|
|
1045
|
-
if (doc.format === 'json') throw new Error('Expected md')
|
|
1046
|
-
expect(doc.format).toBe('md')
|
|
1047
|
-
expect(doc.data.title).toBe('Hello')
|
|
1048
|
-
expect(doc.body).toContain('World')
|
|
1049
|
-
expect(doc.relativePath).toMatch(/^content\/post\.hello\.[a-zA-Z0-9]{12}\.md$/)
|
|
1050
|
-
})
|
|
1051
|
-
})
|
|
1052
|
-
|
|
1053
|
-
describe('complex frontmatter roundtrip', () => {
|
|
1054
|
-
it('preserves nested objects and arrays in markdown frontmatter via gray-matter', async () => {
|
|
1055
|
-
const root = await tmpDir()
|
|
1056
|
-
const schema = {
|
|
1057
|
-
collections: [
|
|
1058
|
-
{
|
|
1059
|
-
name: 'posts',
|
|
1060
|
-
path: 'posts',
|
|
1061
|
-
entries: [
|
|
1062
|
-
{
|
|
1063
|
-
name: 'post',
|
|
1064
|
-
format: 'md' as const,
|
|
1065
|
-
schema: [
|
|
1066
|
-
{ name: 'title', type: 'string' as const },
|
|
1067
|
-
{ name: 'tags', type: 'string' as const, list: true },
|
|
1068
|
-
{ name: 'published', type: 'boolean' as const },
|
|
1069
|
-
],
|
|
1070
|
-
},
|
|
1071
|
-
],
|
|
1072
|
-
},
|
|
1073
|
-
],
|
|
1074
|
-
} as const
|
|
1075
|
-
|
|
1076
|
-
const config = defineCanopyTestConfig({ schema })
|
|
1077
|
-
const store = new ContentStore(root, flattenSchema(schema, config.contentRoot))
|
|
1078
|
-
|
|
1079
|
-
const complexData = {
|
|
1080
|
-
title: 'Complex Post',
|
|
1081
|
-
author: '5NVkkrB1MJUv',
|
|
1082
|
-
tags: ['typed', 'fast'],
|
|
1083
|
-
published: false,
|
|
1084
|
-
blocks: [
|
|
1085
|
-
{
|
|
1086
|
-
template: 'hero',
|
|
1087
|
-
value: {
|
|
1088
|
-
headline: 'Hero block',
|
|
1089
|
-
body: 'Hero copy',
|
|
1090
|
-
},
|
|
1091
|
-
},
|
|
1092
|
-
{
|
|
1093
|
-
template: 'cta',
|
|
1094
|
-
value: {
|
|
1095
|
-
title: 'Try CanopyCMS',
|
|
1096
|
-
ctaText: 'Click me',
|
|
1097
|
-
},
|
|
1098
|
-
},
|
|
1099
|
-
],
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
await store.write(unsafeAsLogicalPath('content/posts'), unsafeAsEntrySlug('complex'), {
|
|
1103
|
-
format: 'md',
|
|
1104
|
-
data: complexData,
|
|
1105
|
-
body: '# Hello World\n\nSome **bold** text with `code`.',
|
|
1106
|
-
})
|
|
1107
|
-
|
|
1108
|
-
const doc = await store.read(
|
|
1109
|
-
unsafeAsLogicalPath('content/posts'),
|
|
1110
|
-
unsafeAsEntrySlug('complex'),
|
|
1111
|
-
)
|
|
1112
|
-
if (doc.format === 'json') throw new Error('expected markdown')
|
|
1113
|
-
|
|
1114
|
-
// Verify all frontmatter data survived the roundtrip
|
|
1115
|
-
expect(doc.data.title).toBe('Complex Post')
|
|
1116
|
-
expect(doc.data.author).toBe('5NVkkrB1MJUv')
|
|
1117
|
-
expect(doc.data.tags).toEqual(['typed', 'fast'])
|
|
1118
|
-
expect(doc.data.published).toBe(false)
|
|
1119
|
-
expect(doc.data.blocks).toEqual(complexData.blocks)
|
|
1120
|
-
|
|
1121
|
-
// Verify body survived
|
|
1122
|
-
expect(doc.body).toContain('# Hello World')
|
|
1123
|
-
expect(doc.body).toContain('Some **bold** text')
|
|
1124
|
-
})
|
|
1125
|
-
})
|
|
1126
|
-
})
|