canopycms 0.0.67 → 0.0.68-int.101
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/README.md +24 -17
- package/dist/ai/handler.js +8 -0
- package/dist/api/admin-branch-health.js +17 -20
- package/dist/api/admin.d.ts +30 -9
- package/dist/api/admin.js +32 -2
- package/dist/api/assets.js +84 -76
- package/dist/api/branch-create-window.d.ts +12 -0
- package/dist/api/branch-create-window.js +13 -0
- package/dist/api/branch-review.d.ts +3 -3
- package/dist/api/branch-review.js +14 -5
- package/dist/api/branch-status.d.ts +5 -5
- package/dist/api/branch-status.js +26 -6
- package/dist/api/branch-withdraw.d.ts +2 -2
- package/dist/api/branch-withdraw.js +7 -2
- package/dist/api/branch.d.ts +15 -2
- package/dist/api/branch.js +118 -92
- package/dist/api/client.d.ts +18 -6
- package/dist/api/client.js +79 -12
- package/dist/api/content.d.ts +6 -5
- package/dist/api/content.js +44 -41
- package/dist/api/entries.js +16 -15
- package/dist/api/github-sync.d.ts +19 -1
- package/dist/api/github-sync.js +57 -10
- package/dist/api/index.d.ts +1 -1
- package/dist/api/reference-options.js +1 -1
- package/dist/api/resolve-references.js +14 -40
- package/dist/api/settings-helpers.d.ts +1 -1
- package/dist/api/settings-helpers.js +1 -4
- package/dist/api/user.d.ts +3 -0
- package/dist/api/user.js +3 -0
- package/dist/assets/asset-url.d.ts +22 -16
- package/dist/assets/asset-url.js +42 -25
- package/dist/assets/factory.d.ts +5 -0
- package/dist/assets/factory.js +5 -0
- package/dist/assets/finalize.js +3 -0
- package/dist/assets/index.d.ts +1 -1
- package/dist/assets/materialize.d.ts +153 -0
- package/dist/assets/materialize.js +402 -0
- package/dist/assets/store-local.d.ts +19 -5
- package/dist/assets/store-local.js +69 -10
- package/dist/assets/store-s3.d.ts +38 -7
- package/dist/assets/store-s3.js +128 -22
- package/dist/assets/transform-directives.d.ts +52 -13
- package/dist/assets/transform-directives.js +87 -27
- package/dist/assets/transform.d.ts +3 -3
- package/dist/assets/transform.js +13 -8
- package/dist/assets/types.d.ts +31 -4
- package/dist/auth/file-based-auth-cache.js +7 -8
- package/dist/authorization/content.d.ts +5 -4
- package/dist/authorization/content.js +5 -5
- package/dist/authorization/path.d.ts +11 -6
- package/dist/authorization/path.js +11 -6
- package/dist/authorization/types.d.ts +2 -2
- package/dist/branch-health.d.ts +7 -1
- package/dist/branch-health.js +8 -3
- package/dist/branch-metadata.d.ts +5 -5
- package/dist/branch-metadata.js +44 -36
- package/dist/branch-provisioning.d.ts +167 -0
- package/dist/branch-provisioning.js +501 -0
- package/dist/branch-schema-cache.d.ts +5 -3
- package/dist/branch-schema-cache.js +29 -11
- package/dist/branch-sparse.d.ts +23 -0
- package/dist/branch-sparse.js +118 -0
- package/dist/branch-workspace.d.ts +42 -3
- package/dist/branch-workspace.js +240 -94
- package/dist/build/asset-refs.d.ts +93 -0
- package/dist/build/asset-refs.js +251 -0
- package/dist/build-identity.d.ts +3 -0
- package/dist/build-identity.js +13 -0
- package/dist/cli/asset-refs.d.ts +52 -0
- package/dist/cli/asset-refs.js +194 -0
- package/dist/cli/cli.d.ts +15 -1
- package/dist/cli/cli.js +5928 -2195
- package/dist/cli/configured-asset-store.d.ts +12 -0
- package/dist/cli/configured-asset-store.js +46 -0
- package/dist/cli/generate-ai-content.js +2185 -587
- package/dist/cli/github-app-manifest.d.ts +4 -3
- package/dist/cli/github-app-manifest.js +4 -3
- package/dist/cli/init.js +30 -27
- package/dist/cli/migrate.js +12 -8
- package/dist/cli/sync.js +83 -37
- package/dist/cli/template-files/Dockerfile.cms.template +8 -0
- package/dist/cli/template-files/cdk-app.ts.template +4 -0
- package/dist/cli/template-files/cms-stack.ts.template +23 -12
- package/dist/cli/template-files/deploy-cms.yml.template +2 -0
- package/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/config/helpers.js +1 -2
- package/dist/config/schemas/config.d.ts +19 -32
- package/dist/config/schemas/config.js +9 -11
- package/dist/config/schemas/field.js +1 -0
- package/dist/config/schemas/media.d.ts +4 -13
- package/dist/config/schemas/media.js +5 -8
- package/dist/config/schemas/url.d.ts +4 -20
- package/dist/config/schemas/url.js +6 -22
- package/dist/config/types.d.ts +30 -20
- package/dist/config/validation.d.ts +6 -0
- package/dist/config/validation.js +37 -0
- package/dist/content-listing.d.ts +12 -12
- package/dist/content-listing.js +10 -6
- package/dist/content-reader.d.ts +11 -2
- package/dist/content-reader.js +22 -14
- package/dist/content-store.d.ts +37 -9
- package/dist/content-store.js +70 -43
- package/dist/content-tree.d.ts +1 -1
- package/dist/content-tree.js +2 -2
- package/dist/context.d.ts +27 -9
- package/dist/context.js +154 -70
- package/dist/editor/BranchManager.d.ts +7 -1
- package/dist/editor/BranchManager.js +29 -14
- package/dist/editor/CanopyEditor.d.ts +1 -1
- package/dist/editor/CanopyEditor.js +2 -6
- package/dist/editor/Editor.d.ts +4 -3
- package/dist/editor/Editor.js +32 -22
- package/dist/editor/FormRenderer.js +34 -18
- package/dist/editor/PreviewFrame.d.ts +24 -0
- package/dist/editor/PreviewFrame.js +140 -0
- package/dist/editor/admin/SystemHealthPanel.js +47 -7
- package/dist/editor/components/BranchesDrawer.d.ts +9 -0
- package/dist/editor/components/BranchesDrawer.js +4 -0
- package/dist/editor/components/NoEditPermissionNotice.d.ts +10 -0
- package/dist/editor/components/NoEditPermissionNotice.js +17 -0
- package/dist/editor/context/AssetContext.d.ts +18 -35
- package/dist/editor/context/AssetContext.js +20 -29
- package/dist/editor/context/index.d.ts +1 -1
- package/dist/editor/context/index.js +1 -1
- package/dist/editor/editor-config.d.ts +1 -2
- package/dist/editor/editor-config.js +0 -23
- package/dist/editor/editor-utils.d.ts +24 -21
- package/dist/editor/editor-utils.js +75 -62
- package/dist/editor/fields/BlockField.d.ts +1 -0
- package/dist/editor/fields/BlockField.js +6 -4
- package/dist/editor/fields/CodeField.d.ts +1 -0
- package/dist/editor/fields/CodeField.js +2 -2
- package/dist/editor/fields/DateTimeField.d.ts +1 -0
- package/dist/editor/fields/DateTimeField.js +2 -2
- package/dist/editor/fields/FieldDescription.d.ts +16 -0
- package/dist/editor/fields/FieldDescription.js +11 -0
- package/dist/editor/fields/ImageField.d.ts +1 -0
- package/dist/editor/fields/ImageField.js +3 -2
- package/dist/editor/fields/InlineGroupField.js +4 -1
- package/dist/editor/fields/MarkdownField.d.ts +1 -0
- package/dist/editor/fields/MarkdownField.js +115 -20
- package/dist/editor/fields/NumberField.d.ts +1 -0
- package/dist/editor/fields/NumberField.js +2 -2
- package/dist/editor/fields/NumberListField.d.ts +1 -0
- package/dist/editor/fields/NumberListField.js +2 -2
- package/dist/editor/fields/ObjectField.d.ts +1 -0
- package/dist/editor/fields/ObjectField.js +5 -2
- package/dist/editor/fields/ReferenceField.d.ts +1 -0
- package/dist/editor/fields/ReferenceField.js +5 -4
- package/dist/editor/fields/SelectField.d.ts +1 -0
- package/dist/editor/fields/SelectField.js +2 -2
- package/dist/editor/fields/StringListField.d.ts +1 -0
- package/dist/editor/fields/StringListField.js +2 -2
- package/dist/editor/fields/TextField.d.ts +1 -0
- package/dist/editor/fields/TextField.js +2 -2
- package/dist/editor/fields/ToggleField.d.ts +1 -0
- package/dist/editor/fields/ToggleField.js +7 -4
- package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +1 -1
- package/dist/editor/fields/entry-link/InsertEntryLink.js +3 -2
- package/dist/editor/fields/mdx-jsx-support.d.ts +1 -0
- package/dist/editor/fields/mdx-jsx-support.js +137 -0
- package/dist/editor/hooks/create-branch-request.d.ts +20 -0
- package/dist/editor/hooks/create-branch-request.js +52 -0
- package/dist/editor/hooks/useBranchActions.d.ts +13 -2
- package/dist/editor/hooks/useBranchActions.js +48 -17
- package/dist/editor/hooks/useBranchManager.d.ts +21 -0
- package/dist/editor/hooks/useBranchManager.js +204 -37
- package/dist/editor/hooks/useBranchesData.d.ts +6 -0
- package/dist/editor/hooks/useBranchesData.js +9 -3
- package/dist/editor/hooks/useCommentSystem.js +25 -6
- package/dist/editor/hooks/useDraftManager.d.ts +16 -1
- package/dist/editor/hooks/useDraftManager.js +193 -36
- package/dist/editor/hooks/useEntriesData.js +3 -3
- package/dist/editor/hooks/useEntryManager.d.ts +2 -1
- package/dist/editor/hooks/useEntryManager.js +34 -19
- package/dist/editor/media/AssetCard.d.ts +1 -1
- package/dist/editor/media/crop-math.d.ts +3 -4
- package/dist/editor/media/crop-math.js +11 -22
- package/dist/editor/media/editor-image-src.d.ts +7 -0
- package/dist/editor/media/editor-image-src.js +12 -0
- package/dist/editor/preview-asset-base.d.ts +8 -0
- package/dist/editor/preview-asset-base.js +13 -0
- package/dist/editor/preview-bridge.d.ts +9 -21
- package/dist/editor/preview-bridge.js +19 -139
- package/dist/editor/preview-path.d.ts +10 -0
- package/dist/editor/preview-path.js +20 -0
- package/dist/editor/theme.js +2 -2
- package/dist/entry-schema-registry.d.ts +4 -4
- package/dist/entry-schema-registry.js +10 -6
- package/dist/entry-schema.d.ts +27 -4
- package/dist/entry-schema.js +20 -3
- package/dist/git-manager.d.ts +173 -6
- package/dist/git-manager.js +594 -76
- package/dist/github-service.d.ts +15 -0
- package/dist/github-service.js +19 -1
- package/dist/http/handler.js +37 -13
- package/dist/http/index.d.ts +2 -0
- package/dist/http/index.js +2 -0
- package/dist/http/router.d.ts +2 -0
- package/dist/http/router.js +2 -1
- package/dist/http/types.d.ts +2 -0
- package/dist/http/worker-not-ready.d.ts +10 -0
- package/dist/http/worker-not-ready.js +24 -0
- package/dist/operating-mode/client-unsafe-strategy.js +0 -6
- package/dist/operating-mode/types.d.ts +1 -4
- package/dist/paths/branch-name.d.ts +5 -0
- package/dist/paths/branch-name.js +9 -0
- package/dist/paths/branch.d.ts +6 -2
- package/dist/paths/branch.js +16 -10
- package/dist/paths/index.d.ts +3 -3
- package/dist/paths/index.js +3 -3
- package/dist/paths/normalize.d.ts +7 -0
- package/dist/paths/normalize.js +9 -0
- package/dist/paths/validation.js +0 -2
- package/dist/preview.d.ts +8 -0
- package/dist/preview.js +7 -0
- package/dist/reference-resolver.d.ts +5 -21
- package/dist/reference-resolver.js +9 -44
- package/dist/resolve-canopy-user.js +2 -1
- package/dist/schema/schema-store.d.ts +54 -31
- package/dist/schema/schema-store.js +77 -31
- package/dist/server.d.ts +64 -9
- package/dist/server.js +48 -8
- package/dist/services.d.ts +18 -6
- package/dist/services.js +85 -70
- package/dist/settings-workspace.d.ts +23 -3
- package/dist/settings-workspace.js +197 -45
- package/dist/static/seo.d.ts +2 -14
- package/dist/static/seo.js +2 -25
- package/dist/submission-attribution.d.ts +73 -0
- package/dist/submission-attribution.js +221 -0
- package/dist/sync-core.d.ts +12 -1
- package/dist/sync-core.js +31 -13
- package/dist/task-queue/worker-status.d.ts +8 -0
- package/dist/task-queue/worker-status.js +17 -0
- package/dist/types.d.ts +31 -2
- package/dist/utils/content-serialize.d.ts +5 -2
- package/dist/utils/content-serialize.js +98 -24
- package/dist/utils/content-write-lock.d.ts +18 -7
- package/dist/utils/content-write-lock.js +18 -9
- package/dist/utils/debug.d.ts +8 -0
- package/dist/utils/debug.js +10 -2
- package/dist/utils/git.d.ts +32 -0
- package/dist/utils/git.js +42 -0
- package/dist/utils/occ-json-write.js +10 -2
- package/dist/utils/provision-log.d.ts +25 -0
- package/dist/utils/provision-log.js +49 -0
- package/dist/utils/provisioning-lock.d.ts +35 -5
- package/dist/utils/provisioning-lock.js +73 -12
- package/dist/utils/request-timing.d.ts +25 -0
- package/dist/utils/request-timing.js +101 -0
- package/dist/utils/sanitize-href.d.ts +8 -22
- package/dist/utils/sanitize-href.js +11 -28
- package/dist/utils/url-prefix.d.ts +30 -0
- package/dist/utils/url-prefix.js +61 -0
- package/dist/utils/yaml-source-splice.d.ts +58 -0
- package/dist/utils/yaml-source-splice.js +515 -0
- package/dist/validation/entry-validator.js +9 -3
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/worker/canopy-state.d.ts +45 -0
- package/dist/worker/canopy-state.js +75 -0
- package/dist/worker/cms-worker.d.ts +8 -3
- package/dist/worker/cms-worker.js +38 -2
- package/dist/worker/git-sync.d.ts +30 -12
- package/dist/worker/git-sync.js +253 -58
- package/dist/worker/history-rewrite.d.ts +1 -1
- package/dist/worker/history-rewrite.js +1 -1
- package/dist/worker/provisioned-workspace.d.ts +35 -0
- package/dist/worker/provisioned-workspace.js +50 -0
- package/dist/worker/rebase.d.ts +1 -1
- package/dist/worker/rebase.js +95 -23
- package/dist/worker/remote-git-maintenance.d.ts +3 -0
- package/dist/worker/remote-git-maintenance.js +11 -0
- package/dist/worker/sparse-cone.d.ts +20 -0
- package/dist/worker/sparse-cone.js +132 -0
- package/dist/worker/task-runner.js +99 -14
- package/dist/worker/worker-context.d.ts +6 -1
- package/package.json +8 -2
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { ContentFormat, FlatSchemaItem, EntrySchema } from './config/index.js';
|
|
5
5
|
import type { LogicalPath, PhysicalPath, Slug, ContentId } from './paths/types.js';
|
|
6
|
-
import { ContentStore, type ReferenceResolveCache } from './content-store.js';
|
|
6
|
+
import { ContentStore, type ReferenceResolveCache, type ReferenceTargetAccess } from './content-store.js';
|
|
7
7
|
/** An entry listing item with raw filesystem data; no API-specific fields like canEdit. */
|
|
8
8
|
export interface CollectionListItem {
|
|
9
9
|
logicalPath: LogicalPath;
|
|
@@ -105,9 +105,9 @@ export interface ListEntriesOptions<T = Record<string, unknown>> {
|
|
|
105
105
|
* not per referencing entry, since a single {@link ReferenceResolveCache} spans the whole
|
|
106
106
|
* call. Nothing is constructed and nothing is scanned when this is off.
|
|
107
107
|
*
|
|
108
|
-
* **
|
|
109
|
-
*
|
|
110
|
-
*
|
|
108
|
+
* **Targets get the same path-ACL rule as `read()`**: a target the user may not read
|
|
109
|
+
* resolves to a `RestrictedReference` (title, URL, `unavailable: true`), never its data. A
|
|
110
|
+
* filtered-out entry is never resolved at all.
|
|
111
111
|
*/
|
|
112
112
|
resolveReferences?: boolean;
|
|
113
113
|
}
|
|
@@ -123,8 +123,8 @@ export interface ListEntriesOptions<T = Record<string, unknown>> {
|
|
|
123
123
|
* unfiltered exactly as build-time callers want.
|
|
124
124
|
*/
|
|
125
125
|
export interface ContentVisibilityOptions {
|
|
126
|
-
/** Return false to drop an entry. Receives the entry's
|
|
127
|
-
shouldInclude?: (
|
|
126
|
+
/** Return false to drop an entry. Receives the entry's logical path (see `entryLogicalPath`). */
|
|
127
|
+
shouldInclude?: (logicalPath: LogicalPath) => boolean;
|
|
128
128
|
}
|
|
129
129
|
/** A collection node from the flattened schema. */
|
|
130
130
|
export type CollectionSchemaItem = Extract<FlatSchemaItem, {
|
|
@@ -145,10 +145,13 @@ export type CollectionSchemaItem = Extract<FlatSchemaItem, {
|
|
|
145
145
|
* whole batch. content-index-registry.ts holds stores through `WeakRef` + a
|
|
146
146
|
* `FinalizationRegistry`, so these short-lived instances stay collectable.
|
|
147
147
|
*/
|
|
148
|
-
export declare const createReferenceResolver: (branchRoot: string, flatSchema: FlatSchemaItem[], contentRootName: string) =>
|
|
148
|
+
export declare const createReferenceResolver: (branchRoot: string, flatSchema: FlatSchemaItem[], contentRootName: string, visibility: ContentVisibilityOptions | undefined) => ListingReferenceResolver;
|
|
149
|
+
/** What a listing call resolves references through; see {@link createReferenceResolver}. */
|
|
150
|
+
export interface ListingReferenceResolver {
|
|
149
151
|
store: ContentStore;
|
|
150
152
|
cache: ReferenceResolveCache;
|
|
151
|
-
|
|
153
|
+
access: ReferenceTargetAccess | undefined;
|
|
154
|
+
}
|
|
152
155
|
/**
|
|
153
156
|
* Resolve `reference` fields in a collection's listed entries, in place of their raw data.
|
|
154
157
|
*
|
|
@@ -157,10 +160,7 @@ export declare const createReferenceResolver: (branchRoot: string, flatSchema: F
|
|
|
157
160
|
* what says which fields are references at all; an entry whose type has no resolvable schema
|
|
158
161
|
* is returned untouched rather than guessed at.
|
|
159
162
|
*/
|
|
160
|
-
export declare const resolveCollectionItemReferences: (items: CollectionListItem[], collection: CollectionSchemaItem, resolver:
|
|
161
|
-
store: ContentStore;
|
|
162
|
-
cache: ReferenceResolveCache;
|
|
163
|
-
}) => Promise<CollectionListItem[]>;
|
|
163
|
+
export declare const resolveCollectionItemReferences: (items: CollectionListItem[], collection: CollectionSchemaItem, resolver: ListingReferenceResolver) => Promise<CollectionListItem[]>;
|
|
164
164
|
/**
|
|
165
165
|
* List all content entries as a flat array: walks the schema to discover collections, reads
|
|
166
166
|
* entries from each, and returns a flat list suitable for generateStaticParams, search
|
package/dist/content-listing.js
CHANGED
|
@@ -9,7 +9,7 @@ import { findBodyFieldName } from './utils/body-field.js';
|
|
|
9
9
|
import { computeEntryUrl } from './utils/entry-url.js';
|
|
10
10
|
import { asRecord, getFormatExtension } from './utils/format.js';
|
|
11
11
|
import { resolveCollectionPath } from './content-id-index.js';
|
|
12
|
-
import { validateAndNormalizePath } from './paths/index.js';
|
|
12
|
+
import { entryLogicalPath, validateAndNormalizePath } from './paths/index.js';
|
|
13
13
|
import { isNotFoundError, getErrorMessage } from './utils/error.js';
|
|
14
14
|
import { createDebugLogger } from './utils/debug.js';
|
|
15
15
|
import { ContentStore, ContentStoreError, createReferenceResolveCache, } from './content-store.js';
|
|
@@ -81,9 +81,13 @@ import { parseTypedFilename } from './utils/typed-filename.js';
|
|
|
81
81
|
* whole batch. content-index-registry.ts holds stores through `WeakRef` + a
|
|
82
82
|
* `FinalizationRegistry`, so these short-lived instances stay collectable.
|
|
83
83
|
*/
|
|
84
|
-
export const createReferenceResolver = (branchRoot, flatSchema, contentRootName) => ({
|
|
84
|
+
export const createReferenceResolver = (branchRoot, flatSchema, contentRootName, visibility) => ({
|
|
85
85
|
store: new ContentStore(branchRoot, flatSchema, { contentRootName }),
|
|
86
86
|
cache: createReferenceResolveCache(),
|
|
87
|
+
// The predicate that hides an entry from the listing is the one that restricts it as a
|
|
88
|
+
// reference target, so a target is never shown in a sibling's data that its own listing
|
|
89
|
+
// row would have hidden.
|
|
90
|
+
access: visibility?.shouldInclude,
|
|
87
91
|
});
|
|
88
92
|
/**
|
|
89
93
|
* Resolve `reference` fields in a collection's listed entries, in place of their raw data.
|
|
@@ -99,7 +103,7 @@ export const resolveCollectionItemReferences = async (items, collection, resolve
|
|
|
99
103
|
return item;
|
|
100
104
|
return {
|
|
101
105
|
...item,
|
|
102
|
-
data: await resolver.store.resolveReferences(item.data, fields, resolver.cache),
|
|
106
|
+
data: await resolver.store.resolveReferences(item.data, fields, resolver.cache, resolver.access),
|
|
103
107
|
};
|
|
104
108
|
}));
|
|
105
109
|
/**
|
|
@@ -135,11 +139,11 @@ export async function listEntries(branchRoot, flatSchema, contentRootName, optio
|
|
|
135
139
|
// One store + cache for the whole call, or nothing at all when the caller did not opt in.
|
|
136
140
|
// See the `resolveReferences` option for the cost this buys back.
|
|
137
141
|
const resolver = options?.resolveReferences
|
|
138
|
-
? createReferenceResolver(branchRoot, flatSchema, contentRootName)
|
|
142
|
+
? createReferenceResolver(branchRoot, flatSchema, contentRootName, visibility)
|
|
139
143
|
: null;
|
|
140
144
|
const collectionResults = await Promise.all(collections.map(async (collection) => {
|
|
141
145
|
const entries = await listCollectionEntries(branchRoot, collection, (file) => skippedFiles.push(file));
|
|
142
|
-
const visible = shouldInclude ? entries.filter((e) => shouldInclude(e.
|
|
146
|
+
const visible = shouldInclude ? entries.filter((e) => shouldInclude(e.logicalPath)) : entries;
|
|
143
147
|
// Resolve AFTER the visibility filter (a denied entry is never resolved, so its
|
|
144
148
|
// references cost nothing and leak nothing) and BEFORE the mapping below, so
|
|
145
149
|
// `extract` and `filter` both see resolved data — which is the entire point.
|
|
@@ -330,7 +334,7 @@ export const listCollectionEntries = async (root, collection, onSkip) => {
|
|
|
330
334
|
readEntryData(absolutePath, format, bodyField),
|
|
331
335
|
]);
|
|
332
336
|
const item = {
|
|
333
|
-
logicalPath:
|
|
337
|
+
logicalPath: entryLogicalPath(collection.logicalPath, slug),
|
|
334
338
|
contentId,
|
|
335
339
|
slug,
|
|
336
340
|
collectionPath: collection.logicalPath,
|
package/dist/content-reader.d.ts
CHANGED
|
@@ -7,7 +7,16 @@ export interface ContentReaderOptions {
|
|
|
7
7
|
basePathOverride?: string;
|
|
8
8
|
defaultBranch?: string;
|
|
9
9
|
createdBy?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Provision a workspace for a branch that has none. Off by default: a `branch` handed in from a
|
|
12
|
+
* request (`?branch=`) would otherwise let any visitor create a workspace per distinct name, so
|
|
13
|
+
* turn it on only when every branch name the reader sees is trusted.
|
|
14
|
+
*/
|
|
10
15
|
allowCreateBranch?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* The sole branch resolver when given, replacing `allowCreateBranch`: null reads as NOT_FOUND.
|
|
18
|
+
* Reads from the checkout (static deployments, builds) never call it.
|
|
19
|
+
*/
|
|
11
20
|
getBranchContext?: (branch: string) => Promise<BranchContext | null>;
|
|
12
21
|
}
|
|
13
22
|
export interface ReadContentInput {
|
|
@@ -84,7 +93,7 @@ export interface ContentReader {
|
|
|
84
93
|
}>;
|
|
85
94
|
}
|
|
86
95
|
/**
|
|
87
|
-
* Server-side helper to read content directly from a branch workspace.
|
|
88
|
-
*
|
|
96
|
+
* Server-side helper to read content directly from a branch workspace. A branch with no workspace
|
|
97
|
+
* reads as NOT_FOUND unless `allowCreateBranch` or a `getBranchContext` resolver says otherwise.
|
|
89
98
|
*/
|
|
90
99
|
export declare const createContentReader: (options: ContentReaderOptions) => ContentReader;
|
package/dist/content-reader.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { loadBranchContext, loadOrCreateBranchContext } from './branch-workspace.js';
|
|
2
2
|
import { ContentStore, ContentStoreError } from './content-store.js';
|
|
3
|
-
import { resolveBranchPaths, } from './paths/index.js';
|
|
3
|
+
import { namesNoWorkspace, resolveBranchPaths, } from './paths/index.js';
|
|
4
4
|
import { trimSlashes } from './paths/normalize.js';
|
|
5
5
|
import { isIndexSlug } from './utils/entry-url.js';
|
|
6
6
|
import { isDeployedStatic, isBuildMode, readsFromCheckout } from './build-mode.js';
|
|
7
7
|
import { isNotFoundError } from './utils/error.js';
|
|
8
8
|
import { resolveEntryLinksInData } from './entry-link-resolver.js';
|
|
9
9
|
/**
|
|
10
|
-
* Server-side helper to read content directly from a branch workspace.
|
|
11
|
-
*
|
|
10
|
+
* Server-side helper to read content directly from a branch workspace. A branch with no workspace
|
|
11
|
+
* reads as NOT_FOUND unless `allowCreateBranch` or a `getBranchContext` resolver says otherwise.
|
|
12
12
|
*/
|
|
13
13
|
export const createContentReader = (options) => {
|
|
14
14
|
const services = options.services;
|
|
@@ -18,7 +18,7 @@ export const createContentReader = (options) => {
|
|
|
18
18
|
services.config.defaultActiveBranch ??
|
|
19
19
|
services.config.defaultBaseBranch ??
|
|
20
20
|
'main';
|
|
21
|
-
const allowCreateBranch = options.allowCreateBranch ??
|
|
21
|
+
const allowCreateBranch = options.allowCreateBranch ?? false;
|
|
22
22
|
const createdBy = options.createdBy ?? 'canopycms-content-reader';
|
|
23
23
|
const resolveBranchContext = async (branchName) => {
|
|
24
24
|
// Static deployments and builds read from the checkout: loadOrCreateBranchContext
|
|
@@ -34,11 +34,11 @@ export const createContentReader = (options) => {
|
|
|
34
34
|
remoteUrl: services.config.defaultRemoteUrl,
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
|
-
// Check custom resolver first (e.g., from HTTP handler)
|
|
38
37
|
if (options.getBranchContext) {
|
|
39
|
-
const
|
|
40
|
-
if (
|
|
41
|
-
|
|
38
|
+
const resolved = await options.getBranchContext(branchName);
|
|
39
|
+
if (!resolved)
|
|
40
|
+
throw new ContentStoreError(`Branch not found: ${branchName}`, 'NOT_FOUND');
|
|
41
|
+
return resolved;
|
|
42
42
|
}
|
|
43
43
|
if (allowCreateBranch) {
|
|
44
44
|
return loadOrCreateBranchContext({
|
|
@@ -50,11 +50,14 @@ export const createContentReader = (options) => {
|
|
|
50
50
|
remoteUrl: services.config.defaultRemoteUrl,
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
|
-
// Not allowed to create — must exist
|
|
54
53
|
const existing = await loadBranchContext({
|
|
55
54
|
branchName,
|
|
56
55
|
mode: operatingMode,
|
|
57
56
|
basePathOverride,
|
|
57
|
+
}).catch((err) => {
|
|
58
|
+
if (namesNoWorkspace(err))
|
|
59
|
+
return null;
|
|
60
|
+
throw err;
|
|
58
61
|
});
|
|
59
62
|
if (!existing)
|
|
60
63
|
throw new ContentStoreError(`Branch not found: ${branchName}`, 'NOT_FOUND');
|
|
@@ -128,10 +131,10 @@ export const createContentReader = (options) => {
|
|
|
128
131
|
throw new ContentStoreError(`Path is not a collection: ${entryPath}`, 'NO_SCHEMA_ITEM');
|
|
129
132
|
}
|
|
130
133
|
// Get the path WITHOUT reading the file
|
|
131
|
-
let
|
|
134
|
+
let logicalPath;
|
|
132
135
|
// Absolute filesystem path to the entry file. Surfaced on read() / readByUrlPath()
|
|
133
136
|
// for server-side colocated-artifact reads (e.g. a sibling profile.json). Already
|
|
134
|
-
// computed here
|
|
137
|
+
// computed here by path resolution; just carried through.
|
|
135
138
|
let physicalPath;
|
|
136
139
|
// Entry type + content ID come out of path resolution (buildPaths() derives both from the
|
|
137
140
|
// schema item / filename), surfaced on read() / readByUrlPath() so callers can route or
|
|
@@ -140,7 +143,7 @@ export const createContentReader = (options) => {
|
|
|
140
143
|
let entryId;
|
|
141
144
|
try {
|
|
142
145
|
const resolved = await store.resolveDocumentPath(entryPath, slug ?? '');
|
|
143
|
-
|
|
146
|
+
logicalPath = resolved.logicalPath;
|
|
144
147
|
physicalPath = resolved.absolutePath;
|
|
145
148
|
// resolveDocumentPath() always sets entryTypeName for a valid schema item, so no
|
|
146
149
|
// further fallback is needed here; `id` is the one field that can be legitimately
|
|
@@ -161,10 +164,14 @@ export const createContentReader = (options) => {
|
|
|
161
164
|
if (input.urlAddressableOnly && !store.declaresEntryType(entryPath, entryType)) {
|
|
162
165
|
throw new ContentStoreError(`Entry type '${entryType}' is not declared by ${entryPath}`, 'NO_SCHEMA_ITEM');
|
|
163
166
|
}
|
|
164
|
-
// Check permissions BEFORE reading the file (security)
|
|
167
|
+
// Check permissions BEFORE reading the file (security). The same checker then judges every
|
|
168
|
+
// reference target, so a reference cannot carry a target's data past these rules.
|
|
165
169
|
const shouldCheckPermissions = !(isDeployedStatic(services.config) || isBuildMode());
|
|
170
|
+
let referenceAccess;
|
|
166
171
|
if (shouldCheckPermissions) {
|
|
167
|
-
const
|
|
172
|
+
const checkAccess = await services.createContentAccessChecker(context, branchRoot, user);
|
|
173
|
+
referenceAccess = (targetPath) => checkAccess(targetPath, 'read').allowed;
|
|
174
|
+
const access = checkAccess(logicalPath, 'read');
|
|
168
175
|
if (!access.allowed) {
|
|
169
176
|
if (services.config.mode !== 'prod') {
|
|
170
177
|
const reasons = [];
|
|
@@ -187,6 +194,7 @@ export const createContentReader = (options) => {
|
|
|
187
194
|
try {
|
|
188
195
|
const doc = await store.read(entryPath, slug ?? '', {
|
|
189
196
|
resolveReferences: input.resolveReferences ?? true,
|
|
197
|
+
referenceAccess,
|
|
190
198
|
});
|
|
191
199
|
return { doc, store, physicalPath, entryType, entryId };
|
|
192
200
|
}
|
package/dist/content-store.d.ts
CHANGED
|
@@ -148,7 +148,8 @@ export interface ContentStoreOptions {
|
|
|
148
148
|
* shorter than the `ContentStore` whose memoized `idIndex()` it sits on, so it adds no
|
|
149
149
|
* staleness window and is out of scope for the generation-marker protocol in
|
|
150
150
|
* `docs/concurrency.md`. Never make one module-global, persist one, or reuse one across
|
|
151
|
-
* requests.
|
|
151
|
+
* requests. A cached value has the batch's {@link ReferenceTargetAccess} already applied, so a
|
|
152
|
+
* cache must never be shared by two readers.
|
|
152
153
|
*
|
|
153
154
|
* Misses are memoized alongside hits so one batch stays internally coherent: a shared block
|
|
154
155
|
* resolving to data on page 1 and `null` on page 40 of one sitemap is worse than either
|
|
@@ -157,6 +158,21 @@ export interface ContentStoreOptions {
|
|
|
157
158
|
export type ReferenceResolveCache = Map<string, Promise<Record<string, unknown> | null>>;
|
|
158
159
|
/** Create a cache for one batch of reference resolution. See {@link ReferenceResolveCache}. */
|
|
159
160
|
export declare const createReferenceResolveCache: () => ReferenceResolveCache;
|
|
161
|
+
/**
|
|
162
|
+
* Whether the reader may see a reference's target in full, given the target's logical path
|
|
163
|
+
* (`entryLogicalPath`). A denied target resolves to a `RestrictedReference` (entry-schema.ts).
|
|
164
|
+
*
|
|
165
|
+
* Built from `services.createContentAccessChecker` for the request's user, so reference
|
|
166
|
+
* targets are judged by the same rules as the entry being read. Omitted, every target resolves
|
|
167
|
+
* in full, which is what a build and a static deployment want (merged content is public there).
|
|
168
|
+
* Every request-time surface that returns resolved data to a user passes one.
|
|
169
|
+
*/
|
|
170
|
+
export type ReferenceTargetAccess = (logicalPath: LogicalPath) => boolean;
|
|
171
|
+
export interface ReadOptions {
|
|
172
|
+
resolveReferences?: boolean;
|
|
173
|
+
/** See {@link ReferenceTargetAccess}. */
|
|
174
|
+
referenceAccess?: ReferenceTargetAccess;
|
|
175
|
+
}
|
|
160
176
|
export declare class ContentStore {
|
|
161
177
|
private readonly root;
|
|
162
178
|
/** See ContentStoreOptions.contentRootName — the ID index scan root. */
|
|
@@ -347,6 +363,8 @@ export declare class ContentStore {
|
|
|
347
363
|
resolveDocumentPath(schemaPath: LogicalPath, slug?: string): Promise<{
|
|
348
364
|
absolutePath: string;
|
|
349
365
|
relativePath: PhysicalPath;
|
|
366
|
+
/** The entry's logical path, the form path-permission rules match (`entryLogicalPath`). */
|
|
367
|
+
logicalPath: LogicalPath;
|
|
350
368
|
id?: string;
|
|
351
369
|
/**
|
|
352
370
|
* Always populated for a valid schema item: the collection branch below resolves a name
|
|
@@ -363,9 +381,11 @@ export declare class ContentStore {
|
|
|
363
381
|
*/
|
|
364
382
|
existed: boolean;
|
|
365
383
|
}>;
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
384
|
+
/**
|
|
385
|
+
* Read one entry. References resolve unless `resolveReferences: false`; a target that
|
|
386
|
+
* `referenceAccess` denies resolves to a `RestrictedReference` rather than its data.
|
|
387
|
+
*/
|
|
388
|
+
read(collectionPath: LogicalPath, slug?: Slug | '', options?: ReadOptions): Promise<ContentDocument>;
|
|
369
389
|
write(collectionPath: LogicalPath, slug: (Slug | "") | undefined, input: WriteInput, entryTypeName?: string, existingId?: ContentId): Promise<ContentDocument>;
|
|
370
390
|
/**
|
|
371
391
|
* Read an entry by its ID (UUID).
|
|
@@ -457,12 +477,15 @@ export declare class ContentStore {
|
|
|
457
477
|
* a target referenced by many entries is read once; omit `cache` for exactly `read()`'s
|
|
458
478
|
* unmemoized behavior.
|
|
459
479
|
*
|
|
460
|
-
*
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
480
|
+
* `access` applies to every target exactly as in `read()`; see {@link ReferenceTargetAccess}.
|
|
481
|
+
*/
|
|
482
|
+
resolveReferences(data: Record<string, unknown>, fields: EntrySchema, cache?: ReferenceResolveCache, access?: ReferenceTargetAccess): Promise<Record<string, unknown>>;
|
|
483
|
+
/**
|
|
484
|
+
* Resolve one reference id the way a reference field resolves it, for the editor's
|
|
485
|
+
* live-preview endpoint (api/resolve-references.ts), which has ids but no field to walk. Null
|
|
486
|
+
* when the id names no entry. No body is embedded, since there is no field to ask for one.
|
|
464
487
|
*/
|
|
465
|
-
|
|
488
|
+
resolveReferenceTarget(id: string, access?: ReferenceTargetAccess): Promise<Record<string, unknown> | null>;
|
|
466
489
|
private resolveReferencesInData;
|
|
467
490
|
/**
|
|
468
491
|
* Resolve a single reference ID to full entry data, memoized when a batch cache is supplied.
|
|
@@ -503,5 +526,10 @@ export declare class ContentStore {
|
|
|
503
526
|
* still runs the full refresh-and-retry; a cache only makes repeats of the SAME id share it.
|
|
504
527
|
*/
|
|
505
528
|
private resolveSingleReferenceUncached;
|
|
529
|
+
/**
|
|
530
|
+
* The entry type an entry file was written as, by its filename's type token, falling back to
|
|
531
|
+
* the collection's default the way `read()` does for a legacy untyped file.
|
|
532
|
+
*/
|
|
533
|
+
private entryTypeOf;
|
|
506
534
|
private resolveSingleReferenceOnce;
|
|
507
535
|
}
|
package/dist/content-store.js
CHANGED
|
@@ -20,7 +20,8 @@ import { serializeFrontmatter, serializeYaml } from './utils/content-serialize.j
|
|
|
20
20
|
import { withLock } from './utils/async-mutex.js';
|
|
21
21
|
import { ContentWriteLockBusyError, DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS, withContentWriteLock, } from './utils/content-write-lock.js';
|
|
22
22
|
import { findBodyFieldName } from './utils/body-field.js';
|
|
23
|
-
import { buildResolvedReference } from './entry-schema.js';
|
|
23
|
+
import { buildResolvedReference, buildRestrictedReference } from './entry-schema.js';
|
|
24
|
+
import { resolveEntryTitle } from './utils/title-field.js';
|
|
24
25
|
import { computeEntryUrl } from './utils/entry-url.js';
|
|
25
26
|
import { findUrlPathClaimant } from './url-collision.js';
|
|
26
27
|
import { ContentIdIndex, extractIdFromFilename, extractSlugFromFilename, extractEntryTypeFromFilename, resolveCollectionPath, } from './content-id-index.js';
|
|
@@ -30,7 +31,7 @@ import { generateId } from './id.js';
|
|
|
30
31
|
import { isNodeError } from './utils/error.js';
|
|
31
32
|
import { filePathExists, readFileIfExists } from './utils/fs.js';
|
|
32
33
|
import { asRecord, getFormatExtension } from './utils/format.js';
|
|
33
|
-
import { normalizeFilesystemPath, parseSlug, } from './paths/index.js';
|
|
34
|
+
import { entryLogicalPath, normalizeFilesystemPath, parseSlug, } from './paths/index.js';
|
|
34
35
|
/**
|
|
35
36
|
* Acquire `keys` in canonical (sorted) order before running `fn`, ruling out AB-BA
|
|
36
37
|
* deadlocks between callers with overlapping key sets. Only renameEntry() needs two
|
|
@@ -602,6 +603,7 @@ export class ContentStore {
|
|
|
602
603
|
return {
|
|
603
604
|
absolutePath: resolved,
|
|
604
605
|
relativePath: path.relative(this.root, resolved),
|
|
606
|
+
logicalPath: entryLogicalPath(schemaItem.logicalPath, safeSlug),
|
|
605
607
|
id,
|
|
606
608
|
entryTypeName: finalEntryTypeName,
|
|
607
609
|
existed,
|
|
@@ -632,6 +634,10 @@ export class ContentStore {
|
|
|
632
634
|
const schemaItem = this.assertSchemaItem(schemaPath);
|
|
633
635
|
return await this.buildPaths(schemaItem, slug);
|
|
634
636
|
}
|
|
637
|
+
/**
|
|
638
|
+
* Read one entry. References resolve unless `resolveReferences: false`; a target that
|
|
639
|
+
* `referenceAccess` denies resolves to a `RestrictedReference` rather than its data.
|
|
640
|
+
*/
|
|
635
641
|
async read(collectionPath, slug = '', options = {}) {
|
|
636
642
|
const schemaItem = this.assertSchemaItem(collectionPath);
|
|
637
643
|
const { absolutePath, relativePath, entryTypeName: resolvedEntryTypeName, } = await this.buildPaths(schemaItem, slug);
|
|
@@ -702,7 +708,7 @@ export class ContentStore {
|
|
|
702
708
|
};
|
|
703
709
|
}
|
|
704
710
|
if (options.resolveReferences !== false) {
|
|
705
|
-
doc.data = await this.resolveReferencesInData(doc.data, fields);
|
|
711
|
+
doc.data = await this.resolveReferencesInData(doc.data, fields, undefined, options.referenceAccess);
|
|
706
712
|
}
|
|
707
713
|
doc.version = stat.mtimeMs;
|
|
708
714
|
return doc;
|
|
@@ -1382,21 +1388,26 @@ export class ContentStore {
|
|
|
1382
1388
|
* a target referenced by many entries is read once; omit `cache` for exactly `read()`'s
|
|
1383
1389
|
* unmemoized behavior.
|
|
1384
1390
|
*
|
|
1385
|
-
*
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1391
|
+
* `access` applies to every target exactly as in `read()`; see {@link ReferenceTargetAccess}.
|
|
1392
|
+
*/
|
|
1393
|
+
async resolveReferences(data, fields, cache, access) {
|
|
1394
|
+
return this.resolveReferencesInData(data, fields, cache, access);
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* Resolve one reference id the way a reference field resolves it, for the editor's
|
|
1398
|
+
* live-preview endpoint (api/resolve-references.ts), which has ids but no field to walk. Null
|
|
1399
|
+
* when the id names no entry. No body is embedded, since there is no field to ask for one.
|
|
1389
1400
|
*/
|
|
1390
|
-
async
|
|
1391
|
-
return this.
|
|
1401
|
+
async resolveReferenceTarget(id, access) {
|
|
1402
|
+
return this.resolveSingleReference(id, await this.idIndex(), false, undefined, access);
|
|
1392
1403
|
}
|
|
1393
|
-
async resolveReferencesInData(data, fields, cache) {
|
|
1404
|
+
async resolveReferencesInData(data, fields, cache, access) {
|
|
1394
1405
|
const resolved = { ...data };
|
|
1395
1406
|
const idIndex = await this.idIndex();
|
|
1396
1407
|
for (const field of fields) {
|
|
1397
1408
|
// Inline groups are transparent — recurse into their children at the same data level
|
|
1398
1409
|
if (field.type === 'group') {
|
|
1399
|
-
const groupResolved = await this.resolveReferencesInData(resolved, field.fields, cache);
|
|
1410
|
+
const groupResolved = await this.resolveReferencesInData(resolved, field.fields, cache, access);
|
|
1400
1411
|
Object.assign(resolved, groupResolved);
|
|
1401
1412
|
continue;
|
|
1402
1413
|
}
|
|
@@ -1407,11 +1418,11 @@ export class ContentStore {
|
|
|
1407
1418
|
// which routinely contains both kinds at once. See ReferenceFieldConfig.includeBody.
|
|
1408
1419
|
const includeBody = field.includeBody === true;
|
|
1409
1420
|
if (typeof value === 'string' && value) {
|
|
1410
|
-
resolved[field.name] = await this.resolveSingleReference(value, idIndex, includeBody, cache);
|
|
1421
|
+
resolved[field.name] = await this.resolveSingleReference(value, idIndex, includeBody, cache, access);
|
|
1411
1422
|
}
|
|
1412
1423
|
else if (field.list && Array.isArray(value)) {
|
|
1413
1424
|
resolved[field.name] = await Promise.all(value.map((id) => typeof id === 'string'
|
|
1414
|
-
? this.resolveSingleReference(id, idIndex, includeBody, cache)
|
|
1425
|
+
? this.resolveSingleReference(id, idIndex, includeBody, cache, access)
|
|
1415
1426
|
: null));
|
|
1416
1427
|
}
|
|
1417
1428
|
}
|
|
@@ -1421,11 +1432,11 @@ export class ContentStore {
|
|
|
1421
1432
|
continue;
|
|
1422
1433
|
if (objectField.list && Array.isArray(value)) {
|
|
1423
1434
|
resolved[field.name] = await Promise.all(value.map((item) => typeof item === 'object' && item !== null
|
|
1424
|
-
? this.resolveReferencesInData(item, objectField.fields, cache)
|
|
1435
|
+
? this.resolveReferencesInData(item, objectField.fields, cache, access)
|
|
1425
1436
|
: item));
|
|
1426
1437
|
}
|
|
1427
1438
|
else if (typeof value === 'object') {
|
|
1428
|
-
resolved[field.name] = await this.resolveReferencesInData(value, objectField.fields, cache);
|
|
1439
|
+
resolved[field.name] = await this.resolveReferencesInData(value, objectField.fields, cache, access);
|
|
1429
1440
|
}
|
|
1430
1441
|
}
|
|
1431
1442
|
else if (field.type === 'block' && Array.isArray(value)) {
|
|
@@ -1439,7 +1450,7 @@ export class ContentStore {
|
|
|
1439
1450
|
return block;
|
|
1440
1451
|
return {
|
|
1441
1452
|
...b,
|
|
1442
|
-
value: await this.resolveReferencesInData(b.value, template.fields, cache),
|
|
1453
|
+
value: await this.resolveReferencesInData(b.value, template.fields, cache, access),
|
|
1443
1454
|
};
|
|
1444
1455
|
}));
|
|
1445
1456
|
}
|
|
@@ -1470,9 +1481,9 @@ export class ContentStore {
|
|
|
1470
1481
|
* `includeBody` is opt-in per field: an embedded body sits inside the memoized object and is
|
|
1471
1482
|
* cloned once per referencing entry.
|
|
1472
1483
|
*/
|
|
1473
|
-
resolveSingleReference(id, idIndex, includeBody, cache) {
|
|
1484
|
+
resolveSingleReference(id, idIndex, includeBody, cache, access) {
|
|
1474
1485
|
if (!cache)
|
|
1475
|
-
return this.resolveSingleReferenceUncached(id, idIndex, includeBody);
|
|
1486
|
+
return this.resolveSingleReferenceUncached(id, idIndex, includeBody, access);
|
|
1476
1487
|
// The key carries `includeBody`, not just the id: two fields can reference the SAME target
|
|
1477
1488
|
// with different settings, and sharing one entry between them would make the shape depend
|
|
1478
1489
|
// on which field the walk reached first.
|
|
@@ -1482,7 +1493,7 @@ export class ContentStore {
|
|
|
1482
1493
|
// Store the in-flight promise with no `await` in between, so concurrent lookups from
|
|
1483
1494
|
// one Promise.all batch find it and collapse onto a single read. Memoizing the promise
|
|
1484
1495
|
// also shares the self-healing retry below rather than repeating it.
|
|
1485
|
-
pending = this.resolveSingleReferenceUncached(id, idIndex, includeBody);
|
|
1496
|
+
pending = this.resolveSingleReferenceUncached(id, idIndex, includeBody, access);
|
|
1486
1497
|
cache.set(key, pending);
|
|
1487
1498
|
}
|
|
1488
1499
|
// The cached promise always has this handler attached, so it is never an unhandled
|
|
@@ -1503,18 +1514,30 @@ export class ContentStore {
|
|
|
1503
1514
|
* A {@link ReferenceResolveCache} sits ABOVE this, never inside it, so every distinct id
|
|
1504
1515
|
* still runs the full refresh-and-retry; a cache only makes repeats of the SAME id share it.
|
|
1505
1516
|
*/
|
|
1506
|
-
async resolveSingleReferenceUncached(id, idIndex, includeBody) {
|
|
1507
|
-
const first = await this.resolveSingleReferenceOnce(id, idIndex, includeBody);
|
|
1517
|
+
async resolveSingleReferenceUncached(id, idIndex, includeBody, access) {
|
|
1518
|
+
const first = await this.resolveSingleReferenceOnce(id, idIndex, includeBody, access);
|
|
1508
1519
|
if (first !== STALE_LOOKUP)
|
|
1509
1520
|
return first;
|
|
1510
1521
|
// Force a rebuild (throttled). Even when this caller loses the throttle, retry against the
|
|
1511
1522
|
// live index: a sibling lookup in the same batch may have won it and rebuilt (idIndex()
|
|
1512
1523
|
// dedupes in-flight builds), so every miss in a Promise.all batch heals.
|
|
1513
1524
|
await this.refreshIndexForSuspiciousLookup();
|
|
1514
|
-
const second = await this.resolveSingleReferenceOnce(id, await this.idIndex(), includeBody);
|
|
1525
|
+
const second = await this.resolveSingleReferenceOnce(id, await this.idIndex(), includeBody, access);
|
|
1515
1526
|
return second === STALE_LOOKUP ? null : second;
|
|
1516
1527
|
}
|
|
1517
|
-
|
|
1528
|
+
/**
|
|
1529
|
+
* The entry type an entry file was written as, by its filename's type token, falling back to
|
|
1530
|
+
* the collection's default the way `read()` does for a legacy untyped file.
|
|
1531
|
+
*/
|
|
1532
|
+
entryTypeOf(collectionPath, relativePath) {
|
|
1533
|
+
const schemaItem = this.assertSchemaItem(collectionPath);
|
|
1534
|
+
if (schemaItem.type === 'entry-type')
|
|
1535
|
+
return schemaItem;
|
|
1536
|
+
const entries = schemaItem.entries;
|
|
1537
|
+
const typeName = extractEntryTypeFromFilename(path.basename(relativePath));
|
|
1538
|
+
return entries?.find((e) => e.name === typeName) ?? getDefaultEntryType(schemaItem.entries);
|
|
1539
|
+
}
|
|
1540
|
+
async resolveSingleReferenceOnce(id, idIndex, includeBody, access) {
|
|
1518
1541
|
try {
|
|
1519
1542
|
const location = idIndex.findById(id);
|
|
1520
1543
|
if (!location)
|
|
@@ -1526,32 +1549,36 @@ export class ContentStore {
|
|
|
1526
1549
|
const doc = await this.read(location.collection, location.slug, {
|
|
1527
1550
|
resolveReferences: false,
|
|
1528
1551
|
});
|
|
1529
|
-
// `urlPath` is what makes a resolved reference linkable
|
|
1530
|
-
//
|
|
1531
|
-
//
|
|
1532
|
-
// table.
|
|
1533
|
-
//
|
|
1534
|
-
// Deliberately `computeEntryUrl` (utils/entry-url.ts), the same forward
|
|
1535
|
-
// collection+slug -> url rule `listEntries` publishes as `item.urlPath` and
|
|
1552
|
+
// `urlPath` is what makes a resolved reference linkable without a second listing pass to
|
|
1553
|
+
// build a contentId -> url table. Deliberately `computeEntryUrl` (utils/entry-url.ts), the
|
|
1554
|
+
// forward collection+slug -> url rule `listEntries` publishes as `item.urlPath` and
|
|
1536
1555
|
// `entry-link-resolver.ts` uses for `entry:ID` links -- NOT the reverse url -> entry
|
|
1537
|
-
// resolver in url-path-resolver.ts
|
|
1538
|
-
//
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1556
|
+
// resolver in url-path-resolver.ts: this is the surface that DEFINES an entry's URL, so
|
|
1557
|
+
// sourcing it from the resolver that consumes that definition would invert the dependency.
|
|
1558
|
+
const meta = {
|
|
1559
|
+
id,
|
|
1560
|
+
slug: location.slug,
|
|
1561
|
+
collection: location.collection,
|
|
1562
|
+
urlPath: computeEntryUrl(location.collection, location.slug, this.contentRootName),
|
|
1563
|
+
};
|
|
1564
|
+
// Checked on the target's logical path, the space path rules are written in. The title is
|
|
1565
|
+
// the same `resolveEntryTitle` chain the entries API labels a listed entry with.
|
|
1566
|
+
if (access && !access(entryLogicalPath(location.collection, location.slug))) {
|
|
1567
|
+
const entryType = this.entryTypeOf(location.collection, location.relativePath);
|
|
1568
|
+
return buildRestrictedReference(meta, resolveEntryTitle(doc.data, {
|
|
1569
|
+
schema: entryType?.schema,
|
|
1570
|
+
entryTypeLabel: entryType?.label,
|
|
1571
|
+
slug: location.slug,
|
|
1572
|
+
}));
|
|
1573
|
+
}
|
|
1574
|
+
// `buildResolvedReference` owns the assembly order (data, embedded body, reserved
|
|
1575
|
+
// metadata); its doc comment says why the order is the contract.
|
|
1544
1576
|
//
|
|
1545
1577
|
// `'body' in doc` narrows the ContentDocument union to its markdown variant, so
|
|
1546
1578
|
// `doc.body`/`doc.bodyFieldName` are reachable at all -- a type guard, not a redundant
|
|
1547
1579
|
// runtime check. Only a field that asked to EMBED its target passes a body at all.
|
|
1548
1580
|
const bodyForEmbed = includeBody && 'body' in doc ? { fieldName: doc.bodyFieldName, value: doc.body } : undefined;
|
|
1549
|
-
return buildResolvedReference(doc.data,
|
|
1550
|
-
id,
|
|
1551
|
-
slug: location.slug,
|
|
1552
|
-
collection: location.collection,
|
|
1553
|
-
urlPath: computeEntryUrl(location.collection, location.slug, this.contentRootName),
|
|
1554
|
-
}, bodyForEmbed);
|
|
1581
|
+
return buildResolvedReference(doc.data, meta, bodyForEmbed);
|
|
1555
1582
|
}
|
|
1556
1583
|
catch (error) {
|
|
1557
1584
|
// Index hit but the file is gone — the typical symptom of an external
|
package/dist/content-tree.d.ts
CHANGED
|
@@ -136,7 +136,7 @@ export interface BuildContentTreeOptions<T = unknown, TEntryTypes = DefaultEntry
|
|
|
136
136
|
*
|
|
137
137
|
* Same flag, same default (`false`) and same reasoning as
|
|
138
138
|
* `ListEntriesOptions.resolveReferences` in content-listing.ts — see it for why the default
|
|
139
|
-
* is opt-in, what it costs, and
|
|
139
|
+
* is opt-in, what it costs, and how path ACLs apply to the resolved targets.
|
|
140
140
|
*/
|
|
141
141
|
resolveReferences?: boolean;
|
|
142
142
|
}
|
package/dist/content-tree.js
CHANGED
|
@@ -70,11 +70,11 @@ export async function buildContentTree(branchRoot, flatSchema, contentRootName,
|
|
|
70
70
|
// read once for the entire tree rather than once per collection. Null unless opted in —
|
|
71
71
|
// see the `resolveReferences` option above.
|
|
72
72
|
const resolver = options?.resolveReferences
|
|
73
|
-
? createReferenceResolver(branchRoot, flatSchema, contentRootName)
|
|
73
|
+
? createReferenceResolver(branchRoot, flatSchema, contentRootName, visibility)
|
|
74
74
|
: null;
|
|
75
75
|
const listVisibleEntries = async (collection) => {
|
|
76
76
|
const entries = await listCollectionEntries(branchRoot, collection);
|
|
77
|
-
const visible = shouldInclude ? entries.filter((e) => shouldInclude(e.
|
|
77
|
+
const visible = shouldInclude ? entries.filter((e) => shouldInclude(e.logicalPath)) : entries;
|
|
78
78
|
// Resolved here rather than at the two node-building sites so BOTH inherit it — the same
|
|
79
79
|
// reason the ACL filter lives here. A denied entry is filtered above and never resolved.
|
|
80
80
|
return resolver ? resolveCollectionItemReferences(visible, collection, resolver) : visible;
|