canopycms 0.0.0 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/plugin.d.ts +8 -0
- package/dist/auth/plugin.d.ts.map +1 -1
- package/dist/build-mode.d.ts +15 -5
- package/dist/build-mode.d.ts.map +1 -1
- package/dist/build-mode.js +18 -8
- package/dist/build-mode.js.map +1 -1
- package/dist/cli/init.d.ts +2 -2
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +37 -36
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/template-files/ai-config.ts.template +21 -0
- package/dist/cli/template-files/ai-route.ts.template +10 -0
- package/dist/cli/template-files/canopy.ts.template +24 -0
- package/dist/cli/templates.d.ts +5 -1
- package/dist/cli/templates.d.ts.map +1 -1
- package/dist/cli/templates.js +9 -2
- package/dist/cli/templates.js.map +1 -1
- package/dist/config/schemas/config.d.ts +4 -0
- package/dist/config/schemas/config.d.ts.map +1 -1
- package/dist/config/schemas/config.js +2 -0
- package/dist/config/schemas/config.js.map +1 -1
- package/dist/config/types.d.ts +5 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/content-reader.js +2 -2
- package/dist/content-reader.js.map +1 -1
- package/dist/context.js +5 -5
- package/dist/context.js.map +1 -1
- package/dist/operating-mode/client-unsafe-strategy.d.ts.map +1 -1
- package/dist/operating-mode/client-unsafe-strategy.js +15 -18
- package/dist/operating-mode/client-unsafe-strategy.js.map +1 -1
- package/dist/operating-mode/types.d.ts +8 -0
- package/dist/operating-mode/types.d.ts.map +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -0
- package/dist/server.js.map +1 -1
- package/package.json +5 -4
- package/src/cli/init.ts +43 -38
- package/dist/__integration__/fixtures/content-seeds.d.ts +0 -43
- package/dist/__integration__/fixtures/content-seeds.d.ts.map +0 -1
- package/dist/__integration__/fixtures/content-seeds.js +0 -99
- package/dist/__integration__/fixtures/content-seeds.js.map +0 -1
- package/dist/__integration__/fixtures/schemas.d.ts +0 -12
- package/dist/__integration__/fixtures/schemas.d.ts.map +0 -1
- package/dist/__integration__/fixtures/schemas.js +0 -65
- package/dist/__integration__/fixtures/schemas.js.map +0 -1
- package/dist/__integration__/test-utils/api-client.d.ts +0 -123
- package/dist/__integration__/test-utils/api-client.d.ts.map +0 -1
- package/dist/__integration__/test-utils/api-client.js +0 -118
- package/dist/__integration__/test-utils/api-client.js.map +0 -1
- package/dist/__integration__/test-utils/multi-user.d.ts +0 -25
- package/dist/__integration__/test-utils/multi-user.d.ts.map +0 -1
- package/dist/__integration__/test-utils/multi-user.js +0 -105
- package/dist/__integration__/test-utils/multi-user.js.map +0 -1
- package/dist/__integration__/test-utils/test-workspace.d.ts +0 -25
- package/dist/__integration__/test-utils/test-workspace.d.ts.map +0 -1
- package/dist/__integration__/test-utils/test-workspace.js +0 -102
- package/dist/__integration__/test-utils/test-workspace.js.map +0 -1
- package/dist/editor/BranchManager.stories.d.ts +0 -8
- package/dist/editor/BranchManager.stories.d.ts.map +0 -1
- package/dist/editor/BranchManager.stories.js +0 -74
- package/dist/editor/BranchManager.stories.js.map +0 -1
- package/dist/editor/CanopyEditor.stories.d.ts +0 -7
- package/dist/editor/CanopyEditor.stories.d.ts.map +0 -1
- package/dist/editor/CanopyEditor.stories.js +0 -99
- package/dist/editor/CanopyEditor.stories.js.map +0 -1
- package/dist/editor/CommentsPanel.stories.d.ts +0 -10
- package/dist/editor/CommentsPanel.stories.d.ts.map +0 -1
- package/dist/editor/CommentsPanel.stories.js +0 -175
- package/dist/editor/CommentsPanel.stories.js.map +0 -1
- package/dist/editor/Editor.stories.d.ts +0 -7
- package/dist/editor/Editor.stories.d.ts.map +0 -1
- package/dist/editor/Editor.stories.js +0 -95
- package/dist/editor/Editor.stories.js.map +0 -1
- package/dist/editor/EditorPanes.stories.d.ts +0 -7
- package/dist/editor/EditorPanes.stories.d.ts.map +0 -1
- package/dist/editor/EditorPanes.stories.js +0 -116
- package/dist/editor/EditorPanes.stories.js.map +0 -1
- package/dist/editor/EntryNavigator.stories.d.ts +0 -8
- package/dist/editor/EntryNavigator.stories.d.ts.map +0 -1
- package/dist/editor/EntryNavigator.stories.js +0 -42
- package/dist/editor/EntryNavigator.stories.js.map +0 -1
- package/dist/editor/FormRenderer.stories.d.ts +0 -7
- package/dist/editor/FormRenderer.stories.d.ts.map +0 -1
- package/dist/editor/FormRenderer.stories.js +0 -115
- package/dist/editor/FormRenderer.stories.js.map +0 -1
- package/dist/editor/GroupManager.stories.d.ts +0 -19
- package/dist/editor/GroupManager.stories.d.ts.map +0 -1
- package/dist/editor/GroupManager.stories.js +0 -265
- package/dist/editor/GroupManager.stories.js.map +0 -1
- package/dist/editor/PermissionManager.stories.d.ts +0 -20
- package/dist/editor/PermissionManager.stories.d.ts.map +0 -1
- package/dist/editor/PermissionManager.stories.js +0 -506
- package/dist/editor/PermissionManager.stories.js.map +0 -1
- package/dist/editor/comments/FieldWrapper.stories.d.ts +0 -10
- package/dist/editor/comments/FieldWrapper.stories.d.ts.map +0 -1
- package/dist/editor/comments/FieldWrapper.stories.js +0 -173
- package/dist/editor/comments/FieldWrapper.stories.js.map +0 -1
- package/dist/editor/fields/BlockField.stories.d.ts +0 -7
- package/dist/editor/fields/BlockField.stories.d.ts.map +0 -1
- package/dist/editor/fields/BlockField.stories.js +0 -50
- package/dist/editor/fields/BlockField.stories.js.map +0 -1
- package/dist/editor/fields/fields.stories.d.ts +0 -8
- package/dist/editor/fields/fields.stories.d.ts.map +0 -1
- package/dist/editor/fields/fields.stories.js +0 -34
- package/dist/editor/fields/fields.stories.js.map +0 -1
- package/dist/test-utils/api-test-helpers.d.ts +0 -238
- package/dist/test-utils/api-test-helpers.d.ts.map +0 -1
- package/dist/test-utils/api-test-helpers.js +0 -347
- package/dist/test-utils/api-test-helpers.js.map +0 -1
- package/dist/test-utils/console-spy.d.ts +0 -56
- package/dist/test-utils/console-spy.d.ts.map +0 -1
- package/dist/test-utils/console-spy.js +0 -81
- package/dist/test-utils/console-spy.js.map +0 -1
- package/dist/test-utils/git-helpers.d.ts +0 -21
- package/dist/test-utils/git-helpers.d.ts.map +0 -1
- package/dist/test-utils/git-helpers.js +0 -23
- package/dist/test-utils/git-helpers.js.map +0 -1
- package/dist/test-utils/index.d.ts +0 -5
- package/dist/test-utils/index.d.ts.map +0 -1
- package/dist/test-utils/index.js +0 -4
- package/dist/test-utils/index.js.map +0 -1
- package/src/__integration__/errors/invalid-content.test.ts +0 -238
- package/src/__integration__/errors/permission-denied.test.ts +0 -220
- package/src/__integration__/fixtures/content-seeds.ts +0 -105
- package/src/__integration__/fixtures/schemas.ts +0 -67
- package/src/__integration__/initialization/prod-sim-init.test.ts +0 -139
- package/src/__integration__/permissions/path-permissions.test.ts +0 -314
- package/src/__integration__/permissions/role-permissions.test.ts +0 -354
- package/src/__integration__/permissions/settings-branch-isolation.test.ts +0 -317
- package/src/__integration__/settings/groups-api.test.ts +0 -403
- package/src/__integration__/test-utils/api-client.ts +0 -167
- package/src/__integration__/test-utils/multi-user.ts +0 -129
- package/src/__integration__/test-utils/test-workspace.ts +0 -130
- package/src/__integration__/user/user-context.test.ts +0 -174
- package/src/__integration__/validation/input-validation.test.ts +0 -166
- package/src/__integration__/workflows/api-editing-workflow.test.ts +0 -244
- package/src/__integration__/workflows/conflict-resolution.test.ts +0 -259
- package/src/__integration__/workflows/editing-workflow.test.ts +0 -205
- package/src/__integration__/workflows/review-workflow.test.ts +0 -260
- package/src/ai/__tests__/build.integration.test.ts +0 -224
- package/src/ai/__tests__/generate.integration.test.ts +0 -495
- package/src/ai/__tests__/handler.integration.test.ts +0 -212
- package/src/ai/__tests__/json-to-markdown.test.ts +0 -553
- package/src/ai/generate.ts +0 -410
- package/src/ai/handler.ts +0 -123
- package/src/ai/index.ts +0 -26
- package/src/ai/json-to-markdown.ts +0 -424
- package/src/ai/resolve-branch.ts +0 -34
- package/src/ai/types.ts +0 -160
- package/src/api/AGENTS.md +0 -81
- package/src/api/__test__/mock-client.ts +0 -404
- package/src/api/assets.test.ts +0 -140
- package/src/api/assets.ts +0 -154
- package/src/api/branch-merge.test.ts +0 -163
- package/src/api/branch-merge.ts +0 -113
- package/src/api/branch-review.test.ts +0 -297
- package/src/api/branch-review.ts +0 -136
- package/src/api/branch-status.test.ts +0 -85
- package/src/api/branch-status.ts +0 -153
- package/src/api/branch-withdraw.test.ts +0 -146
- package/src/api/branch-withdraw.ts +0 -81
- package/src/api/branch-workflow.integration.test.ts +0 -578
- package/src/api/branch.test.ts +0 -620
- package/src/api/branch.ts +0 -492
- package/src/api/client.test.ts +0 -349
- package/src/api/client.ts +0 -506
- package/src/api/comments.test.ts +0 -285
- package/src/api/comments.ts +0 -210
- package/src/api/content.test.ts +0 -345
- package/src/api/content.ts +0 -454
- package/src/api/entries.test.ts +0 -1339
- package/src/api/entries.ts +0 -650
- package/src/api/github-sync.ts +0 -144
- package/src/api/groups.test.ts +0 -1013
- package/src/api/groups.ts +0 -375
- package/src/api/guards.test.ts +0 -533
- package/src/api/guards.ts +0 -271
- package/src/api/index.ts +0 -87
- package/src/api/permissions.test.ts +0 -766
- package/src/api/permissions.ts +0 -334
- package/src/api/reference-options.ts +0 -118
- package/src/api/resolve-references.ts +0 -107
- package/src/api/route-builder.ts +0 -289
- package/src/api/schema.test.ts +0 -840
- package/src/api/schema.ts +0 -936
- package/src/api/security.test.ts +0 -233
- package/src/api/settings-helpers.ts +0 -84
- package/src/api/types.ts +0 -40
- package/src/api/user.test.ts +0 -127
- package/src/api/user.ts +0 -42
- package/src/api/validators.test.ts +0 -275
- package/src/api/validators.ts +0 -176
- package/src/asset-store.test.ts +0 -37
- package/src/asset-store.ts +0 -110
- package/src/auth/cache.ts +0 -7
- package/src/auth/caching-auth-plugin.test.ts +0 -154
- package/src/auth/caching-auth-plugin.ts +0 -109
- package/src/auth/context-helpers.ts +0 -75
- package/src/auth/file-based-auth-cache.test.ts +0 -257
- package/src/auth/file-based-auth-cache.ts +0 -279
- package/src/auth/index.ts +0 -12
- package/src/auth/plugin.ts +0 -51
- package/src/auth/types.ts +0 -38
- package/src/authorization/__tests__/branch.test.ts +0 -260
- package/src/authorization/__tests__/content.test.ts +0 -142
- package/src/authorization/__tests__/path.test.ts +0 -133
- package/src/authorization/__tests__/permissions-loader.test.ts +0 -200
- package/src/authorization/branch.ts +0 -94
- package/src/authorization/content.ts +0 -93
- package/src/authorization/groups/index.ts +0 -11
- package/src/authorization/groups/loader.ts +0 -127
- package/src/authorization/groups/schema.ts +0 -48
- package/src/authorization/helpers.ts +0 -48
- package/src/authorization/index.ts +0 -84
- package/src/authorization/path.ts +0 -112
- package/src/authorization/permissions/index.ts +0 -11
- package/src/authorization/permissions/loader.ts +0 -116
- package/src/authorization/permissions/schema.ts +0 -66
- package/src/authorization/test-utils.ts +0 -15
- package/src/authorization/types.ts +0 -66
- package/src/authorization/validation.test.ts +0 -100
- package/src/authorization/validation.ts +0 -62
- package/src/branch-metadata.test.ts +0 -168
- package/src/branch-metadata.ts +0 -166
- package/src/branch-registry.test.ts +0 -248
- package/src/branch-registry.ts +0 -152
- package/src/branch-schema-cache.test.ts +0 -275
- package/src/branch-schema-cache.ts +0 -189
- package/src/branch-workspace.test.ts +0 -183
- package/src/branch-workspace.ts +0 -124
- package/src/build/generate-ai-content.ts +0 -78
- package/src/build/index.ts +0 -8
- package/src/build-mode.ts +0 -27
- package/src/cli/generate-ai-content.ts +0 -100
- package/src/cli/init.test.ts +0 -240
- package/src/cli/templates/canopy.ts.template +0 -55
- package/src/cli/templates.ts +0 -47
- package/src/client.ts +0 -12
- package/src/comment-store.test.ts +0 -442
- package/src/comment-store.ts +0 -301
- package/src/config/__tests__/config.test.ts +0 -513
- package/src/config/flatten.ts +0 -174
- package/src/config/helpers.ts +0 -167
- package/src/config/index.ts +0 -86
- package/src/config/schemas/collection.ts +0 -67
- package/src/config/schemas/config.ts +0 -77
- package/src/config/schemas/field.ts +0 -108
- package/src/config/schemas/media.ts +0 -27
- package/src/config/schemas/permissions.ts +0 -21
- package/src/config/types.ts +0 -321
- package/src/config/validation.ts +0 -70
- package/src/config-test.ts +0 -65
- package/src/config.ts +0 -11
- package/src/content-id-index.test.ts +0 -512
- package/src/content-id-index.ts +0 -479
- package/src/content-reader.test.ts +0 -478
- package/src/content-reader.ts +0 -214
- package/src/content-store.test.ts +0 -1126
- package/src/content-store.ts +0 -793
- package/src/context.ts +0 -111
- package/src/editor/BranchManager.stories.tsx +0 -80
- package/src/editor/BranchManager.test.tsx +0 -324
- package/src/editor/BranchManager.tsx +0 -461
- package/src/editor/CanopyEditor.stories.tsx +0 -128
- package/src/editor/CanopyEditor.test.tsx +0 -81
- package/src/editor/CanopyEditor.tsx +0 -73
- package/src/editor/CanopyEditorPage.test.tsx +0 -59
- package/src/editor/CanopyEditorPage.tsx +0 -25
- package/src/editor/CommentsPanel.stories.tsx +0 -184
- package/src/editor/CommentsPanel.tsx +0 -338
- package/src/editor/Editor.integration.test.tsx +0 -227
- package/src/editor/Editor.stories.tsx +0 -119
- package/src/editor/Editor.tsx +0 -1221
- package/src/editor/EditorPanes.stories.tsx +0 -256
- package/src/editor/EditorPanes.test.tsx +0 -77
- package/src/editor/EditorPanes.tsx +0 -180
- package/src/editor/EntryNavigator.stories.tsx +0 -65
- package/src/editor/EntryNavigator.test.tsx +0 -598
- package/src/editor/EntryNavigator.tsx +0 -665
- package/src/editor/FormRenderer.stories.tsx +0 -212
- package/src/editor/FormRenderer.test.tsx +0 -194
- package/src/editor/FormRenderer.tsx +0 -432
- package/src/editor/GroupManager.stories.tsx +0 -301
- package/src/editor/GroupManager.test.tsx +0 -682
- package/src/editor/GroupManager.tsx +0 -9
- package/src/editor/PermissionManager.stories.tsx +0 -539
- package/src/editor/PermissionManager.test.tsx +0 -864
- package/src/editor/PermissionManager.tsx +0 -12
- package/src/editor/canopy-path.test.ts +0 -23
- package/src/editor/canopy-path.ts +0 -52
- package/src/editor/client-reference-resolver.ts +0 -118
- package/src/editor/comments/BranchComments.tsx +0 -93
- package/src/editor/comments/EntryComments.tsx +0 -94
- package/src/editor/comments/FieldWrapper.stories.tsx +0 -210
- package/src/editor/comments/FieldWrapper.tsx +0 -129
- package/src/editor/comments/InlineCommentThread.test.tsx +0 -384
- package/src/editor/comments/InlineCommentThread.tsx +0 -246
- package/src/editor/comments/ThreadCarousel.test.tsx +0 -393
- package/src/editor/comments/ThreadCarousel.tsx +0 -525
- package/src/editor/components/ConfirmDeleteModal.tsx +0 -49
- package/src/editor/components/EditorContext.tsx +0 -49
- package/src/editor/components/EditorFooter.tsx +0 -47
- package/src/editor/components/EditorHeader.tsx +0 -492
- package/src/editor/components/EditorSidebar.tsx +0 -193
- package/src/editor/components/EntryCreateModal.tsx +0 -193
- package/src/editor/components/RenameEntryModal.tsx +0 -152
- package/src/editor/components/UserBadge.test.tsx +0 -274
- package/src/editor/components/UserBadge.tsx +0 -240
- package/src/editor/components/index.ts +0 -6
- package/src/editor/context/ApiClientContext.tsx +0 -56
- package/src/editor/context/EditorStateContext.tsx +0 -221
- package/src/editor/context/index.ts +0 -40
- package/src/editor/editor-config.test.ts +0 -385
- package/src/editor/editor-config.ts +0 -94
- package/src/editor/editor-utils.test.ts +0 -772
- package/src/editor/editor-utils.ts +0 -303
- package/src/editor/env.ts +0 -4
- package/src/editor/fields/BlockField.stories.tsx +0 -79
- package/src/editor/fields/BlockField.tsx +0 -267
- package/src/editor/fields/CodeField.tsx +0 -41
- package/src/editor/fields/MarkdownField.tsx +0 -205
- package/src/editor/fields/ObjectField.tsx +0 -71
- package/src/editor/fields/ReferenceField.tsx +0 -138
- package/src/editor/fields/SelectField.tsx +0 -76
- package/src/editor/fields/TextField.tsx +0 -35
- package/src/editor/fields/ToggleField.tsx +0 -37
- package/src/editor/fields/fields.stories.tsx +0 -40
- package/src/editor/group-manager/ExternalGroupsTab.tsx +0 -114
- package/src/editor/group-manager/GroupCard.tsx +0 -102
- package/src/editor/group-manager/GroupForm.tsx +0 -66
- package/src/editor/group-manager/InternalGroupsTab.tsx +0 -147
- package/src/editor/group-manager/MemberList.tsx +0 -184
- package/src/editor/group-manager/hooks/useExternalGroupSearch.ts +0 -63
- package/src/editor/group-manager/hooks/useGroupState.ts +0 -134
- package/src/editor/group-manager/hooks/useUserSearch.ts +0 -84
- package/src/editor/group-manager/index.tsx +0 -210
- package/src/editor/group-manager/types.ts +0 -28
- package/src/editor/hooks/README.md +0 -26
- package/src/editor/hooks/__test__/test-utils.tsx +0 -183
- package/src/editor/hooks/index.ts +0 -23
- package/src/editor/hooks/useBranchActions.test.tsx +0 -267
- package/src/editor/hooks/useBranchActions.tsx +0 -121
- package/src/editor/hooks/useBranchManager.test.tsx +0 -391
- package/src/editor/hooks/useBranchManager.tsx +0 -326
- package/src/editor/hooks/useCommentSystem.test.ts +0 -615
- package/src/editor/hooks/useCommentSystem.ts +0 -347
- package/src/editor/hooks/useDraftManager.test.ts +0 -375
- package/src/editor/hooks/useDraftManager.ts +0 -259
- package/src/editor/hooks/useEditorLayout.test.ts +0 -147
- package/src/editor/hooks/useEditorLayout.ts +0 -67
- package/src/editor/hooks/useEntryManager.test.ts +0 -588
- package/src/editor/hooks/useEntryManager.ts +0 -387
- package/src/editor/hooks/useGroupManager.test.ts +0 -277
- package/src/editor/hooks/useGroupManager.ts +0 -139
- package/src/editor/hooks/usePermissionManager.test.ts +0 -211
- package/src/editor/hooks/usePermissionManager.ts +0 -113
- package/src/editor/hooks/useReferenceResolution.ts +0 -248
- package/src/editor/hooks/useSchemaManager.test.ts +0 -370
- package/src/editor/hooks/useSchemaManager.ts +0 -310
- package/src/editor/hooks/useUserContext.tsx +0 -57
- package/src/editor/hooks/useUserMetadata.test.ts +0 -191
- package/src/editor/hooks/useUserMetadata.ts +0 -71
- package/src/editor/permission-manager/GroupSelector.tsx +0 -73
- package/src/editor/permission-manager/PermissionEditor.tsx +0 -321
- package/src/editor/permission-manager/PermissionLevelBadge.tsx +0 -53
- package/src/editor/permission-manager/PermissionTree.tsx +0 -237
- package/src/editor/permission-manager/UserSelector.tsx +0 -95
- package/src/editor/permission-manager/constants.tsx +0 -18
- package/src/editor/permission-manager/hooks/useGroupsAndUsers.ts +0 -153
- package/src/editor/permission-manager/hooks/usePermissionTree.ts +0 -200
- package/src/editor/permission-manager/index.tsx +0 -294
- package/src/editor/permission-manager/types.ts +0 -58
- package/src/editor/permission-manager/utils.ts +0 -179
- package/src/editor/preview-bridge.test.tsx +0 -50
- package/src/editor/preview-bridge.tsx +0 -294
- package/src/editor/schema-editor/CollectionEditor.test.tsx +0 -238
- package/src/editor/schema-editor/CollectionEditor.tsx +0 -520
- package/src/editor/schema-editor/EntryTypeEditor.test.tsx +0 -215
- package/src/editor/schema-editor/EntryTypeEditor.tsx +0 -367
- package/src/editor/schema-editor/index.ts +0 -19
- package/src/editor/setup-test-dom.ts +0 -10
- package/src/editor/test-setup.ts +0 -33
- package/src/editor/theme.tsx +0 -119
- package/src/editor/utils/env.ts +0 -39
- package/src/entry-schema-registry.test.ts +0 -281
- package/src/entry-schema-registry.ts +0 -121
- package/src/entry-schema.ts +0 -84
- package/src/git-manager.test.ts +0 -552
- package/src/git-manager.ts +0 -667
- package/src/github-service.test.ts +0 -312
- package/src/github-service.ts +0 -295
- package/src/http/handler.test.ts +0 -275
- package/src/http/handler.ts +0 -280
- package/src/http/index.ts +0 -11
- package/src/http/router.ts +0 -164
- package/src/http/types.ts +0 -44
- package/src/id.test.ts +0 -48
- package/src/id.ts +0 -22
- package/src/index.ts +0 -26
- package/src/operating-mode/__tests__/strategies.test.ts +0 -511
- package/src/operating-mode/client-safe-strategy.ts +0 -184
- package/src/operating-mode/client-unsafe-strategy.ts +0 -303
- package/src/operating-mode/client.ts +0 -13
- package/src/operating-mode/index.ts +0 -34
- package/src/operating-mode/types.ts +0 -186
- package/src/paths/__tests__/branch.test.ts +0 -53
- package/src/paths/__tests__/normalize.test.ts +0 -141
- package/src/paths/__tests__/resolve.test.ts +0 -207
- package/src/paths/__tests__/validation.test.ts +0 -61
- package/src/paths/branch.ts +0 -115
- package/src/paths/index.ts +0 -73
- package/src/paths/normalize-server.ts +0 -40
- package/src/paths/normalize.ts +0 -107
- package/src/paths/resolve.ts +0 -61
- package/src/paths/test-utils.ts +0 -37
- package/src/paths/types.ts +0 -68
- package/src/paths/validation.test.ts +0 -480
- package/src/paths/validation.ts +0 -391
- package/src/reference-resolver.test.ts +0 -107
- package/src/reference-resolver.ts +0 -157
- package/src/schema/index.ts +0 -29
- package/src/schema/meta-loader.ts +0 -366
- package/src/schema/resolver.ts +0 -83
- package/src/schema/schema-store-types.ts +0 -56
- package/src/schema/schema-store.test.ts +0 -816
- package/src/schema/schema-store.ts +0 -795
- package/src/schema/types.ts +0 -33
- package/src/schema-meta-loader.test.ts +0 -447
- package/src/server.ts +0 -15
- package/src/services.test.ts +0 -559
- package/src/services.ts +0 -373
- package/src/settings-branch-utils.ts +0 -53
- package/src/settings-workspace.ts +0 -156
- package/src/task-queue/README.md +0 -144
- package/src/task-queue/index.ts +0 -14
- package/src/task-queue/task-queue.test.ts +0 -524
- package/src/task-queue/task-queue.ts +0 -514
- package/src/task-queue/types.ts +0 -41
- package/src/test-utils/api-test-helpers.ts +0 -445
- package/src/test-utils/console-spy.test.ts +0 -14
- package/src/test-utils/console-spy.ts +0 -125
- package/src/test-utils/git-helpers.ts +0 -31
- package/src/test-utils/index.ts +0 -4
- package/src/types.ts +0 -54
- package/src/user.ts +0 -118
- package/src/utils/debug.test.ts +0 -114
- package/src/utils/debug.ts +0 -127
- package/src/utils/error.test.ts +0 -92
- package/src/utils/error.ts +0 -83
- package/src/utils/format.ts +0 -12
- package/src/validation/__tests__/field-traversal.test.ts +0 -263
- package/src/validation/deletion-checker.ts +0 -234
- package/src/validation/field-traversal.ts +0 -146
- package/src/validation/reference-validator.ts +0 -168
- package/src/worker/cms-worker-rebase.test.ts +0 -473
- package/src/worker/cms-worker.ts +0 -777
- package/src/worker/integration.test.ts +0 -289
- package/src/worker/task-queue-config.ts +0 -25
- package/src/worker/task-queue.test.ts +0 -452
- package/src/worker/task-queue.ts +0 -58
- /package/{src/cli/templates → dist/cli/template-files}/Dockerfile.cms.template +0 -0
- /package/{src/cli/templates → dist/cli/template-files}/canopycms.config.ts.template +0 -0
- /package/{src/cli/templates → dist/cli/template-files}/deploy-cms.yml.template +0 -0
- /package/{src/cli/templates → dist/cli/template-files}/edit-page.tsx.template +0 -0
- /package/{src/cli/templates → dist/cli/template-files}/route.ts.template +0 -0
- /package/{src/cli/templates → dist/cli/template-files}/schemas.ts.template +0 -0
package/src/api/entries.ts
DELETED
|
@@ -1,650 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises'
|
|
2
|
-
import type { Dirent } from 'node:fs'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import { z } from 'zod'
|
|
5
|
-
|
|
6
|
-
import matter from 'gray-matter'
|
|
7
|
-
|
|
8
|
-
import type { ContentFormat, FlatSchemaItem, EntryTypeConfig } from '../config'
|
|
9
|
-
import { ContentStore, ContentStoreError } from '../content-store'
|
|
10
|
-
import type { ApiContext, ApiRequest, ApiResponse } from './types'
|
|
11
|
-
import type { BranchContextWithSchema } from '../types'
|
|
12
|
-
import { defineEndpoint } from './route-builder'
|
|
13
|
-
import { getFormatExtension } from '../utils/format'
|
|
14
|
-
import { resolveCollectionPath } from '../content-id-index'
|
|
15
|
-
import {
|
|
16
|
-
validateAndNormalizePath,
|
|
17
|
-
normalizeFilesystemPath,
|
|
18
|
-
parseSlug,
|
|
19
|
-
parseLogicalPath,
|
|
20
|
-
} from '../paths'
|
|
21
|
-
import { isNotFoundError } from '../utils/error'
|
|
22
|
-
import { isValidId } from '../id'
|
|
23
|
-
import type { LogicalPath, PhysicalPath, EntrySlug, ContentId } from '../paths/types'
|
|
24
|
-
import { branchNameSchema, logicalPathSchema } from './validators'
|
|
25
|
-
import { SchemaOps } from '../schema/schema-store'
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Summary of an entry type for client display.
|
|
29
|
-
* Simplified from EntryTypeConfig - doesn't include full field definitions.
|
|
30
|
-
*/
|
|
31
|
-
export interface EntryTypeSummary {
|
|
32
|
-
name: string
|
|
33
|
-
label?: string
|
|
34
|
-
format: ContentFormat
|
|
35
|
-
default?: boolean
|
|
36
|
-
maxItems?: number
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface CollectionItem {
|
|
40
|
-
logicalPath: LogicalPath
|
|
41
|
-
contentId: ContentId // 12-char content ID
|
|
42
|
-
slug: EntrySlug
|
|
43
|
-
collectionPath: LogicalPath
|
|
44
|
-
collectionName: string
|
|
45
|
-
format: ContentFormat
|
|
46
|
-
entryType: string // The entry type name (from typed entries)
|
|
47
|
-
physicalPath: PhysicalPath
|
|
48
|
-
title?: string
|
|
49
|
-
updatedAt?: string
|
|
50
|
-
exists?: boolean
|
|
51
|
-
canEdit?: boolean
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface ListEntriesParams {
|
|
55
|
-
branch: string
|
|
56
|
-
collection?: LogicalPath
|
|
57
|
-
limit?: number
|
|
58
|
-
cursor?: string
|
|
59
|
-
q?: string
|
|
60
|
-
recursive?: boolean
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export interface ListEntriesResponse {
|
|
64
|
-
entries: CollectionItem[]
|
|
65
|
-
pagination: {
|
|
66
|
-
cursor?: string
|
|
67
|
-
hasMore: boolean
|
|
68
|
-
limit: number
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** Response type for listing entries */
|
|
73
|
-
export type EntriesResponse = ApiResponse<ListEntriesResponse>
|
|
74
|
-
|
|
75
|
-
// ============================================================================
|
|
76
|
-
// Zod Schemas for Validation
|
|
77
|
-
// ============================================================================
|
|
78
|
-
|
|
79
|
-
const listEntriesParamsSchema = z.object({
|
|
80
|
-
branch: branchNameSchema,
|
|
81
|
-
collection: logicalPathSchema.optional(),
|
|
82
|
-
limit: z.number().optional(),
|
|
83
|
-
cursor: z.string().optional(),
|
|
84
|
-
q: z.string().optional(),
|
|
85
|
-
recursive: z.boolean().optional(),
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Validate and normalize a path relative to root.
|
|
90
|
-
* Throws ContentStoreError on traversal attempt.
|
|
91
|
-
*/
|
|
92
|
-
const normalizePath = (root: string, target: string): string => {
|
|
93
|
-
const result = validateAndNormalizePath(root, target)
|
|
94
|
-
if (!result.valid) {
|
|
95
|
-
throw new ContentStoreError(result.error || 'Path traversal detected')
|
|
96
|
-
}
|
|
97
|
-
return result.normalizedPath!
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const readTitle = async (filePath: string, format: ContentFormat): Promise<string | undefined> => {
|
|
101
|
-
try {
|
|
102
|
-
const raw = await fs.readFile(filePath, 'utf8')
|
|
103
|
-
if (format === 'json') {
|
|
104
|
-
const parsed = JSON.parse(raw) as Record<string, unknown>
|
|
105
|
-
const title = parsed.title ?? parsed.name
|
|
106
|
-
return typeof title === 'string' ? title : undefined
|
|
107
|
-
}
|
|
108
|
-
const parsed = matter(raw)
|
|
109
|
-
const frontmatterTitle =
|
|
110
|
-
(parsed.data as Record<string, unknown>)?.title ??
|
|
111
|
-
(parsed.data as Record<string, unknown>)?.name
|
|
112
|
-
return typeof frontmatterTitle === 'string' ? frontmatterTitle : undefined
|
|
113
|
-
} catch {
|
|
114
|
-
return undefined
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Sort entries by the collection's order array.
|
|
120
|
-
* Items in the order array come first (in order), items not in the array come at the end alphabetically by slug.
|
|
121
|
-
* @param entries - The entries to sort
|
|
122
|
-
* @param order - The order array (embedded IDs)
|
|
123
|
-
* @returns Sorted entries
|
|
124
|
-
*/
|
|
125
|
-
const sortEntriesByOrder = (
|
|
126
|
-
entries: CollectionItem[],
|
|
127
|
-
order?: readonly string[],
|
|
128
|
-
): CollectionItem[] => {
|
|
129
|
-
if (!order || order.length === 0) {
|
|
130
|
-
// No order defined, sort alphabetically by slug
|
|
131
|
-
return entries.sort((a, b) => a.slug.localeCompare(b.slug))
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Create a map of contentId to order index
|
|
135
|
-
const orderMap = new Map<string, number>()
|
|
136
|
-
order.forEach((id, index) => orderMap.set(id, index))
|
|
137
|
-
|
|
138
|
-
return entries.sort((a, b) => {
|
|
139
|
-
const aIndex = orderMap.get(a.contentId)
|
|
140
|
-
const bIndex = orderMap.get(b.contentId)
|
|
141
|
-
|
|
142
|
-
// Both in order array: sort by order index
|
|
143
|
-
if (aIndex !== undefined && bIndex !== undefined) {
|
|
144
|
-
return aIndex - bIndex
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Only a is in order: a comes first
|
|
148
|
-
if (aIndex !== undefined) return -1
|
|
149
|
-
|
|
150
|
-
// Only b is in order: b comes first
|
|
151
|
-
if (bIndex !== undefined) return 1
|
|
152
|
-
|
|
153
|
-
// Neither in order: sort alphabetically by slug
|
|
154
|
-
return a.slug.localeCompare(b.slug)
|
|
155
|
-
})
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Parse a filename: {type}.{slug}.{id}.{ext}
|
|
160
|
-
* Returns { type, slug, id } or null if the filename doesn't match the pattern.
|
|
161
|
-
*/
|
|
162
|
-
const parseTypedFilename = (
|
|
163
|
-
filename: string,
|
|
164
|
-
entryTypes: readonly EntryTypeConfig[],
|
|
165
|
-
): { type: string; slug: EntrySlug; id: ContentId } | null => {
|
|
166
|
-
// Remove extension
|
|
167
|
-
const lastDot = filename.lastIndexOf('.')
|
|
168
|
-
if (lastDot === -1) return null
|
|
169
|
-
const nameWithoutExt = filename.slice(0, lastDot)
|
|
170
|
-
|
|
171
|
-
// Parse: {type}.{slug}.{id}
|
|
172
|
-
const parts = nameWithoutExt.split('.')
|
|
173
|
-
if (parts.length >= 3) {
|
|
174
|
-
// Check if first part matches a known entry type
|
|
175
|
-
const potentialType = parts[0]
|
|
176
|
-
const matchingType = entryTypes.find((e) => e.name === potentialType)
|
|
177
|
-
if (matchingType) {
|
|
178
|
-
const id = parts[parts.length - 1]
|
|
179
|
-
if (!isValidId(id)) return null
|
|
180
|
-
const slug = parts.slice(1, -1).join('.')
|
|
181
|
-
return {
|
|
182
|
-
type: potentialType,
|
|
183
|
-
slug: slug as EntrySlug,
|
|
184
|
-
id: id as ContentId,
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return null
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const listCollectionEntries = async (
|
|
193
|
-
root: string,
|
|
194
|
-
collection: FlatSchemaItem,
|
|
195
|
-
): Promise<CollectionItem[]> => {
|
|
196
|
-
// Only collections with entries can be listed
|
|
197
|
-
if (collection.type !== 'collection' || !collection.entries) {
|
|
198
|
-
return []
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const entryTypes = collection.entries as readonly EntryTypeConfig[]
|
|
202
|
-
|
|
203
|
-
// Build a map of extension to entry types for efficient lookup
|
|
204
|
-
const extToTypes = new Map<string, EntryTypeConfig[]>()
|
|
205
|
-
for (const entryType of entryTypes) {
|
|
206
|
-
const ext = getFormatExtension(entryType.format)
|
|
207
|
-
const existing = extToTypes.get(ext) || []
|
|
208
|
-
existing.push(entryType)
|
|
209
|
-
extToTypes.set(ext, existing)
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Get all valid extensions for this collection
|
|
213
|
-
const validExts = Array.from(extToTypes.keys())
|
|
214
|
-
|
|
215
|
-
// Resolve the full collection path with embedded IDs
|
|
216
|
-
// e.g., "content/docs/api" → "content/docs.bChqT78gcaLd/api.meiuwxTSo7UN"
|
|
217
|
-
const collectionRoot = await resolveCollectionPath(root, collection.logicalPath)
|
|
218
|
-
|
|
219
|
-
if (!collectionRoot) {
|
|
220
|
-
// Collection directory doesn't exist yet
|
|
221
|
-
return []
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
normalizePath(root, collectionRoot)
|
|
225
|
-
let dirents: Dirent[]
|
|
226
|
-
try {
|
|
227
|
-
dirents = await fs.readdir(collectionRoot, { withFileTypes: true })
|
|
228
|
-
} catch (err: unknown) {
|
|
229
|
-
if (isNotFoundError(err)) return []
|
|
230
|
-
throw err
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Filter to files with valid extensions
|
|
234
|
-
const files = dirents
|
|
235
|
-
.filter(
|
|
236
|
-
(d) =>
|
|
237
|
-
d.isFile() &&
|
|
238
|
-
validExts.some((ext) => d.name.endsWith(ext)) &&
|
|
239
|
-
d.name !== '.collection.json',
|
|
240
|
-
)
|
|
241
|
-
.sort((a, b) => a.name.localeCompare(b.name))
|
|
242
|
-
|
|
243
|
-
// Parallelize file stats and title reads for better performance
|
|
244
|
-
const entries = await Promise.all(
|
|
245
|
-
files.map(async (file) => {
|
|
246
|
-
const absolutePath = path.join(collectionRoot, file.name)
|
|
247
|
-
const relativePath = normalizePath(root, absolutePath)
|
|
248
|
-
|
|
249
|
-
// Parse the filename to extract type, slug, and id
|
|
250
|
-
const parsed = parseTypedFilename(file.name, entryTypes)
|
|
251
|
-
if (!parsed) {
|
|
252
|
-
console.warn(
|
|
253
|
-
`Skipping file with unrecognized filename format: ${file.name} (expected {type}.{slug}.{id}.{ext} with a known entry type and valid 12-char Base58 ID)`,
|
|
254
|
-
)
|
|
255
|
-
return null
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const { type: entryTypeName, slug, id: contentId } = parsed
|
|
259
|
-
|
|
260
|
-
// Determine the entry type and format
|
|
261
|
-
// Type is always in filename now
|
|
262
|
-
const entryType = entryTypes.find((e) => e.name === entryTypeName)
|
|
263
|
-
const format: ContentFormat = entryType?.format || 'json'
|
|
264
|
-
|
|
265
|
-
const [stats, title] = await Promise.all([
|
|
266
|
-
fs.stat(absolutePath),
|
|
267
|
-
readTitle(absolutePath, format),
|
|
268
|
-
])
|
|
269
|
-
|
|
270
|
-
const item: CollectionItem = {
|
|
271
|
-
// Safe: both collection.logicalPath (LogicalPath) and slug (EntrySlug) are branded
|
|
272
|
-
logicalPath: `${collection.logicalPath}/${slug}` as LogicalPath,
|
|
273
|
-
contentId, // 12-char content ID extracted from filename
|
|
274
|
-
slug,
|
|
275
|
-
collectionPath: collection.logicalPath,
|
|
276
|
-
collectionName: collection.name,
|
|
277
|
-
format,
|
|
278
|
-
entryType: entryTypeName || 'default',
|
|
279
|
-
physicalPath: relativePath as PhysicalPath,
|
|
280
|
-
title: title ?? entryType?.label, // Fall back to entry type label if no title in content
|
|
281
|
-
updatedAt: stats.mtime.toISOString(),
|
|
282
|
-
exists: true,
|
|
283
|
-
}
|
|
284
|
-
return item
|
|
285
|
-
}),
|
|
286
|
-
)
|
|
287
|
-
|
|
288
|
-
// Filter out nulls from failed parses
|
|
289
|
-
return entries.filter((e): e is CollectionItem => e !== null)
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* List entries from a collection and all its nested child collections
|
|
294
|
-
*/
|
|
295
|
-
const listCollectionEntriesRecursive = async (
|
|
296
|
-
root: string,
|
|
297
|
-
targetPath: string,
|
|
298
|
-
flatCollections: FlatSchemaItem[],
|
|
299
|
-
): Promise<CollectionItem[]> => {
|
|
300
|
-
// Find all collections that are descendants of the target path
|
|
301
|
-
const descendants = flatCollections.filter((item) => {
|
|
302
|
-
return item.logicalPath === targetPath || item.logicalPath.startsWith(`${targetPath}/`)
|
|
303
|
-
})
|
|
304
|
-
|
|
305
|
-
// Parallelize listing entries from all descendant collections
|
|
306
|
-
const collectionsWithEntries = descendants.filter(
|
|
307
|
-
(item) => item.type === 'collection' && item.entries,
|
|
308
|
-
)
|
|
309
|
-
const results = await Promise.all(
|
|
310
|
-
collectionsWithEntries.map((item) => listCollectionEntries(root, item)),
|
|
311
|
-
)
|
|
312
|
-
|
|
313
|
-
return results.flat()
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const listEntriesHandler = async (
|
|
317
|
-
gc: { branchContext: BranchContextWithSchema },
|
|
318
|
-
ctx: ApiContext,
|
|
319
|
-
req: ApiRequest,
|
|
320
|
-
params: z.infer<typeof listEntriesParamsSchema>,
|
|
321
|
-
): Promise<EntriesResponse> => {
|
|
322
|
-
if (!params.branch) {
|
|
323
|
-
return { ok: false, status: 400, error: 'branch is required' }
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const { branchContext } = gc
|
|
327
|
-
const root = branchContext.branchRoot
|
|
328
|
-
const flatSchema = branchContext.flatSchema
|
|
329
|
-
const flatCollections = flatSchema
|
|
330
|
-
|
|
331
|
-
const targetPath = params.collection ? normalizeFilesystemPath(params.collection) : undefined
|
|
332
|
-
let targetCollections = flatCollections
|
|
333
|
-
|
|
334
|
-
if (targetPath) {
|
|
335
|
-
const match = flatCollections.find((c) => c.logicalPath === targetPath)
|
|
336
|
-
if (!match) {
|
|
337
|
-
return { ok: false, status: 404, error: 'Collection not found' }
|
|
338
|
-
}
|
|
339
|
-
targetCollections = [match]
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const maxLimit = 200
|
|
343
|
-
const limit = Math.min(Math.max(params.limit ?? 50, 1), maxLimit)
|
|
344
|
-
const offset = Number.isFinite(Number(params.cursor)) ? Number(params.cursor) : 0
|
|
345
|
-
const search = params.q?.toLowerCase()
|
|
346
|
-
const recursive = params.recursive ?? false
|
|
347
|
-
|
|
348
|
-
const entries: CollectionItem[] = []
|
|
349
|
-
|
|
350
|
-
if (recursive && targetPath) {
|
|
351
|
-
// Recursive mode: list entries from target collection and all its children
|
|
352
|
-
try {
|
|
353
|
-
const items = await listCollectionEntriesRecursive(root, targetPath, flatCollections)
|
|
354
|
-
// For recursive mode, we can't easily apply per-collection ordering
|
|
355
|
-
// Sort alphabetically for now (ordering is collection-specific)
|
|
356
|
-
items.sort((a, b) => a.slug.localeCompare(b.slug))
|
|
357
|
-
for (const item of items) {
|
|
358
|
-
// Use the physicalPath for access control
|
|
359
|
-
const readAccess = await ctx.services.checkContentAccess(
|
|
360
|
-
branchContext,
|
|
361
|
-
root,
|
|
362
|
-
item.physicalPath,
|
|
363
|
-
req.user,
|
|
364
|
-
'read',
|
|
365
|
-
)
|
|
366
|
-
if (!readAccess.allowed) continue
|
|
367
|
-
const editAccess = await ctx.services.checkContentAccess(
|
|
368
|
-
branchContext,
|
|
369
|
-
root,
|
|
370
|
-
item.physicalPath,
|
|
371
|
-
req.user,
|
|
372
|
-
'edit',
|
|
373
|
-
)
|
|
374
|
-
if (search) {
|
|
375
|
-
const haystack =
|
|
376
|
-
`${item.slug} ${item.title ?? ''} ${item.collectionName ?? ''}`.toLowerCase()
|
|
377
|
-
if (!haystack.includes(search)) {
|
|
378
|
-
continue
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
entries.push({ ...item, canEdit: editAccess.allowed })
|
|
382
|
-
}
|
|
383
|
-
} catch (err) {
|
|
384
|
-
if (err instanceof ContentStoreError) {
|
|
385
|
-
return { ok: false, status: 400, error: err.message }
|
|
386
|
-
}
|
|
387
|
-
throw err
|
|
388
|
-
}
|
|
389
|
-
} else {
|
|
390
|
-
// Non-recursive mode: list entries from target collections
|
|
391
|
-
for (const item of targetCollections) {
|
|
392
|
-
// Only process collections (skip entry-types as they are metadata, not navigable nodes)
|
|
393
|
-
if (item.type !== 'collection') continue
|
|
394
|
-
|
|
395
|
-
try {
|
|
396
|
-
const items = await listCollectionEntries(root, item)
|
|
397
|
-
// Sort by collection's order array (items in order first, then alphabetically)
|
|
398
|
-
sortEntriesByOrder(items, item.order)
|
|
399
|
-
for (const entry of items) {
|
|
400
|
-
// Use the physicalPath for access control
|
|
401
|
-
const readAccess = await ctx.services.checkContentAccess(
|
|
402
|
-
branchContext,
|
|
403
|
-
root,
|
|
404
|
-
entry.physicalPath,
|
|
405
|
-
req.user,
|
|
406
|
-
'read',
|
|
407
|
-
)
|
|
408
|
-
if (!readAccess.allowed) continue
|
|
409
|
-
const editAccess = await ctx.services.checkContentAccess(
|
|
410
|
-
branchContext,
|
|
411
|
-
root,
|
|
412
|
-
entry.physicalPath,
|
|
413
|
-
req.user,
|
|
414
|
-
'edit',
|
|
415
|
-
)
|
|
416
|
-
if (search) {
|
|
417
|
-
const haystack =
|
|
418
|
-
`${entry.slug} ${entry.title ?? ''} ${entry.collectionName ?? ''}`.toLowerCase()
|
|
419
|
-
if (!haystack.includes(search)) {
|
|
420
|
-
continue
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
entries.push({ ...entry, canEdit: editAccess.allowed })
|
|
424
|
-
}
|
|
425
|
-
} catch (err) {
|
|
426
|
-
if (err instanceof ContentStoreError) {
|
|
427
|
-
return { ok: false, status: 400, error: err.message }
|
|
428
|
-
}
|
|
429
|
-
throw err
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
const paged = entries.slice(offset, offset + limit)
|
|
435
|
-
const nextCursor = offset + limit < entries.length ? String(offset + limit) : undefined
|
|
436
|
-
|
|
437
|
-
return {
|
|
438
|
-
ok: true,
|
|
439
|
-
status: 200,
|
|
440
|
-
data: {
|
|
441
|
-
entries: paged,
|
|
442
|
-
pagination: {
|
|
443
|
-
cursor: nextCursor,
|
|
444
|
-
hasMore: Boolean(nextCursor),
|
|
445
|
-
limit,
|
|
446
|
-
},
|
|
447
|
-
},
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// ============================================================================
|
|
452
|
-
// Route Definitions with defineEndpoint
|
|
453
|
-
// ============================================================================
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* List entries for a branch
|
|
457
|
-
* GET /:branch/entries
|
|
458
|
-
*/
|
|
459
|
-
export const listEntries = defineEndpoint({
|
|
460
|
-
namespace: 'entries',
|
|
461
|
-
name: 'list',
|
|
462
|
-
method: 'GET',
|
|
463
|
-
path: '/:branch/entries',
|
|
464
|
-
params: listEntriesParamsSchema,
|
|
465
|
-
responseType: 'EntriesResponse',
|
|
466
|
-
response: {} as EntriesResponse,
|
|
467
|
-
defaultMockData: {
|
|
468
|
-
collections: [],
|
|
469
|
-
entries: [],
|
|
470
|
-
pagination: {
|
|
471
|
-
hasMore: false,
|
|
472
|
-
limit: 50,
|
|
473
|
-
},
|
|
474
|
-
},
|
|
475
|
-
guards: ['schema'] as const,
|
|
476
|
-
handler: listEntriesHandler,
|
|
477
|
-
})
|
|
478
|
-
|
|
479
|
-
// ============================================================================
|
|
480
|
-
// Delete Entry
|
|
481
|
-
// ============================================================================
|
|
482
|
-
|
|
483
|
-
/** Response type for deleting an entry */
|
|
484
|
-
export type DeleteEntryResponse = ApiResponse<{
|
|
485
|
-
deleted: boolean
|
|
486
|
-
contentId?: string
|
|
487
|
-
}>
|
|
488
|
-
|
|
489
|
-
const deleteEntryParamsSchema = z.object({
|
|
490
|
-
branch: branchNameSchema,
|
|
491
|
-
entryPath: logicalPathSchema, // Format: collectionPath/slug
|
|
492
|
-
})
|
|
493
|
-
|
|
494
|
-
/**
|
|
495
|
-
* Delete an entry and update the collection's order array.
|
|
496
|
-
* DELETE /:branch/entries/...entryPath
|
|
497
|
-
* Note: Uses catch-all to support paths with slashes (e.g., content/posts/hello-world)
|
|
498
|
-
*/
|
|
499
|
-
const deleteEntryHandler = async (
|
|
500
|
-
gc: { branchContext: BranchContextWithSchema },
|
|
501
|
-
ctx: ApiContext,
|
|
502
|
-
req: ApiRequest,
|
|
503
|
-
params: z.infer<typeof deleteEntryParamsSchema>,
|
|
504
|
-
): Promise<DeleteEntryResponse> => {
|
|
505
|
-
const { branchContext } = gc
|
|
506
|
-
|
|
507
|
-
// Parse entryPath to get collection and slug
|
|
508
|
-
// Format: collectionPath/slug (e.g., "posts/hello-world" or "docs/api/getting-started")
|
|
509
|
-
// Re-validate after decoding to prevent double-encoding path traversal attacks
|
|
510
|
-
const decoded = decodeURIComponent(params.entryPath)
|
|
511
|
-
const entryPathResult = parseLogicalPath(decoded)
|
|
512
|
-
if (!entryPathResult.ok) {
|
|
513
|
-
return {
|
|
514
|
-
ok: false,
|
|
515
|
-
status: 400,
|
|
516
|
-
error: `Invalid entry path: ${entryPathResult.error}`,
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
const entryPath = entryPathResult.path
|
|
520
|
-
|
|
521
|
-
const lastSlash = entryPath.lastIndexOf('/')
|
|
522
|
-
if (lastSlash === -1) {
|
|
523
|
-
return {
|
|
524
|
-
ok: false,
|
|
525
|
-
status: 400,
|
|
526
|
-
error: 'Invalid entry path format. Expected: collectionPath/slug',
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
const collectionPath = entryPath.slice(0, lastSlash)
|
|
531
|
-
const slug = entryPath.slice(lastSlash + 1)
|
|
532
|
-
|
|
533
|
-
if (!collectionPath || !slug) {
|
|
534
|
-
return {
|
|
535
|
-
ok: false,
|
|
536
|
-
status: 400,
|
|
537
|
-
error: 'Invalid entry path format. Expected: collectionPath/slug',
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
const flatSchema = branchContext.flatSchema
|
|
542
|
-
|
|
543
|
-
// Check edit permission on the entry
|
|
544
|
-
// Build the physical path for permission check
|
|
545
|
-
const collection = flatSchema.find(
|
|
546
|
-
(item) => item.type === 'collection' && item.logicalPath === collectionPath,
|
|
547
|
-
)
|
|
548
|
-
if (!collection) {
|
|
549
|
-
return { ok: false, status: 404, error: 'Collection not found' }
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
const contentStore = new ContentStore(branchContext.branchRoot, flatSchema)
|
|
553
|
-
const collectionLogicalPath = collectionPath as LogicalPath
|
|
554
|
-
// Validate slug extracted from the path
|
|
555
|
-
const slugResult = parseSlug(slug, 'entry')
|
|
556
|
-
if (!slugResult.ok) {
|
|
557
|
-
return {
|
|
558
|
-
ok: false,
|
|
559
|
-
status: 400,
|
|
560
|
-
error: `Invalid entry slug: ${slugResult.error}`,
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
const entrySlug = slugResult.slug as EntrySlug
|
|
564
|
-
|
|
565
|
-
// Resolve the real physical path before checking permissions
|
|
566
|
-
let physicalPath: PhysicalPath
|
|
567
|
-
try {
|
|
568
|
-
const resolved = await contentStore.resolveDocumentPath(collectionLogicalPath, entrySlug)
|
|
569
|
-
physicalPath = resolved.relativePath
|
|
570
|
-
} catch (err) {
|
|
571
|
-
if (isNotFoundError(err)) {
|
|
572
|
-
return { ok: false, status: 404, error: 'Entry not found' }
|
|
573
|
-
}
|
|
574
|
-
return { ok: false, status: 400, error: 'Invalid entry path' }
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// Check edit access using the real physical path
|
|
578
|
-
const editAccess = await ctx.services.checkContentAccess(
|
|
579
|
-
branchContext,
|
|
580
|
-
branchContext.branchRoot,
|
|
581
|
-
physicalPath,
|
|
582
|
-
req.user,
|
|
583
|
-
'edit',
|
|
584
|
-
)
|
|
585
|
-
if (!editAccess.allowed) {
|
|
586
|
-
return {
|
|
587
|
-
ok: false,
|
|
588
|
-
status: 403,
|
|
589
|
-
error: 'Edit permission required to delete entry',
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
try {
|
|
594
|
-
// Get the entry's content ID before deleting (for order update)
|
|
595
|
-
const contentId = await contentStore.getIdForEntry(collectionLogicalPath, entrySlug)
|
|
596
|
-
|
|
597
|
-
// Delete the entry
|
|
598
|
-
await contentStore.delete(collectionLogicalPath, entrySlug)
|
|
599
|
-
|
|
600
|
-
// Update the collection's order array to remove the deleted item
|
|
601
|
-
if (contentId && collection.type === 'collection' && collection.order) {
|
|
602
|
-
const schemaStore = new SchemaOps(branchContext.branchRoot, ctx.services.entrySchemaRegistry)
|
|
603
|
-
const newOrder = collection.order.filter((id) => id !== contentId)
|
|
604
|
-
if (newOrder.length !== collection.order.length) {
|
|
605
|
-
await schemaStore.updateOrder(collectionPath as LogicalPath, newOrder as string[])
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
return {
|
|
610
|
-
ok: true,
|
|
611
|
-
status: 200,
|
|
612
|
-
data: { deleted: true, contentId: contentId || undefined },
|
|
613
|
-
}
|
|
614
|
-
} catch (err) {
|
|
615
|
-
if (isNotFoundError(err)) {
|
|
616
|
-
return { ok: false, status: 404, error: 'Entry not found' }
|
|
617
|
-
}
|
|
618
|
-
return {
|
|
619
|
-
ok: false,
|
|
620
|
-
status: 500,
|
|
621
|
-
error: err instanceof Error ? err.message : 'Failed to delete entry',
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
/**
|
|
627
|
-
* Delete an entry
|
|
628
|
-
* DELETE /:branch/entries/...entryPath
|
|
629
|
-
* Note: Uses catch-all to support paths with slashes (e.g., content/posts/hello-world)
|
|
630
|
-
*/
|
|
631
|
-
export const deleteEntry = defineEndpoint({
|
|
632
|
-
namespace: 'entries',
|
|
633
|
-
name: 'delete',
|
|
634
|
-
method: 'DELETE',
|
|
635
|
-
path: '/:branch/entries/...entryPath',
|
|
636
|
-
params: deleteEntryParamsSchema,
|
|
637
|
-
responseType: 'DeleteEntryResponse',
|
|
638
|
-
response: {} as DeleteEntryResponse,
|
|
639
|
-
defaultMockData: { deleted: true },
|
|
640
|
-
guards: ['schema'] as const,
|
|
641
|
-
handler: deleteEntryHandler,
|
|
642
|
-
})
|
|
643
|
-
|
|
644
|
-
/**
|
|
645
|
-
* Exported routes for router registration
|
|
646
|
-
*/
|
|
647
|
-
export const ENTRY_ROUTES = {
|
|
648
|
-
list: listEntries,
|
|
649
|
-
delete: deleteEntry,
|
|
650
|
-
} as const
|