@storyblok/schema 0.1.0-alpha.1 → 0.1.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/field-plugins/index.cjs +4 -0
  2. package/dist/field-plugins/index.d.cts +2 -0
  3. package/dist/field-plugins/index.d.mts +2 -0
  4. package/dist/field-plugins/index.mjs +3 -0
  5. package/dist/field-plugins/storyblok-color-field.cjs +32 -0
  6. package/dist/field-plugins/storyblok-color-field.cjs.map +1 -0
  7. package/dist/field-plugins/storyblok-color-field.d.cts +16 -0
  8. package/dist/field-plugins/storyblok-color-field.d.mts +16 -0
  9. package/dist/field-plugins/storyblok-color-field.mjs +32 -0
  10. package/dist/field-plugins/storyblok-color-field.mjs.map +1 -0
  11. package/dist/generated/overlay/zod.gen.cjs +0 -2
  12. package/dist/generated/overlay/zod.gen.cjs.map +1 -1
  13. package/dist/generated/overlay/zod.gen.mjs +228 -229
  14. package/dist/generated/overlay/zod.gen.mjs.map +1 -1
  15. package/dist/generated/types/field.d.cts +31 -15
  16. package/dist/generated/types/field.d.mts +31 -15
  17. package/dist/generated/types/mapi-story.d.cts +4 -4
  18. package/dist/generated/types/mapi-story.d.mts +4 -4
  19. package/dist/generated/types/story.d.cts +3 -3
  20. package/dist/generated/types/story.d.mts +3 -3
  21. package/dist/helpers/define-field-plugin.cjs +21 -0
  22. package/dist/helpers/define-field-plugin.cjs.map +1 -0
  23. package/dist/helpers/define-field-plugin.d.cts +33 -0
  24. package/dist/helpers/define-field-plugin.d.mts +33 -0
  25. package/dist/helpers/define-field-plugin.mjs +20 -0
  26. package/dist/helpers/define-field-plugin.mjs.map +1 -0
  27. package/dist/helpers/define-schema.cjs +22 -0
  28. package/dist/helpers/define-schema.cjs.map +1 -0
  29. package/dist/helpers/define-schema.d.cts +28 -0
  30. package/dist/helpers/define-schema.d.mts +28 -0
  31. package/dist/helpers/define-schema.mjs +21 -0
  32. package/dist/helpers/define-schema.mjs.map +1 -0
  33. package/dist/helpers/schema-type.d.cts +16 -8
  34. package/dist/helpers/schema-type.d.mts +16 -8
  35. package/dist/index.cjs +4 -0
  36. package/dist/index.d.cts +3 -1
  37. package/dist/index.d.mts +3 -1
  38. package/dist/index.mjs +3 -1
  39. package/dist/validators/internal-schemas.mjs +1 -1
  40. package/dist/validators/shapes.cjs.map +1 -1
  41. package/dist/validators/shapes.d.cts +10 -1
  42. package/dist/validators/shapes.d.mts +10 -1
  43. package/dist/validators/shapes.mjs.map +1 -1
  44. package/dist/validators/validate-schema.cjs +21 -1
  45. package/dist/validators/validate-schema.cjs.map +1 -1
  46. package/dist/validators/validate-schema.d.cts +2 -1
  47. package/dist/validators/validate-schema.d.mts +2 -1
  48. package/dist/validators/validate-schema.mjs +21 -1
  49. package/dist/validators/validate-schema.mjs.map +1 -1
  50. package/dist/validators/validate-story.cjs +40 -12
  51. package/dist/validators/validate-story.cjs.map +1 -1
  52. package/dist/validators/validate-story.mjs +40 -13
  53. package/dist/validators/validate-story.mjs.map +1 -1
  54. package/package.json +13 -2
