@createcms/core 0.1.1 → 0.2.0
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/index.cjs +105 -0
- package/dist/index.d.cts +73 -3
- package/dist/index.d.ts +73 -3
- package/dist/index.js +105 -0
- package/dist/next/index.d.cts +54 -1
- package/dist/next/index.d.ts +54 -1
- package/dist/plugins/ab-test/index.cjs +4 -0
- package/dist/plugins/ab-test/index.d.cts +54 -1
- package/dist/plugins/ab-test/index.d.ts +54 -1
- package/dist/plugins/ab-test/index.js +4 -0
- package/dist/plugins/consent/index.d.cts +54 -1
- package/dist/plugins/consent/index.d.ts +54 -1
- package/dist/plugins/i18n/index.cjs +4 -0
- package/dist/plugins/i18n/index.d.cts +54 -1
- package/dist/plugins/i18n/index.d.ts +54 -1
- package/dist/plugins/i18n/index.js +4 -0
- package/dist/plugins/multi-tenant/index.d.cts +54 -1
- package/dist/plugins/multi-tenant/index.d.ts +54 -1
- package/dist/react/blocks.d.cts +54 -1
- package/dist/react/blocks.d.ts +54 -1
- package/dist/react/index.cjs +4 -0
- package/dist/react/index.d.cts +58 -1
- package/dist/react/index.d.ts +58 -1
- package/dist/react/index.js +4 -0
- package/dist/react/tracking.d.cts +54 -1
- package/dist/react/tracking.d.ts +54 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1076,6 +1076,10 @@ const CMS_ERRORS = {
|
|
|
1076
1076
|
status: 404,
|
|
1077
1077
|
message: 'Parent block not found'
|
|
1078
1078
|
},
|
|
1079
|
+
BLOCK_NOT_ALLOWED_IN_PARENT: {
|
|
1080
|
+
status: 400,
|
|
1081
|
+
message: 'This block type is not allowed inside the target parent'
|
|
1082
|
+
},
|
|
1079
1083
|
ROOT_NOT_FOUND: {
|
|
1080
1084
|
status: 404,
|
|
1081
1085
|
message: 'Root block not found in snapshot'
|
|
@@ -4974,6 +4978,88 @@ notFound = 'ROOT_NOT_FOUND') {
|
|
|
4974
4978
|
}
|
|
4975
4979
|
}
|
|
4976
4980
|
|
|
4981
|
+
/** Builds the {@link PlacementIndex} from a collection's `structure` + blocks. */ function buildPlacementIndex(structure, blocks) {
|
|
4982
|
+
const rules = new Map();
|
|
4983
|
+
if (structure) {
|
|
4984
|
+
for (const [parent, entry] of Object.entries(structure)){
|
|
4985
|
+
if (!entry) continue;
|
|
4986
|
+
const accepts = entry.accepts;
|
|
4987
|
+
if (Array.isArray(accepts)) {
|
|
4988
|
+
// Concrete whitelist (incl. the empty "holds nothing" list).
|
|
4989
|
+
rules.set(parent, {
|
|
4990
|
+
mode: 'only',
|
|
4991
|
+
set: new Set(accepts)
|
|
4992
|
+
});
|
|
4993
|
+
} else if (entry.excludes && entry.excludes.length > 0) {
|
|
4994
|
+
// Open base ('*' or omitted) minus an explicit blacklist.
|
|
4995
|
+
rules.set(parent, {
|
|
4996
|
+
mode: 'except',
|
|
4997
|
+
set: new Set(entry.excludes)
|
|
4998
|
+
});
|
|
4999
|
+
}
|
|
5000
|
+
// else: open ('*' / nothing, no excludes) — no rule.
|
|
5001
|
+
}
|
|
5002
|
+
}
|
|
5003
|
+
const containers = new Set();
|
|
5004
|
+
if (blocks) {
|
|
5005
|
+
for (const [name, def] of Object.entries(blocks)){
|
|
5006
|
+
if (def.allowChildren === true) containers.add(name);
|
|
5007
|
+
}
|
|
5008
|
+
}
|
|
5009
|
+
return {
|
|
5010
|
+
rules,
|
|
5011
|
+
containers
|
|
5012
|
+
};
|
|
5013
|
+
}
|
|
5014
|
+
/**
|
|
5015
|
+
* Throws `BLOCK_NOT_ALLOWED_IN_PARENT` when placing a `childType` block under a
|
|
5016
|
+
* `parentType` block would violate the collection's rules. `parentType` must be
|
|
5017
|
+
* the literal `'root'` when the parent is the collection root.
|
|
5018
|
+
*
|
|
5019
|
+
* Two gates, in order:
|
|
5020
|
+
* 1. Container gate — a non-root parent whose `allowChildren` is not `true`
|
|
5021
|
+
* rejects every child.
|
|
5022
|
+
* 2. Acceptance gate — the parent's `accepts`/`excludes` rule, if any:
|
|
5023
|
+
* `only` rejects a child not in the set; `except` rejects a child in the set.
|
|
5024
|
+
*
|
|
5025
|
+
* A parent with no rule (open) and the root (always a container) pass gate 1
|
|
5026
|
+
* and/or gate 2 trivially, so collections without a `structure` map only feel
|
|
5027
|
+
* the `allowChildren` gate.
|
|
5028
|
+
*/ function assertPlacementAllowed(index, childType, parentType) {
|
|
5029
|
+
if (parentType !== 'root' && !index.containers.has(parentType)) throw new CMSError('BLOCK_NOT_ALLOWED_IN_PARENT', {
|
|
5030
|
+
message: `Block '${parentType}' does not accept child blocks (its 'allowChildren' is not set)`,
|
|
5031
|
+
data: {
|
|
5032
|
+
childType,
|
|
5033
|
+
parentType,
|
|
5034
|
+
reason: 'not-a-container'
|
|
5035
|
+
}
|
|
5036
|
+
});
|
|
5037
|
+
const rule = index.rules.get(parentType);
|
|
5038
|
+
if (!rule) return;
|
|
5039
|
+
if (rule.mode === 'only' && !rule.set.has(childType)) throw new CMSError('BLOCK_NOT_ALLOWED_IN_PARENT', {
|
|
5040
|
+
message: `Block '${parentType}' accepts only [${[
|
|
5041
|
+
...rule.set
|
|
5042
|
+
].join(', ')}] — ` + `'${childType}' is not allowed inside it`,
|
|
5043
|
+
data: {
|
|
5044
|
+
childType,
|
|
5045
|
+
parentType,
|
|
5046
|
+
accepts: [
|
|
5047
|
+
...rule.set
|
|
5048
|
+
]
|
|
5049
|
+
}
|
|
5050
|
+
});
|
|
5051
|
+
if (rule.mode === 'except' && rule.set.has(childType)) throw new CMSError('BLOCK_NOT_ALLOWED_IN_PARENT', {
|
|
5052
|
+
message: `Block '${childType}' is not allowed inside '${parentType}' (excluded)`,
|
|
5053
|
+
data: {
|
|
5054
|
+
childType,
|
|
5055
|
+
parentType,
|
|
5056
|
+
excludes: [
|
|
5057
|
+
...rule.set
|
|
5058
|
+
]
|
|
5059
|
+
}
|
|
5060
|
+
});
|
|
5061
|
+
}
|
|
5062
|
+
|
|
4977
5063
|
function assembleBlockTree(blocks, rootId) {
|
|
4978
5064
|
const deletedBlockIds = new Set();
|
|
4979
5065
|
const nodeMap = new Map();
|
|
@@ -5568,6 +5654,10 @@ const blockTreeNodeSchema = z__namespace.lazy(()=>z__namespace.object({
|
|
|
5568
5654
|
function createBlocksEndpoints(def, cmsCtx) {
|
|
5569
5655
|
const { db } = cmsCtx;
|
|
5570
5656
|
const collectionName = def.name;
|
|
5657
|
+
// Derived once per collection: the placement rules the create/move/duplicate
|
|
5658
|
+
// routes enforce — `accepts`/`excludes` from `structure` plus the
|
|
5659
|
+
// `allowChildren` container gate from the block defs.
|
|
5660
|
+
const placementIndex = buildPlacementIndex(def.structure, def.blocks);
|
|
5571
5661
|
return {
|
|
5572
5662
|
/**
|
|
5573
5663
|
* Creates a new root (page/entry) with initial draft branch and commit.
|
|
@@ -5940,6 +6030,10 @@ function createBlocksEndpoints(def, cmsCtx) {
|
|
|
5940
6030
|
if (parentVersion.deleted) throw new CMSError('BLOCK_ALREADY_DELETED', {
|
|
5941
6031
|
message: errorMessages.blockAlreadyDeleted(parentBlockId)
|
|
5942
6032
|
});
|
|
6033
|
+
// Enforce the collection's placement rules. The root block's id equals
|
|
6034
|
+
// the rootId and is stored with `type === collectionName`, so normalize
|
|
6035
|
+
// it to the literal 'root' the structure map keys on.
|
|
6036
|
+
assertPlacementAllowed(placementIndex, type, parentBlockId === rootId ? 'root' : parentVersion.type);
|
|
5943
6037
|
const childBlockId = newId('block');
|
|
5944
6038
|
const blockProps = properties ?? {};
|
|
5945
6039
|
const newChildrenArray = [
|
|
@@ -6147,6 +6241,7 @@ function createBlocksEndpoints(def, cmsCtx) {
|
|
|
6147
6241
|
if (newParent.deleted) throw new CMSError('BLOCK_ALREADY_DELETED', {
|
|
6148
6242
|
message: errorMessages.blockAlreadyDeleted(input.newParentBlockId)
|
|
6149
6243
|
});
|
|
6244
|
+
assertPlacementAllowed(placementIndex, movedBlock.type, input.newParentBlockId === input.rootId ? 'root' : newParent.type);
|
|
6150
6245
|
const oldChildren = (oldParent.children ?? []).filter((id)=>id !== input.blockId);
|
|
6151
6246
|
const newChildren = [
|
|
6152
6247
|
...newParent.children ?? []
|
|
@@ -6434,6 +6529,7 @@ function createBlocksEndpoints(def, cmsCtx) {
|
|
|
6434
6529
|
if (parentVersion.deleted) throw new CMSError('BLOCK_ALREADY_DELETED', {
|
|
6435
6530
|
message: errorMessages.blockAlreadyDeleted(input.targetParentBlockId)
|
|
6436
6531
|
});
|
|
6532
|
+
assertPlacementAllowed(placementIndex, sourceVersion.type, input.targetParentBlockId === input.rootId ? 'root' : parentVersion.type);
|
|
6437
6533
|
const topLevelCopyId = copies[0].newBlockId;
|
|
6438
6534
|
const updatedChildren = [
|
|
6439
6535
|
...parentVersion.children ?? []
|
|
@@ -13725,6 +13821,15 @@ function definePluginSchema(schema) {
|
|
|
13725
13821
|
/**
|
|
13726
13822
|
* Defines a content collection with a root block and a set of child blocks.
|
|
13727
13823
|
*
|
|
13824
|
+
* Blocks may be referenced (`blocks: { hero }`) or written inline
|
|
13825
|
+
* (`blocks: { hero: { label, properties } }`). For the inline form the `blocks`
|
|
13826
|
+
* parameter is shaped as `{ [K in keyof TBlocks]: AnyBlockDefinition } & TBlocks`
|
|
13827
|
+
* — the mapped half gives each block value the concrete `BlockDefinition`
|
|
13828
|
+
* contextual type so editors autocomplete its fields (`label`, `properties`,
|
|
13829
|
+
* `events`, …) instead of falling back to globals, while the `& TBlocks` half
|
|
13830
|
+
* keeps inferring each block's specific shape (so the typed create/update API
|
|
13831
|
+
* and `structure` autocomplete still see the exact block keys and properties).
|
|
13832
|
+
*
|
|
13728
13833
|
* @example
|
|
13729
13834
|
* ```ts
|
|
13730
13835
|
* const pages = defineCollection({
|
package/dist/index.d.cts
CHANGED
|
@@ -3862,7 +3862,6 @@ type BlockDefinition<TProps extends Record<string, BlockProperty> = Record<strin
|
|
|
3862
3862
|
allowChildren?: false;
|
|
3863
3863
|
} | {
|
|
3864
3864
|
allowChildren: true;
|
|
3865
|
-
allowedChildBlocks?: string[];
|
|
3866
3865
|
});
|
|
3867
3866
|
type AnyBlockDefinition = BlockDefinition<Record<string, BlockProperty>, Record<string, EventDeclaration>>;
|
|
3868
3867
|
/** Discriminated union input for creating a block: `{ type: 'paragraph', properties: { text: '...' } }`. */
|
|
@@ -4021,6 +4020,51 @@ type ListBranchesResult = {
|
|
|
4021
4020
|
type RootDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
|
|
4022
4021
|
properties: TProps;
|
|
4023
4022
|
};
|
|
4023
|
+
/**
|
|
4024
|
+
* One PARENT's placement rule inside a collection's {@link CollectionStructure}
|
|
4025
|
+
* — declares which child block types that parent (or the literal `'root'`) may
|
|
4026
|
+
* contain. There are three mutually-exclusive modes, enforced by the type:
|
|
4027
|
+
*
|
|
4028
|
+
* - **open** — `{}` or `{ accepts: '*' }`: holds any block. (Same as having no
|
|
4029
|
+
* entry at all; `'*'` is just an explicit, readable form.)
|
|
4030
|
+
* - **whitelist** — `{ accepts: ['a', 'b'] }`: holds ONLY `a`/`b`. Fail-closed —
|
|
4031
|
+
* a block added to the collection later is rejected until listed. `excludes`
|
|
4032
|
+
* is forbidden here (a concrete `accepts` already says exactly what's allowed).
|
|
4033
|
+
* - **blacklist** — `{ excludes: ['z'] }` (or `{ accepts: '*', excludes: ['z'] }`):
|
|
4034
|
+
* holds anything EXCEPT `z`. Fail-open — a block added later is accepted.
|
|
4035
|
+
*
|
|
4036
|
+
* Whether a parent accepts children AT ALL is the separate, coarser
|
|
4037
|
+
* `allowChildren` gate on the block (the root always accepts children); these
|
|
4038
|
+
* rules only refine WHICH children an accepting parent may hold.
|
|
4039
|
+
*/
|
|
4040
|
+
type BlockStructureEntry<TBlockName extends string> = {
|
|
4041
|
+
/** `'*'` = open base (optional, for readability). */
|
|
4042
|
+
accepts?: '*';
|
|
4043
|
+
/** Holds anything except these. */
|
|
4044
|
+
excludes?: readonly TBlockName[];
|
|
4045
|
+
} | {
|
|
4046
|
+
/** Holds ONLY these block types. */
|
|
4047
|
+
accepts: readonly TBlockName[];
|
|
4048
|
+
/**
|
|
4049
|
+
* Forbidden alongside a concrete `accepts` list — the list already names
|
|
4050
|
+
* exactly what is allowed, so `excludes` would be ignored.
|
|
4051
|
+
*/
|
|
4052
|
+
excludes?: "Remove 'excludes': a concrete 'accepts' list already defines exactly which blocks are allowed. Use accepts: '*' with excludes for an all-except list.";
|
|
4053
|
+
};
|
|
4054
|
+
/**
|
|
4055
|
+
* Placement rules for a collection, keyed by PARENT block name (or the literal
|
|
4056
|
+
* `'root'` for the top level). Open by default: a parent with no entry holds any
|
|
4057
|
+
* block. The keys and the `accepts` / `excludes` block names autocomplete against
|
|
4058
|
+
* the collection's block names and are checked at compile time by
|
|
4059
|
+
* {@link defineCollection} (the field type alone enforces this — no extra step).
|
|
4060
|
+
*
|
|
4061
|
+
* This is the single source of truth that the visual editor (drop-zone gating)
|
|
4062
|
+
* and the server guard (createBlock / moveBlock / duplicateBlock) both read,
|
|
4063
|
+
* alongside each block's `allowChildren` flag, so they can never diverge.
|
|
4064
|
+
*/
|
|
4065
|
+
type CollectionStructure<TBlocks extends Record<string, AnyBlockDefinition>> = {
|
|
4066
|
+
[K in keyof TBlocks | 'root']?: BlockStructureEntry<keyof TBlocks & string>;
|
|
4067
|
+
};
|
|
4024
4068
|
type SlugConfig = {
|
|
4025
4069
|
enabled: false;
|
|
4026
4070
|
} | {
|
|
@@ -4053,6 +4097,15 @@ type CollectionDefinition<TProps extends Record<string, BlockProperty> = Record<
|
|
|
4053
4097
|
* regardless of this flag). Any collection can still be a reference target.
|
|
4054
4098
|
*/
|
|
4055
4099
|
reusableBlock?: boolean;
|
|
4100
|
+
/**
|
|
4101
|
+
* Placement rules keyed by PARENT block name (or `'root'`) — which children
|
|
4102
|
+
* each container may hold, via `accepts` (whitelist) / `excludes` (blacklist)
|
|
4103
|
+
* (see {@link CollectionStructure}). Read by the editor and the server guard
|
|
4104
|
+
* together with each block's `allowChildren` flag. Open by default; block
|
|
4105
|
+
* names are checked at compile time by the field type itself, so a typo is a
|
|
4106
|
+
* compile error at the `defineCollection` call site.
|
|
4107
|
+
*/
|
|
4108
|
+
structure?: CollectionStructure<TBlocks>;
|
|
4056
4109
|
};
|
|
4057
4110
|
type AnyCollectionDefinition = CollectionDefinition<Record<string, BlockProperty>, Record<string, AnyBlockDefinition>>;
|
|
4058
4111
|
type CollectionWithName = Omit<AnyCollectionDefinition, 'blocks'> & {
|
|
@@ -9760,6 +9813,10 @@ declare const CMS_ERRORS: {
|
|
|
9760
9813
|
readonly status: 404;
|
|
9761
9814
|
readonly message: "Parent block not found";
|
|
9762
9815
|
};
|
|
9816
|
+
readonly BLOCK_NOT_ALLOWED_IN_PARENT: {
|
|
9817
|
+
readonly status: 400;
|
|
9818
|
+
readonly message: "This block type is not allowed inside the target parent";
|
|
9819
|
+
};
|
|
9763
9820
|
readonly ROOT_NOT_FOUND: {
|
|
9764
9821
|
readonly status: 404;
|
|
9765
9822
|
readonly message: "Root block not found in snapshot";
|
|
@@ -10220,6 +10277,15 @@ declare function defineRoot<const TProps extends Record<string, BlockProperty>>(
|
|
|
10220
10277
|
/**
|
|
10221
10278
|
* Defines a content collection with a root block and a set of child blocks.
|
|
10222
10279
|
*
|
|
10280
|
+
* Blocks may be referenced (`blocks: { hero }`) or written inline
|
|
10281
|
+
* (`blocks: { hero: { label, properties } }`). For the inline form the `blocks`
|
|
10282
|
+
* parameter is shaped as `{ [K in keyof TBlocks]: AnyBlockDefinition } & TBlocks`
|
|
10283
|
+
* — the mapped half gives each block value the concrete `BlockDefinition`
|
|
10284
|
+
* contextual type so editors autocomplete its fields (`label`, `properties`,
|
|
10285
|
+
* `events`, …) instead of falling back to globals, while the `& TBlocks` half
|
|
10286
|
+
* keeps inferring each block's specific shape (so the typed create/update API
|
|
10287
|
+
* and `structure` autocomplete still see the exact block keys and properties).
|
|
10288
|
+
*
|
|
10223
10289
|
* @example
|
|
10224
10290
|
* ```ts
|
|
10225
10291
|
* const pages = defineCollection({
|
|
@@ -10229,7 +10295,11 @@ declare function defineRoot<const TProps extends Record<string, BlockProperty>>(
|
|
|
10229
10295
|
* });
|
|
10230
10296
|
* ```
|
|
10231
10297
|
*/
|
|
10232
|
-
declare function defineCollection<const TProps extends Record<string, BlockProperty>, const TBlocks extends Record<string, AnyBlockDefinition> = Record<string, never>>(collection: CollectionDefinition<TProps, TBlocks>
|
|
10298
|
+
declare function defineCollection<const TProps extends Record<string, BlockProperty>, const TBlocks extends Record<string, AnyBlockDefinition> = Record<string, never>>(collection: Omit<CollectionDefinition<TProps, TBlocks>, 'blocks'> & {
|
|
10299
|
+
blocks?: {
|
|
10300
|
+
[K in keyof TBlocks]: AnyBlockDefinition;
|
|
10301
|
+
} & TBlocks;
|
|
10302
|
+
}): CollectionDefinition<TProps, TBlocks>;
|
|
10233
10303
|
type ExtractReferencedCollections<T extends Record<string, AnyCollectionDefinition>> = {
|
|
10234
10304
|
[K in keyof T]: T[K] extends CollectionDefinition<infer _P, infer TBlocks> ? {
|
|
10235
10305
|
[B in keyof TBlocks]: TBlocks[B] extends BlockDefinition<infer BProps, any> ? {
|
|
@@ -10274,4 +10344,4 @@ declare function defineCollections<const TCollections extends Record<string, Any
|
|
|
10274
10344
|
declare function defineAuthMiddleware(middleware: CMSMiddleware): CMSMiddleware;
|
|
10275
10345
|
|
|
10276
10346
|
export { CMSClientError, CMSError, CMS_ERRORS, buildFullPath, cmsContext, cmsMeta, createCMS, createCMSClient, createCMSEndpoint, createCMSQuery, defineAuthMiddleware, defineBlock, defineCollection, defineCollections, defineColumns, defineCoreSchema, definePluginSchema, defineRoot, defineTable, getCMSErrorCode, isCMSError, newId, normalizeSlug, registerIdPrefix, rootRevalidateTag, runPruningPass, splitPath, trackingId };
|
|
10277
|
-
export type { AnyBlockDefinition, AnyCollectionDefinition, Approval, Asset, AssetFolder, BlockDefinition, BlockEventFire, BlockEventNames, BlockProperty, BlockPropertyType, BlockTreeNode, BlockVersion, Branch, BranchListItem, CMSAPIError, CMSAfterHook, CMSAfterHookContext, CMSAtomListener, CMSBeforeHook, CMSBeforeHookContext, CMSClientInstance, CMSClientOptions, CMSClientPlugin, CMSClientStore, CMSConfigHooks, CMSCoreRootPruningPlan, CMSDefinition, CMSEndpointContext, CMSEndpointCtx, CMSEndpointKey, CMSEndpointMeta, CMSErrorCode, CMSFetch, CMSHandlerCtx, CMSHookAction, CMSHooks, CMSMiddleware, CMSMiddlewareCtx, CMSMiddlewareRequest, CMSOperation, CMSPlugin, CMSPluginContext, CMSPluginInitOptions, CMSPluginInitResult, CMSPluginPruning, CMSPluginPruningExecuteContext, CMSPluginPruningExecuteResult, CMSPluginPruningMetrics, CMSPluginPruningPlanContext, CMSPluginRootPruningPlan, CMSProcedureCtx, CMSSchemaConfig, CMSSystemHandlerCtx, CMSUserConfig, CollectionDefinition, CollectionWithName, CommentMention, CommentMessage, CommentThread, Commit, CommitSnapshot, ContentUsage, DataRetentionConfig, DrizzleInstance, EnumMap, EventDeclaration, IdPrefix, InferBlockInput, InferBlockProperties, InferBlockTreeNode, InferCreateBlockInput, InferEventParams, InferPartialBlockProperties, InferPluginContext, InferPluginEndpoints, InferPluginErrorCodes, InferUpdateBlockInput, ListBranchesResult, ListMergeRequestsResult, ListNotificationsResult, ListRootsResult, MediaUploadFileState, MediaUploadOptions, MediaUploadState, MergeConflict, MergeRequest, MergeRequestListItem, MiddlewareResult, NewApproval, NewAsset, NewAssetFolder, NewBlockVersion, NewBranch, NewCommentMention, NewCommentMessage, NewCommentThread, NewCommit, NewCommitSnapshot, NewContentUsage, NewMergeConflict, NewMergeRequest, NewNotification, NewPublication, NewRedirect, NewRoot, NewSearchIndex, NewTemplate, NewTemplateVariableUsage, NewVariable, Notification, NotificationInput, NotificationListItem, NotificationPayload, NotificationService, NotificationType, OnNotificationHandler, PruningPassOptions, PruningPassResult, PruningResult, Publication, QueryState, Redirect, ResolvedReference, ResolvedScope, ResolvedSlugConfig, RevalidateConfig, RevalidateEvent, RevalidateHandler, Root, RootDefinition, RootListItem, RootSummary, ScalarBlockProperty, SchemaModule, ScopeConditionFactory, SearchIndex, TableMap, TableScope, Template, TemplateVariableUsage, Variable };
|
|
10347
|
+
export type { AnyBlockDefinition, AnyCollectionDefinition, Approval, Asset, AssetFolder, BlockDefinition, BlockEventFire, BlockEventNames, BlockProperty, BlockPropertyType, BlockStructureEntry, BlockTreeNode, BlockVersion, Branch, BranchListItem, CMSAPIError, CMSAfterHook, CMSAfterHookContext, CMSAtomListener, CMSBeforeHook, CMSBeforeHookContext, CMSClientInstance, CMSClientOptions, CMSClientPlugin, CMSClientStore, CMSConfigHooks, CMSCoreRootPruningPlan, CMSDefinition, CMSEndpointContext, CMSEndpointCtx, CMSEndpointKey, CMSEndpointMeta, CMSErrorCode, CMSFetch, CMSHandlerCtx, CMSHookAction, CMSHooks, CMSMiddleware, CMSMiddlewareCtx, CMSMiddlewareRequest, CMSOperation, CMSPlugin, CMSPluginContext, CMSPluginInitOptions, CMSPluginInitResult, CMSPluginPruning, CMSPluginPruningExecuteContext, CMSPluginPruningExecuteResult, CMSPluginPruningMetrics, CMSPluginPruningPlanContext, CMSPluginRootPruningPlan, CMSProcedureCtx, CMSSchemaConfig, CMSSystemHandlerCtx, CMSUserConfig, CollectionDefinition, CollectionStructure, CollectionWithName, CommentMention, CommentMessage, CommentThread, Commit, CommitSnapshot, ContentUsage, DataRetentionConfig, DrizzleInstance, EnumMap, EventDeclaration, IdPrefix, InferBlockInput, InferBlockProperties, InferBlockTreeNode, InferCreateBlockInput, InferEventParams, InferPartialBlockProperties, InferPluginContext, InferPluginEndpoints, InferPluginErrorCodes, InferUpdateBlockInput, ListBranchesResult, ListMergeRequestsResult, ListNotificationsResult, ListRootsResult, MediaUploadFileState, MediaUploadOptions, MediaUploadState, MergeConflict, MergeRequest, MergeRequestListItem, MiddlewareResult, NewApproval, NewAsset, NewAssetFolder, NewBlockVersion, NewBranch, NewCommentMention, NewCommentMessage, NewCommentThread, NewCommit, NewCommitSnapshot, NewContentUsage, NewMergeConflict, NewMergeRequest, NewNotification, NewPublication, NewRedirect, NewRoot, NewSearchIndex, NewTemplate, NewTemplateVariableUsage, NewVariable, Notification, NotificationInput, NotificationListItem, NotificationPayload, NotificationService, NotificationType, OnNotificationHandler, PruningPassOptions, PruningPassResult, PruningResult, Publication, QueryState, Redirect, ResolvedReference, ResolvedScope, ResolvedSlugConfig, RevalidateConfig, RevalidateEvent, RevalidateHandler, Root, RootDefinition, RootListItem, RootSummary, ScalarBlockProperty, SchemaModule, ScopeConditionFactory, SearchIndex, TableMap, TableScope, Template, TemplateVariableUsage, Variable };
|
package/dist/index.d.ts
CHANGED
|
@@ -3862,7 +3862,6 @@ type BlockDefinition<TProps extends Record<string, BlockProperty> = Record<strin
|
|
|
3862
3862
|
allowChildren?: false;
|
|
3863
3863
|
} | {
|
|
3864
3864
|
allowChildren: true;
|
|
3865
|
-
allowedChildBlocks?: string[];
|
|
3866
3865
|
});
|
|
3867
3866
|
type AnyBlockDefinition = BlockDefinition<Record<string, BlockProperty>, Record<string, EventDeclaration>>;
|
|
3868
3867
|
/** Discriminated union input for creating a block: `{ type: 'paragraph', properties: { text: '...' } }`. */
|
|
@@ -4021,6 +4020,51 @@ type ListBranchesResult = {
|
|
|
4021
4020
|
type RootDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
|
|
4022
4021
|
properties: TProps;
|
|
4023
4022
|
};
|
|
4023
|
+
/**
|
|
4024
|
+
* One PARENT's placement rule inside a collection's {@link CollectionStructure}
|
|
4025
|
+
* — declares which child block types that parent (or the literal `'root'`) may
|
|
4026
|
+
* contain. There are three mutually-exclusive modes, enforced by the type:
|
|
4027
|
+
*
|
|
4028
|
+
* - **open** — `{}` or `{ accepts: '*' }`: holds any block. (Same as having no
|
|
4029
|
+
* entry at all; `'*'` is just an explicit, readable form.)
|
|
4030
|
+
* - **whitelist** — `{ accepts: ['a', 'b'] }`: holds ONLY `a`/`b`. Fail-closed —
|
|
4031
|
+
* a block added to the collection later is rejected until listed. `excludes`
|
|
4032
|
+
* is forbidden here (a concrete `accepts` already says exactly what's allowed).
|
|
4033
|
+
* - **blacklist** — `{ excludes: ['z'] }` (or `{ accepts: '*', excludes: ['z'] }`):
|
|
4034
|
+
* holds anything EXCEPT `z`. Fail-open — a block added later is accepted.
|
|
4035
|
+
*
|
|
4036
|
+
* Whether a parent accepts children AT ALL is the separate, coarser
|
|
4037
|
+
* `allowChildren` gate on the block (the root always accepts children); these
|
|
4038
|
+
* rules only refine WHICH children an accepting parent may hold.
|
|
4039
|
+
*/
|
|
4040
|
+
type BlockStructureEntry<TBlockName extends string> = {
|
|
4041
|
+
/** `'*'` = open base (optional, for readability). */
|
|
4042
|
+
accepts?: '*';
|
|
4043
|
+
/** Holds anything except these. */
|
|
4044
|
+
excludes?: readonly TBlockName[];
|
|
4045
|
+
} | {
|
|
4046
|
+
/** Holds ONLY these block types. */
|
|
4047
|
+
accepts: readonly TBlockName[];
|
|
4048
|
+
/**
|
|
4049
|
+
* Forbidden alongside a concrete `accepts` list — the list already names
|
|
4050
|
+
* exactly what is allowed, so `excludes` would be ignored.
|
|
4051
|
+
*/
|
|
4052
|
+
excludes?: "Remove 'excludes': a concrete 'accepts' list already defines exactly which blocks are allowed. Use accepts: '*' with excludes for an all-except list.";
|
|
4053
|
+
};
|
|
4054
|
+
/**
|
|
4055
|
+
* Placement rules for a collection, keyed by PARENT block name (or the literal
|
|
4056
|
+
* `'root'` for the top level). Open by default: a parent with no entry holds any
|
|
4057
|
+
* block. The keys and the `accepts` / `excludes` block names autocomplete against
|
|
4058
|
+
* the collection's block names and are checked at compile time by
|
|
4059
|
+
* {@link defineCollection} (the field type alone enforces this — no extra step).
|
|
4060
|
+
*
|
|
4061
|
+
* This is the single source of truth that the visual editor (drop-zone gating)
|
|
4062
|
+
* and the server guard (createBlock / moveBlock / duplicateBlock) both read,
|
|
4063
|
+
* alongside each block's `allowChildren` flag, so they can never diverge.
|
|
4064
|
+
*/
|
|
4065
|
+
type CollectionStructure<TBlocks extends Record<string, AnyBlockDefinition>> = {
|
|
4066
|
+
[K in keyof TBlocks | 'root']?: BlockStructureEntry<keyof TBlocks & string>;
|
|
4067
|
+
};
|
|
4024
4068
|
type SlugConfig = {
|
|
4025
4069
|
enabled: false;
|
|
4026
4070
|
} | {
|
|
@@ -4053,6 +4097,15 @@ type CollectionDefinition<TProps extends Record<string, BlockProperty> = Record<
|
|
|
4053
4097
|
* regardless of this flag). Any collection can still be a reference target.
|
|
4054
4098
|
*/
|
|
4055
4099
|
reusableBlock?: boolean;
|
|
4100
|
+
/**
|
|
4101
|
+
* Placement rules keyed by PARENT block name (or `'root'`) — which children
|
|
4102
|
+
* each container may hold, via `accepts` (whitelist) / `excludes` (blacklist)
|
|
4103
|
+
* (see {@link CollectionStructure}). Read by the editor and the server guard
|
|
4104
|
+
* together with each block's `allowChildren` flag. Open by default; block
|
|
4105
|
+
* names are checked at compile time by the field type itself, so a typo is a
|
|
4106
|
+
* compile error at the `defineCollection` call site.
|
|
4107
|
+
*/
|
|
4108
|
+
structure?: CollectionStructure<TBlocks>;
|
|
4056
4109
|
};
|
|
4057
4110
|
type AnyCollectionDefinition = CollectionDefinition<Record<string, BlockProperty>, Record<string, AnyBlockDefinition>>;
|
|
4058
4111
|
type CollectionWithName = Omit<AnyCollectionDefinition, 'blocks'> & {
|
|
@@ -9760,6 +9813,10 @@ declare const CMS_ERRORS: {
|
|
|
9760
9813
|
readonly status: 404;
|
|
9761
9814
|
readonly message: "Parent block not found";
|
|
9762
9815
|
};
|
|
9816
|
+
readonly BLOCK_NOT_ALLOWED_IN_PARENT: {
|
|
9817
|
+
readonly status: 400;
|
|
9818
|
+
readonly message: "This block type is not allowed inside the target parent";
|
|
9819
|
+
};
|
|
9763
9820
|
readonly ROOT_NOT_FOUND: {
|
|
9764
9821
|
readonly status: 404;
|
|
9765
9822
|
readonly message: "Root block not found in snapshot";
|
|
@@ -10220,6 +10277,15 @@ declare function defineRoot<const TProps extends Record<string, BlockProperty>>(
|
|
|
10220
10277
|
/**
|
|
10221
10278
|
* Defines a content collection with a root block and a set of child blocks.
|
|
10222
10279
|
*
|
|
10280
|
+
* Blocks may be referenced (`blocks: { hero }`) or written inline
|
|
10281
|
+
* (`blocks: { hero: { label, properties } }`). For the inline form the `blocks`
|
|
10282
|
+
* parameter is shaped as `{ [K in keyof TBlocks]: AnyBlockDefinition } & TBlocks`
|
|
10283
|
+
* — the mapped half gives each block value the concrete `BlockDefinition`
|
|
10284
|
+
* contextual type so editors autocomplete its fields (`label`, `properties`,
|
|
10285
|
+
* `events`, …) instead of falling back to globals, while the `& TBlocks` half
|
|
10286
|
+
* keeps inferring each block's specific shape (so the typed create/update API
|
|
10287
|
+
* and `structure` autocomplete still see the exact block keys and properties).
|
|
10288
|
+
*
|
|
10223
10289
|
* @example
|
|
10224
10290
|
* ```ts
|
|
10225
10291
|
* const pages = defineCollection({
|
|
@@ -10229,7 +10295,11 @@ declare function defineRoot<const TProps extends Record<string, BlockProperty>>(
|
|
|
10229
10295
|
* });
|
|
10230
10296
|
* ```
|
|
10231
10297
|
*/
|
|
10232
|
-
declare function defineCollection<const TProps extends Record<string, BlockProperty>, const TBlocks extends Record<string, AnyBlockDefinition> = Record<string, never>>(collection: CollectionDefinition<TProps, TBlocks>
|
|
10298
|
+
declare function defineCollection<const TProps extends Record<string, BlockProperty>, const TBlocks extends Record<string, AnyBlockDefinition> = Record<string, never>>(collection: Omit<CollectionDefinition<TProps, TBlocks>, 'blocks'> & {
|
|
10299
|
+
blocks?: {
|
|
10300
|
+
[K in keyof TBlocks]: AnyBlockDefinition;
|
|
10301
|
+
} & TBlocks;
|
|
10302
|
+
}): CollectionDefinition<TProps, TBlocks>;
|
|
10233
10303
|
type ExtractReferencedCollections<T extends Record<string, AnyCollectionDefinition>> = {
|
|
10234
10304
|
[K in keyof T]: T[K] extends CollectionDefinition<infer _P, infer TBlocks> ? {
|
|
10235
10305
|
[B in keyof TBlocks]: TBlocks[B] extends BlockDefinition<infer BProps, any> ? {
|
|
@@ -10274,4 +10344,4 @@ declare function defineCollections<const TCollections extends Record<string, Any
|
|
|
10274
10344
|
declare function defineAuthMiddleware(middleware: CMSMiddleware): CMSMiddleware;
|
|
10275
10345
|
|
|
10276
10346
|
export { CMSClientError, CMSError, CMS_ERRORS, buildFullPath, cmsContext, cmsMeta, createCMS, createCMSClient, createCMSEndpoint, createCMSQuery, defineAuthMiddleware, defineBlock, defineCollection, defineCollections, defineColumns, defineCoreSchema, definePluginSchema, defineRoot, defineTable, getCMSErrorCode, isCMSError, newId, normalizeSlug, registerIdPrefix, rootRevalidateTag, runPruningPass, splitPath, trackingId };
|
|
10277
|
-
export type { AnyBlockDefinition, AnyCollectionDefinition, Approval, Asset, AssetFolder, BlockDefinition, BlockEventFire, BlockEventNames, BlockProperty, BlockPropertyType, BlockTreeNode, BlockVersion, Branch, BranchListItem, CMSAPIError, CMSAfterHook, CMSAfterHookContext, CMSAtomListener, CMSBeforeHook, CMSBeforeHookContext, CMSClientInstance, CMSClientOptions, CMSClientPlugin, CMSClientStore, CMSConfigHooks, CMSCoreRootPruningPlan, CMSDefinition, CMSEndpointContext, CMSEndpointCtx, CMSEndpointKey, CMSEndpointMeta, CMSErrorCode, CMSFetch, CMSHandlerCtx, CMSHookAction, CMSHooks, CMSMiddleware, CMSMiddlewareCtx, CMSMiddlewareRequest, CMSOperation, CMSPlugin, CMSPluginContext, CMSPluginInitOptions, CMSPluginInitResult, CMSPluginPruning, CMSPluginPruningExecuteContext, CMSPluginPruningExecuteResult, CMSPluginPruningMetrics, CMSPluginPruningPlanContext, CMSPluginRootPruningPlan, CMSProcedureCtx, CMSSchemaConfig, CMSSystemHandlerCtx, CMSUserConfig, CollectionDefinition, CollectionWithName, CommentMention, CommentMessage, CommentThread, Commit, CommitSnapshot, ContentUsage, DataRetentionConfig, DrizzleInstance, EnumMap, EventDeclaration, IdPrefix, InferBlockInput, InferBlockProperties, InferBlockTreeNode, InferCreateBlockInput, InferEventParams, InferPartialBlockProperties, InferPluginContext, InferPluginEndpoints, InferPluginErrorCodes, InferUpdateBlockInput, ListBranchesResult, ListMergeRequestsResult, ListNotificationsResult, ListRootsResult, MediaUploadFileState, MediaUploadOptions, MediaUploadState, MergeConflict, MergeRequest, MergeRequestListItem, MiddlewareResult, NewApproval, NewAsset, NewAssetFolder, NewBlockVersion, NewBranch, NewCommentMention, NewCommentMessage, NewCommentThread, NewCommit, NewCommitSnapshot, NewContentUsage, NewMergeConflict, NewMergeRequest, NewNotification, NewPublication, NewRedirect, NewRoot, NewSearchIndex, NewTemplate, NewTemplateVariableUsage, NewVariable, Notification, NotificationInput, NotificationListItem, NotificationPayload, NotificationService, NotificationType, OnNotificationHandler, PruningPassOptions, PruningPassResult, PruningResult, Publication, QueryState, Redirect, ResolvedReference, ResolvedScope, ResolvedSlugConfig, RevalidateConfig, RevalidateEvent, RevalidateHandler, Root, RootDefinition, RootListItem, RootSummary, ScalarBlockProperty, SchemaModule, ScopeConditionFactory, SearchIndex, TableMap, TableScope, Template, TemplateVariableUsage, Variable };
|
|
10347
|
+
export type { AnyBlockDefinition, AnyCollectionDefinition, Approval, Asset, AssetFolder, BlockDefinition, BlockEventFire, BlockEventNames, BlockProperty, BlockPropertyType, BlockStructureEntry, BlockTreeNode, BlockVersion, Branch, BranchListItem, CMSAPIError, CMSAfterHook, CMSAfterHookContext, CMSAtomListener, CMSBeforeHook, CMSBeforeHookContext, CMSClientInstance, CMSClientOptions, CMSClientPlugin, CMSClientStore, CMSConfigHooks, CMSCoreRootPruningPlan, CMSDefinition, CMSEndpointContext, CMSEndpointCtx, CMSEndpointKey, CMSEndpointMeta, CMSErrorCode, CMSFetch, CMSHandlerCtx, CMSHookAction, CMSHooks, CMSMiddleware, CMSMiddlewareCtx, CMSMiddlewareRequest, CMSOperation, CMSPlugin, CMSPluginContext, CMSPluginInitOptions, CMSPluginInitResult, CMSPluginPruning, CMSPluginPruningExecuteContext, CMSPluginPruningExecuteResult, CMSPluginPruningMetrics, CMSPluginPruningPlanContext, CMSPluginRootPruningPlan, CMSProcedureCtx, CMSSchemaConfig, CMSSystemHandlerCtx, CMSUserConfig, CollectionDefinition, CollectionStructure, CollectionWithName, CommentMention, CommentMessage, CommentThread, Commit, CommitSnapshot, ContentUsage, DataRetentionConfig, DrizzleInstance, EnumMap, EventDeclaration, IdPrefix, InferBlockInput, InferBlockProperties, InferBlockTreeNode, InferCreateBlockInput, InferEventParams, InferPartialBlockProperties, InferPluginContext, InferPluginEndpoints, InferPluginErrorCodes, InferUpdateBlockInput, ListBranchesResult, ListMergeRequestsResult, ListNotificationsResult, ListRootsResult, MediaUploadFileState, MediaUploadOptions, MediaUploadState, MergeConflict, MergeRequest, MergeRequestListItem, MiddlewareResult, NewApproval, NewAsset, NewAssetFolder, NewBlockVersion, NewBranch, NewCommentMention, NewCommentMessage, NewCommentThread, NewCommit, NewCommitSnapshot, NewContentUsage, NewMergeConflict, NewMergeRequest, NewNotification, NewPublication, NewRedirect, NewRoot, NewSearchIndex, NewTemplate, NewTemplateVariableUsage, NewVariable, Notification, NotificationInput, NotificationListItem, NotificationPayload, NotificationService, NotificationType, OnNotificationHandler, PruningPassOptions, PruningPassResult, PruningResult, Publication, QueryState, Redirect, ResolvedReference, ResolvedScope, ResolvedSlugConfig, RevalidateConfig, RevalidateEvent, RevalidateHandler, Root, RootDefinition, RootListItem, RootSummary, ScalarBlockProperty, SchemaModule, ScopeConditionFactory, SearchIndex, TableMap, TableScope, Template, TemplateVariableUsage, Variable };
|
package/dist/index.js
CHANGED
|
@@ -1051,6 +1051,10 @@ const CMS_ERRORS = {
|
|
|
1051
1051
|
status: 404,
|
|
1052
1052
|
message: 'Parent block not found'
|
|
1053
1053
|
},
|
|
1054
|
+
BLOCK_NOT_ALLOWED_IN_PARENT: {
|
|
1055
|
+
status: 400,
|
|
1056
|
+
message: 'This block type is not allowed inside the target parent'
|
|
1057
|
+
},
|
|
1054
1058
|
ROOT_NOT_FOUND: {
|
|
1055
1059
|
status: 404,
|
|
1056
1060
|
message: 'Root block not found in snapshot'
|
|
@@ -4949,6 +4953,88 @@ notFound = 'ROOT_NOT_FOUND') {
|
|
|
4949
4953
|
}
|
|
4950
4954
|
}
|
|
4951
4955
|
|
|
4956
|
+
/** Builds the {@link PlacementIndex} from a collection's `structure` + blocks. */ function buildPlacementIndex(structure, blocks) {
|
|
4957
|
+
const rules = new Map();
|
|
4958
|
+
if (structure) {
|
|
4959
|
+
for (const [parent, entry] of Object.entries(structure)){
|
|
4960
|
+
if (!entry) continue;
|
|
4961
|
+
const accepts = entry.accepts;
|
|
4962
|
+
if (Array.isArray(accepts)) {
|
|
4963
|
+
// Concrete whitelist (incl. the empty "holds nothing" list).
|
|
4964
|
+
rules.set(parent, {
|
|
4965
|
+
mode: 'only',
|
|
4966
|
+
set: new Set(accepts)
|
|
4967
|
+
});
|
|
4968
|
+
} else if (entry.excludes && entry.excludes.length > 0) {
|
|
4969
|
+
// Open base ('*' or omitted) minus an explicit blacklist.
|
|
4970
|
+
rules.set(parent, {
|
|
4971
|
+
mode: 'except',
|
|
4972
|
+
set: new Set(entry.excludes)
|
|
4973
|
+
});
|
|
4974
|
+
}
|
|
4975
|
+
// else: open ('*' / nothing, no excludes) — no rule.
|
|
4976
|
+
}
|
|
4977
|
+
}
|
|
4978
|
+
const containers = new Set();
|
|
4979
|
+
if (blocks) {
|
|
4980
|
+
for (const [name, def] of Object.entries(blocks)){
|
|
4981
|
+
if (def.allowChildren === true) containers.add(name);
|
|
4982
|
+
}
|
|
4983
|
+
}
|
|
4984
|
+
return {
|
|
4985
|
+
rules,
|
|
4986
|
+
containers
|
|
4987
|
+
};
|
|
4988
|
+
}
|
|
4989
|
+
/**
|
|
4990
|
+
* Throws `BLOCK_NOT_ALLOWED_IN_PARENT` when placing a `childType` block under a
|
|
4991
|
+
* `parentType` block would violate the collection's rules. `parentType` must be
|
|
4992
|
+
* the literal `'root'` when the parent is the collection root.
|
|
4993
|
+
*
|
|
4994
|
+
* Two gates, in order:
|
|
4995
|
+
* 1. Container gate — a non-root parent whose `allowChildren` is not `true`
|
|
4996
|
+
* rejects every child.
|
|
4997
|
+
* 2. Acceptance gate — the parent's `accepts`/`excludes` rule, if any:
|
|
4998
|
+
* `only` rejects a child not in the set; `except` rejects a child in the set.
|
|
4999
|
+
*
|
|
5000
|
+
* A parent with no rule (open) and the root (always a container) pass gate 1
|
|
5001
|
+
* and/or gate 2 trivially, so collections without a `structure` map only feel
|
|
5002
|
+
* the `allowChildren` gate.
|
|
5003
|
+
*/ function assertPlacementAllowed(index, childType, parentType) {
|
|
5004
|
+
if (parentType !== 'root' && !index.containers.has(parentType)) throw new CMSError('BLOCK_NOT_ALLOWED_IN_PARENT', {
|
|
5005
|
+
message: `Block '${parentType}' does not accept child blocks (its 'allowChildren' is not set)`,
|
|
5006
|
+
data: {
|
|
5007
|
+
childType,
|
|
5008
|
+
parentType,
|
|
5009
|
+
reason: 'not-a-container'
|
|
5010
|
+
}
|
|
5011
|
+
});
|
|
5012
|
+
const rule = index.rules.get(parentType);
|
|
5013
|
+
if (!rule) return;
|
|
5014
|
+
if (rule.mode === 'only' && !rule.set.has(childType)) throw new CMSError('BLOCK_NOT_ALLOWED_IN_PARENT', {
|
|
5015
|
+
message: `Block '${parentType}' accepts only [${[
|
|
5016
|
+
...rule.set
|
|
5017
|
+
].join(', ')}] — ` + `'${childType}' is not allowed inside it`,
|
|
5018
|
+
data: {
|
|
5019
|
+
childType,
|
|
5020
|
+
parentType,
|
|
5021
|
+
accepts: [
|
|
5022
|
+
...rule.set
|
|
5023
|
+
]
|
|
5024
|
+
}
|
|
5025
|
+
});
|
|
5026
|
+
if (rule.mode === 'except' && rule.set.has(childType)) throw new CMSError('BLOCK_NOT_ALLOWED_IN_PARENT', {
|
|
5027
|
+
message: `Block '${childType}' is not allowed inside '${parentType}' (excluded)`,
|
|
5028
|
+
data: {
|
|
5029
|
+
childType,
|
|
5030
|
+
parentType,
|
|
5031
|
+
excludes: [
|
|
5032
|
+
...rule.set
|
|
5033
|
+
]
|
|
5034
|
+
}
|
|
5035
|
+
});
|
|
5036
|
+
}
|
|
5037
|
+
|
|
4952
5038
|
function assembleBlockTree(blocks, rootId) {
|
|
4953
5039
|
const deletedBlockIds = new Set();
|
|
4954
5040
|
const nodeMap = new Map();
|
|
@@ -5543,6 +5629,10 @@ const blockTreeNodeSchema = z.lazy(()=>z.object({
|
|
|
5543
5629
|
function createBlocksEndpoints(def, cmsCtx) {
|
|
5544
5630
|
const { db } = cmsCtx;
|
|
5545
5631
|
const collectionName = def.name;
|
|
5632
|
+
// Derived once per collection: the placement rules the create/move/duplicate
|
|
5633
|
+
// routes enforce — `accepts`/`excludes` from `structure` plus the
|
|
5634
|
+
// `allowChildren` container gate from the block defs.
|
|
5635
|
+
const placementIndex = buildPlacementIndex(def.structure, def.blocks);
|
|
5546
5636
|
return {
|
|
5547
5637
|
/**
|
|
5548
5638
|
* Creates a new root (page/entry) with initial draft branch and commit.
|
|
@@ -5915,6 +6005,10 @@ function createBlocksEndpoints(def, cmsCtx) {
|
|
|
5915
6005
|
if (parentVersion.deleted) throw new CMSError('BLOCK_ALREADY_DELETED', {
|
|
5916
6006
|
message: errorMessages.blockAlreadyDeleted(parentBlockId)
|
|
5917
6007
|
});
|
|
6008
|
+
// Enforce the collection's placement rules. The root block's id equals
|
|
6009
|
+
// the rootId and is stored with `type === collectionName`, so normalize
|
|
6010
|
+
// it to the literal 'root' the structure map keys on.
|
|
6011
|
+
assertPlacementAllowed(placementIndex, type, parentBlockId === rootId ? 'root' : parentVersion.type);
|
|
5918
6012
|
const childBlockId = newId('block');
|
|
5919
6013
|
const blockProps = properties ?? {};
|
|
5920
6014
|
const newChildrenArray = [
|
|
@@ -6122,6 +6216,7 @@ function createBlocksEndpoints(def, cmsCtx) {
|
|
|
6122
6216
|
if (newParent.deleted) throw new CMSError('BLOCK_ALREADY_DELETED', {
|
|
6123
6217
|
message: errorMessages.blockAlreadyDeleted(input.newParentBlockId)
|
|
6124
6218
|
});
|
|
6219
|
+
assertPlacementAllowed(placementIndex, movedBlock.type, input.newParentBlockId === input.rootId ? 'root' : newParent.type);
|
|
6125
6220
|
const oldChildren = (oldParent.children ?? []).filter((id)=>id !== input.blockId);
|
|
6126
6221
|
const newChildren = [
|
|
6127
6222
|
...newParent.children ?? []
|
|
@@ -6409,6 +6504,7 @@ function createBlocksEndpoints(def, cmsCtx) {
|
|
|
6409
6504
|
if (parentVersion.deleted) throw new CMSError('BLOCK_ALREADY_DELETED', {
|
|
6410
6505
|
message: errorMessages.blockAlreadyDeleted(input.targetParentBlockId)
|
|
6411
6506
|
});
|
|
6507
|
+
assertPlacementAllowed(placementIndex, sourceVersion.type, input.targetParentBlockId === input.rootId ? 'root' : parentVersion.type);
|
|
6412
6508
|
const topLevelCopyId = copies[0].newBlockId;
|
|
6413
6509
|
const updatedChildren = [
|
|
6414
6510
|
...parentVersion.children ?? []
|
|
@@ -13700,6 +13796,15 @@ function definePluginSchema(schema) {
|
|
|
13700
13796
|
/**
|
|
13701
13797
|
* Defines a content collection with a root block and a set of child blocks.
|
|
13702
13798
|
*
|
|
13799
|
+
* Blocks may be referenced (`blocks: { hero }`) or written inline
|
|
13800
|
+
* (`blocks: { hero: { label, properties } }`). For the inline form the `blocks`
|
|
13801
|
+
* parameter is shaped as `{ [K in keyof TBlocks]: AnyBlockDefinition } & TBlocks`
|
|
13802
|
+
* — the mapped half gives each block value the concrete `BlockDefinition`
|
|
13803
|
+
* contextual type so editors autocomplete its fields (`label`, `properties`,
|
|
13804
|
+
* `events`, …) instead of falling back to globals, while the `& TBlocks` half
|
|
13805
|
+
* keeps inferring each block's specific shape (so the typed create/update API
|
|
13806
|
+
* and `structure` autocomplete still see the exact block keys and properties).
|
|
13807
|
+
*
|
|
13703
13808
|
* @example
|
|
13704
13809
|
* ```ts
|
|
13705
13810
|
* const pages = defineCollection({
|
package/dist/next/index.d.cts
CHANGED
|
@@ -68,12 +68,56 @@ type BlockDefinition<TProps extends Record<string, BlockProperty> = Record<strin
|
|
|
68
68
|
allowChildren?: false;
|
|
69
69
|
} | {
|
|
70
70
|
allowChildren: true;
|
|
71
|
-
allowedChildBlocks?: string[];
|
|
72
71
|
});
|
|
73
72
|
type AnyBlockDefinition = BlockDefinition<Record<string, BlockProperty>, Record<string, EventDeclaration>>;
|
|
74
73
|
type RootDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
|
|
75
74
|
properties: TProps;
|
|
76
75
|
};
|
|
76
|
+
/**
|
|
77
|
+
* One PARENT's placement rule inside a collection's {@link CollectionStructure}
|
|
78
|
+
* — declares which child block types that parent (or the literal `'root'`) may
|
|
79
|
+
* contain. There are three mutually-exclusive modes, enforced by the type:
|
|
80
|
+
*
|
|
81
|
+
* - **open** — `{}` or `{ accepts: '*' }`: holds any block. (Same as having no
|
|
82
|
+
* entry at all; `'*'` is just an explicit, readable form.)
|
|
83
|
+
* - **whitelist** — `{ accepts: ['a', 'b'] }`: holds ONLY `a`/`b`. Fail-closed —
|
|
84
|
+
* a block added to the collection later is rejected until listed. `excludes`
|
|
85
|
+
* is forbidden here (a concrete `accepts` already says exactly what's allowed).
|
|
86
|
+
* - **blacklist** — `{ excludes: ['z'] }` (or `{ accepts: '*', excludes: ['z'] }`):
|
|
87
|
+
* holds anything EXCEPT `z`. Fail-open — a block added later is accepted.
|
|
88
|
+
*
|
|
89
|
+
* Whether a parent accepts children AT ALL is the separate, coarser
|
|
90
|
+
* `allowChildren` gate on the block (the root always accepts children); these
|
|
91
|
+
* rules only refine WHICH children an accepting parent may hold.
|
|
92
|
+
*/
|
|
93
|
+
type BlockStructureEntry<TBlockName extends string> = {
|
|
94
|
+
/** `'*'` = open base (optional, for readability). */
|
|
95
|
+
accepts?: '*';
|
|
96
|
+
/** Holds anything except these. */
|
|
97
|
+
excludes?: readonly TBlockName[];
|
|
98
|
+
} | {
|
|
99
|
+
/** Holds ONLY these block types. */
|
|
100
|
+
accepts: readonly TBlockName[];
|
|
101
|
+
/**
|
|
102
|
+
* Forbidden alongside a concrete `accepts` list — the list already names
|
|
103
|
+
* exactly what is allowed, so `excludes` would be ignored.
|
|
104
|
+
*/
|
|
105
|
+
excludes?: "Remove 'excludes': a concrete 'accepts' list already defines exactly which blocks are allowed. Use accepts: '*' with excludes for an all-except list.";
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Placement rules for a collection, keyed by PARENT block name (or the literal
|
|
109
|
+
* `'root'` for the top level). Open by default: a parent with no entry holds any
|
|
110
|
+
* block. The keys and the `accepts` / `excludes` block names autocomplete against
|
|
111
|
+
* the collection's block names and are checked at compile time by
|
|
112
|
+
* {@link defineCollection} (the field type alone enforces this — no extra step).
|
|
113
|
+
*
|
|
114
|
+
* This is the single source of truth that the visual editor (drop-zone gating)
|
|
115
|
+
* and the server guard (createBlock / moveBlock / duplicateBlock) both read,
|
|
116
|
+
* alongside each block's `allowChildren` flag, so they can never diverge.
|
|
117
|
+
*/
|
|
118
|
+
type CollectionStructure<TBlocks extends Record<string, AnyBlockDefinition>> = {
|
|
119
|
+
[K in keyof TBlocks | 'root']?: BlockStructureEntry<keyof TBlocks & string>;
|
|
120
|
+
};
|
|
77
121
|
type SlugConfig = {
|
|
78
122
|
enabled: false;
|
|
79
123
|
} | {
|
|
@@ -97,6 +141,15 @@ type CollectionDefinition<TProps extends Record<string, BlockProperty> = Record<
|
|
|
97
141
|
* regardless of this flag). Any collection can still be a reference target.
|
|
98
142
|
*/
|
|
99
143
|
reusableBlock?: boolean;
|
|
144
|
+
/**
|
|
145
|
+
* Placement rules keyed by PARENT block name (or `'root'`) — which children
|
|
146
|
+
* each container may hold, via `accepts` (whitelist) / `excludes` (blacklist)
|
|
147
|
+
* (see {@link CollectionStructure}). Read by the editor and the server guard
|
|
148
|
+
* together with each block's `allowChildren` flag. Open by default; block
|
|
149
|
+
* names are checked at compile time by the field type itself, so a typo is a
|
|
150
|
+
* compile error at the `defineCollection` call site.
|
|
151
|
+
*/
|
|
152
|
+
structure?: CollectionStructure<TBlocks>;
|
|
100
153
|
};
|
|
101
154
|
type AnyCollectionDefinition = CollectionDefinition<Record<string, BlockProperty>, Record<string, AnyBlockDefinition>>;
|
|
102
155
|
type RevalidateEvent<TCollections extends Record<string, AnyCollectionDefinition> = Record<string, AnyCollectionDefinition>> = {
|