import { contentTypes, z } from '@strapi/utils'; import { buildBlocksInputSchema } from './blocks-schema.mjs'; const isCustomFieldAttribute = (attr)=>typeof attr === 'object' && attr !== null && attr.type === 'customField' && typeof attr.customField === 'string'; /** * Builds a structured Zod object schema for a Strapi component UID. * Declared as a regular function so it is hoisted above `attributeToInputSchema` * — the two functions are mutually recursive (component attrs recurse into * attributeToInputSchema; attributeToInputSchema calls this for 'component' cases). * * @param strapi - Strapi instance (components registry available post-load). * @param componentUid - e.g. "common.seo". * @param visited - cycle-guard; prevents infinite recursion on self-referencing components. */ function buildComponentInputSchema(strapi, componentUid, visited = new Set()) { if (visited.has(componentUid) === true) { // Circular reference — fall back to permissive but non-empty JSON Schema return z.record(z.string(), z.unknown()); } const componentsMap = strapi.components; const component = componentsMap[componentUid]; if (component === undefined) { return z.record(z.string(), z.unknown()); } visited.add(componentUid); const shape = {}; for (const [key, attr] of Object.entries(component.attributes)){ if (key === 'id') { continue; } shape[key] = attributeToInputSchema(strapi, attr, visited); } visited.delete(componentUid); return z.object(shape).strict(); } /** * Maps a single Strapi attribute to a Zod input schema, carrying constraints * (min, max, minLength, maxLength, required, enum values, etc.). * * Mirrors the `mapAttributeToInputSchema` logic from * `packages/core/core/src/core-api/routes/validation/mappers.ts` — kept inline * here to avoid a cross-package import from @strapi/content-manager into * @strapi/core (which is not a listed dependency). * */ const attributeToInputSchema = (strapi, attr, visited = new Set())=>{ switch(attr.type){ case 'string': case 'text': case 'richtext': case 'password': { const { required, minLength, maxLength } = attr; let s = z.string(); if (minLength !== undefined) s = s.min(minLength); if (maxLength !== undefined) s = s.max(maxLength); return required === true ? s : s.optional(); } case 'email': { const { required } = attr; const s = z.string().email(); return required === true ? s : s.optional(); } case 'uid': { const { required } = attr; const s = z.string(); return required === true ? s : s.optional(); } case 'integer': { const { required, min, max } = attr; let s = z.number().int(); if (min !== undefined) s = s.min(min); if (max !== undefined) s = s.max(max); return required === true ? s : s.optional(); } case 'biginteger': { const { required } = attr; const s = z.string(); return required === true ? s : s.optional(); } case 'decimal': case 'float': { const { required, min, max } = attr; let s = z.number(); if (min !== undefined) s = s.min(min); if (max !== undefined) s = s.max(max); return required === true ? s : s.optional(); } case 'boolean': { const { required } = attr; const s = z.boolean(); return required === true ? s : s.optional(); } case 'date': case 'datetime': case 'time': case 'timestamp': { const { required } = attr; const s = z.string(); return required === true ? s : s.optional(); } case 'enumeration': { const { required, enum: values } = attr; if (Array.isArray(values) && values.length > 0) { const s = z.enum(values); return required === true ? s : s.optional(); } const s = z.string(); return required === true ? s : s.optional(); } case 'json': { const { required } = attr; const s = z.any(); return required === true ? s : s.optional(); } case 'blocks': { const { required } = attr; const s = buildBlocksInputSchema(); return required === true ? s : s.optional(); } case 'component': { // Cast to a plain record to avoid generic defaults on `repeatable` (Constants.False) const componentAttr = attr; const componentUid = componentAttr.component; const componentSchema = componentUid !== undefined ? buildComponentInputSchema(strapi, componentUid, visited) : z.record(z.string(), z.unknown()); let s = componentAttr.repeatable === true ? z.array(componentSchema) : componentSchema; if (componentAttr.repeatable === true && componentAttr.min !== undefined) { s = s.min(componentAttr.min); } if (componentAttr.repeatable === true && componentAttr.max !== undefined) { s = s.max(componentAttr.max); } return componentAttr.required === true ? s : s.optional(); } case 'dynamiczone': { const dzAttr = attr; let s = z.array(z.any()); if (dzAttr.min !== undefined) s = s.min(dzAttr.min); if (dzAttr.max !== undefined) s = s.max(dzAttr.max); return dzAttr.required === true ? s : s.optional(); } case 'media': { const mediaAttr = attr; const s = mediaAttr.multiple === true ? z.array(z.any()) : z.any(); return mediaAttr.required === true ? s : s.optional(); } case 'relation': { const relAttr = attr; const isToMany = relAttr.relation?.endsWith('ToMany') === true; const relDocumentId = z.string().min(1).describe('Strapi document ID (e.g. "z7v8zma53x01r6oceimv922b").'); const relLongHand = z.object({ documentId: relDocumentId, locale: z.string().optional().describe('Target locale. Defaults to source document locale.'), status: z.enum([ 'draft', 'published' ]).optional().describe('Target version status. Defaults based on draftAndPublish config.') }).strict(); let s; if (isToMany === true) { const relEntry = z.union([ relDocumentId, relLongHand ]); const relConnectPosition = z.object({ before: z.string().optional().describe('Document ID to insert before.'), after: z.string().optional().describe('Document ID to insert after.'), start: z.boolean().optional().describe('Insert at start of list.'), end: z.boolean().optional().describe('Insert at end of list (default).') }).strict(); const relConnectEntry = z.object({ documentId: relDocumentId, locale: z.string().optional(), status: z.enum([ 'draft', 'published' ]).optional(), position: relConnectPosition.optional().describe('Ordering hint. Default: { end: true }.') }).strict(); s = z.object({ connect: z.array(z.union([ relDocumentId, relConnectEntry ])).optional().describe('Add relations. Each entry: documentId string, or { documentId, locale?, status?, position? }.'), disconnect: z.array(relEntry).optional().describe('Remove relations. Each entry: documentId string, or { documentId, locale?, status? }.'), set: z.union([ z.array(relEntry), z.null() ]).optional().describe('Replace all relations. Array replaces existing; null clears all. Mutually exclusive with connect/disconnect.') }).strict(); } else { s = z.union([ relDocumentId, relLongHand, z.null().describe('Set to null to clear the relation.') ]); } return relAttr.required === true ? s : s.optional(); } default: { const unknownAttr = attr; if (isCustomFieldAttribute(unknownAttr)) { const customField = strapi.get('custom-fields').get(unknownAttr.customField); if (customField !== undefined) { return attributeToInputSchema(strapi, { ...unknownAttr, type: customField.type }, visited); } } return z.unknown(); } } }; /** * Derives a per-content-type `data` Zod schema from the model's writable attributes. * Uses `contentTypes.isWritableAttribute` to filter system-managed keys * (id, documentId, timestamps, createdBy, updatedBy, localizations, locale, etc.). * Unknown keys are rejected (strict mode) — invalid field names fail at the MCP boundary. */ const buildDataSchema = (strapi, schema, attributes, permittedFields)=>{ const shape = {}; for (const [key, attr] of Object.entries(attributes)){ const isPermitted = permittedFields === null || permittedFields === undefined || permittedFields.has(key) === true; if (isPermitted === true && contentTypes.isWritableAttribute(schema, key) === true && contentTypes.isPrivateAttribute(schema, key) !== true) { shape[key] = attributeToInputSchema(strapi, attr); } } return z.object(shape).strict().describe('Document field values to write.'); }; export { attributeToInputSchema, buildComponentInputSchema, buildDataSchema }; //# sourceMappingURL=data-schema.mjs.map