UNPKG

@backland/schema

Version:

TypeScript schema declaration and validation library with static type inference

641 lines (634 loc) 20 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.GraphQLParser = void 0; exports.createHooks = createHooks; exports.describeField = describeField; var _utils = require("@backland/utils"); var _graphql = require("graphql"); var _ObjectType = require("../ObjectType"); var _AliasField = require("../fields/AliasField"); var _LiteralField = require("../fields/LiteralField"); var _MetaFieldField = require("../fields/MetaFieldField"); var _ObjectField = require("../fields/ObjectField"); var _UnionField = require("../fields/UnionField"); var _isHiddenFieldName = require("../isHiddenFieldName"); var _parseTypeName = require("../parseTypeName"); var _GraphQLDateType = require("./GraphQLDateType"); var _GraphQLNullType = require("./GraphQLNullType"); var _GraphQLPhoneType = require("./GraphQLPhoneType"); var _GraphQLUlidType = require("./GraphQLUlidType"); function createHooks() { return { onField: _utils.hooks.parallel(), onFieldConfigMap: _utils.hooks.parallel(), onFieldResult: _utils.hooks.parallel(), willCreateObjectType: _utils.hooks.parallel() }; } const resultsCache = (0, _utils.createStore)(); const graphqlTypesRegister = (0, _utils.createStore)(); const fieldsRegister = (0, _utils.createStore)(); function wrapCreationWithCache(name, create) { return function creator(...args) { if (graphqlTypesRegister.has(name)) { return graphqlTypesRegister.get(name); } const result = create(...args); graphqlTypesRegister.set(name, result); return graphqlTypesRegister.get(name); }; } class GraphQLParser { static resultsCache = resultsCache; static graphqlTypesRegister = graphqlTypesRegister; static fieldsRegister = fieldsRegister; static reset = () => { resultsCache.clear(); graphqlTypesRegister.clear(); fieldsRegister.clear(); }; static objectIds = object => { if (!(0, _ObjectType.isObject)(object)) { throw new _utils.RuntimeError(`Invalid Object.`, { object }); } const { id: objectId } = (0, _utils.nonNullValues)({ id: object.id }, 'The provided object should be identified before converting. ' + 'You can use object.identify("abc")'); const cacheId = `${[objectId].filter(Boolean).join()}`; // return { cacheId, object, objectId }; }; static objectToGraphQL(init) { const { path } = init; const objectInput = init.object; const { cacheId, objectId } = this.objectIds(objectInput); const { implements: parents } = objectInput.meta; if (resultsCache.has(cacheId)) { return resultsCache.get(cacheId); } // save reference for circular dependencies const graphqlParsed = {}; resultsCache.set(cacheId, graphqlParsed); const buildFields = (options, _getType) => { const { interfaces: currentInterfacesOption } = options; options.interfaces = () => { const currentInterfaces = typeof currentInterfacesOption === 'function' ? currentInterfacesOption() : currentInterfacesOption || []; const types = parents ? parents.map(parent => _ObjectType.ObjectType.register.get(parent)) : []; return [...currentInterfaces, ...types.map(type => { return type.graphqlInterfaceType(); })]; }; options.fields = () => { var _options$middleware; const helpers = objectInput.helpers(); const objectMiddleware = objectInput.graphQLMiddleware || []; const builders = []; const hooks = createHooks(); (_options$middleware = options.middleware) === null || _options$middleware === void 0 ? void 0 : _options$middleware.call(options, hooks); objectMiddleware.forEach(fn => fn(hooks)); const fieldsConfigMap = {}; const aliases = []; helpers.list.forEach(({ name: fieldName, instance, plainField }) => { if ((0, _isHiddenFieldName.isHiddenFieldName)(fieldName)) return; if (plainField.hidden) return; if (plainField.type === 'alias') { _AliasField.AliasField.assert(instance); const subTypeName = (0, _parseTypeName.parseTypeName)({ field: plainField, fieldName, parentName: objectId }); aliases.push({ fieldName: subTypeName, instance, parentName: objectId, path: [objectId, fieldName] }); return; } const field = this.fieldToGraphQL({ field: instance, fieldName, parentName: objectId, path: path || [objectId] }); hooks.onFieldResult.exec(field); builders.push(field); }); function _useConvertFieldResult(next) { const field = { description: next.description, type: _getType(next) }; const origin = // @ts-ignore objectInput.definition[next.fieldName] || undefined; if (origin !== null && origin !== void 0 && origin.hidden) return; if ((origin === null || origin === void 0 ? void 0 : origin.defaultValue) !== undefined) { // @ts-ignore field.defaultValue = origin.defaultValue; } fieldsConfigMap[next.fieldName] = field; hooks.onField.exec(next, field); } builders.forEach(_useConvertFieldResult); aliases.forEach(({ instance, fieldName, parentName, path }) => { const { type } = instance.asFinalFieldDef.def; _useConvertFieldResult(this.fieldToGraphQL({ field: (0, _ObjectType.__getCachedFieldInstance)(type), fieldName, parentName, path })); }); hooks.onFieldConfigMap.exec(fieldsConfigMap); hooks.willCreateObjectType.exec(fieldsConfigMap); return fieldsConfigMap; }; return options; }; function getType(_options = {}) { const __options = { fields: {}, name: objectId, ..._options }; const { name } = __options; if (graphqlTypesRegister.has(name)) { return graphqlTypesRegister.get(name); } buildFields(__options, el => el.type()); const result = new _graphql.GraphQLObjectType(__options); graphqlTypesRegister.set(__options.name, result); return result; } function getInputType(_options = {}) { const options = { fields: {}, name: `${objectId}Input`, ..._options }; const { name } = options; if (graphqlTypesRegister.has(name)) { return graphqlTypesRegister.get(name); } buildFields(options, el => el.inputType()); const result = new _graphql.GraphQLInputObjectType(options); graphqlTypesRegister.set(name, result); return result; } function interfaceType(_options = {}) { const options = { fields: {}, name: `${objectId}Interface`, ..._options }; const { name } = options; if (graphqlTypesRegister.has(name)) { return graphqlTypesRegister.get(name); } buildFields(options, el => el.type()); const result = new _graphql.GraphQLInterfaceType(options); graphqlTypesRegister.set(name, result); return result; } function getSDL() { const result = getType(); const object = new _graphql.GraphQLSchema({ types: [result] }); return (0, _graphql.printSchema)(object); } function getInputSDL() { const object = new _graphql.GraphQLSchema({ types: [getInputType()] }); return (0, _graphql.printSchema)(object); } const parsed = { getInputType, getType, inputToString: getInputSDL, interfaceType, object: objectInput, typeToString: getSDL }; Object.assign(graphqlParsed, parsed); return graphqlParsed; } static fieldToGraphQL(init) { const { fieldName, parentName } = init; const fieldClone = init.field; const plainField = fieldClone.asFinalFieldDef; const { optional, typeName } = fieldClone; const { description } = plainField; const path = [...init.path, fieldName]; const cacheId = path.join('--'); if (resultsCache.has(cacheId)) { return fieldsRegister.get(cacheId); } // save reference for circular dependencies const fieldParsed = {}; fieldsRegister.set(cacheId, fieldParsed); const subTypeName = (0, _parseTypeName.parseTypeName)({ field: fieldClone, fieldName, parentName }); const self = this; // @ts-ignore const creators = { ID() { return { inputType: () => _graphql.GraphQLID, type: () => _graphql.GraphQLID }; }, alias() { return {}; // handled in object parser; }, any() { const create = wrapCreationWithCache('Any', () => new _graphql.GraphQLScalarType({ name: 'Any' })); return { inputType: create, type: create }; }, array() { const { utils: { listItemType: innerFieldType } } = fieldClone; const id = (0, _parseTypeName.parseTypeName)({ field: innerFieldType, fieldName, parentName }); const convertFieldResult = self.fieldToGraphQL({ field: innerFieldType, fieldName, parentName, path }); return { inputType: wrapCreationWithCache(`${id}ListInput`, (...args) => { return new _graphql.GraphQLList(convertFieldResult.inputType(...args)); }), type: wrapCreationWithCache(`${id}List`, (...args) => { return new _graphql.GraphQLList(convertFieldResult.type(...args)); }) }; }, boolean() { return { inputType: () => _graphql.GraphQLBoolean, type: () => _graphql.GraphQLBoolean }; }, cursor() { const cursor = fieldClone; return { inputType: cursor.utils.object.graphqlInputType, type: cursor.utils.object.graphqlType }; }, date() { return { inputType: () => _GraphQLDateType.GraphQLDateType, type: () => _GraphQLDateType.GraphQLDateType }; }, email() { return { inputType: () => _graphql.GraphQLString, type: () => _graphql.GraphQLString }; }, enum() { function createEnum(options) { const values = {}; (0, _utils.assertEqual)(fieldClone.type, 'enum'); fieldClone.def.forEach(key => { values[key] = { value: key }; }); return new _graphql.GraphQLEnumType({ name: subTypeName, ...options, values }); } return { inputType: wrapCreationWithCache(subTypeName, createEnum), type: wrapCreationWithCache(subTypeName, createEnum) }; }, float() { return { inputType: () => _graphql.GraphQLFloat, type: () => _graphql.GraphQLFloat }; }, int() { return { inputType: () => _graphql.GraphQLInt, type: () => _graphql.GraphQLInt }; }, literal() { if (!_LiteralField.LiteralField.is(fieldClone)) throw new Error('ts'); const { description, def } = fieldClone; const recordName = (0, _parseTypeName.parseTypeName)({ field: fieldClone, fieldName, parentName }); function createLiteral(options) { return new _graphql.GraphQLScalarType({ ...options, description: JSON.stringify(description || `Literal value: ${def.value}`), name: recordName, parseValue(value) { return _LiteralField.LiteralField.utils.deserialize({ ...def, value }); }, serialize(value) { return _LiteralField.LiteralField.utils.deserialize({ ...def, value }); } }); } return { inputType: wrapCreationWithCache(recordName, createLiteral), type: wrapCreationWithCache(recordName, createLiteral) }; }, meta() { throw new Error('meta field'); }, null() { return { inputType: () => _GraphQLNullType.GraphQLNullType, type: () => _GraphQLNullType.GraphQLNullType }; }, object() { (0, _utils.assertEqual)(fieldClone.type, 'object'); const id = (0, _parseTypeName.parseTypeName)({ field: fieldClone, fieldName, parentName }); const def = _ObjectType.ObjectType.is(fieldClone.def) ? fieldClone.def.clone(el => el.def()) : fieldClone.def; // @ts-ignore const object = _ObjectType.ObjectType.getOrSet(id, def); const res = self.objectToGraphQL({ object }); return { inputType: res.getInputType, type: res.getType }; }, phone() { return { inputType: () => new _GraphQLPhoneType.GraphQLPhoneType(), type: () => new _GraphQLPhoneType.GraphQLPhoneType() }; }, record() { const recordName = (0, _parseTypeName.parseTypeName)({ field: fieldClone, fieldName, parentName }); function createRecord(options) { return new _graphql.GraphQLScalarType({ ...options, name: recordName, parseValue(value) { return fieldClone.parse(value); }, serialize(value) { return fieldClone.parse(value); } }); } return { inputType: wrapCreationWithCache(recordName, createRecord), type: wrapCreationWithCache(recordName, createRecord) }; }, string() { return { inputType: () => _graphql.GraphQLString, type: () => _graphql.GraphQLString }; }, ulid() { return { inputType: () => _GraphQLUlidType.GraphQLUlidType, type: () => _GraphQLUlidType.GraphQLUlidType }; }, undefined() { const create = wrapCreationWithCache('Undefined', () => new _graphql.GraphQLScalarType({ name: 'Undefined' })); return { inputType: create, type: create }; }, union() { if (!_UnionField.UnionField.is(fieldClone)) throw fieldClone; let descriptions = []; // if all types are objects it can be used as normal GraphQL union // otherwise we need to create a scalar, since GraphQL only accepts unions of objects // - https://github.com/graphql/graphql-js/issues/207 // GraphQL union items cannot be used as input, so we always use scalars for inputs: // - https://github.com/graphql/graphql-spec/issues/488 let areAllObjects = true; fieldClone.utils.fieldTypes.forEach(field => { if (field.type !== 'object') areAllObjects = false; descriptions.push(describeField(field.definition)); }); let description = undefined; descriptions = descriptions.map(el => el.trim()); description = descriptions.join(' | '); if (description.length > 100) { description = `Union of:\n${descriptions.map(el => ` - ${el}`).join('\n')}`; } else { description = `Union of ${descriptions.join(' | ')}`.trim(); } description = description.replace(/ /g, ' '); const scalarUnion = new _graphql.GraphQLScalarType({ description, name: subTypeName, parseValue(value) { return fieldClone.parse(value); }, serialize(value) { return fieldClone.parse(value); } }); return { inputType: wrapCreationWithCache(subTypeName, () => { return scalarUnion; }), type: wrapCreationWithCache(subTypeName, (...options) => { if (!areAllObjects) return scalarUnion; return new _graphql.GraphQLUnionType({ name: subTypeName, ...options, types: fieldClone.utils.fieldTypes.map((field, index) => { if (!_ObjectField.ObjectField.is(field)) throw field; let object = field.utils.object; if (!object.id) { object = object.clone(el => el.objectType(`${subTypeName}_${index}`)); } return object.graphqlType(); }) }); }) }; }, unknown() { const GraphQLUnknownType = new _graphql.GraphQLScalarType({ name: subTypeName }); return { inputType: () => GraphQLUnknownType, type: () => GraphQLUnknownType }; } }; const result = { description, fieldName, inputType(...args) { if (typeName === 'alias') { // dev only assertion, aliasing should be handled in parseObject throw new Error(`can't handle alias in convertField.`); } let _result = creators[typeName]().inputType(...args); if (fieldClone.list) { _result = new _graphql.GraphQLList(_result); } if (!optional && fieldClone.defaultValue === undefined) { _result = new _graphql.GraphQLNonNull(_result); } return _result; }, plainField, type(...args) { if (typeName === 'alias') { // dev only assertion, aliasing should be handled in parseObject throw new Error(`can't handle alias in convertField.`); } let _result = creators[typeName]().type(...args); if (fieldClone.list) { _result = new _graphql.GraphQLList(_result); } if (!optional && (0, _graphql.isNullableType)(_result)) { _result = new _graphql.GraphQLNonNull(_result); } return _result; }, typeName }; Object.assign(fieldParsed, result); return result; } } exports.GraphQLParser = GraphQLParser; function describeField(field) { if (field.name) return field.name; if (field.type === 'literal') { const value = _utils.BJSON.stringify(_LiteralField.LiteralField.utils.deserialize(field.def), { quoteKeys(key) { return ` ${key}`; } }); return `${value}`; } return _utils.BJSON.stringify(field, { handler(payload) { const { value } = payload; if ((value === null || value === void 0 ? void 0 : value.type) === 'object' && value.def) { const meta = (0, _MetaFieldField.getObjectDefinitionMetaField)((value === null || value === void 0 ? void 0 : value.def) || {}); if (meta !== null && meta !== void 0 && meta.def.id) return meta.def.id; return describeField((0, _MetaFieldField.cleanMetaField)(value.def)); } return undefined; }, quoteKeys(key) { return ` ${key}`; }, quoteValues(value, { key }) { if (value === false) return ''; if (key === 'type') return ` ${value} `; return `${value}`; } }); } //# sourceMappingURL=GraphQLParser.js.map