UNPKG

@composedb/devtools

Version:

Development tools for ComposeDB projects.

634 lines (633 loc) 29.2 kB
function _check_private_redeclaration(obj, privateCollection) { if (privateCollection.has(obj)) { throw new TypeError("Cannot initialize the same private elements twice on an object"); } } function _class_apply_descriptor_get(receiver, descriptor) { if (descriptor.get) { return descriptor.get.call(receiver); } return descriptor.value; } function _class_apply_descriptor_set(receiver, descriptor, value) { if (descriptor.set) { descriptor.set.call(receiver, value); } else { if (!descriptor.writable) { throw new TypeError("attempted to set read only private field"); } descriptor.value = value; } } function _class_extract_field_descriptor(receiver, privateMap, action) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to " + action + " private field on non-instance"); } return privateMap.get(receiver); } function _class_private_field_get(receiver, privateMap) { var descriptor = _class_extract_field_descriptor(receiver, privateMap, "get"); return _class_apply_descriptor_get(receiver, descriptor); } function _class_private_field_init(obj, privateMap, value) { _check_private_redeclaration(obj, privateMap); privateMap.set(obj, value); } function _class_private_field_set(receiver, privateMap, value) { var descriptor = _class_extract_field_descriptor(receiver, privateMap, "set"); _class_apply_descriptor_set(receiver, descriptor, value); return value; } import { makeExecutableSchema } from '@graphql-tools/schema'; import { getDirectives, MapperKind, mapSchema } from '@graphql-tools/utils'; import { isEnumType, isInterfaceType, isListType, isNonNullType, isObjectType, isScalarType } from 'graphql'; import { NODE_INTERFACE_NAME } from '../constants.js'; import { getScalarSchema } from './scalars.js'; import { typeDefinitions } from './type-definitions.js'; const ACCOUNT_RELATIONS = { LIST: { type: 'list' }, NONE: { type: 'none' }, SET: { type: 'set', fields: [] }, SINGLE: { type: 'single' } }; var _def = /*#__PURE__*/ new WeakMap(), _schema = /*#__PURE__*/ new WeakMap(); export class SchemaParser { parse() { mapSchema(_class_private_field_get(this, _schema), { [MapperKind.ENUM_TYPE]: (type)=>{ if (type.name !== 'ModelAccountRelation') { _class_private_field_get(this, _def).enums[type.name] = type.getValues().map((v)=>v.name); } return type; }, [MapperKind.INTERFACE_TYPE]: (type)=>{ if (type.name === NODE_INTERFACE_NAME) { return type; } const directives = getDirectives(_class_private_field_get(this, _schema), type); const object = this._parseObject(type, directives); _class_private_field_get(this, _def).objects[type.name] = object; const model = this._parseModelDirective(type, directives, object); if (model == null) { throw new Error(`Missing @createModel or @loadModel directive for interface ${type.name}`); } else { _class_private_field_get(this, _def).models[type.name] = model; } return type; }, [MapperKind.OBJECT_TYPE]: (type)=>{ const directives = getDirectives(_class_private_field_get(this, _schema), type); const indices = this._parseIndices(directives); const object = this._parseObject(type, directives); object.indices = indices; _class_private_field_get(this, _def).objects[type.name] = object; const model = this._parseModelDirective(type, directives, object); if (model != null) { _class_private_field_get(this, _def).models[type.name] = model; } else if (indices.length > 0) { throw new Error('Indices added to type that is not a model'); } return type; }, [MapperKind.UNION_TYPE]: ()=>{ throw new Error('GraphQL unions are not supported'); } }); const modelsNames = Object.keys(_class_private_field_get(this, _def).models); if (modelsNames.length === 0) { throw new Error('No models found in Composite Definition Schema'); } // Once all models are defined, we need to validate the model names used in relations for (const name of modelsNames){ // Validate model names in model relations const model = _class_private_field_get(this, _def).models[name]; if (model.action === 'create') { for (const [key, relation] of Object.entries(model.relations)){ if (relation.type === 'document' && relation.model !== null) { if (relation.model === NODE_INTERFACE_NAME) { relation.model = null; } else { this._validateRelatedModel(key, relation.model); } } } } // Validate model names in object views const object = _class_private_field_get(this, _def).objects[name]; if (object == null) { throw new Error(`Missing object definition for model ${name}`); } for (const [key, field] of Object.entries(object.properties)){ if (field.type === 'view' && field.viewType === 'relation' && field.relation.model !== null) { if (field.relation.model === NODE_INTERFACE_NAME) { field.relation.model = null; } else { this._validateRelatedModel(key, field.relation.model); } } } } return _class_private_field_get(this, _def); } _parseIndices(directives) { return directives.flatMap((d)=>{ if (d.name === 'createIndex' && d.args) { const fields = d.args.fields; return [ { fields } ]; } else { return []; } }); } _validateRelatedModel(key, modelName) { const relatedModel = _class_private_field_get(this, _def).models[modelName]; if (relatedModel == null) { throw new Error(`Missing related model ${modelName} for relation defined on field ${key} of object ${modelName}`); } } _parseModelDirective(type, directives, object) { const createModel = directives.find((d)=>d.name === 'createModel'); const loadModel = directives.find((d)=>d.name === 'loadModel'); if (loadModel != null) { const id = loadModel.args?.id; if (id == null) { throw new Error(`Missing id value for @loadModel directive on object ${type.name}`); } if (createModel != null) { throw new Error(`Unsupported @createModel and @loadModel directives on same object ${type.name}`); } return { action: 'load', interface: isInterfaceType(type), id }; } if (createModel != null) { const isInterface = isInterfaceType(type); const args = createModel.args ?? {}; const accountRelationType = args.accountRelation ?? 'LIST'; const accountRelation = ACCOUNT_RELATIONS[isInterface ? 'NONE' : accountRelationType]; if (accountRelation == null) { throw new Error(`Unsupported accountRelation value ${accountRelationType} for @createModel directive on object ${type.name}`); } const accountRelationValue = { ...accountRelation }; if (accountRelationValue.type === 'set') { const accountRelationFields = args.accountRelationFields; if (accountRelationFields == null) { throw new Error(`Missing accountRelationFields argument for @createModel directive on object ${type.name}`); } if (accountRelationFields.length === 0) { throw new Error(`The accountRelationFields argument must specify at least one field for @createModel directive on object ${type.name}`); } const object = _class_private_field_get(this, _def).objects[type.name]; if (object == null) { throw new Error(`Missing object definition for ${type.name}`); } // Check properties are defined and valid in the JSON schema for the specified fields for (const field of accountRelationFields){ const property = object.properties[field]; if (property == null) { throw new Error(`Missing property ${field} defined in accountRelationFields argument for @createModel directive on object ${type.name}`); } if (!property.required) { throw new Error(`Property ${field} defined in accountRelationFields argument for @createModel directive on object ${type.name} must have a required value`); } if (property.type !== 'enum' && property.type !== 'scalar') { throw new Error(`Property ${field} defined in accountRelationFields argument for @createModel directive on object ${type.name} must use an enum or scalar type`); } } accountRelationValue.fields = accountRelationFields; } if (args.description == null || args.description === '') { throw new Error(`Missing description value for @createModel directive on object ${type.name}`); } const inheritedImmutableFields = type.getInterfaces().flatMap((interfaceObj)=>{ const fields = interfaceObj.getFields(); return Object.values(fields).filter((field)=>{ const { directives } = field.astNode; return directives.some((directive)=>directive.name.value === 'immutable'); }).map((field)=>field.name); }); return { action: 'create', interface: isInterfaceType(type), implements: type.getInterfaces().map((i)=>i.name), immutableFields: Array.from(new Set(Object.keys(object.properties).filter((key)=>object.properties[key].immutable === true).concat(inheritedImmutableFields))), description: args.description, accountRelation: accountRelationValue, relations: object.relations }; } } _parseObject(type, directives) { const { definition, references, relations } = this._parseObjectFields(type, directives); return { // implements: type.getInterfaces().map((i) => i.name), properties: definition, references: Array.from(new Set(references)), relations, indices: [] }; } _parseObjectFields(type, directives) { const objectFields = type.getFields(); const fields = {}; let references = []; const relations = {}; const hasCreateModel = directives.some((directive)=>directive.name === 'createModel') ?? false; for (const [key, value] of Object.entries(objectFields)){ const directives = getDirectives(_class_private_field_get(this, _schema), value); const [innerType, required] = isNonNullType(value.type) ? [ value.type.ofType, true ] : [ value.type, false ]; const relation = this._parseRelations(type.name, key, innerType, directives); if (relation != null) { relations[key] = relation; } const view = this._parseViews(type.name, key, innerType, directives, objectFields); if (view != null) { fields[key] = view; } else if (isListType(innerType)) { const list = this._parseListType(type.name, key, innerType, required, directives, hasCreateModel); fields[key] = list.definition; references = [ ...references, ...list.references ]; } else { const listDirective = directives.find((d)=>d.name === 'list'); if (listDirective != null) { throw new Error(`Unexpected @list directive on field ${key} of object ${type.name}`); } const item = this._parseItemType(type.name, key, value.type, directives, hasCreateModel); fields[key] = item.definition; references = [ ...references, ...item.references ]; } } return { definition: fields, references, relations }; } _parseRelations(objectName, fieldName, type, directives) { for (const directive of directives){ switch(directive.name){ case 'accountReference': if (!isScalarType(type) || type.name !== 'DID') { throw new Error(`Unsupported @accountReference directive on field ${fieldName} of object ${objectName}, @accountReference can only be set on a DID scalar`); } return { type: 'account' }; case 'documentReference': if (!isScalarType(type) || type.name !== 'StreamID') { throw new Error(`Unsupported @documentReference directive on field ${fieldName} of object ${objectName}, @documentReference can only be set on a StreamID scalar`); } return { type: 'document', model: directive.args?.model ?? null }; } } } _parseViews(objectName, fieldName, type, directives, objectFields) { for (const directive of directives){ switch(directive.name){ case 'documentAccount': if (!isScalarType(type) || type.name !== 'DID') { throw new Error(`Unsupported @documentAccount directive on field ${fieldName} of object ${objectName}, @documentAccount can only be set on a DID scalar`); } return { type: 'view', required: true, immutable: false, viewType: 'documentAccount' }; case 'documentVersion': if (!isScalarType(type) || type.name !== 'CommitID') { throw new Error(`Unsupported @documentVersion directive on field ${fieldName} of object ${objectName}, @documentVersion can only be set on a CommitID scalar`); } return { type: 'view', required: true, immutable: false, viewType: 'documentVersion' }; case 'relationDocument': { if (!isInterfaceType(type) && !isObjectType(type)) { throw new Error(`Unsupported @relationDocument directive on field ${fieldName} of object ${objectName}, @relationDocument can only be set on a referenced object`); } const property = directive.args?.property; if (property == null) { throw new Error(`Missing property argument for @relationDocument directive on field ${fieldName} of object ${objectName}`); } if (objectFields[property] == null) { throw new Error(`Missing referenced property ${property} for @relationDocument directive on field ${fieldName} of object ${objectName}`); } return { type: 'view', required: false, immutable: false, viewType: 'relation', relation: { source: 'document', model: type.name === NODE_INTERFACE_NAME ? null : type.name, property } }; } case 'relationFrom': { if (!isListType(type) || !(isInterfaceType(type.ofType) || isObjectType(type.ofType))) { throw new Error(`Unsupported @relationFrom directive on field ${fieldName} of object ${objectName}, @relationFrom can only be set on a list of referenced object`); } const model = type.ofType.name === NODE_INTERFACE_NAME ? null : type.ofType.name; const property = directive.args?.property; if (property == null) { throw new Error(`Missing property argument for @relationFrom directive on field ${fieldName} of object ${objectName}`); } return { type: 'view', required: true, immutable: false, viewType: 'relation', relation: { source: 'queryConnection', model, property } }; } case 'relationCountFrom': { if (!isScalarType(type) || type.name !== 'Int') { throw new Error(`Unsupported @relationCountFrom directive on field ${fieldName} of object ${objectName}, @relationCountFrom can only be set on a Int scalar`); } const model = directive.args?.model; if (model == null) { throw new Error(`Missing model argument for @relationCountFrom directive on field ${fieldName} of object ${objectName}`); } const property = directive.args?.property; if (property == null) { throw new Error(`Missing property argument for @relationCountFrom directive on field ${fieldName} of object ${objectName}`); } return { type: 'view', required: true, immutable: false, viewType: 'relation', relation: { source: 'queryCount', model, property } }; } case 'relationSetFrom': { if (!isObjectType(type)) { throw new Error(`Unsupported @relationSetFrom directive on field ${fieldName} of object ${objectName}, @relationSetFrom can only be set on a referenced object`); } const property = directive.args?.property; if (property == null) { throw new Error(`Missing property argument for @relationSetFrom directive on field ${fieldName} of object ${objectName}`); } return { type: 'view', required: false, viewType: 'relation', relation: { source: 'set', model: type.name, property } }; } } } } _parseListType(objectName, fieldName, type, required, directives, hasCreateModel) { const list = directives.find((d)=>d.name === 'list'); if (list == null) { throw new Error(`Missing @list directive on list field ${fieldName} of object ${objectName}`); } if (list.args?.maxLength == null) { throw new Error(`Missing maxLength value for @list directive on field ${fieldName} of object ${objectName}`); } const item = this._parseItemType(objectName, fieldName, type.ofType, directives, hasCreateModel); const definition = { type: 'list', required, item: item.definition, maxLength: list.args.maxLength }; if (list.args?.minLength != null) { definition.minLength = list.args.minLength; } return { definition, references: item.references }; } _parseItemType(objectName, fieldName, type, directives, hasCreateModel) { const required = isNonNullType(type); const immutable = directives.some((item)=>item.name === 'immutable'); if (immutable && !hasCreateModel) { throw new Error(`Unsupported immutable directive for ${fieldName} on nested object ${objectName}`); } const innerType = required ? type.ofType : type; if (isListType(innerType)) { throw new Error(`Unsupported nested list on field ${fieldName} of object ${objectName}`); } const referenceType = this._getReferenceFieldType(innerType); if (referenceType != null) { return { definition: { type: referenceType, required, immutable, name: innerType.name }, references: [ innerType.name ] }; } if (isScalarType(innerType)) { return { definition: { type: 'scalar', required, immutable, schema: this._parseScalarSchema(objectName, fieldName, innerType, directives) }, references: [] }; } throw new Error(`Unsupported type ${innerType.name} on field ${fieldName} of object ${objectName}`); } _parseScalarSchema(objectName, fieldName, type, directives) { const schema = getScalarSchema(type); const boolean = directives.find((d)=>d.name === 'boolean'); const float = directives.find((d)=>d.name === 'float'); const int = directives.find((d)=>d.name === 'int'); const string = directives.find((d)=>d.name === 'string'); switch(schema.type){ case 'boolean': { const mismatch = [ float, int, string ].find(Boolean); if (mismatch) { throw new Error(`Unexpected @${mismatch.name} directive with type ${type.name} on field ${fieldName} of object ${objectName}`); } break; } case 'integer': { const mismatch = [ boolean, float, string ].find(Boolean); if (mismatch) { throw new Error(`Unexpected @${mismatch.name} directive with type ${type.name} on field ${fieldName} of object ${objectName}`); } return this._validateIntegerSchema(objectName, fieldName, schema, int); } case 'number': { const mismatch = [ boolean, int, string ].find(Boolean); if (mismatch) { throw new Error(`Unexpected @${mismatch.name} directive with type ${type.name} on field ${fieldName} of object ${objectName}`); } return this._validateNumberSchema(objectName, fieldName, schema, float); } case 'string': { const mismatch = [ boolean, float, int ].find(Boolean); if (mismatch) { throw new Error(`Unexpected @${mismatch.name} directive with type ${type.name} on field ${fieldName} of object ${objectName}`); } return this._validateStringSchema(objectName, fieldName, schema, string); } } return schema; } _validateIntegerSchema(objectName, fieldName, schema, directive) { const args = directive?.args; return args ? this._validateNumberArguments(objectName, fieldName, schema, args) : schema; } _validateNumberSchema(objectName, fieldName, schema, directive) { const args = directive?.args; return args ? this._validateNumberArguments(objectName, fieldName, schema, args) : schema; } _validateNumberArguments(objectName, fieldName, schema, args) { if (args.max != null) { schema.maximum = args.max; } if (args.min != null) { schema.minimum = args.min; } if (args.default != null) { if (args.max != null && args.default > args.max) { throw new Error(`Default value is higher than max constraint on field ${fieldName} of object ${objectName}`); } if (args.min != null && args.default < args.min) { throw new Error(`Default value is lower than min constraint on field ${fieldName} of object ${objectName}`); } schema.default = args.default; } return schema; } _validateStringSchema(objectName, fieldName, schema, string) { const defaultValue = string?.args?.default ?? schema.default; const maxLength = string?.args?.maxLength ?? schema.maxLength; const minLength = string?.args?.minLength ?? schema.minLength; if (maxLength == null) { if (string == null) { throw new Error(`Missing @string directive on string field ${fieldName} of object ${objectName}`); } throw new Error(`Missing maxLength value for @string directive on field ${fieldName} of object ${objectName}`); } schema.maxLength = maxLength; if (minLength != null) { schema.minLength = minLength; } if (defaultValue != null) { if (defaultValue.length > maxLength) { throw new Error(`Length of default value is higher than maxLength constraint on field ${fieldName} of object ${objectName}`); } if (minLength != null && defaultValue.length < minLength) { throw new Error(`Length of default value is lower than minLength constraint on field ${fieldName} of object ${objectName}`); } schema.default = defaultValue; } return schema; } _getReferenceFieldType(type) { if (isEnumType(type)) { return 'enum'; } if (isObjectType(type)) { return 'object'; } } constructor(schema){ _class_private_field_init(this, _def, { writable: true, value: { enums: {}, models: {}, objects: {} } }); _class_private_field_init(this, _schema, { writable: true, value: void 0 }); _class_private_field_set(this, _schema, makeExecutableSchema({ typeDefs: [ typeDefinitions, schema ] })); } } export function parseSchema(schema) { return new SchemaParser(schema).parse(); }