@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 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>): 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>): 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({
@@ -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>> = {