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/content-store.ts
DELETED
|
@@ -1,793 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
|
|
4
|
-
import matter from 'gray-matter'
|
|
5
|
-
|
|
6
|
-
import type {
|
|
7
|
-
BlockFieldConfig,
|
|
8
|
-
ContentFormat,
|
|
9
|
-
EntrySchema,
|
|
10
|
-
FlatSchemaItem,
|
|
11
|
-
EntryTypeConfig,
|
|
12
|
-
ObjectFieldConfig,
|
|
13
|
-
} from './config'
|
|
14
|
-
import {
|
|
15
|
-
ContentIdIndex,
|
|
16
|
-
extractIdFromFilename,
|
|
17
|
-
extractSlugFromFilename,
|
|
18
|
-
extractEntryTypeFromFilename,
|
|
19
|
-
resolveCollectionPath,
|
|
20
|
-
} from './content-id-index'
|
|
21
|
-
import { generateId } from './id'
|
|
22
|
-
import { getFormatExtension } from './utils/format'
|
|
23
|
-
import {
|
|
24
|
-
normalizeFilesystemPath,
|
|
25
|
-
type LogicalPath,
|
|
26
|
-
type PhysicalPath,
|
|
27
|
-
type EntrySlug,
|
|
28
|
-
type ContentId,
|
|
29
|
-
} from './paths'
|
|
30
|
-
|
|
31
|
-
export type MarkdownDocument = {
|
|
32
|
-
format: 'md' | 'mdx'
|
|
33
|
-
data: Record<string, unknown>
|
|
34
|
-
body: string
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export type JsonDocument = {
|
|
38
|
-
format: 'json'
|
|
39
|
-
data: Record<string, unknown>
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export type ContentDocument = (MarkdownDocument | JsonDocument) & {
|
|
43
|
-
collection: LogicalPath
|
|
44
|
-
collectionName: string
|
|
45
|
-
relativePath: PhysicalPath
|
|
46
|
-
absolutePath: string
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export type WriteInput =
|
|
50
|
-
| { format: 'md' | 'mdx'; data?: Record<string, unknown>; body: string }
|
|
51
|
-
| { format: 'json'; data: Record<string, unknown> }
|
|
52
|
-
|
|
53
|
-
export class ContentStoreError extends Error {}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Get the default entry type from a collection's entries array.
|
|
57
|
-
* Returns the entry marked as default, or the first one, or undefined if no entries.
|
|
58
|
-
*/
|
|
59
|
-
function getDefaultEntryType(
|
|
60
|
-
entries: readonly EntryTypeConfig[] | undefined,
|
|
61
|
-
): EntryTypeConfig | undefined {
|
|
62
|
-
if (!entries || entries.length === 0) return undefined
|
|
63
|
-
return entries.find((e) => e.default) || entries[0]
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Validates that a slug doesn't contain slashes or backslashes.
|
|
68
|
-
* Slugs must be simple filenames (last path segment only).
|
|
69
|
-
*/
|
|
70
|
-
function validateSlug(slug: string): void {
|
|
71
|
-
if (slug.includes('/')) {
|
|
72
|
-
throw new ContentStoreError(
|
|
73
|
-
'Slugs cannot contain forward slashes. Use nested collections instead.',
|
|
74
|
-
)
|
|
75
|
-
}
|
|
76
|
-
if (slug.includes('\\')) {
|
|
77
|
-
throw new ContentStoreError('Slugs cannot contain backslashes. Use nested collections instead.')
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export class ContentStore {
|
|
82
|
-
private readonly root: string
|
|
83
|
-
private readonly schemaIndex: Map<string, FlatSchemaItem>
|
|
84
|
-
private readonly _idIndex: ContentIdIndex
|
|
85
|
-
private indexLoaded: boolean = false
|
|
86
|
-
|
|
87
|
-
constructor(root: string, flatSchema: FlatSchemaItem[]) {
|
|
88
|
-
this.root = path.resolve(root)
|
|
89
|
-
this.schemaIndex = new Map(flatSchema.map((item) => [item.logicalPath, item]))
|
|
90
|
-
this._idIndex = new ContentIdIndex(this.root)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Get the ID index, ensuring it's loaded first.
|
|
95
|
-
* This getter automatically loads the index on first access.
|
|
96
|
-
*/
|
|
97
|
-
public async idIndex(): Promise<ContentIdIndex> {
|
|
98
|
-
if (!this.indexLoaded) {
|
|
99
|
-
await this._idIndex.buildFromFilenames('content')
|
|
100
|
-
this.indexLoaded = true
|
|
101
|
-
}
|
|
102
|
-
return this._idIndex
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Get all schema items for iteration.
|
|
107
|
-
* Used internally by ReferenceResolver for path matching.
|
|
108
|
-
*/
|
|
109
|
-
public getSchemaItems(): IterableIterator<FlatSchemaItem> {
|
|
110
|
-
return this.schemaIndex.values()
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
private assertSchemaItem(path: LogicalPath): FlatSchemaItem {
|
|
114
|
-
const normalized = normalizeFilesystemPath(path)
|
|
115
|
-
const item = this.schemaIndex.get(normalized)
|
|
116
|
-
if (!item) {
|
|
117
|
-
throw new ContentStoreError(`Unknown schema item: ${path}`)
|
|
118
|
-
}
|
|
119
|
-
return item
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
private assertCollection(collectionPath: LogicalPath): FlatSchemaItem & { type: 'collection' } {
|
|
123
|
-
const item = this.assertSchemaItem(collectionPath)
|
|
124
|
-
if (item.type !== 'collection') {
|
|
125
|
-
throw new ContentStoreError(`Path is not a collection: ${collectionPath}`)
|
|
126
|
-
}
|
|
127
|
-
return item
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Build absolute and relative paths with security validation.
|
|
132
|
-
* All entries use the unified filename pattern: {type}.{slug}.{id}.{ext}
|
|
133
|
-
*
|
|
134
|
-
* SECURITY BOUNDARY: This method prevents path traversal attacks by:
|
|
135
|
-
* 1. Validating that resolved paths stay within the content root
|
|
136
|
-
* 2. Checking slugs for malicious patterns (via validateSlug)
|
|
137
|
-
* 3. Using path.resolve to normalize paths before validation
|
|
138
|
-
*
|
|
139
|
-
* This validation is performed BEFORE file I/O in resolveDocumentPath(),
|
|
140
|
-
* ensuring permission checks happen before any file system access.
|
|
141
|
-
*
|
|
142
|
-
* @param options.existingId - Optional ID to use (for edits). If not provided, generates new ID.
|
|
143
|
-
* @param options.entryTypeName - For collections with multiple entry types, specify which one to use. Defaults to the default entry type.
|
|
144
|
-
*/
|
|
145
|
-
private async buildPaths(
|
|
146
|
-
schemaItem: FlatSchemaItem,
|
|
147
|
-
slug: string,
|
|
148
|
-
options: { existingId?: string; entryTypeName?: string } = {},
|
|
149
|
-
): Promise<{
|
|
150
|
-
absolutePath: string
|
|
151
|
-
relativePath: PhysicalPath
|
|
152
|
-
id?: string
|
|
153
|
-
}> {
|
|
154
|
-
const rootWithSep = this.root.endsWith(path.sep) ? this.root : `${this.root}${path.sep}`
|
|
155
|
-
|
|
156
|
-
// Entry-type items: delegate to their parent collection.
|
|
157
|
-
// Uses the same {type}.{slug}.{id}.{ext} pattern as all entries.
|
|
158
|
-
// NOTE: The API layer always resolves paths via resolvePath(), which returns
|
|
159
|
-
// the parent collection directly, so this branch may only fire on direct
|
|
160
|
-
// ContentStore usage (e.g., store.read('content/home', '')).
|
|
161
|
-
if (schemaItem.type === 'entry-type') {
|
|
162
|
-
const parentPath = schemaItem.parentPath || ''
|
|
163
|
-
const parentCollection = this.schemaIndex.get(parentPath)
|
|
164
|
-
if (!parentCollection || parentCollection.type !== 'collection') {
|
|
165
|
-
throw new ContentStoreError(
|
|
166
|
-
`Parent collection not found for entry type: ${schemaItem.name}`,
|
|
167
|
-
)
|
|
168
|
-
}
|
|
169
|
-
// Use provided slug, falling back to entry type name
|
|
170
|
-
const effectiveSlug = slug || schemaItem.name
|
|
171
|
-
return this.buildPaths(parentCollection, effectiveSlug, {
|
|
172
|
-
...options,
|
|
173
|
-
entryTypeName: schemaItem.name,
|
|
174
|
-
})
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Collection entries: {type}.{slug}.{id}.{ext}
|
|
178
|
-
if (schemaItem.type === 'collection') {
|
|
179
|
-
const safeSlug = slug.replace(/^\/+/, '')
|
|
180
|
-
if (!safeSlug) {
|
|
181
|
-
throw new ContentStoreError('Slug is required for collection entries')
|
|
182
|
-
}
|
|
183
|
-
// Security: Validate slug format (prevents ../../../etc/passwd)
|
|
184
|
-
validateSlug(safeSlug)
|
|
185
|
-
|
|
186
|
-
// Determine which entry type to use
|
|
187
|
-
let entryTypeConfig: EntryTypeConfig | undefined
|
|
188
|
-
if (options.entryTypeName) {
|
|
189
|
-
// Use specified entry type
|
|
190
|
-
entryTypeConfig = schemaItem.entries?.find((e) => e.name === options.entryTypeName)
|
|
191
|
-
if (!entryTypeConfig) {
|
|
192
|
-
throw new ContentStoreError(
|
|
193
|
-
`Entry type '${options.entryTypeName}' not found in collection`,
|
|
194
|
-
)
|
|
195
|
-
}
|
|
196
|
-
} else {
|
|
197
|
-
// Use default entry type
|
|
198
|
-
entryTypeConfig = getDefaultEntryType(schemaItem.entries)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const format = entryTypeConfig?.format || 'json'
|
|
202
|
-
const ext = getFormatExtension(format)
|
|
203
|
-
const entryTypeName = entryTypeConfig?.name || 'entry'
|
|
204
|
-
|
|
205
|
-
// Resolve the full collection path with embedded IDs
|
|
206
|
-
// e.g., "content/docs/api" → "content/docs.bChqT78gcaLd/api.meiuwxTSo7UN"
|
|
207
|
-
let collectionRoot = await resolveCollectionPath(this.root, schemaItem.logicalPath)
|
|
208
|
-
|
|
209
|
-
if (!collectionRoot) {
|
|
210
|
-
// Collection directory doesn't exist yet - use logical path
|
|
211
|
-
// (Directory will be created on write if needed)
|
|
212
|
-
collectionRoot = path.resolve(this.root, schemaItem.logicalPath)
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Security: Prevent path traversal at collection level
|
|
216
|
-
if (!collectionRoot.startsWith(rootWithSep)) {
|
|
217
|
-
throw new ContentStoreError('Path traversal detected')
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Check if file already exists (editing case)
|
|
221
|
-
let id = options.existingId
|
|
222
|
-
let existingFilename: string | undefined
|
|
223
|
-
let existingEntryType: string | undefined
|
|
224
|
-
|
|
225
|
-
if (!id) {
|
|
226
|
-
// Try to find existing file with this slug
|
|
227
|
-
const entries = await fs.readdir(collectionRoot, { withFileTypes: true }).catch(() => [])
|
|
228
|
-
const existingFile = entries.find((entry) => {
|
|
229
|
-
if (entry.isDirectory()) return false
|
|
230
|
-
// Extract entry type from filename to check slug properly
|
|
231
|
-
const fileEntryType = extractEntryTypeFromFilename(entry.name)
|
|
232
|
-
const existingSlug = extractSlugFromFilename(entry.name, fileEntryType || undefined)
|
|
233
|
-
return existingSlug === safeSlug
|
|
234
|
-
})
|
|
235
|
-
|
|
236
|
-
if (existingFile) {
|
|
237
|
-
id = extractIdFromFilename(existingFile.name) || undefined
|
|
238
|
-
// Remember original filename for legacy files without IDs
|
|
239
|
-
existingFilename = existingFile.name
|
|
240
|
-
// Extract and preserve entry type from existing file (immutable after creation)
|
|
241
|
-
existingEntryType = extractEntryTypeFromFilename(existingFile.name) || undefined
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// For existing entries, preserve the entry type (immutable after creation)
|
|
246
|
-
// For new entries, use the specified entry type
|
|
247
|
-
const finalEntryTypeName = existingEntryType || entryTypeName
|
|
248
|
-
|
|
249
|
-
// Build filename: use existing filename if found, or generate new one with ID
|
|
250
|
-
let filename: string
|
|
251
|
-
if (existingFilename && !id) {
|
|
252
|
-
// Legacy file without embedded ID - use original filename
|
|
253
|
-
filename = existingFilename
|
|
254
|
-
} else {
|
|
255
|
-
// Generate new ID if needed
|
|
256
|
-
if (!id) {
|
|
257
|
-
id = generateId()
|
|
258
|
-
}
|
|
259
|
-
// Build filename with embedded ID: type.slug.id.ext
|
|
260
|
-
// Use finalEntryTypeName to preserve entry type for existing entries
|
|
261
|
-
filename = `${finalEntryTypeName}.${safeSlug}.${id}${ext}`
|
|
262
|
-
}
|
|
263
|
-
const resolved = path.resolve(collectionRoot, filename)
|
|
264
|
-
const collectionRootWithSep = collectionRoot.endsWith(path.sep)
|
|
265
|
-
? collectionRoot
|
|
266
|
-
: `${collectionRoot}${path.sep}`
|
|
267
|
-
|
|
268
|
-
// Security: Prevent path traversal at entry level
|
|
269
|
-
if (!resolved.startsWith(collectionRootWithSep)) {
|
|
270
|
-
throw new ContentStoreError('Path traversal detected')
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return {
|
|
274
|
-
absolutePath: resolved,
|
|
275
|
-
relativePath: path.relative(this.root, resolved) as PhysicalPath,
|
|
276
|
-
id,
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
throw new ContentStoreError('Invalid schema item type')
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Path resolution: resolves a URL path to a schema item
|
|
285
|
-
* - Try as collection + slug (last segment = slug)
|
|
286
|
-
*/
|
|
287
|
-
resolvePath(pathSegments: string[]): {
|
|
288
|
-
schemaItem: FlatSchemaItem
|
|
289
|
-
slug: EntrySlug
|
|
290
|
-
} {
|
|
291
|
-
if (pathSegments.length === 0) {
|
|
292
|
-
throw new ContentStoreError('Empty path')
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
const logicalPath = pathSegments.join('/')
|
|
296
|
-
|
|
297
|
-
// Try as collection + slug
|
|
298
|
-
// Last segment of an API-validated LogicalPath; safe to cast (no slashes, no traversal)
|
|
299
|
-
const slug = pathSegments[pathSegments.length - 1] as EntrySlug
|
|
300
|
-
const collectionPath = pathSegments.slice(0, -1).join('/')
|
|
301
|
-
const normalizedCollection = normalizeFilesystemPath(collectionPath)
|
|
302
|
-
const collection = this.schemaIndex.get(normalizedCollection)
|
|
303
|
-
|
|
304
|
-
if (collection?.type === 'collection' && collection.entries) {
|
|
305
|
-
return {
|
|
306
|
-
schemaItem: collection,
|
|
307
|
-
slug,
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
throw new ContentStoreError(`No schema item found for path: ${logicalPath}`)
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
async resolveDocumentPath(schemaPath: LogicalPath, slug = '') {
|
|
315
|
-
const schemaItem = this.assertSchemaItem(schemaPath)
|
|
316
|
-
return await this.buildPaths(schemaItem, slug)
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
async read(
|
|
320
|
-
collectionPath: LogicalPath,
|
|
321
|
-
slug: EntrySlug | '' = '',
|
|
322
|
-
options: { resolveReferences?: boolean } = {},
|
|
323
|
-
): Promise<ContentDocument> {
|
|
324
|
-
const schemaItem = this.assertSchemaItem(collectionPath)
|
|
325
|
-
const { absolutePath, relativePath } = await this.buildPaths(schemaItem, slug)
|
|
326
|
-
const raw = await fs.readFile(absolutePath, 'utf8')
|
|
327
|
-
|
|
328
|
-
let doc: ContentDocument
|
|
329
|
-
let format: ContentFormat
|
|
330
|
-
let fields: EntrySchema
|
|
331
|
-
|
|
332
|
-
if (schemaItem.type === 'entry-type') {
|
|
333
|
-
// Entry type from unified model
|
|
334
|
-
format = schemaItem.format
|
|
335
|
-
fields = schemaItem.schema
|
|
336
|
-
} else {
|
|
337
|
-
// Collection entry
|
|
338
|
-
const defaultEntry = getDefaultEntryType(schemaItem.entries)
|
|
339
|
-
format = defaultEntry?.format || 'json'
|
|
340
|
-
fields = defaultEntry?.schema || []
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
if (format === 'json') {
|
|
344
|
-
const data = JSON.parse(raw) as Record<string, unknown>
|
|
345
|
-
doc = {
|
|
346
|
-
collection: schemaItem.logicalPath,
|
|
347
|
-
collectionName: schemaItem.name,
|
|
348
|
-
format: 'json',
|
|
349
|
-
data,
|
|
350
|
-
relativePath,
|
|
351
|
-
absolutePath,
|
|
352
|
-
}
|
|
353
|
-
} else {
|
|
354
|
-
const parsed = matter(raw)
|
|
355
|
-
doc = {
|
|
356
|
-
collection: schemaItem.logicalPath,
|
|
357
|
-
collectionName: schemaItem.name,
|
|
358
|
-
format: format,
|
|
359
|
-
data: (parsed.data as Record<string, unknown>) ?? {},
|
|
360
|
-
body: parsed.content,
|
|
361
|
-
relativePath,
|
|
362
|
-
absolutePath,
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// Automatic reference resolution (defaults to true)
|
|
367
|
-
if (options.resolveReferences !== false) {
|
|
368
|
-
doc.data = await this.resolveReferencesInData(doc.data, fields)
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
return doc
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
async write(
|
|
375
|
-
collectionPath: LogicalPath,
|
|
376
|
-
slug: EntrySlug | '' = '',
|
|
377
|
-
input: WriteInput,
|
|
378
|
-
entryTypeName?: string,
|
|
379
|
-
): Promise<ContentDocument> {
|
|
380
|
-
const idIndex = await this.idIndex()
|
|
381
|
-
const schemaItem = this.assertSchemaItem(collectionPath)
|
|
382
|
-
|
|
383
|
-
// Determine expected format based on entry type
|
|
384
|
-
let expectedFormat: ContentFormat
|
|
385
|
-
if (schemaItem.type === 'entry-type') {
|
|
386
|
-
expectedFormat = schemaItem.format
|
|
387
|
-
} else {
|
|
388
|
-
// For collections, determine format from specified or default entry type
|
|
389
|
-
let entryTypeConfig: EntryTypeConfig | undefined
|
|
390
|
-
if (entryTypeName) {
|
|
391
|
-
entryTypeConfig = schemaItem.entries?.find((e) => e.name === entryTypeName)
|
|
392
|
-
if (!entryTypeConfig) {
|
|
393
|
-
throw new ContentStoreError(`Entry type '${entryTypeName}' not found in collection`)
|
|
394
|
-
}
|
|
395
|
-
} else {
|
|
396
|
-
entryTypeConfig = getDefaultEntryType(schemaItem.entries)
|
|
397
|
-
}
|
|
398
|
-
expectedFormat = entryTypeConfig?.format || 'json'
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
if (expectedFormat !== input.format) {
|
|
402
|
-
throw new ContentStoreError(`Format mismatch: expects ${expectedFormat}, got ${input.format}`)
|
|
403
|
-
}
|
|
404
|
-
const { absolutePath, relativePath, id } = await this.buildPaths(schemaItem, slug, {
|
|
405
|
-
entryTypeName,
|
|
406
|
-
})
|
|
407
|
-
await fs.mkdir(path.dirname(absolutePath), { recursive: true })
|
|
408
|
-
|
|
409
|
-
if (input.format === 'json') {
|
|
410
|
-
const json = JSON.stringify(input.data ?? {}, null, 2)
|
|
411
|
-
await fs.writeFile(absolutePath, `${json}\n`, 'utf8')
|
|
412
|
-
|
|
413
|
-
// Update index (ID is already in filename)
|
|
414
|
-
if (id) {
|
|
415
|
-
const existing = idIndex.findById(id)
|
|
416
|
-
if (existing) {
|
|
417
|
-
// Update if path changed, otherwise do nothing
|
|
418
|
-
if (existing.relativePath !== relativePath) {
|
|
419
|
-
idIndex.updatePath(existing.id, relativePath)
|
|
420
|
-
}
|
|
421
|
-
} else {
|
|
422
|
-
// Add new entry to index
|
|
423
|
-
idIndex.add({
|
|
424
|
-
type: 'entry',
|
|
425
|
-
relativePath,
|
|
426
|
-
collection: collectionPath,
|
|
427
|
-
slug: slug || undefined,
|
|
428
|
-
})
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
return {
|
|
433
|
-
collection: schemaItem.logicalPath,
|
|
434
|
-
collectionName: schemaItem.name,
|
|
435
|
-
format: 'json',
|
|
436
|
-
data: input.data ?? {},
|
|
437
|
-
relativePath,
|
|
438
|
-
absolutePath,
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
const file = matter.stringify(input.body, input.data ?? {})
|
|
443
|
-
await fs.writeFile(absolutePath, file, 'utf8')
|
|
444
|
-
|
|
445
|
-
// Update index (ID is already in filename)
|
|
446
|
-
if (id) {
|
|
447
|
-
const existing = idIndex.findById(id)
|
|
448
|
-
if (existing) {
|
|
449
|
-
// Update if path changed, otherwise do nothing
|
|
450
|
-
if (existing.relativePath !== relativePath) {
|
|
451
|
-
idIndex.updatePath(existing.id, relativePath)
|
|
452
|
-
}
|
|
453
|
-
} else {
|
|
454
|
-
// Add new entry to index
|
|
455
|
-
idIndex.add({
|
|
456
|
-
type: 'entry',
|
|
457
|
-
relativePath,
|
|
458
|
-
collection: collectionPath,
|
|
459
|
-
slug: slug || undefined,
|
|
460
|
-
})
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
return {
|
|
465
|
-
collection: schemaItem.logicalPath,
|
|
466
|
-
collectionName: schemaItem.name,
|
|
467
|
-
format: input.format,
|
|
468
|
-
data: input.data ?? {},
|
|
469
|
-
body: input.body,
|
|
470
|
-
relativePath,
|
|
471
|
-
absolutePath,
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
/**
|
|
476
|
-
* Read an entry by its ID (UUID).
|
|
477
|
-
* Returns null if the ID doesn't exist or points to a collection.
|
|
478
|
-
*/
|
|
479
|
-
async readById(id: ContentId): Promise<ContentDocument | null> {
|
|
480
|
-
const idIndex = await this.idIndex()
|
|
481
|
-
const location = idIndex.findById(id)
|
|
482
|
-
if (!location || location.type !== 'entry') return null
|
|
483
|
-
return this.read(location.collection!, location.slug!)
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
/**
|
|
487
|
-
* Get the ID for an entry given its collection and slug.
|
|
488
|
-
* Returns null if no ID exists yet.
|
|
489
|
-
*/
|
|
490
|
-
async getIdForEntry(collectionPath: LogicalPath, slug: EntrySlug): Promise<ContentId | null> {
|
|
491
|
-
const idIndex = await this.idIndex()
|
|
492
|
-
const { relativePath } = await this.buildPaths(this.assertCollection(collectionPath), slug)
|
|
493
|
-
return idIndex.findByPath(relativePath)
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
/**
|
|
497
|
-
* Delete an entry and remove it from the index.
|
|
498
|
-
*/
|
|
499
|
-
async delete(collectionPath: LogicalPath, slug: EntrySlug): Promise<void> {
|
|
500
|
-
const idIndex = await this.idIndex()
|
|
501
|
-
const collection = this.assertCollection(collectionPath)
|
|
502
|
-
const { absolutePath, relativePath } = await this.buildPaths(collection, slug)
|
|
503
|
-
|
|
504
|
-
// Get ID before deleting
|
|
505
|
-
const id = idIndex.findByPath(relativePath)
|
|
506
|
-
|
|
507
|
-
// Delete file
|
|
508
|
-
await fs.unlink(absolutePath)
|
|
509
|
-
|
|
510
|
-
// Remove from index
|
|
511
|
-
if (id) {
|
|
512
|
-
idIndex.remove(id)
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
/**
|
|
517
|
-
* Rename an entry by changing its slug (middle segment of filename).
|
|
518
|
-
* Entry filename pattern: {entryTypeName}.{slug}.{id}.{ext}
|
|
519
|
-
*
|
|
520
|
-
* @param collectionPath - Logical path to the collection
|
|
521
|
-
* @param currentSlug - Current slug of the entry
|
|
522
|
-
* @param newSlug - New slug (must be unique within collection)
|
|
523
|
-
* @returns Object with new logical path
|
|
524
|
-
* @throws ContentStoreError if entry doesn't exist, new slug conflicts, or validation fails
|
|
525
|
-
*/
|
|
526
|
-
async renameEntry(
|
|
527
|
-
collectionPath: LogicalPath,
|
|
528
|
-
currentSlug: EntrySlug,
|
|
529
|
-
newSlug: EntrySlug,
|
|
530
|
-
): Promise<{ newPath: LogicalPath }> {
|
|
531
|
-
const idIndex = await this.idIndex()
|
|
532
|
-
const collection = this.assertCollection(collectionPath)
|
|
533
|
-
|
|
534
|
-
// Validate new slug format
|
|
535
|
-
validateSlug(newSlug)
|
|
536
|
-
const safeNewSlug = newSlug.replace(/^\/+/, '')
|
|
537
|
-
if (!safeNewSlug) {
|
|
538
|
-
throw new ContentStoreError('New slug cannot be empty')
|
|
539
|
-
}
|
|
540
|
-
if (!/^[a-z0-9][a-z0-9-]*$/.test(safeNewSlug)) {
|
|
541
|
-
throw new ContentStoreError(
|
|
542
|
-
'Slug must start with a letter or number and contain only lowercase letters, numbers, and hyphens',
|
|
543
|
-
)
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// Get current file path
|
|
547
|
-
const { absolutePath: currentPath, relativePath: currentRelPath } = await this.buildPaths(
|
|
548
|
-
collection,
|
|
549
|
-
currentSlug,
|
|
550
|
-
)
|
|
551
|
-
|
|
552
|
-
// Verify current file exists
|
|
553
|
-
try {
|
|
554
|
-
await fs.access(currentPath)
|
|
555
|
-
} catch {
|
|
556
|
-
throw new ContentStoreError(`Entry not found: ${currentSlug}`)
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// If slugs are the same, no-op
|
|
560
|
-
if (currentSlug === safeNewSlug) {
|
|
561
|
-
return { newPath: `${collectionPath}/${currentSlug}` as LogicalPath }
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
// Extract entry type name and extension from current filename
|
|
565
|
-
const currentFilename = path.basename(currentPath)
|
|
566
|
-
const parts = currentFilename.split('.')
|
|
567
|
-
if (parts.length < 4) {
|
|
568
|
-
throw new ContentStoreError(`Invalid entry filename format: ${currentFilename}`)
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
const entryTypeName = parts[0]
|
|
572
|
-
const contentId = parts[parts.length - 2]
|
|
573
|
-
const ext = `.${parts[parts.length - 1]}`
|
|
574
|
-
|
|
575
|
-
// Build new filename with new slug
|
|
576
|
-
const newFilename = `${entryTypeName}.${safeNewSlug}.${contentId}${ext}`
|
|
577
|
-
const parentDir = path.dirname(currentPath)
|
|
578
|
-
const newPath = path.join(parentDir, newFilename)
|
|
579
|
-
|
|
580
|
-
// Check if any file with the new slug already exists (regardless of ID)
|
|
581
|
-
// Need to check for pattern: {entryTypeName}.{newSlug}.{any-id}{ext}
|
|
582
|
-
try {
|
|
583
|
-
const entries = await fs.readdir(parentDir, { withFileTypes: true })
|
|
584
|
-
for (const entry of entries) {
|
|
585
|
-
if (entry.isDirectory()) continue
|
|
586
|
-
|
|
587
|
-
// Extract slug from filename using the same pattern
|
|
588
|
-
const existingSlug = extractSlugFromFilename(entry.name, entryTypeName)
|
|
589
|
-
if (existingSlug === safeNewSlug) {
|
|
590
|
-
throw new ContentStoreError(
|
|
591
|
-
`Entry with slug "${safeNewSlug}" already exists in collection "${collectionPath}"`,
|
|
592
|
-
)
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
} catch (err) {
|
|
596
|
-
// Re-throw ContentStoreError (e.g., "already exists") — ignore filesystem errors
|
|
597
|
-
if (err instanceof ContentStoreError) {
|
|
598
|
-
throw err
|
|
599
|
-
}
|
|
600
|
-
// Ignore other errors (e.g., ENOENT if parent dir doesn't exist)
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
// Atomically rename the file
|
|
604
|
-
await fs.rename(currentPath, newPath)
|
|
605
|
-
|
|
606
|
-
// Update the ID index
|
|
607
|
-
const newRelativePath = path.relative(this.root, newPath) as PhysicalPath
|
|
608
|
-
const entryId = idIndex.findByPath(currentRelPath)
|
|
609
|
-
if (entryId) {
|
|
610
|
-
idIndex.updatePath(entryId, newRelativePath)
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// Return new logical path
|
|
614
|
-
const newLogicalPath = `${collectionPath}/${safeNewSlug}` as LogicalPath
|
|
615
|
-
return { newPath: newLogicalPath }
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
/**
|
|
619
|
-
* List all entries in a collection.
|
|
620
|
-
* Returns array of entry metadata (relativePath, collection, slug).
|
|
621
|
-
* Returns empty array if the collection doesn't exist.
|
|
622
|
-
*/
|
|
623
|
-
async listCollectionEntries(collectionPath: LogicalPath): Promise<
|
|
624
|
-
Array<{
|
|
625
|
-
relativePath: PhysicalPath
|
|
626
|
-
collection: LogicalPath
|
|
627
|
-
slug: EntrySlug
|
|
628
|
-
}>
|
|
629
|
-
> {
|
|
630
|
-
const idIndex = await this.idIndex()
|
|
631
|
-
|
|
632
|
-
// Try to find the collection in the schema index
|
|
633
|
-
// The schema index uses normalized logical paths like "content/authors"
|
|
634
|
-
// But we might receive either "authors" or "content/authors"
|
|
635
|
-
const normalized = normalizeFilesystemPath(collectionPath)
|
|
636
|
-
let item = this.schemaIndex.get(normalized)
|
|
637
|
-
|
|
638
|
-
// If not found by full path, try matching the last segment
|
|
639
|
-
// (handles cases where caller passes "posts" instead of "content/posts")
|
|
640
|
-
if (!item) {
|
|
641
|
-
for (const schemaItem of this.schemaIndex.values()) {
|
|
642
|
-
if (schemaItem.type === 'collection') {
|
|
643
|
-
const lastSegment = schemaItem.logicalPath.split('/').pop()
|
|
644
|
-
if (lastSegment === collectionPath) {
|
|
645
|
-
item = schemaItem
|
|
646
|
-
break
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
// Return empty array if collection doesn't exist or isn't a collection
|
|
653
|
-
if (!item || item.type !== 'collection') {
|
|
654
|
-
return []
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
const collection = item
|
|
658
|
-
|
|
659
|
-
// Get entries directly from collection index (O(1) + O(m))
|
|
660
|
-
// The index now stores logical collection paths, so we can look up directly
|
|
661
|
-
const baseEntries = idIndex.getEntriesInCollection(collection.logicalPath)
|
|
662
|
-
|
|
663
|
-
// Filter and map to required format
|
|
664
|
-
const entries: Array<{
|
|
665
|
-
relativePath: PhysicalPath
|
|
666
|
-
collection: LogicalPath
|
|
667
|
-
slug: EntrySlug
|
|
668
|
-
}> = []
|
|
669
|
-
|
|
670
|
-
for (const location of baseEntries) {
|
|
671
|
-
if (location.type === 'entry' && location.slug) {
|
|
672
|
-
// Include entries in this collection or subcollections
|
|
673
|
-
if (
|
|
674
|
-
location.collection === collection.logicalPath ||
|
|
675
|
-
location.collection?.startsWith(collection.logicalPath + '/')
|
|
676
|
-
) {
|
|
677
|
-
entries.push({
|
|
678
|
-
relativePath: location.relativePath,
|
|
679
|
-
collection: location.collection,
|
|
680
|
-
slug: location.slug,
|
|
681
|
-
})
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
return entries
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
/**
|
|
690
|
-
* Recursively resolve reference fields in data.
|
|
691
|
-
* This traverses objects, arrays, and blocks to find and resolve all reference fields.
|
|
692
|
-
*/
|
|
693
|
-
private async resolveReferencesInData(
|
|
694
|
-
data: Record<string, unknown>,
|
|
695
|
-
fields: EntrySchema,
|
|
696
|
-
): Promise<Record<string, unknown>> {
|
|
697
|
-
const resolved = { ...data }
|
|
698
|
-
const idIndex = await this.idIndex()
|
|
699
|
-
|
|
700
|
-
for (const field of fields) {
|
|
701
|
-
const value = data[field.name]
|
|
702
|
-
|
|
703
|
-
if (field.type === 'reference') {
|
|
704
|
-
// Single reference
|
|
705
|
-
if (typeof value === 'string' && value) {
|
|
706
|
-
resolved[field.name] = await this.resolveSingleReference(value, idIndex)
|
|
707
|
-
}
|
|
708
|
-
// Array of references (list: true)
|
|
709
|
-
else if (field.list && Array.isArray(value)) {
|
|
710
|
-
resolved[field.name] = await Promise.all(
|
|
711
|
-
value.map((id) =>
|
|
712
|
-
typeof id === 'string' ? this.resolveSingleReference(id, idIndex) : null,
|
|
713
|
-
),
|
|
714
|
-
)
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
// Recursively handle nested objects
|
|
718
|
-
else if (field.type === 'object' && value) {
|
|
719
|
-
const objectField = field as ObjectFieldConfig
|
|
720
|
-
if (!objectField.fields) continue
|
|
721
|
-
if (objectField.list && Array.isArray(value)) {
|
|
722
|
-
resolved[field.name] = await Promise.all(
|
|
723
|
-
value.map((item) =>
|
|
724
|
-
typeof item === 'object' && item !== null
|
|
725
|
-
? this.resolveReferencesInData(item as Record<string, unknown>, objectField.fields)
|
|
726
|
-
: item,
|
|
727
|
-
),
|
|
728
|
-
)
|
|
729
|
-
} else if (typeof value === 'object') {
|
|
730
|
-
resolved[field.name] = await this.resolveReferencesInData(
|
|
731
|
-
value as Record<string, unknown>,
|
|
732
|
-
objectField.fields,
|
|
733
|
-
)
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
// Recursively handle blocks
|
|
737
|
-
else if (field.type === 'block' && Array.isArray(value)) {
|
|
738
|
-
const blockField = field as BlockFieldConfig
|
|
739
|
-
resolved[field.name] = await Promise.all(
|
|
740
|
-
(value as unknown[]).map(async (block) => {
|
|
741
|
-
const b = block as Record<string, unknown>
|
|
742
|
-
if (!b || typeof b.value !== 'object') return block
|
|
743
|
-
const template = blockField.templates.find((t) => t.name === b.template)
|
|
744
|
-
if (!template) return block
|
|
745
|
-
|
|
746
|
-
return {
|
|
747
|
-
...b,
|
|
748
|
-
value: await this.resolveReferencesInData(
|
|
749
|
-
b.value as Record<string, unknown>,
|
|
750
|
-
template.fields,
|
|
751
|
-
),
|
|
752
|
-
}
|
|
753
|
-
}),
|
|
754
|
-
)
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
return resolved
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
/**
|
|
762
|
-
* Resolve a single reference ID to full entry data.
|
|
763
|
-
* Returns null if the reference is invalid or missing.
|
|
764
|
-
* Includes id, slug, and collection fields for debugging.
|
|
765
|
-
*/
|
|
766
|
-
private async resolveSingleReference(
|
|
767
|
-
id: string,
|
|
768
|
-
idIndex: ContentIdIndex,
|
|
769
|
-
): Promise<Record<string, unknown> | null> {
|
|
770
|
-
try {
|
|
771
|
-
const location = idIndex.findById(id)
|
|
772
|
-
|
|
773
|
-
if (!location || location.type !== 'entry' || !location.collection || !location.slug) {
|
|
774
|
-
return null
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
// Read the referenced entry WITHOUT resolving its references (prevent infinite loops)
|
|
778
|
-
const doc = await this.read(location.collection, location.slug, {
|
|
779
|
-
resolveReferences: false,
|
|
780
|
-
})
|
|
781
|
-
|
|
782
|
-
return {
|
|
783
|
-
id,
|
|
784
|
-
slug: location.slug,
|
|
785
|
-
collection: location.collection,
|
|
786
|
-
...doc.data,
|
|
787
|
-
}
|
|
788
|
-
} catch (error) {
|
|
789
|
-
console.error(`Failed to resolve reference ${id}:`, error)
|
|
790
|
-
return null
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
}
|