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/paths/validation.ts
DELETED
|
@@ -1,391 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Path validation utilities.
|
|
3
|
-
*
|
|
4
|
-
* Security-focused validation for content paths and slugs.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { normalizeFilesystemPath, hasTraversalSequence } from './normalize'
|
|
8
|
-
import type {
|
|
9
|
-
LogicalPath,
|
|
10
|
-
PhysicalPath,
|
|
11
|
-
ContentId,
|
|
12
|
-
BranchName,
|
|
13
|
-
EntrySlug,
|
|
14
|
-
CollectionSlug,
|
|
15
|
-
} from './types'
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Base58 alphabet used for content IDs (excludes ambiguous: 0, O, I, l)
|
|
19
|
-
*/
|
|
20
|
-
const BASE58_PATTERN = '[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]'
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Pattern matching exactly a 12-character content ID
|
|
24
|
-
*/
|
|
25
|
-
const CONTENT_ID_PATTERN = new RegExp(`^${BASE58_PATTERN}{12}$`)
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Pattern matching a physical path segment with embedded ID.
|
|
29
|
-
* Matches patterns like:
|
|
30
|
-
* - `post.my-slug.abc123def456.json` (entry)
|
|
31
|
-
* - `posts.abc123def456` (collection directory)
|
|
32
|
-
*/
|
|
33
|
-
const PHYSICAL_SEGMENT_PATTERN = new RegExp(`\\.${BASE58_PATTERN}{12}(?:\\.[a-z]+)?$`)
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Validate a content path for security.
|
|
37
|
-
*
|
|
38
|
-
* @param path - The path to validate
|
|
39
|
-
* @param rootPath - The root directory (for traversal check)
|
|
40
|
-
* @returns Validation result
|
|
41
|
-
*/
|
|
42
|
-
export function validateContentPath(
|
|
43
|
-
path: string,
|
|
44
|
-
rootPath: string,
|
|
45
|
-
): { valid: boolean; error?: string } {
|
|
46
|
-
const normalized = normalizeFilesystemPath(path)
|
|
47
|
-
|
|
48
|
-
// Check for traversal sequences
|
|
49
|
-
if (hasTraversalSequence(normalized)) {
|
|
50
|
-
return { valid: false, error: 'Path contains traversal sequence' }
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Check path doesn't escape root
|
|
54
|
-
const normalizedRoot = normalizeFilesystemPath(rootPath)
|
|
55
|
-
if (!normalized.startsWith(normalizedRoot) && normalized !== normalizedRoot) {
|
|
56
|
-
// Allow paths that are relative within the root
|
|
57
|
-
const normalizedPath = `${normalizedRoot}/${normalized}`
|
|
58
|
-
if (hasTraversalSequence(normalizedPath)) {
|
|
59
|
-
return { valid: false, error: 'Path escapes root directory' }
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return { valid: true }
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Validate a collection path.
|
|
68
|
-
*
|
|
69
|
-
* @param collectionPath - The collection path to validate
|
|
70
|
-
* @returns true if valid, false otherwise
|
|
71
|
-
*/
|
|
72
|
-
export function isValidCollectionPath(collectionPath: string): boolean {
|
|
73
|
-
if (!collectionPath || collectionPath.length === 0) {
|
|
74
|
-
return false
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Normalize and check for traversal
|
|
78
|
-
const normalized = normalizeFilesystemPath(collectionPath)
|
|
79
|
-
if (hasTraversalSequence(normalized)) {
|
|
80
|
-
return false
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Collection paths should only contain alphanumeric, hyphens, underscores, and forward slashes
|
|
84
|
-
const validPattern = /^[a-zA-Z0-9_/-]+$/
|
|
85
|
-
return validPattern.test(normalized)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Sanitize a string for use in paths by removing dangerous characters.
|
|
90
|
-
*
|
|
91
|
-
* @param input - The string to sanitize
|
|
92
|
-
* @returns Sanitized string safe for path use
|
|
93
|
-
*/
|
|
94
|
-
export function sanitizeForPath(input: string): string {
|
|
95
|
-
return input
|
|
96
|
-
.replace(/[<>:"|?*\\]/g, '') // Remove invalid filesystem chars
|
|
97
|
-
.replace(/\.{2,}/g, '.') // Collapse multiple dots
|
|
98
|
-
.replace(/^\./, '') // Remove leading dot
|
|
99
|
-
.trim()
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Check if a path segment contains an embedded content ID.
|
|
104
|
-
*
|
|
105
|
-
* Physical paths have segments with embedded 12-char IDs:
|
|
106
|
-
* - `post.my-slug.abc123def456.json` (entry file)
|
|
107
|
-
* - `posts.abc123def456` (collection directory)
|
|
108
|
-
*
|
|
109
|
-
* @param segment - A single path segment (no slashes)
|
|
110
|
-
* @returns true if segment contains embedded ID pattern
|
|
111
|
-
*/
|
|
112
|
-
export function hasEmbeddedContentId(segment: string): boolean {
|
|
113
|
-
return PHYSICAL_SEGMENT_PATTERN.test(segment)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Check if a path appears to be a physical path (contains embedded content IDs).
|
|
118
|
-
*
|
|
119
|
-
* Physical paths have the format:
|
|
120
|
-
* - `content/posts.abc123/post.hello.def456.json`
|
|
121
|
-
*
|
|
122
|
-
* Logical paths do not have embedded IDs:
|
|
123
|
-
* - `content/posts/hello` or `posts/hello`
|
|
124
|
-
*
|
|
125
|
-
* @param path - The path to check
|
|
126
|
-
* @returns true if any segment contains an embedded content ID
|
|
127
|
-
*/
|
|
128
|
-
export function looksLikePhysicalPath(path: string): boolean {
|
|
129
|
-
const segments = path.split('/')
|
|
130
|
-
return segments.some(hasEmbeddedContentId)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Check if a path appears to be a logical path (no embedded content IDs).
|
|
135
|
-
*
|
|
136
|
-
* @param path - The path to check
|
|
137
|
-
* @returns true if no segments contain embedded content IDs
|
|
138
|
-
*/
|
|
139
|
-
export function looksLikeLogicalPath(path: string): boolean {
|
|
140
|
-
return !looksLikePhysicalPath(path)
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Validate and cast a string to LogicalPath.
|
|
145
|
-
*
|
|
146
|
-
* Use this at API boundaries to validate incoming path strings
|
|
147
|
-
* and cast them to the branded LogicalPath type.
|
|
148
|
-
*
|
|
149
|
-
* @param path - The path string to validate
|
|
150
|
-
* @returns Object with success flag and either the typed path or an error
|
|
151
|
-
*
|
|
152
|
-
* @example
|
|
153
|
-
* ```ts
|
|
154
|
-
* const result = parseLogicalPath(params.collectionPath)
|
|
155
|
-
* if (!result.ok) {
|
|
156
|
-
* return { ok: false, status: 400, error: result.error }
|
|
157
|
-
* }
|
|
158
|
-
* const collectionPath: LogicalPath = result.path
|
|
159
|
-
* ```
|
|
160
|
-
*/
|
|
161
|
-
export function parseLogicalPath(
|
|
162
|
-
path: string,
|
|
163
|
-
): { ok: true; path: LogicalPath } | { ok: false; error: string } {
|
|
164
|
-
// Basic validation
|
|
165
|
-
if (!path || typeof path !== 'string') {
|
|
166
|
-
return { ok: false, error: 'Path is required' }
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Normalize backslashes to forward slashes (consistent with parsePermissionPath)
|
|
170
|
-
const normalized = path.replace(/\\/g, '/')
|
|
171
|
-
|
|
172
|
-
// Security check
|
|
173
|
-
if (hasTraversalSequence(normalized)) {
|
|
174
|
-
return { ok: false, error: 'Path contains traversal sequence' }
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Check it's not a physical path
|
|
178
|
-
if (looksLikePhysicalPath(normalized)) {
|
|
179
|
-
return {
|
|
180
|
-
ok: false,
|
|
181
|
-
error:
|
|
182
|
-
'Path appears to be a physical path (contains embedded content ID). Expected a logical path.',
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return { ok: true, path: normalized as LogicalPath }
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Validate and cast a string to PhysicalPath.
|
|
191
|
-
*
|
|
192
|
-
* Use this at API boundaries to validate incoming path strings
|
|
193
|
-
* and cast them to the branded PhysicalPath type.
|
|
194
|
-
*
|
|
195
|
-
* @param path - The path string to validate
|
|
196
|
-
* @returns Object with success flag and either the typed path or an error
|
|
197
|
-
*/
|
|
198
|
-
export function parsePhysicalPath(
|
|
199
|
-
path: string,
|
|
200
|
-
): { ok: true; path: PhysicalPath } | { ok: false; error: string } {
|
|
201
|
-
// Basic validation
|
|
202
|
-
if (!path || typeof path !== 'string') {
|
|
203
|
-
return { ok: false, error: 'Path is required' }
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Normalize backslashes to forward slashes (consistent with parseLogicalPath)
|
|
207
|
-
const normalized = path.replace(/\\/g, '/')
|
|
208
|
-
|
|
209
|
-
// Security check
|
|
210
|
-
if (hasTraversalSequence(normalized)) {
|
|
211
|
-
return { ok: false, error: 'Path contains traversal sequence' }
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Check it looks like a physical path
|
|
215
|
-
if (!looksLikePhysicalPath(normalized)) {
|
|
216
|
-
return {
|
|
217
|
-
ok: false,
|
|
218
|
-
error:
|
|
219
|
-
'Path appears to be a logical path (no embedded content ID). Expected a physical path.',
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
return { ok: true, path: normalized as PhysicalPath }
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Check if a string is a valid 12-character content ID.
|
|
228
|
-
*
|
|
229
|
-
* @param id - The string to check
|
|
230
|
-
* @returns true if valid Base58 12-char ID
|
|
231
|
-
*/
|
|
232
|
-
export function isValidContentId(id: string): boolean {
|
|
233
|
-
return CONTENT_ID_PATTERN.test(id)
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Parse and validate a ContentId from a string.
|
|
238
|
-
* Validates Base58 format and 12-character length.
|
|
239
|
-
*
|
|
240
|
-
* @param id - The string to validate
|
|
241
|
-
* @returns Object with success flag and either the typed ID or an error
|
|
242
|
-
*
|
|
243
|
-
* @example
|
|
244
|
-
* ```ts
|
|
245
|
-
* const result = parseContentId(fileId)
|
|
246
|
-
* if (!result.ok) {
|
|
247
|
-
* throw new Error(result.error)
|
|
248
|
-
* }
|
|
249
|
-
* const contentId: ContentId = result.id
|
|
250
|
-
* ```
|
|
251
|
-
*/
|
|
252
|
-
export function parseContentId(
|
|
253
|
-
id: string,
|
|
254
|
-
): { ok: true; id: ContentId } | { ok: false; error: string } {
|
|
255
|
-
if (!id || typeof id !== 'string') {
|
|
256
|
-
return { ok: false, error: 'Content ID is required' }
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (!isValidContentId(id)) {
|
|
260
|
-
return {
|
|
261
|
-
ok: false,
|
|
262
|
-
error: `Invalid content ID format (expected 12 Base58 characters, got: ${id})`,
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
return { ok: true, id: id as ContentId }
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Parse and validate a BranchName.
|
|
271
|
-
* Checks git branch naming rules.
|
|
272
|
-
*
|
|
273
|
-
* @param name - The branch name to validate
|
|
274
|
-
* @returns Object with success flag and either the typed name or an error
|
|
275
|
-
*
|
|
276
|
-
* @example
|
|
277
|
-
* ```ts
|
|
278
|
-
* const result = parseBranchName(params.branch)
|
|
279
|
-
* if (!result.ok) {
|
|
280
|
-
* return { ok: false, status: 400, error: result.error }
|
|
281
|
-
* }
|
|
282
|
-
* const branchName: BranchName = result.name
|
|
283
|
-
* ```
|
|
284
|
-
*/
|
|
285
|
-
export function parseBranchName(
|
|
286
|
-
name: string,
|
|
287
|
-
): { ok: true; name: BranchName } | { ok: false; error: string } {
|
|
288
|
-
if (!name || typeof name !== 'string') {
|
|
289
|
-
return { ok: false, error: 'Branch name is required' }
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Length limit (branch names become directory names)
|
|
293
|
-
if (name.length > 250) {
|
|
294
|
-
return { ok: false, error: 'Branch name too long (max 250 characters)' }
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Git branch name rules
|
|
298
|
-
if (name.includes('..')) {
|
|
299
|
-
return { ok: false, error: 'Branch name cannot contain ".."' }
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
if (name.startsWith('/') || name.endsWith('/') || name.includes('//')) {
|
|
303
|
-
return { ok: false, error: 'Invalid branch name format (invalid slashes)' }
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
if (name.includes(' ')) {
|
|
307
|
-
return { ok: false, error: 'Branch name cannot contain spaces' }
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Additional git restrictions
|
|
311
|
-
if (name.startsWith('.') || name.endsWith('.')) {
|
|
312
|
-
return { ok: false, error: 'Branch name cannot start or end with a dot' }
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (name.includes('@{')) {
|
|
316
|
-
return { ok: false, error: 'Branch name cannot contain "@{"' }
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// Git-forbidden characters: ~ ^ : ? * [ \ and control chars
|
|
320
|
-
// eslint-disable-next-line no-control-regex -- intentional: git forbids control characters in branch names
|
|
321
|
-
if (/[~^:?*[\\\x00-\x1f\x7f]/.test(name)) {
|
|
322
|
-
return { ok: false, error: 'Branch name contains invalid characters' }
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (name.endsWith('.lock')) {
|
|
326
|
-
return { ok: false, error: 'Branch name cannot end with ".lock"' }
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
return { ok: true, name: name as BranchName }
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* Parse and validate a slug (collection or entry).
|
|
334
|
-
* Validates format and length constraints.
|
|
335
|
-
*
|
|
336
|
-
* @param slug - The slug to validate
|
|
337
|
-
* @param type - Whether this is a collection or entry slug
|
|
338
|
-
* @returns Object with success flag and either the typed slug or an error
|
|
339
|
-
*
|
|
340
|
-
* @example
|
|
341
|
-
* ```ts
|
|
342
|
-
* const result = parseSlug(params.slug, 'entry')
|
|
343
|
-
* if (!result.ok) {
|
|
344
|
-
* return { ok: false, status: 400, error: result.error }
|
|
345
|
-
* }
|
|
346
|
-
* const entrySlug: EntrySlug = result.slug
|
|
347
|
-
* ```
|
|
348
|
-
*/
|
|
349
|
-
export function parseSlug(
|
|
350
|
-
slug: string,
|
|
351
|
-
type: 'collection' | 'entry',
|
|
352
|
-
): { ok: true; slug: CollectionSlug | EntrySlug } | { ok: false; error: string } {
|
|
353
|
-
if (!slug || typeof slug !== 'string') {
|
|
354
|
-
return {
|
|
355
|
-
ok: false,
|
|
356
|
-
error: `${type === 'collection' ? 'Collection' : 'Entry'} slug is required`,
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Check length (filesystem path safety)
|
|
361
|
-
if (slug.length > 64) {
|
|
362
|
-
return { ok: false, error: 'Slug too long (max 64 characters)' }
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// Check for path separators and traversal
|
|
366
|
-
if (slug.includes('/') || slug.includes('\\')) {
|
|
367
|
-
return {
|
|
368
|
-
ok: false,
|
|
369
|
-
error: 'Slug cannot contain path separators',
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (slug === '.' || slug === '..') {
|
|
374
|
-
return {
|
|
375
|
-
ok: false,
|
|
376
|
-
error: 'Slug cannot be a traversal sequence',
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// Validation from ContentStore.renameEntry
|
|
381
|
-
if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) {
|
|
382
|
-
return {
|
|
383
|
-
ok: false,
|
|
384
|
-
error:
|
|
385
|
-
'Slug must start with a letter or number and contain only lowercase letters, numbers, and hyphens',
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// Cast to appropriate branded type
|
|
390
|
-
return { ok: true, slug: slug as CollectionSlug | EntrySlug }
|
|
391
|
-
}
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises'
|
|
2
|
-
import os from 'node:os'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
|
|
5
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
6
|
-
|
|
7
|
-
import type { CanopyConfig } from './config'
|
|
8
|
-
import { flattenSchema } from './config'
|
|
9
|
-
import { ContentIdIndex } from './content-id-index'
|
|
10
|
-
import { ContentStore } from './content-store'
|
|
11
|
-
import { ReferenceResolver } from './reference-resolver'
|
|
12
|
-
import { unsafeAsLogicalPath } from './paths/test-utils'
|
|
13
|
-
|
|
14
|
-
describe('ReferenceResolver', () => {
|
|
15
|
-
let tempDir: string
|
|
16
|
-
let store: ContentStore
|
|
17
|
-
let idIndex: ContentIdIndex
|
|
18
|
-
let resolver: ReferenceResolver
|
|
19
|
-
|
|
20
|
-
beforeEach(async () => {
|
|
21
|
-
// Create temp directory with content structure
|
|
22
|
-
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'canopy-test-'))
|
|
23
|
-
await fs.mkdir(path.join(tempDir, 'content', 'authors'), {
|
|
24
|
-
recursive: true,
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
// Create test author files with embedded IDs: {type}.{slug}.{id}.{ext}
|
|
28
|
-
const aliceId = 'aXice123ABC4' // 12 chars, valid Base58
|
|
29
|
-
const bobId = 'bob456XYZ789' // 12 chars, valid Base58
|
|
30
|
-
|
|
31
|
-
await fs.writeFile(
|
|
32
|
-
path.join(tempDir, 'content', 'authors', `author.alice.${aliceId}.json`),
|
|
33
|
-
JSON.stringify({ slug: 'alice', name: 'Alice' }),
|
|
34
|
-
)
|
|
35
|
-
await fs.writeFile(
|
|
36
|
-
path.join(tempDir, 'content', 'authors', `author.bob.${bobId}.json`),
|
|
37
|
-
JSON.stringify({ slug: 'bob', name: 'Bob' }),
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
// Initialize store and index
|
|
41
|
-
const schema = {
|
|
42
|
-
collections: [
|
|
43
|
-
{
|
|
44
|
-
name: 'authors',
|
|
45
|
-
path: 'authors',
|
|
46
|
-
entries: [
|
|
47
|
-
{
|
|
48
|
-
name: 'author',
|
|
49
|
-
format: 'json' as const,
|
|
50
|
-
schema: [{ name: 'name', type: 'string' as const, label: 'Name' }],
|
|
51
|
-
},
|
|
52
|
-
],
|
|
53
|
-
},
|
|
54
|
-
],
|
|
55
|
-
} as const
|
|
56
|
-
|
|
57
|
-
const config: CanopyConfig = {
|
|
58
|
-
contentRoot: 'content',
|
|
59
|
-
gitBotAuthorName: 'Test Bot',
|
|
60
|
-
gitBotAuthorEmail: 'test@example.com',
|
|
61
|
-
mode: 'prod',
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
store = new ContentStore(tempDir, flattenSchema(schema, config.contentRoot))
|
|
65
|
-
idIndex = await store.idIndex()
|
|
66
|
-
resolver = new ReferenceResolver(store, idIndex)
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
afterEach(async () => {
|
|
70
|
-
// Clean up temp directory
|
|
71
|
-
await fs.rm(tempDir, { recursive: true, force: true })
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
describe('loadReferenceOptions', () => {
|
|
75
|
-
it('loads options when collection path does not include content/ prefix', async () => {
|
|
76
|
-
// REGRESSION TEST: ID index stores "content/authors" but schema specifies "authors"
|
|
77
|
-
// This ensures the collection path normalization works correctly
|
|
78
|
-
const options = await resolver.loadReferenceOptions([unsafeAsLogicalPath('authors')], 'name')
|
|
79
|
-
|
|
80
|
-
expect(options).toHaveLength(2)
|
|
81
|
-
const labels = options.map((o) => o.label).sort()
|
|
82
|
-
expect(labels).toEqual(['Alice', 'Bob'])
|
|
83
|
-
expect(options.every((o) => o.id && o.collection)).toBe(true)
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
it('loads options when collection path includes content/ prefix', async () => {
|
|
87
|
-
// REGRESSION TEST: Should also work if "content/authors" is explicitly specified
|
|
88
|
-
const options = await resolver.loadReferenceOptions(
|
|
89
|
-
[unsafeAsLogicalPath('content/authors')],
|
|
90
|
-
'name',
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
expect(options).toHaveLength(2)
|
|
94
|
-
const labels = options.map((o) => o.label).sort()
|
|
95
|
-
expect(labels).toEqual(['Alice', 'Bob'])
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
it('returns empty array for non-existent collection', async () => {
|
|
99
|
-
const options = await resolver.loadReferenceOptions(
|
|
100
|
-
[unsafeAsLogicalPath('nonexistent')],
|
|
101
|
-
'name',
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
expect(options).toHaveLength(0)
|
|
105
|
-
})
|
|
106
|
-
})
|
|
107
|
-
})
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import path from 'node:path'
|
|
2
|
-
|
|
3
|
-
import type { ContentStore } from './content-store'
|
|
4
|
-
import type { ContentIdIndex } from './content-id-index'
|
|
5
|
-
import { extractSlugFromFilename } from './content-id-index'
|
|
6
|
-
import type { LogicalPath, PhysicalPath, EntrySlug } from './paths'
|
|
7
|
-
|
|
8
|
-
export interface ResolvedReference {
|
|
9
|
-
id: string
|
|
10
|
-
exists: boolean
|
|
11
|
-
displayValue: string
|
|
12
|
-
collection?: LogicalPath
|
|
13
|
-
slug?: EntrySlug
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface ReferenceOption {
|
|
17
|
-
id: string
|
|
18
|
-
label: string
|
|
19
|
-
collection: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* ReferenceResolver resolves content IDs to display values for reference fields.
|
|
24
|
-
*
|
|
25
|
-
* This class provides utilities for:
|
|
26
|
-
* - Resolving a single ID to its display value (e.g., title)
|
|
27
|
-
* - Loading all available options for a reference field
|
|
28
|
-
* - Filtering options by collection constraints
|
|
29
|
-
* - Searching options by display value
|
|
30
|
-
*/
|
|
31
|
-
export class ReferenceResolver {
|
|
32
|
-
constructor(
|
|
33
|
-
private store: ContentStore,
|
|
34
|
-
private idIndex: ContentIdIndex,
|
|
35
|
-
) {}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Resolve a content ID to a display value.
|
|
39
|
-
* Returns null if the ID doesn't exist or points to a collection.
|
|
40
|
-
*
|
|
41
|
-
* @param id - The content ID to resolve
|
|
42
|
-
* @param displayField - The field to use for display value (default: 'title')
|
|
43
|
-
*/
|
|
44
|
-
async resolve(id: string, displayField = 'title'): Promise<ResolvedReference | null> {
|
|
45
|
-
const location = this.idIndex.findById(id)
|
|
46
|
-
|
|
47
|
-
if (!location || location.type !== 'entry') {
|
|
48
|
-
return {
|
|
49
|
-
id,
|
|
50
|
-
exists: false,
|
|
51
|
-
displayValue: id, // Fallback to showing the ID itself
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
try {
|
|
56
|
-
const doc = await this.store.read(location.collection!, location.slug!)
|
|
57
|
-
const displayValue = String(doc.data[displayField] || doc.data.title || location.slug)
|
|
58
|
-
|
|
59
|
-
return {
|
|
60
|
-
id,
|
|
61
|
-
exists: true,
|
|
62
|
-
displayValue,
|
|
63
|
-
collection: location.collection,
|
|
64
|
-
slug: location.slug,
|
|
65
|
-
}
|
|
66
|
-
} catch (error) {
|
|
67
|
-
console.error('Failed to resolve reference:', { id, error })
|
|
68
|
-
return {
|
|
69
|
-
id,
|
|
70
|
-
exists: false,
|
|
71
|
-
displayValue: id,
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Load all available reference options for a reference field.
|
|
78
|
-
*
|
|
79
|
-
* This method scans the specified collections and returns options
|
|
80
|
-
* suitable for a dropdown/select field.
|
|
81
|
-
*
|
|
82
|
-
* @param collections - Collection paths to search (e.g., ['posts', 'docs'])
|
|
83
|
-
* @param displayField - Field to use for option labels (default: 'title')
|
|
84
|
-
* @param search - Optional search string to filter options
|
|
85
|
-
*/
|
|
86
|
-
async loadReferenceOptions(
|
|
87
|
-
collections: LogicalPath[],
|
|
88
|
-
displayField = 'title',
|
|
89
|
-
search?: string,
|
|
90
|
-
): Promise<ReferenceOption[]> {
|
|
91
|
-
const options: ReferenceOption[] = []
|
|
92
|
-
|
|
93
|
-
for (const collectionPath of collections) {
|
|
94
|
-
// Get all entries in this collection
|
|
95
|
-
const entries = await this.listEntriesInCollection(collectionPath)
|
|
96
|
-
|
|
97
|
-
for (const entry of entries) {
|
|
98
|
-
const id = this.idIndex.findByPath(entry.relativePath)
|
|
99
|
-
if (!id) continue
|
|
100
|
-
|
|
101
|
-
try {
|
|
102
|
-
// The slug from the index may include the entry type prefix for new-format files
|
|
103
|
-
// (e.g., "author.alice" instead of just "alice"). We need to strip the type prefix
|
|
104
|
-
// before passing to store.read() to avoid double-prefixing.
|
|
105
|
-
// Use extractSlugFromFilename to properly extract just the slug part.
|
|
106
|
-
const filename = path.basename(entry.relativePath)
|
|
107
|
-
const normalizedSlug = extractSlugFromFilename(filename)
|
|
108
|
-
|
|
109
|
-
const doc = await this.store.read(entry.collection, normalizedSlug as EntrySlug)
|
|
110
|
-
const label = String(doc.data[displayField] || doc.data.title || normalizedSlug)
|
|
111
|
-
|
|
112
|
-
// Apply search filter if provided
|
|
113
|
-
if (search && !label.toLowerCase().includes(search.toLowerCase())) {
|
|
114
|
-
continue
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
options.push({
|
|
118
|
-
id,
|
|
119
|
-
label,
|
|
120
|
-
collection: entry.collection,
|
|
121
|
-
})
|
|
122
|
-
} catch (error) {
|
|
123
|
-
// Skip entries that can't be read
|
|
124
|
-
console.error('Failed to read entry for reference options:', {
|
|
125
|
-
collection: entry.collection,
|
|
126
|
-
slug: entry.slug,
|
|
127
|
-
error,
|
|
128
|
-
})
|
|
129
|
-
continue
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return options.sort((a, b) => a.label.localeCompare(b.label))
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Helper to list all entries in a collection.
|
|
139
|
-
*/
|
|
140
|
-
private async listEntriesInCollection(collectionPath: LogicalPath): Promise<
|
|
141
|
-
Array<{
|
|
142
|
-
relativePath: PhysicalPath
|
|
143
|
-
collection: LogicalPath
|
|
144
|
-
slug: EntrySlug
|
|
145
|
-
}>
|
|
146
|
-
> {
|
|
147
|
-
return this.store.listCollectionEntries(collectionPath)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Resolve multiple IDs at once.
|
|
152
|
-
* Useful for displaying lists of referenced items.
|
|
153
|
-
*/
|
|
154
|
-
async resolveMany(ids: string[], displayField = 'title'): Promise<(ResolvedReference | null)[]> {
|
|
155
|
-
return Promise.all(ids.map((id) => this.resolve(id, displayField)))
|
|
156
|
-
}
|
|
157
|
-
}
|
package/src/schema/index.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Schema Module
|
|
3
|
-
*
|
|
4
|
-
* This module provides schema loading and resolution for CanopyCMS.
|
|
5
|
-
* Schema structure is defined in .collection.json files (single source of truth)
|
|
6
|
-
* while field schemas are defined in an entry schema registry for reusability.
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* ```ts
|
|
10
|
-
* import { resolveSchema } from 'canopycms/schema'
|
|
11
|
-
*
|
|
12
|
-
* const { schema, sources } = await resolveSchema(contentRoot, entrySchemaRegistry)
|
|
13
|
-
* ```
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
// Types
|
|
17
|
-
export type { EntrySchemaRegistry, SchemaSourceInfo, SchemaResolutionResult } from './types'
|
|
18
|
-
|
|
19
|
-
// Meta loader (low-level API)
|
|
20
|
-
export {
|
|
21
|
-
loadCollectionMetaFiles,
|
|
22
|
-
resolveCollectionReferences,
|
|
23
|
-
watchCollectionMetaFiles,
|
|
24
|
-
type CollectionMeta,
|
|
25
|
-
type RootCollectionMeta,
|
|
26
|
-
} from './meta-loader'
|
|
27
|
-
|
|
28
|
-
// Resolver (high-level API)
|
|
29
|
-
export { resolveSchema, hasSchemaFiles, isValidSchema } from './resolver'
|