@@ -1 +1 @@
1
- {"version":3,"file":"validate-story.cjs","names":["zAssetFieldValue","zMultilinkFieldValue","zTableFieldValue","zRichtextFieldValue","zPluginFieldValue","isRecord","toValues"],"sources":["../../src/validators/validate-story.ts"],"sourcesContent":["import type { StandardSchemaV1 } from '@standard-schema/spec';\nimport type { SchemaBlockLike, SchemaFieldLike, SchemaLike } from './shapes';\nimport type { ValidationIssue, ValidationResult } from './types';\nimport {\n zAssetFieldValue,\n zMultilinkFieldValue,\n zPluginFieldValue,\n zRichtextFieldValue,\n zTableFieldValue,\n} from './internal-schemas';\nimport { isRecord, toValues } from './shapes';\n\n/** Field-content keys that are not user-defined fields. */\nconst RESERVED_KEYS = new Set(['_uid', 'component', '_editable']);\n\n/** Maps a Standard Schema validator to a {@link ValidationIssue} reporter at `path`. */\nfunction checkValue(\n schema: StandardSchemaV1,\n value: unknown,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n const result = schema['~standard'].validate(value);\n // The Zod schemas are synchronous; a thenable result would indicate misuse.\n if (result instanceof Promise) {\n return;\n }\n if (result.issues) {\n for (const issue of result.issues) {\n const issuePath = (issue.path ?? []).map(segment =>\n (typeof segment === 'object' && segment !== null ? String(segment.key) : (segment as string | number)),\n );\n issues.push({\n severity: 'error',\n code: 'invalid_value',\n path: [...path, ...issuePath],\n entity,\n message: issue.message,\n });\n }\n }\n}\n\nfunction validateFieldValue(\n field: SchemaFieldLike,\n value: unknown,\n blocksByName: Map<string, SchemaBlockLike>,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n switch (field.type) {\n case 'asset':\n checkValue(zAssetFieldValue, value, path, entity, issues);\n break;\n case 'multiasset':\n if (!Array.isArray(value)) {\n pushTypeIssue(value, 'array', path, entity, issues);\n break;\n }\n value.forEach((item, index) => checkValue(zAssetFieldValue, item, [...path, index], entity, issues));\n checkCount(value.length, field.minimum_entries, field.maximum_entries, 'asset(s)', path, entity, issues);\n break;\n case 'multilink':\n checkValue(zMultilinkFieldValue, value, path, entity, issues);\n break;\n case 'table':\n checkValue(zTableFieldValue, value, path, entity, issues);\n break;\n case 'richtext':\n checkValue(zRichtextFieldValue, value, path, entity, issues);\n validateRichtextBloks(value, blocksByName, path, issues);\n break;\n case 'custom':\n checkValue(zPluginFieldValue, value, path, entity, issues);\n break;\n case 'bloks':\n if (!Array.isArray(value)) {\n pushTypeIssue(value, 'array', path, entity, issues);\n break;\n }\n checkCount(value.length, field.minimum, field.maximum, 'block(s)', path, entity, issues);\n value.forEach((item, index) => {\n if (\n field.allow && field.allow.length > 0\n && isRecord(item) && typeof item.component === 'string'\n && !field.allow.includes(item.component)\n ) {\n issues.push({\n severity: 'error',\n code: 'disallowed_component',\n path: [...path, index, 'component'],\n entity,\n message: `Component \"${item.component}\" is not allowed in field \"${field.name}\"; allowed: ${field.allow.join(', ')}.`,\n });\n }\n validateBlokContent(item, blocksByName, [...path, index], issues);\n });\n break;\n case 'text':\n case 'textarea':\n case 'markdown':\n if (typeof value !== 'string') {\n pushTypeIssue(value, 'string', path, entity, issues);\n break;\n }\n checkStringLength(field, value, path, entity, issues);\n break;\n case 'option':\n case 'datetime':\n if (typeof value !== 'string') {\n pushTypeIssue(value, 'string', path, entity, issues);\n }\n break;\n case 'number':\n if (typeof value !== 'number') {\n pushTypeIssue(value, 'number', path, entity, issues);\n break;\n }\n if (field.min_value != null && value < field.min_value) {\n pushConstraint(`Value ${value} is below the minimum of ${field.min_value}.`, path, entity, issues);\n }\n if (field.max_value != null && value > field.max_value) {\n pushConstraint(`Value ${value} exceeds the maximum of ${field.max_value}.`, path, entity, issues);\n }\n if (field.decimals != null && decimalPlaces(value) > field.decimals) {\n pushConstraint(`Value ${value} has more than ${field.decimals} decimal place(s).`, path, entity, issues);\n }\n if (field.steps != null && field.steps > 0 && !isMultipleOf(value, field.steps, field.min_value ?? 0)) {\n const base = field.min_value ?? 0;\n pushConstraint(\n `Value ${value} is not a multiple of the step ${field.steps}${base ? ` (offset from ${base})` : ''}.`,\n path,\n entity,\n issues,\n );\n }\n break;\n case 'boolean':\n if (typeof value !== 'boolean') {\n pushTypeIssue(value, 'boolean', path, entity, issues);\n }\n break;\n case 'options':\n if (!Array.isArray(value) || value.some(item => typeof item !== 'string')) {\n pushTypeIssue(value, 'string[]', path, entity, issues);\n break;\n }\n checkCount(value.length, toCount(field.min_options), toCount(field.max_options), 'option(s)', path, entity, issues);\n break;\n case 'section':\n case 'tab':\n // Layout-only field types carry no content value.\n break;\n default:\n // Exhaustiveness guard: when a new `FieldType` is added, this fails to\n // compile until the field type is handled (or explicitly skipped) above.\n field.type satisfies never;\n break;\n }\n}\n\n/** Reports a constraint (bound/length/count) violation as an error issue. */\nfunction pushConstraint(\n message: string,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n issues.push({ severity: 'error', code: 'constraint_violation', path, entity, message });\n}\n\n/** Checks an array length against optional inclusive `min`/`max` bounds. */\nfunction checkCount(\n length: number,\n min: number | undefined,\n max: number | undefined,\n noun: string,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n if (min != null && length < min) {\n pushConstraint(`Expected at least ${min} ${noun}, received ${length}.`, path, entity, issues);\n }\n if (max != null && length > max) {\n pushConstraint(`Expected at most ${max} ${noun}, received ${length}.`, path, entity, issues);\n }\n}\n\n/** Checks a string against optional `max_length`/`maxlength` and `minlength` bounds. */\nfunction checkStringLength(\n field: SchemaFieldLike,\n value: string,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n const max = field.max_length ?? field.maxlength;\n if (max != null && value.length > max) {\n pushConstraint(`Text length ${value.length} exceeds the maximum of ${max}.`, path, entity, issues);\n }\n if (field.minlength != null && value.length < field.minlength) {\n pushConstraint(`Text length ${value.length} is below the minimum of ${field.minlength}.`, path, entity, issues);\n }\n}\n\n/** Counts a number's fractional digits, handling exponential notation (e.g. `1e-7` → 7). */\nfunction decimalPlaces(value: number): number {\n if (!Number.isFinite(value)) {\n return 0;\n }\n const text = String(value).toLowerCase();\n const [mantissa, exponent] = text.split('e');\n const fractionDigits = mantissa.includes('.') ? mantissa.split('.')[1].length : 0;\n // A negative exponent (e.g. `1e-7`) adds that many fractional digits.\n return exponent ? Math.max(0, fractionDigits - Number(exponent)) : fractionDigits;\n}\n\n/** Whether `value` lands on a `step` increment offset from `base`, with float tolerance. */\nfunction isMultipleOf(value: number, step: number, base: number): boolean {\n const ratio = (value - base) / step;\n const tolerance = 1e-9 * Math.max(1, Math.abs(ratio));\n return Math.abs(ratio - Math.round(ratio)) <= tolerance;\n}\n\n/** Parses a numeric constraint stored as a string (e.g. `min_options`). Empty/non-numeric → undefined. */\nfunction toCount(value: string | undefined): number | undefined {\n if (value == null || value === '') {\n return undefined;\n }\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : undefined;\n}\n\nfunction pushTypeIssue(\n value: unknown,\n expected: string,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n issues.push({\n severity: 'error',\n code: 'invalid_value',\n path,\n entity,\n message: `Expected ${expected}, received ${value === null ? 'null' : typeof value}.`,\n });\n}\n\n/** Walks richtext `content` nodes and validates embedded bloks (`type: 'blok'`). */\nfunction validateRichtextBloks(\n value: unknown,\n blocksByName: Map<string, SchemaBlockLike>,\n path: (string | number)[],\n issues: ValidationIssue[],\n): void {\n if (!isRecord(value) || !Array.isArray(value.content)) {\n return;\n }\n value.content.forEach((node, index) => {\n if (!isRecord(node)) {\n return;\n }\n if (node.type === 'blok' && isRecord(node.attrs) && Array.isArray(node.attrs.body)) {\n node.attrs.body.forEach((blok, blokIndex) =>\n validateBlokContent(blok, blocksByName, [...path, 'content', index, 'attrs', 'body', blokIndex], issues),\n );\n }\n else if (Array.isArray(node.content)) {\n // Recurse into nested marks/nodes that may themselves embed bloks.\n validateRichtextBloks(node, blocksByName, [...path, 'content', index], issues);\n }\n });\n}\n\n/** Validates a single blok content object against its component definition. */\nfunction validateBlokContent(\n content: unknown,\n blocksByName: Map<string, SchemaBlockLike>,\n path: (string | number)[],\n issues: ValidationIssue[],\n): void {\n if (!isRecord(content)) {\n issues.push({\n severity: 'error',\n code: 'invalid_content',\n path,\n entity: 'story',\n message: 'Expected a block content object.',\n });\n return;\n }\n\n const component = content.component;\n const block = typeof component === 'string' ? blocksByName.get(component) : undefined;\n if (!block) {\n issues.push({\n severity: 'error',\n code: 'unknown_component',\n path: [...path, 'component'],\n entity: 'story',\n message: `Unknown component \"${String(component)}\".`,\n });\n return;\n }\n\n const entity = `block:${block.name}`;\n const fields = block.fields ?? [];\n const fieldsByName = new Map(fields.map(field => [field.name, field]));\n\n for (const key of Object.keys(content)) {\n if (!RESERVED_KEYS.has(key) && !fieldsByName.has(key)) {\n issues.push({\n severity: 'warning',\n code: 'unknown_field',\n path: [...path, key],\n entity,\n message: `Unknown field \"${key}\" on component \"${block.name}\".`,\n });\n }\n }\n\n for (const field of fields) {\n const value = content[field.name];\n if (value === undefined || value === null) {\n if (field.required) {\n issues.push({\n severity: 'error',\n code: 'missing_required_field',\n path: [...path, field.name],\n entity,\n message: `Missing required field \"${field.name}\" on component \"${block.name}\".`,\n });\n }\n continue;\n }\n validateFieldValue(field, value, blocksByName, [...path, field.name], entity, issues);\n }\n}\n\n/**\n * Validates a story's content against a schema without throwing. Reports unknown\n * components (error), unknown fields (warning), missing required fields (error),\n * and invalid field-value shapes (error), recursing into nested `bloks` and\n * richtext-embedded bloks.\n *\n * @example\n * const result = validateStory(story, { blocks: { page, hero } });\n */\nexport function validateStory(story: unknown, schema: SchemaLike): ValidationResult {\n const issues: ValidationIssue[] = [];\n const blocksByName = new Map(toValues(schema.blocks).map(block => [block.name, block]));\n const content = isRecord(story) ? story.content : undefined;\n validateBlokContent(content, blocksByName, ['content'], issues);\n return { ok: issues.every(issue => issue.severity !== 'error'), issues };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"validate-story.cjs","names":["z","zAssetFieldValue","zMultilinkFieldValue","zTableFieldValue","zRichtextFieldValue","isRecord","toValues"],"sources":["../../src/validators/validate-story.ts"],"sourcesContent":["import { z } from 'zod';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport type { SchemaBlockLike, SchemaFieldLike, SchemaLike } from './shapes';\nimport type { ValidationIssue, ValidationResult } from './types';\nimport {\n zAssetFieldValue,\n zMultilinkFieldValue,\n zRichtextFieldValue,\n zTableFieldValue,\n} from './internal-schemas';\nimport { isRecord, toValues } from './shapes';\n\n/** Field-content keys that are not user-defined fields. */\nconst RESERVED_KEYS = new Set(['_uid', 'component', '_editable']);\n\n/**\n * Relaxed plugin envelope used by the `custom` case. Mirrors the generated\n * `zPluginFieldValue` but relaxes `_uid` from a UUID to a plain string, matching\n * the CMS, which persists arbitrary `_uid` strings. Kept local so a codegen\n * regenerate cannot revert it.\n */\nconst zPluginEnvelope = z.object({ plugin: z.string(), _uid: z.optional(z.string()) });\n\n/** Maps a Standard Schema validator to a {@link ValidationIssue} reporter at `path`. */\nfunction checkValue(\n schema: StandardSchemaV1,\n value: unknown,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n const result = schema['~standard'].validate(value);\n // `validateStory` is synchronous. The internal Zod schemas never return a\n // thenable, but a registered field plugin may ship an async validator — which\n // cannot be awaited here. Surface it as an error instead of silently passing,\n // which would report a false `ok: true`.\n if (result instanceof Promise) {\n issues.push({\n severity: 'error',\n code: 'async_validator_unsupported',\n path,\n entity,\n message: 'Field plugin validator is asynchronous; validateStory runs synchronously and cannot await it.',\n });\n return;\n }\n if (result.issues) {\n for (const issue of result.issues) {\n const issuePath = (issue.path ?? []).map(segment =>\n (typeof segment === 'object' && segment !== null ? String(segment.key) : (segment as string | number)),\n );\n issues.push({\n severity: 'error',\n code: 'invalid_value',\n path: [...path, ...issuePath],\n entity,\n message: issue.message,\n });\n }\n }\n}\n\nfunction validateFieldValue(\n field: SchemaFieldLike,\n value: unknown,\n blocksByName: Map<string, SchemaBlockLike>,\n fieldPluginsByType: Map<string, StandardSchemaV1>,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n switch (field.type) {\n case 'asset':\n checkValue(zAssetFieldValue, value, path, entity, issues);\n break;\n case 'multiasset':\n if (!Array.isArray(value)) {\n pushTypeIssue(value, 'array', path, entity, issues);\n break;\n }\n value.forEach((item, index) => checkValue(zAssetFieldValue, item, [...path, index], entity, issues));\n checkCount(value.length, field.minimum_entries, field.maximum_entries, 'asset(s)', path, entity, issues);\n break;\n case 'multilink':\n checkValue(zMultilinkFieldValue, value, path, entity, issues);\n break;\n case 'table':\n checkValue(zTableFieldValue, value, path, entity, issues);\n break;\n case 'richtext':\n checkValue(zRichtextFieldValue, value, path, entity, issues);\n validateRichtextBloks(value, blocksByName, fieldPluginsByType, path, issues);\n break;\n case 'custom': {\n checkValue(zPluginEnvelope, value, path, entity, issues);\n const validator = field.field_type ? fieldPluginsByType.get(field.field_type) : undefined;\n if (validator && isRecord(value)) {\n // Envelope keys sit alongside the plugin's own keys; strip them so the\n // plugin validator sees only its value. Sibling keys keep issue paths\n // accurate (an issue at ['color'] maps to [...path, 'color']).\n const { plugin: _plugin, _uid, ...pluginValue } = value;\n checkValue(validator, pluginValue, path, entity, issues);\n }\n break;\n }\n case 'bloks':\n if (!Array.isArray(value)) {\n pushTypeIssue(value, 'array', path, entity, issues);\n break;\n }\n checkCount(value.length, field.minimum, field.maximum, 'block(s)', path, entity, issues);\n value.forEach((item, index) => {\n if (\n field.allow && field.allow.length > 0\n && isRecord(item) && typeof item.component === 'string'\n && !field.allow.includes(item.component)\n ) {\n issues.push({\n severity: 'error',\n code: 'disallowed_component',\n path: [...path, index, 'component'],\n entity,\n message: `Component \"${item.component}\" is not allowed in field \"${field.name}\"; allowed: ${field.allow.join(', ')}.`,\n });\n }\n validateBlokContent(item, blocksByName, fieldPluginsByType, [...path, index], issues);\n });\n break;\n case 'text':\n case 'textarea':\n case 'markdown':\n if (typeof value !== 'string') {\n pushTypeIssue(value, 'string', path, entity, issues);\n break;\n }\n checkStringLength(field, value, path, entity, issues);\n break;\n case 'option':\n case 'datetime':\n if (typeof value !== 'string') {\n pushTypeIssue(value, 'string', path, entity, issues);\n }\n break;\n case 'number':\n if (typeof value !== 'number') {\n pushTypeIssue(value, 'number', path, entity, issues);\n break;\n }\n if (field.min_value != null && value < field.min_value) {\n pushConstraint(`Value ${value} is below the minimum of ${field.min_value}.`, path, entity, issues);\n }\n if (field.max_value != null && value > field.max_value) {\n pushConstraint(`Value ${value} exceeds the maximum of ${field.max_value}.`, path, entity, issues);\n }\n if (field.decimals != null && decimalPlaces(value) > field.decimals) {\n pushConstraint(`Value ${value} has more than ${field.decimals} decimal place(s).`, path, entity, issues);\n }\n if (field.steps != null && field.steps > 0 && !isMultipleOf(value, field.steps, field.min_value ?? 0)) {\n const base = field.min_value ?? 0;\n pushConstraint(\n `Value ${value} is not a multiple of the step ${field.steps}${base ? ` (offset from ${base})` : ''}.`,\n path,\n entity,\n issues,\n );\n }\n break;\n case 'boolean':\n if (typeof value !== 'boolean') {\n pushTypeIssue(value, 'boolean', path, entity, issues);\n }\n break;\n case 'options':\n if (!Array.isArray(value) || value.some(item => typeof item !== 'string')) {\n pushTypeIssue(value, 'string[]', path, entity, issues);\n break;\n }\n checkCount(value.length, toCount(field.min_options), toCount(field.max_options), 'option(s)', path, entity, issues);\n break;\n case 'section':\n case 'tab':\n // Layout-only field types carry no content value.\n break;\n default:\n // Exhaustiveness guard: when a new `FieldType` is added, this fails to\n // compile until the field type is handled (or explicitly skipped) above.\n field.type satisfies never;\n break;\n }\n}\n\n/** Reports a constraint (bound/length/count) violation as an error issue. */\nfunction pushConstraint(\n message: string,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n issues.push({ severity: 'error', code: 'constraint_violation', path, entity, message });\n}\n\n/** Checks an array length against optional inclusive `min`/`max` bounds. */\nfunction checkCount(\n length: number,\n min: number | undefined,\n max: number | undefined,\n noun: string,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n if (min != null && length < min) {\n pushConstraint(`Expected at least ${min} ${noun}, received ${length}.`, path, entity, issues);\n }\n if (max != null && length > max) {\n pushConstraint(`Expected at most ${max} ${noun}, received ${length}.`, path, entity, issues);\n }\n}\n\n/** Checks a string against optional `max_length`/`maxlength` and `minlength` bounds. */\nfunction checkStringLength(\n field: SchemaFieldLike,\n value: string,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n const max = field.max_length ?? field.maxlength;\n if (max != null && value.length > max) {\n pushConstraint(`Text length ${value.length} exceeds the maximum of ${max}.`, path, entity, issues);\n }\n if (field.minlength != null && value.length < field.minlength) {\n pushConstraint(`Text length ${value.length} is below the minimum of ${field.minlength}.`, path, entity, issues);\n }\n}\n\n/** Counts a number's fractional digits, handling exponential notation (e.g. `1e-7` → 7). */\nfunction decimalPlaces(value: number): number {\n if (!Number.isFinite(value)) {\n return 0;\n }\n const text = String(value).toLowerCase();\n const [mantissa, exponent] = text.split('e');\n const fractionDigits = mantissa.includes('.') ? mantissa.split('.')[1].length : 0;\n // A negative exponent (e.g. `1e-7`) adds that many fractional digits.\n return exponent ? Math.max(0, fractionDigits - Number(exponent)) : fractionDigits;\n}\n\n/** Whether `value` lands on a `step` increment offset from `base`, with float tolerance. */\nfunction isMultipleOf(value: number, step: number, base: number): boolean {\n const ratio = (value - base) / step;\n const tolerance = 1e-9 * Math.max(1, Math.abs(ratio));\n return Math.abs(ratio - Math.round(ratio)) <= tolerance;\n}\n\n/** Parses a numeric constraint stored as a string (e.g. `min_options`). Empty/non-numeric → undefined. */\nfunction toCount(value: string | undefined): number | undefined {\n if (value == null || value === '') {\n return undefined;\n }\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : undefined;\n}\n\nfunction pushTypeIssue(\n value: unknown,\n expected: string,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n issues.push({\n severity: 'error',\n code: 'invalid_value',\n path,\n entity,\n message: `Expected ${expected}, received ${value === null ? 'null' : typeof value}.`,\n });\n}\n\n/** Walks richtext `content` nodes and validates embedded bloks (`type: 'blok'`). */\nfunction validateRichtextBloks(\n value: unknown,\n blocksByName: Map<string, SchemaBlockLike>,\n fieldPluginsByType: Map<string, StandardSchemaV1>,\n path: (string | number)[],\n issues: ValidationIssue[],\n): void {\n if (!isRecord(value) || !Array.isArray(value.content)) {\n return;\n }\n value.content.forEach((node, index) => {\n if (!isRecord(node)) {\n return;\n }\n if (node.type === 'blok' && isRecord(node.attrs) && Array.isArray(node.attrs.body)) {\n node.attrs.body.forEach((blok, blokIndex) =>\n validateBlokContent(blok, blocksByName, fieldPluginsByType, [...path, 'content', index, 'attrs', 'body', blokIndex], issues),\n );\n }\n else if (Array.isArray(node.content)) {\n // Recurse into nested marks/nodes that may themselves embed bloks.\n validateRichtextBloks(node, blocksByName, fieldPluginsByType, [...path, 'content', index], issues);\n }\n });\n}\n\n/** Validates a single blok content object against its component definition. */\nfunction validateBlokContent(\n content: unknown,\n blocksByName: Map<string, SchemaBlockLike>,\n fieldPluginsByType: Map<string, StandardSchemaV1>,\n path: (string | number)[],\n issues: ValidationIssue[],\n): void {\n if (!isRecord(content)) {\n issues.push({\n severity: 'error',\n code: 'invalid_content',\n path,\n entity: 'story',\n message: 'Expected a block content object.',\n });\n return;\n }\n\n const component = content.component;\n const block = typeof component === 'string' ? blocksByName.get(component) : undefined;\n if (!block) {\n issues.push({\n severity: 'error',\n code: 'unknown_component',\n path: [...path, 'component'],\n entity: 'story',\n message: `Unknown component \"${String(component)}\".`,\n });\n return;\n }\n\n const entity = `block:${block.name}`;\n const fields = block.fields ?? [];\n const fieldsByName = new Map(fields.map(field => [field.name, field]));\n\n for (const key of Object.keys(content)) {\n if (!RESERVED_KEYS.has(key) && !fieldsByName.has(key)) {\n issues.push({\n severity: 'warning',\n code: 'unknown_field',\n path: [...path, key],\n entity,\n message: `Unknown field \"${key}\" on component \"${block.name}\".`,\n });\n }\n }\n\n for (const field of fields) {\n const value = content[field.name];\n if (value === undefined || value === null) {\n if (field.required) {\n issues.push({\n severity: 'error',\n code: 'missing_required_field',\n path: [...path, field.name],\n entity,\n message: `Missing required field \"${field.name}\" on component \"${block.name}\".`,\n });\n }\n continue;\n }\n validateFieldValue(field, value, blocksByName, fieldPluginsByType, [...path, field.name], entity, issues);\n }\n}\n\n/**\n * Validates a story's content against a schema without throwing. Reports unknown\n * components (error), unknown fields (warning), missing required fields (error),\n * and invalid field-value shapes (error), recursing into nested `bloks` and\n * richtext-embedded bloks.\n *\n * @example\n * const result = validateStory(story, { blocks: { page, hero } });\n */\nexport function validateStory(story: unknown, schema: SchemaLike): ValidationResult {\n const issues: ValidationIssue[] = [];\n const blocksByName = new Map(toValues(schema.blocks).map(block => [block.name, block]));\n const fieldPluginsByType = new Map(\n toValues(schema.fieldPlugins).map(plugin => [plugin.fieldType, plugin.value]),\n );\n const content = isRecord(story) ? story.content : undefined;\n validateBlokContent(content, blocksByName, fieldPluginsByType, ['content'], issues);\n return { ok: issues.every(issue => issue.severity !== 'error'), issues };\n}\n"],"mappings":"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"}
@@ -1,7 +1,8 @@
1
1
  import { isRecord } from "../utils/is-record.mjs";
2
2
  import { toValues } from "./shapes.mjs";
3
- import { zAssetFieldValue, zMultilinkFieldValue, zPluginFieldValue, zRichtextFieldValue, zTableFieldValue } from "../generated/overlay/zod.gen.mjs";
3
+ import { zAssetFieldValue, zMultilinkFieldValue, zRichtextFieldValue, zTableFieldValue } from "../generated/overlay/zod.gen.mjs";
4
4
  import "./internal-schemas.mjs";
5
+ import { z } from "zod";
5
6
 
6
7
  //#region src/validators/validate-story.ts
7
8
  /** Field-content keys that are not user-defined fields. */
@@ -10,10 +11,29 @@ const RESERVED_KEYS = new Set([
10
11
  "component",
11
12
  "_editable"
12
13
  ]);
14
+ /**
15
+ * Relaxed plugin envelope used by the `custom` case. Mirrors the generated
16
+ * `zPluginFieldValue` but relaxes `_uid` from a UUID to a plain string, matching
17
+ * the CMS, which persists arbitrary `_uid` strings. Kept local so a codegen
18
+ * regenerate cannot revert it.
19
+ */
20
+ const zPluginEnvelope = z.object({
21
+ plugin: z.string(),
22
+ _uid: z.optional(z.string())
23
+ });
13
24
  /** Maps a Standard Schema validator to a {@link ValidationIssue} reporter at `path`. */
14
25
  function checkValue(schema, value, path, entity, issues) {
15
26
  const result = schema["~standard"].validate(value);
16
- if (result instanceof Promise) return;
27
+ if (result instanceof Promise) {
28
+ issues.push({
29
+ severity: "error",
30
+ code: "async_validator_unsupported",
31
+ path,
32
+ entity,
33
+ message: "Field plugin validator is asynchronous; validateStory runs synchronously and cannot await it."
34
+ });
35
+ return;
36
+ }
17
37
  if (result.issues) for (const issue of result.issues) {
18
38
  const issuePath = (issue.path ?? []).map((segment) => typeof segment === "object" && segment !== null ? String(segment.key) : segment);
19
39
  issues.push({
@@ -25,7 +45,7 @@ function checkValue(schema, value, path, entity, issues) {
25
45
  });
26
46
  }
27
47
  }
28
- function validateFieldValue(field, value, blocksByName, path, entity, issues) {
48
+ function validateFieldValue(field, value, blocksByName, fieldPluginsByType, path, entity, issues) {
29
49
  switch (field.type) {
30
50
  case "asset":
31
51
  checkValue(zAssetFieldValue, value, path, entity, issues);
@@ -46,11 +66,17 @@ function validateFieldValue(field, value, blocksByName, path, entity, issues) {
46
66
  break;
47
67
  case "richtext":
48
68
  checkValue(zRichtextFieldValue, value, path, entity, issues);
49
- validateRichtextBloks(value, blocksByName, path, issues);
69
+ validateRichtextBloks(value, blocksByName, fieldPluginsByType, path, issues);
50
70
  break;
51
- case "custom":
52
- checkValue(zPluginFieldValue, value, path, entity, issues);
71
+ case "custom": {
72
+ checkValue(zPluginEnvelope, value, path, entity, issues);
73
+ const validator = field.field_type ? fieldPluginsByType.get(field.field_type) : void 0;
74
+ if (validator && isRecord(value)) {
75
+ const { plugin: _plugin, _uid, ...pluginValue } = value;
76
+ checkValue(validator, pluginValue, path, entity, issues);
77
+ }
53
78
  break;
79
+ }
54
80
  case "bloks":
55
81
  if (!Array.isArray(value)) {
56
82
  pushTypeIssue(value, "array", path, entity, issues);
@@ -69,7 +95,7 @@ function validateFieldValue(field, value, blocksByName, path, entity, issues) {
69
95
  entity,
70
96
  message: `Component "${item.component}" is not allowed in field "${field.name}"; allowed: ${field.allow.join(", ")}.`
71
97
  });
72
- validateBlokContent(item, blocksByName, [...path, index], issues);
98
+ validateBlokContent(item, blocksByName, fieldPluginsByType, [...path, index], issues);
73
99
  });
74
100
  break;
75
101
  case "text":
@@ -165,11 +191,11 @@ function pushTypeIssue(value, expected, path, entity, issues) {
165
191
  });
166
192
  }
167
193
  /** Walks richtext `content` nodes and validates embedded bloks (`type: 'blok'`). */
168
- function validateRichtextBloks(value, blocksByName, path, issues) {
194
+ function validateRichtextBloks(value, blocksByName, fieldPluginsByType, path, issues) {
169
195
  if (!isRecord(value) || !Array.isArray(value.content)) return;
170
196
  value.content.forEach((node, index) => {
171
197
  if (!isRecord(node)) return;
172
- if (node.type === "blok" && isRecord(node.attrs) && Array.isArray(node.attrs.body)) node.attrs.body.forEach((blok, blokIndex) => validateBlokContent(blok, blocksByName, [
198
+ if (node.type === "blok" && isRecord(node.attrs) && Array.isArray(node.attrs.body)) node.attrs.body.forEach((blok, blokIndex) => validateBlokContent(blok, blocksByName, fieldPluginsByType, [
173
199
  ...path,
174
200
  "content",
175
201
  index,
@@ -177,7 +203,7 @@ function validateRichtextBloks(value, blocksByName, path, issues) {
177
203
  "body",
178
204
  blokIndex
179
205
  ], issues));
180
- else if (Array.isArray(node.content)) validateRichtextBloks(node, blocksByName, [
206
+ else if (Array.isArray(node.content)) validateRichtextBloks(node, blocksByName, fieldPluginsByType, [
181
207
  ...path,
182
208
  "content",
183
209
  index
@@ -185,7 +211,7 @@ function validateRichtextBloks(value, blocksByName, path, issues) {
185
211
  });
186
212
  }
187
213
  /** Validates a single blok content object against its component definition. */
188
- function validateBlokContent(content, blocksByName, path, issues) {
214
+ function validateBlokContent(content, blocksByName, fieldPluginsByType, path, issues) {
189
215
  if (!isRecord(content)) {
190
216
  issues.push({
191
217
  severity: "error",
@@ -230,7 +256,7 @@ function validateBlokContent(content, blocksByName, path, issues) {
230
256
  });
231
257
  continue;
232
258
  }
233
- validateFieldValue(field, value, blocksByName, [...path, field.name], entity, issues);
259
+ validateFieldValue(field, value, blocksByName, fieldPluginsByType, [...path, field.name], entity, issues);
234
260
  }
235
261
  }
236
262
  /**
@@ -245,7 +271,8 @@ function validateBlokContent(content, blocksByName, path, issues) {
245
271
  function validateStory(story, schema) {
246
272
  const issues = [];
247
273
  const blocksByName = new Map(toValues(schema.blocks).map((block) => [block.name, block]));
248
- validateBlokContent(isRecord(story) ? story.content : void 0, blocksByName, ["content"], issues);
274
+ const fieldPluginsByType = new Map(toValues(schema.fieldPlugins).map((plugin) => [plugin.fieldType, plugin.value]));
275
+ validateBlokContent(isRecord(story) ? story.content : void 0, blocksByName, fieldPluginsByType, ["content"], issues);
249
276
  return {
250
277
  ok: issues.every((issue) => issue.severity !== "error"),
251
278
  issues
@@ -1 +1 @@
1
- {"version":3,"file":"validate-story.mjs","names":[],"sources":["../../src/validators/validate-story.ts"],"sourcesContent":["import type { StandardSchemaV1 } from '@standard-schema/spec';\nimport type { SchemaBlockLike, SchemaFieldLike, SchemaLike } from './shapes';\nimport type { ValidationIssue, ValidationResult } from './types';\nimport {\n zAssetFieldValue,\n zMultilinkFieldValue,\n zPluginFieldValue,\n zRichtextFieldValue,\n zTableFieldValue,\n} from './internal-schemas';\nimport { isRecord, toValues } from './shapes';\n\n/** Field-content keys that are not user-defined fields. */\nconst RESERVED_KEYS = new Set(['_uid', 'component', '_editable']);\n\n/** Maps a Standard Schema validator to a {@link ValidationIssue} reporter at `path`. */\nfunction checkValue(\n schema: StandardSchemaV1,\n value: unknown,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n const result = schema['~standard'].validate(value);\n // The Zod schemas are synchronous; a thenable result would indicate misuse.\n if (result instanceof Promise) {\n return;\n }\n if (result.issues) {\n for (const issue of result.issues) {\n const issuePath = (issue.path ?? []).map(segment =>\n (typeof segment === 'object' && segment !== null ? String(segment.key) : (segment as string | number)),\n );\n issues.push({\n severity: 'error',\n code: 'invalid_value',\n path: [...path, ...issuePath],\n entity,\n message: issue.message,\n });\n }\n }\n}\n\nfunction validateFieldValue(\n field: SchemaFieldLike,\n value: unknown,\n blocksByName: Map<string, SchemaBlockLike>,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n switch (field.type) {\n case 'asset':\n checkValue(zAssetFieldValue, value, path, entity, issues);\n break;\n case 'multiasset':\n if (!Array.isArray(value)) {\n pushTypeIssue(value, 'array', path, entity, issues);\n break;\n }\n value.forEach((item, index) => checkValue(zAssetFieldValue, item, [...path, index], entity, issues));\n checkCount(value.length, field.minimum_entries, field.maximum_entries, 'asset(s)', path, entity, issues);\n break;\n case 'multilink':\n checkValue(zMultilinkFieldValue, value, path, entity, issues);\n break;\n case 'table':\n checkValue(zTableFieldValue, value, path, entity, issues);\n break;\n case 'richtext':\n checkValue(zRichtextFieldValue, value, path, entity, issues);\n validateRichtextBloks(value, blocksByName, path, issues);\n break;\n case 'custom':\n checkValue(zPluginFieldValue, value, path, entity, issues);\n break;\n case 'bloks':\n if (!Array.isArray(value)) {\n pushTypeIssue(value, 'array', path, entity, issues);\n break;\n }\n checkCount(value.length, field.minimum, field.maximum, 'block(s)', path, entity, issues);\n value.forEach((item, index) => {\n if (\n field.allow && field.allow.length > 0\n && isRecord(item) && typeof item.component === 'string'\n && !field.allow.includes(item.component)\n ) {\n issues.push({\n severity: 'error',\n code: 'disallowed_component',\n path: [...path, index, 'component'],\n entity,\n message: `Component \"${item.component}\" is not allowed in field \"${field.name}\"; allowed: ${field.allow.join(', ')}.`,\n });\n }\n validateBlokContent(item, blocksByName, [...path, index], issues);\n });\n break;\n case 'text':\n case 'textarea':\n case 'markdown':\n if (typeof value !== 'string') {\n pushTypeIssue(value, 'string', path, entity, issues);\n break;\n }\n checkStringLength(field, value, path, entity, issues);\n break;\n case 'option':\n case 'datetime':\n if (typeof value !== 'string') {\n pushTypeIssue(value, 'string', path, entity, issues);\n }\n break;\n case 'number':\n if (typeof value !== 'number') {\n pushTypeIssue(value, 'number', path, entity, issues);\n break;\n }\n if (field.min_value != null && value < field.min_value) {\n pushConstraint(`Value ${value} is below the minimum of ${field.min_value}.`, path, entity, issues);\n }\n if (field.max_value != null && value > field.max_value) {\n pushConstraint(`Value ${value} exceeds the maximum of ${field.max_value}.`, path, entity, issues);\n }\n if (field.decimals != null && decimalPlaces(value) > field.decimals) {\n pushConstraint(`Value ${value} has more than ${field.decimals} decimal place(s).`, path, entity, issues);\n }\n if (field.steps != null && field.steps > 0 && !isMultipleOf(value, field.steps, field.min_value ?? 0)) {\n const base = field.min_value ?? 0;\n pushConstraint(\n `Value ${value} is not a multiple of the step ${field.steps}${base ? ` (offset from ${base})` : ''}.`,\n path,\n entity,\n issues,\n );\n }\n break;\n case 'boolean':\n if (typeof value !== 'boolean') {\n pushTypeIssue(value, 'boolean', path, entity, issues);\n }\n break;\n case 'options':\n if (!Array.isArray(value) || value.some(item => typeof item !== 'string')) {\n pushTypeIssue(value, 'string[]', path, entity, issues);\n break;\n }\n checkCount(value.length, toCount(field.min_options), toCount(field.max_options), 'option(s)', path, entity, issues);\n break;\n case 'section':\n case 'tab':\n // Layout-only field types carry no content value.\n break;\n default:\n // Exhaustiveness guard: when a new `FieldType` is added, this fails to\n // compile until the field type is handled (or explicitly skipped) above.\n field.type satisfies never;\n break;\n }\n}\n\n/** Reports a constraint (bound/length/count) violation as an error issue. */\nfunction pushConstraint(\n message: string,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n issues.push({ severity: 'error', code: 'constraint_violation', path, entity, message });\n}\n\n/** Checks an array length against optional inclusive `min`/`max` bounds. */\nfunction checkCount(\n length: number,\n min: number | undefined,\n max: number | undefined,\n noun: string,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n if (min != null && length < min) {\n pushConstraint(`Expected at least ${min} ${noun}, received ${length}.`, path, entity, issues);\n }\n if (max != null && length > max) {\n pushConstraint(`Expected at most ${max} ${noun}, received ${length}.`, path, entity, issues);\n }\n}\n\n/** Checks a string against optional `max_length`/`maxlength` and `minlength` bounds. */\nfunction checkStringLength(\n field: SchemaFieldLike,\n value: string,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n const max = field.max_length ?? field.maxlength;\n if (max != null && value.length > max) {\n pushConstraint(`Text length ${value.length} exceeds the maximum of ${max}.`, path, entity, issues);\n }\n if (field.minlength != null && value.length < field.minlength) {\n pushConstraint(`Text length ${value.length} is below the minimum of ${field.minlength}.`, path, entity, issues);\n }\n}\n\n/** Counts a number's fractional digits, handling exponential notation (e.g. `1e-7` → 7). */\nfunction decimalPlaces(value: number): number {\n if (!Number.isFinite(value)) {\n return 0;\n }\n const text = String(value).toLowerCase();\n const [mantissa, exponent] = text.split('e');\n const fractionDigits = mantissa.includes('.') ? mantissa.split('.')[1].length : 0;\n // A negative exponent (e.g. `1e-7`) adds that many fractional digits.\n return exponent ? Math.max(0, fractionDigits - Number(exponent)) : fractionDigits;\n}\n\n/** Whether `value` lands on a `step` increment offset from `base`, with float tolerance. */\nfunction isMultipleOf(value: number, step: number, base: number): boolean {\n const ratio = (value - base) / step;\n const tolerance = 1e-9 * Math.max(1, Math.abs(ratio));\n return Math.abs(ratio - Math.round(ratio)) <= tolerance;\n}\n\n/** Parses a numeric constraint stored as a string (e.g. `min_options`). Empty/non-numeric → undefined. */\nfunction toCount(value: string | undefined): number | undefined {\n if (value == null || value === '') {\n return undefined;\n }\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : undefined;\n}\n\nfunction pushTypeIssue(\n value: unknown,\n expected: string,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n issues.push({\n severity: 'error',\n code: 'invalid_value',\n path,\n entity,\n message: `Expected ${expected}, received ${value === null ? 'null' : typeof value}.`,\n });\n}\n\n/** Walks richtext `content` nodes and validates embedded bloks (`type: 'blok'`). */\nfunction validateRichtextBloks(\n value: unknown,\n blocksByName: Map<string, SchemaBlockLike>,\n path: (string | number)[],\n issues: ValidationIssue[],\n): void {\n if (!isRecord(value) || !Array.isArray(value.content)) {\n return;\n }\n value.content.forEach((node, index) => {\n if (!isRecord(node)) {\n return;\n }\n if (node.type === 'blok' && isRecord(node.attrs) && Array.isArray(node.attrs.body)) {\n node.attrs.body.forEach((blok, blokIndex) =>\n validateBlokContent(blok, blocksByName, [...path, 'content', index, 'attrs', 'body', blokIndex], issues),\n );\n }\n else if (Array.isArray(node.content)) {\n // Recurse into nested marks/nodes that may themselves embed bloks.\n validateRichtextBloks(node, blocksByName, [...path, 'content', index], issues);\n }\n });\n}\n\n/** Validates a single blok content object against its component definition. */\nfunction validateBlokContent(\n content: unknown,\n blocksByName: Map<string, SchemaBlockLike>,\n path: (string | number)[],\n issues: ValidationIssue[],\n): void {\n if (!isRecord(content)) {\n issues.push({\n severity: 'error',\n code: 'invalid_content',\n path,\n entity: 'story',\n message: 'Expected a block content object.',\n });\n return;\n }\n\n const component = content.component;\n const block = typeof component === 'string' ? blocksByName.get(component) : undefined;\n if (!block) {\n issues.push({\n severity: 'error',\n code: 'unknown_component',\n path: [...path, 'component'],\n entity: 'story',\n message: `Unknown component \"${String(component)}\".`,\n });\n return;\n }\n\n const entity = `block:${block.name}`;\n const fields = block.fields ?? [];\n const fieldsByName = new Map(fields.map(field => [field.name, field]));\n\n for (const key of Object.keys(content)) {\n if (!RESERVED_KEYS.has(key) && !fieldsByName.has(key)) {\n issues.push({\n severity: 'warning',\n code: 'unknown_field',\n path: [...path, key],\n entity,\n message: `Unknown field \"${key}\" on component \"${block.name}\".`,\n });\n }\n }\n\n for (const field of fields) {\n const value = content[field.name];\n if (value === undefined || value === null) {\n if (field.required) {\n issues.push({\n severity: 'error',\n code: 'missing_required_field',\n path: [...path, field.name],\n entity,\n message: `Missing required field \"${field.name}\" on component \"${block.name}\".`,\n });\n }\n continue;\n }\n validateFieldValue(field, value, blocksByName, [...path, field.name], entity, issues);\n }\n}\n\n/**\n * Validates a story's content against a schema without throwing. Reports unknown\n * components (error), unknown fields (warning), missing required fields (error),\n * and invalid field-value shapes (error), recursing into nested `bloks` and\n * richtext-embedded bloks.\n *\n * @example\n * const result = validateStory(story, { blocks: { page, hero } });\n */\nexport function validateStory(story: unknown, schema: SchemaLike): ValidationResult {\n const issues: ValidationIssue[] = [];\n const blocksByName = new Map(toValues(schema.blocks).map(block => [block.name, block]));\n const content = isRecord(story) ? story.content : undefined;\n validateBlokContent(content, blocksByName, ['content'], issues);\n return { ok: issues.every(issue => issue.severity !== 'error'), issues };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"validate-story.mjs","names":[],"sources":["../../src/validators/validate-story.ts"],"sourcesContent":["import { z } from 'zod';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport type { SchemaBlockLike, SchemaFieldLike, SchemaLike } from './shapes';\nimport type { ValidationIssue, ValidationResult } from './types';\nimport {\n zAssetFieldValue,\n zMultilinkFieldValue,\n zRichtextFieldValue,\n zTableFieldValue,\n} from './internal-schemas';\nimport { isRecord, toValues } from './shapes';\n\n/** Field-content keys that are not user-defined fields. */\nconst RESERVED_KEYS = new Set(['_uid', 'component', '_editable']);\n\n/**\n * Relaxed plugin envelope used by the `custom` case. Mirrors the generated\n * `zPluginFieldValue` but relaxes `_uid` from a UUID to a plain string, matching\n * the CMS, which persists arbitrary `_uid` strings. Kept local so a codegen\n * regenerate cannot revert it.\n */\nconst zPluginEnvelope = z.object({ plugin: z.string(), _uid: z.optional(z.string()) });\n\n/** Maps a Standard Schema validator to a {@link ValidationIssue} reporter at `path`. */\nfunction checkValue(\n schema: StandardSchemaV1,\n value: unknown,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n const result = schema['~standard'].validate(value);\n // `validateStory` is synchronous. The internal Zod schemas never return a\n // thenable, but a registered field plugin may ship an async validator — which\n // cannot be awaited here. Surface it as an error instead of silently passing,\n // which would report a false `ok: true`.\n if (result instanceof Promise) {\n issues.push({\n severity: 'error',\n code: 'async_validator_unsupported',\n path,\n entity,\n message: 'Field plugin validator is asynchronous; validateStory runs synchronously and cannot await it.',\n });\n return;\n }\n if (result.issues) {\n for (const issue of result.issues) {\n const issuePath = (issue.path ?? []).map(segment =>\n (typeof segment === 'object' && segment !== null ? String(segment.key) : (segment as string | number)),\n );\n issues.push({\n severity: 'error',\n code: 'invalid_value',\n path: [...path, ...issuePath],\n entity,\n message: issue.message,\n });\n }\n }\n}\n\nfunction validateFieldValue(\n field: SchemaFieldLike,\n value: unknown,\n blocksByName: Map<string, SchemaBlockLike>,\n fieldPluginsByType: Map<string, StandardSchemaV1>,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n switch (field.type) {\n case 'asset':\n checkValue(zAssetFieldValue, value, path, entity, issues);\n break;\n case 'multiasset':\n if (!Array.isArray(value)) {\n pushTypeIssue(value, 'array', path, entity, issues);\n break;\n }\n value.forEach((item, index) => checkValue(zAssetFieldValue, item, [...path, index], entity, issues));\n checkCount(value.length, field.minimum_entries, field.maximum_entries, 'asset(s)', path, entity, issues);\n break;\n case 'multilink':\n checkValue(zMultilinkFieldValue, value, path, entity, issues);\n break;\n case 'table':\n checkValue(zTableFieldValue, value, path, entity, issues);\n break;\n case 'richtext':\n checkValue(zRichtextFieldValue, value, path, entity, issues);\n validateRichtextBloks(value, blocksByName, fieldPluginsByType, path, issues);\n break;\n case 'custom': {\n checkValue(zPluginEnvelope, value, path, entity, issues);\n const validator = field.field_type ? fieldPluginsByType.get(field.field_type) : undefined;\n if (validator && isRecord(value)) {\n // Envelope keys sit alongside the plugin's own keys; strip them so the\n // plugin validator sees only its value. Sibling keys keep issue paths\n // accurate (an issue at ['color'] maps to [...path, 'color']).\n const { plugin: _plugin, _uid, ...pluginValue } = value;\n checkValue(validator, pluginValue, path, entity, issues);\n }\n break;\n }\n case 'bloks':\n if (!Array.isArray(value)) {\n pushTypeIssue(value, 'array', path, entity, issues);\n break;\n }\n checkCount(value.length, field.minimum, field.maximum, 'block(s)', path, entity, issues);\n value.forEach((item, index) => {\n if (\n field.allow && field.allow.length > 0\n && isRecord(item) && typeof item.component === 'string'\n && !field.allow.includes(item.component)\n ) {\n issues.push({\n severity: 'error',\n code: 'disallowed_component',\n path: [...path, index, 'component'],\n entity,\n message: `Component \"${item.component}\" is not allowed in field \"${field.name}\"; allowed: ${field.allow.join(', ')}.`,\n });\n }\n validateBlokContent(item, blocksByName, fieldPluginsByType, [...path, index], issues);\n });\n break;\n case 'text':\n case 'textarea':\n case 'markdown':\n if (typeof value !== 'string') {\n pushTypeIssue(value, 'string', path, entity, issues);\n break;\n }\n checkStringLength(field, value, path, entity, issues);\n break;\n case 'option':\n case 'datetime':\n if (typeof value !== 'string') {\n pushTypeIssue(value, 'string', path, entity, issues);\n }\n break;\n case 'number':\n if (typeof value !== 'number') {\n pushTypeIssue(value, 'number', path, entity, issues);\n break;\n }\n if (field.min_value != null && value < field.min_value) {\n pushConstraint(`Value ${value} is below the minimum of ${field.min_value}.`, path, entity, issues);\n }\n if (field.max_value != null && value > field.max_value) {\n pushConstraint(`Value ${value} exceeds the maximum of ${field.max_value}.`, path, entity, issues);\n }\n if (field.decimals != null && decimalPlaces(value) > field.decimals) {\n pushConstraint(`Value ${value} has more than ${field.decimals} decimal place(s).`, path, entity, issues);\n }\n if (field.steps != null && field.steps > 0 && !isMultipleOf(value, field.steps, field.min_value ?? 0)) {\n const base = field.min_value ?? 0;\n pushConstraint(\n `Value ${value} is not a multiple of the step ${field.steps}${base ? ` (offset from ${base})` : ''}.`,\n path,\n entity,\n issues,\n );\n }\n break;\n case 'boolean':\n if (typeof value !== 'boolean') {\n pushTypeIssue(value, 'boolean', path, entity, issues);\n }\n break;\n case 'options':\n if (!Array.isArray(value) || value.some(item => typeof item !== 'string')) {\n pushTypeIssue(value, 'string[]', path, entity, issues);\n break;\n }\n checkCount(value.length, toCount(field.min_options), toCount(field.max_options), 'option(s)', path, entity, issues);\n break;\n case 'section':\n case 'tab':\n // Layout-only field types carry no content value.\n break;\n default:\n // Exhaustiveness guard: when a new `FieldType` is added, this fails to\n // compile until the field type is handled (or explicitly skipped) above.\n field.type satisfies never;\n break;\n }\n}\n\n/** Reports a constraint (bound/length/count) violation as an error issue. */\nfunction pushConstraint(\n message: string,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n issues.push({ severity: 'error', code: 'constraint_violation', path, entity, message });\n}\n\n/** Checks an array length against optional inclusive `min`/`max` bounds. */\nfunction checkCount(\n length: number,\n min: number | undefined,\n max: number | undefined,\n noun: string,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n if (min != null && length < min) {\n pushConstraint(`Expected at least ${min} ${noun}, received ${length}.`, path, entity, issues);\n }\n if (max != null && length > max) {\n pushConstraint(`Expected at most ${max} ${noun}, received ${length}.`, path, entity, issues);\n }\n}\n\n/** Checks a string against optional `max_length`/`maxlength` and `minlength` bounds. */\nfunction checkStringLength(\n field: SchemaFieldLike,\n value: string,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n const max = field.max_length ?? field.maxlength;\n if (max != null && value.length > max) {\n pushConstraint(`Text length ${value.length} exceeds the maximum of ${max}.`, path, entity, issues);\n }\n if (field.minlength != null && value.length < field.minlength) {\n pushConstraint(`Text length ${value.length} is below the minimum of ${field.minlength}.`, path, entity, issues);\n }\n}\n\n/** Counts a number's fractional digits, handling exponential notation (e.g. `1e-7` → 7). */\nfunction decimalPlaces(value: number): number {\n if (!Number.isFinite(value)) {\n return 0;\n }\n const text = String(value).toLowerCase();\n const [mantissa, exponent] = text.split('e');\n const fractionDigits = mantissa.includes('.') ? mantissa.split('.')[1].length : 0;\n // A negative exponent (e.g. `1e-7`) adds that many fractional digits.\n return exponent ? Math.max(0, fractionDigits - Number(exponent)) : fractionDigits;\n}\n\n/** Whether `value` lands on a `step` increment offset from `base`, with float tolerance. */\nfunction isMultipleOf(value: number, step: number, base: number): boolean {\n const ratio = (value - base) / step;\n const tolerance = 1e-9 * Math.max(1, Math.abs(ratio));\n return Math.abs(ratio - Math.round(ratio)) <= tolerance;\n}\n\n/** Parses a numeric constraint stored as a string (e.g. `min_options`). Empty/non-numeric → undefined. */\nfunction toCount(value: string | undefined): number | undefined {\n if (value == null || value === '') {\n return undefined;\n }\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : undefined;\n}\n\nfunction pushTypeIssue(\n value: unknown,\n expected: string,\n path: (string | number)[],\n entity: string,\n issues: ValidationIssue[],\n): void {\n issues.push({\n severity: 'error',\n code: 'invalid_value',\n path,\n entity,\n message: `Expected ${expected}, received ${value === null ? 'null' : typeof value}.`,\n });\n}\n\n/** Walks richtext `content` nodes and validates embedded bloks (`type: 'blok'`). */\nfunction validateRichtextBloks(\n value: unknown,\n blocksByName: Map<string, SchemaBlockLike>,\n fieldPluginsByType: Map<string, StandardSchemaV1>,\n path: (string | number)[],\n issues: ValidationIssue[],\n): void {\n if (!isRecord(value) || !Array.isArray(value.content)) {\n return;\n }\n value.content.forEach((node, index) => {\n if (!isRecord(node)) {\n return;\n }\n if (node.type === 'blok' && isRecord(node.attrs) && Array.isArray(node.attrs.body)) {\n node.attrs.body.forEach((blok, blokIndex) =>\n validateBlokContent(blok, blocksByName, fieldPluginsByType, [...path, 'content', index, 'attrs', 'body', blokIndex], issues),\n );\n }\n else if (Array.isArray(node.content)) {\n // Recurse into nested marks/nodes that may themselves embed bloks.\n validateRichtextBloks(node, blocksByName, fieldPluginsByType, [...path, 'content', index], issues);\n }\n });\n}\n\n/** Validates a single blok content object against its component definition. */\nfunction validateBlokContent(\n content: unknown,\n blocksByName: Map<string, SchemaBlockLike>,\n fieldPluginsByType: Map<string, StandardSchemaV1>,\n path: (string | number)[],\n issues: ValidationIssue[],\n): void {\n if (!isRecord(content)) {\n issues.push({\n severity: 'error',\n code: 'invalid_content',\n path,\n entity: 'story',\n message: 'Expected a block content object.',\n });\n return;\n }\n\n const component = content.component;\n const block = typeof component === 'string' ? blocksByName.get(component) : undefined;\n if (!block) {\n issues.push({\n severity: 'error',\n code: 'unknown_component',\n path: [...path, 'component'],\n entity: 'story',\n message: `Unknown component \"${String(component)}\".`,\n });\n return;\n }\n\n const entity = `block:${block.name}`;\n const fields = block.fields ?? [];\n const fieldsByName = new Map(fields.map(field => [field.name, field]));\n\n for (const key of Object.keys(content)) {\n if (!RESERVED_KEYS.has(key) && !fieldsByName.has(key)) {\n issues.push({\n severity: 'warning',\n code: 'unknown_field',\n path: [...path, key],\n entity,\n message: `Unknown field \"${key}\" on component \"${block.name}\".`,\n });\n }\n }\n\n for (const field of fields) {\n const value = content[field.name];\n if (value === undefined || value === null) {\n if (field.required) {\n issues.push({\n severity: 'error',\n code: 'missing_required_field',\n path: [...path, field.name],\n entity,\n message: `Missing required field \"${field.name}\" on component \"${block.name}\".`,\n });\n }\n continue;\n }\n validateFieldValue(field, value, blocksByName, fieldPluginsByType, [...path, field.name], entity, issues);\n }\n}\n\n/**\n * Validates a story's content against a schema without throwing. Reports unknown\n * components (error), unknown fields (warning), missing required fields (error),\n * and invalid field-value shapes (error), recursing into nested `bloks` and\n * richtext-embedded bloks.\n *\n * @example\n * const result = validateStory(story, { blocks: { page, hero } });\n */\nexport function validateStory(story: unknown, schema: SchemaLike): ValidationResult {\n const issues: ValidationIssue[] = [];\n const blocksByName = new Map(toValues(schema.blocks).map(block => [block.name, block]));\n const fieldPluginsByType = new Map(\n toValues(schema.fieldPlugins).map(plugin => [plugin.fieldType, plugin.value]),\n );\n const content = isRecord(story) ? story.content : undefined;\n validateBlokContent(content, blocksByName, fieldPluginsByType, ['content'], issues);\n return { ok: issues.every(issue => issue.severity !== 'error'), issues };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@storyblok/schema",
3
3
  "type": "module",
4
- "version": "0.1.0-alpha.1",
4
+ "version": "0.1.0-alpha.2",
5
5
  "description": "Storyblok schema types and helpers",
6
6
  "license": "MIT",
7
7
  "homepage": "https://github.com/storyblok/monoblok/tree/main/packages/schema#readme",
@@ -21,11 +21,22 @@
21
21
  "import": "./dist/index.mjs",
22
22
  "require": "./dist/index.cjs"
23
23
  },
24
+ "./field-plugins": {
25
+ "import": "./dist/field-plugins/index.mjs",
26
+ "require": "./dist/field-plugins/index.cjs"
27
+ },
24
28
  "./package.json": "./package.json"
25
29
  },
26
30
  "main": "./dist/index.cjs",
27
31
  "module": "./dist/index.mjs",
28
32
  "types": "./dist/index.d.cts",
33
+ "typesVersions": {
34
+ "*": {
35
+ "field-plugins": [
36
+ "./dist/field-plugins/index.d.cts"
37
+ ]
38
+ }
39
+ },
29
40
  "files": [
30
41
  "dist"
31
42
  ],
@@ -39,7 +50,7 @@
39
50
  "tsdown": "^0.20.3",
40
51
  "typescript": "5.8.3",
41
52
  "vitest": "^4.1.3",
42
- "@storyblok/eslint-config": "0.5.1",
53
+ "@storyblok/eslint-config": "0.6.0",
43
54
  "@storyblok/openapi-codegen": "0.0.1"
44
55
  },
45
56
  "nx": {