UNPKG

@dossierhq/core

Version:

The core Dossier library used by clients and server alike, used to interact with schema and entities directly, as well as remotely through a client.

211 lines 12.7 kB
/// <reference types="./schemaValidate.d.ts" /> import { notOk, ok } from '../ErrorResult.js'; import { RichTextNodeType } from '../Types.js'; import { FieldType, GROUPED_RICH_TEXT_NODE_TYPES, REQUIRED_RICH_TEXT_NODES, } from './SchemaSpecification.js'; const CAMEL_CASE_PATTERN = /^[a-z][a-zA-Z0-9_]*$/; const PASCAL_CASE_PATTERN = /^[A-Z][a-zA-Z0-9_]*$/; const SHARED_FIELD_SPECIFICATION_KEYS = ['name', 'list', 'required', 'adminOnly', 'type']; const FIELD_SPECIFICATION_KEYS = /* @__PURE__ */ (() => ({ [FieldType.Boolean]: SHARED_FIELD_SPECIFICATION_KEYS, [FieldType.Component]: [...SHARED_FIELD_SPECIFICATION_KEYS, 'componentTypes'], [FieldType.Location]: SHARED_FIELD_SPECIFICATION_KEYS, [FieldType.Number]: [...SHARED_FIELD_SPECIFICATION_KEYS, 'integer'], [FieldType.Reference]: [...SHARED_FIELD_SPECIFICATION_KEYS, 'entityTypes'], [FieldType.RichText]: [ ...SHARED_FIELD_SPECIFICATION_KEYS, 'entityTypes', 'linkEntityTypes', 'componentTypes', 'richTextNodes', ], [FieldType.String]: [ ...SHARED_FIELD_SPECIFICATION_KEYS, 'multiline', 'matchPattern', 'values', 'index', ], }))(); export function schemaValidate(schema) { const usedTypeNames = new Set(); for (const [isEntitySpec, typeSpec] of [ ...schema.spec.entityTypes.map((it) => [true, it]), ...schema.spec.componentTypes.map((it) => [false, it]), ]) { const typeCanBePublished = isEntitySpec ? typeSpec.publishable : !typeSpec.adminOnly; if (!PASCAL_CASE_PATTERN.test(typeSpec.name)) { return notOk.BadRequest(`${typeSpec.name}: The type name has to start with an upper-case letter (A-Z) and can only contain letters (a-z, A-Z), numbers and underscore (_), such as MyType_123`); } if (usedTypeNames.has(typeSpec.name)) { return notOk.BadRequest(`${typeSpec.name}: Duplicate type name`); } usedTypeNames.add(typeSpec.name); if (isEntitySpec) { const authKeyPattern = typeSpec.authKeyPattern; if (authKeyPattern) { if (!schema.getPattern(authKeyPattern)) { return notOk.BadRequest(`${typeSpec.name}: Unknown authKeyPattern (${authKeyPattern})`); } } const nameField = typeSpec.nameField; if (nameField) { const nameFieldSpec = typeSpec.fields.find((fieldSpec) => fieldSpec.name === nameField); if (!nameFieldSpec) { return notOk.BadRequest(`${typeSpec.name}: Found no field matching nameField (${nameField})`); } if (nameFieldSpec.type !== FieldType.String || nameFieldSpec.list) { return notOk.BadRequest(`${typeSpec.name}: nameField (${nameField}) should be a string (non-list)`); } } } const usedFieldNames = new Set(); for (const fieldSpec of typeSpec.fields) { if (!CAMEL_CASE_PATTERN.test(fieldSpec.name)) { return notOk.BadRequest(`${typeSpec.name}.${fieldSpec.name}: The field name has to start with a lower-case letter (a-z) and can only contain letters (a-z, A-Z), numbers and underscore (_), such as myField_123`); } if (!isEntitySpec && fieldSpec.name === 'type') { return notOk.BadRequest(`${typeSpec.name}.${fieldSpec.name}: Invalid field name for a component type`); } if (usedFieldNames.has(fieldSpec.name)) { return notOk.BadRequest(`${typeSpec.name}.${fieldSpec.name}: Duplicate field name`); } usedFieldNames.add(fieldSpec.name); if (!(fieldSpec.type in FieldType)) { return notOk.BadRequest(`${typeSpec.name}.${fieldSpec.name}: Specified type ${fieldSpec.type} doesn’t exist`); } for (const key of Object.keys(fieldSpec)) { if (!FIELD_SPECIFICATION_KEYS[fieldSpec.type].includes(key)) { return notOk.BadRequest(`${typeSpec.name}.${fieldSpec.name}: Field with type ${fieldSpec.type} shouldn’t specify ${key}`); } } if ((fieldSpec.type === FieldType.Reference || fieldSpec.type === FieldType.RichText) && fieldSpec.entityTypes && fieldSpec.entityTypes.length > 0) { for (const referencedTypeName of fieldSpec.entityTypes) { const referencedEntityType = schema.getEntityTypeSpecification(referencedTypeName); if (!referencedEntityType) { return notOk.BadRequest(`${typeSpec.name}.${fieldSpec.name}: Referenced entity type in entityTypes ${referencedTypeName} doesn’t exist`); } if (!referencedEntityType.publishable && typeCanBePublished && !fieldSpec.adminOnly) { return notOk.BadRequest(`${typeSpec.name}.${fieldSpec.name}: Referenced entity type in entityTypes (${referencedTypeName}) is not publishable, but neither ${typeSpec.name} nor ${fieldSpec.name} are adminOnly`); } } } if (fieldSpec.type === FieldType.RichText && fieldSpec.linkEntityTypes && fieldSpec.linkEntityTypes.length > 0) { for (const referencedTypeName of fieldSpec.linkEntityTypes) { const referencedEntityType = schema.getEntityTypeSpecification(referencedTypeName); if (!referencedEntityType) { return notOk.BadRequest(`${typeSpec.name}.${fieldSpec.name}: Referenced entity type in linkEntityTypes ${referencedTypeName} doesn’t exist`); } if (!referencedEntityType.publishable && typeCanBePublished && !fieldSpec.adminOnly) { return notOk.BadRequest(`${typeSpec.name}.${fieldSpec.name}: Referenced entity type in linkEntityTypes (${referencedTypeName}) is not publishable, but neither ${typeSpec.name} nor ${fieldSpec.name} are adminOnly`); } } } if ((fieldSpec.type === FieldType.Component || fieldSpec.type === FieldType.RichText) && fieldSpec.componentTypes && fieldSpec.componentTypes.length > 0) { for (const referencedTypeName of fieldSpec.componentTypes) { const referencedComponentType = schema.getComponentTypeSpecification(referencedTypeName); if (!referencedComponentType) { return notOk.BadRequest(`${typeSpec.name}.${fieldSpec.name}: Component type in componentTypes ${referencedTypeName} doesn’t exist`); } if (referencedComponentType.adminOnly && typeCanBePublished && !fieldSpec.adminOnly) { return notOk.BadRequest(`${typeSpec.name}.${fieldSpec.name}: Referenced component type in componentTypes (${referencedTypeName}) is adminOnly, but neither ${typeSpec.name} nor ${fieldSpec.name} are adminOnly`); } } } if (fieldSpec.type === FieldType.RichText && fieldSpec.richTextNodes && fieldSpec.richTextNodes.length > 0) { const usedRichTextNodes = new Set(); for (const richTextNode of fieldSpec.richTextNodes) { if (usedRichTextNodes.has(richTextNode)) { return notOk.BadRequest(`${typeSpec.name}.${fieldSpec.name}: richTextNodes with type ${richTextNode} is duplicated`); } usedRichTextNodes.add(richTextNode); } const missingNodeTypes = REQUIRED_RICH_TEXT_NODES.filter((it) => !usedRichTextNodes.has(it)); if (missingNodeTypes.length > 0) { return notOk.BadRequest(`${typeSpec.name}.${fieldSpec.name}: richTextNodes must include ${missingNodeTypes.join(', ')}`); } for (const nodeGroup of GROUPED_RICH_TEXT_NODE_TYPES) { const usedNodesInGroup = nodeGroup.filter((it) => usedRichTextNodes.has(it)); if (usedNodesInGroup.length > 0 && usedNodesInGroup.length !== nodeGroup.length) { const unusedNodesInGroup = nodeGroup.filter((it) => !usedRichTextNodes.has(it)); return notOk.BadRequest(`${typeSpec.name}.${fieldSpec.name}: richTextNodes includes ${usedNodesInGroup.join(', ')} but must also include related ${unusedNodesInGroup.join(', ')}`); } } if (usedRichTextNodes.size > 0) { if (fieldSpec.componentTypes && fieldSpec.componentTypes.length > 0 && !usedRichTextNodes.has(RichTextNodeType.component)) { return notOk.BadRequest(`${typeSpec.name}.${fieldSpec.name}: componentTypes is specified for field, but richTextNodes is missing component`); } if (fieldSpec.entityTypes && fieldSpec.entityTypes.length > 0 && !usedRichTextNodes.has(RichTextNodeType.entity)) { return notOk.BadRequest(`${typeSpec.name}.${fieldSpec.name}: entityTypes is specified for field, but richTextNodes is missing entity`); } if (fieldSpec.linkEntityTypes && fieldSpec.linkEntityTypes.length > 0 && !usedRichTextNodes.has(RichTextNodeType.entityLink)) { return notOk.BadRequest(`${typeSpec.name}.${fieldSpec.name}: linkEntityTypes is specified for field, but richTextNodes is missing entityLink`); } // Renamed after v 0.4.7 if (usedRichTextNodes.has('valueItem')) { return notOk.BadRequest(`${typeSpec.name}.${fieldSpec.name}: richTextNodes can’t include valueItem, it has been renamed to component`); } } } if (fieldSpec.type === FieldType.String && fieldSpec.matchPattern) { const pattern = schema.getPattern(fieldSpec.matchPattern); if (!pattern) { return notOk.BadRequest(`${typeSpec.name}.${fieldSpec.name}: Unknown matchPattern (${fieldSpec.matchPattern})`); } } if (fieldSpec.type === FieldType.String && fieldSpec.matchPattern && fieldSpec.values.length > 0) { return notOk.BadRequest(`${typeSpec.name}.${fieldSpec.name}: Can’t specify both matchPattern and values`); } if (fieldSpec.type === FieldType.String && fieldSpec.index) { const index = schema.getIndex(fieldSpec.index); if (!index) { return notOk.BadRequest(`${typeSpec.name}.${fieldSpec.name}: Unknown index (${fieldSpec.index})`); } } } } const usedPatterns = new Set(); for (const patternSpec of schema.spec.patterns) { if (usedPatterns.has(patternSpec.name)) { return notOk.BadRequest(`${patternSpec.name}: Duplicate pattern name`); } if (!CAMEL_CASE_PATTERN.test(patternSpec.name)) { return notOk.BadRequest(`${patternSpec.name}: The pattern name has to start with a lower-case letter (a-z) and can only contain letters (a-z, A-Z), numbers and underscore (_), such as myPattern_123`); } usedPatterns.add(patternSpec.name); try { new RegExp(patternSpec.pattern); // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (_error) { return notOk.BadRequest(`${patternSpec.name}: Invalid regex`); } } const usedIndexes = new Set(); for (const indexSpec of schema.spec.indexes) { if (usedIndexes.has(indexSpec.name)) { return notOk.BadRequest(`${indexSpec.name}: Duplicate index name`); } if (!CAMEL_CASE_PATTERN.test(indexSpec.name)) { return notOk.BadRequest(`${indexSpec.name}: The index name has to start with a lower-case letter (a-z) and can only contain letters (a-z, A-Z), numbers and underscore (_), such as myIndex_123`); } usedIndexes.add(indexSpec.name); } return ok(undefined); } //# sourceMappingURL=schemaValidate.js.map