UNPKG

graphile-build-pg

Version:

Build a GraphQL schema by reflection over a PostgreSQL schema. Easy to customize since it's built with plugins on graphile-build

1,406 lines (1,339 loc) 65.3 kB
// @flow import type { Plugin } from "graphile-build"; import makeGraphQLJSONType from "../GraphQLJSON"; import { parseInterval } from "../postgresInterval"; const ENUM_DOMAIN_SUFFIX = "_enum_domain"; function indent(str) { return " " + str.replace(/\n/g, "\n "); } function identity(value) { return value; } export default (function PgTypesPlugin( builder, { pgExtendedTypes = true, // Adding hstore support is technically a breaking change; this allows people to opt out easily: pgSkipHstore = false, pgGeometricTypes = false, pgUseCustomNetworkScalars = false, disableIssue390Fix = false, } ) { // XXX: most of this should be in an "init" hook, not a "build" hook builder.hook( "build", build => { const { pgIntrospectionResultsByKind: introspectionResultsByKind, getPgFakeEnumIdentifier, getTypeByName, pgSql: sql, inflection, graphql, } = build; /* * Note these do not do `foo.bind(build)` because they want to reference * the *latest* value of foo (i.e. after all the build hooks run) rather * than the current value of foo in this current hook. * * Also don't use this in your own code, only construct types *after* the * build hook has completed (i.e. 'init' or later). * * TODO:v5: move this to the 'init' hook. */ const newWithHooks = (...args) => build.newWithHooks(...args); const addType = (...args) => build.addType(...args); const { GraphQLNonNull, GraphQLString, GraphQLInt, GraphQLFloat, GraphQLBoolean, GraphQLList, GraphQLEnumType, GraphQLObjectType, GraphQLInputObjectType, GraphQLScalarType, isInputType, getNamedType, Kind, } = graphql; const gqlTypeByTypeIdGenerator = {}; const gqlInputTypeByTypeIdGenerator = {}; if (build.pgGqlTypeByTypeId || build.pgGqlInputTypeByTypeId) { // I don't expect anyone to receive this error, because I don't think anyone uses this interface. throw new Error( "Sorry! This interface is no longer supported because it is not granular enough. It's not hard to port it to the new system - please contact Benjie and he'll walk you through it." ); } const gqlTypeByTypeIdAndModifier = { ...build.pgGqlTypeByTypeIdAndModifier, }; const gqlInputTypeByTypeIdAndModifier = { ...build.pgGqlInputTypeByTypeIdAndModifier, }; const isNull = val => val == null || val.__isNull; const pg2GqlMapper = {}; const pg2gqlForType = type => { if (pg2GqlMapper[type.id]) { const map = pg2GqlMapper[type.id].map; return val => (isNull(val) ? null : map(val)); } else if (type.domainBaseType) { return pg2gqlForType(type.domainBaseType); } else if (type.isPgArray) { const elementHandler = pg2gqlForType(type.arrayItemType); return val => { if (isNull(val)) return null; if (!Array.isArray(val)) { throw new Error( `Expected array when converting PostgreSQL data into GraphQL; failing type: '${type.namespaceName}.${type.name}'` ); } return val.map(elementHandler); }; } else { return identity; } }; const pg2gql = (val, type) => pg2gqlForType(type)(val); const gql2pg = (val, type, modifier) => { if (modifier === undefined) { let stack; try { throw new Error(); } catch (e) { stack = e.stack; } // eslint-disable-next-line no-console console.warn( "gql2pg should be called with three arguments, the third being the type modifier (or `null`); " + (stack || "") ); // Hack for backwards compatibility: modifier = null; } if (val == null) { return sql.null; } if (pg2GqlMapper[type.id]) { return pg2GqlMapper[type.id].unmap(val, modifier); } else if (type.domainBaseType) { return gql2pg(val, type.domainBaseType, type.domainTypeModifier); } else if (type.isPgArray) { if (!Array.isArray(val)) { throw new Error( `Expected array when converting GraphQL data into PostgreSQL data; failing type: '${ type.namespaceName }.${type.name}' (type: ${type === null ? "null" : typeof type})` ); } return sql.fragment`array[${sql.join( val.map(v => gql2pg(v, type.arrayItemType, modifier)), ", " )}]::${ type.isFake ? sql.identifier("unknown") : sql.identifier(type.namespaceName, type.name) }`; } else { return sql.value(val); } }; const makeIntervalFields = () => { return { seconds: { description: build.wrapDescription( "A quantity of seconds. This is the only non-integer field, as all the other fields will dump their overflow into a smaller unit of time. Intervals don’t have a smaller unit than seconds.", "field" ), type: GraphQLFloat, }, minutes: { description: build.wrapDescription( "A quantity of minutes.", "field" ), type: GraphQLInt, }, hours: { description: build.wrapDescription("A quantity of hours.", "field"), type: GraphQLInt, }, days: { description: build.wrapDescription("A quantity of days.", "field"), type: GraphQLInt, }, months: { description: build.wrapDescription( "A quantity of months.", "field" ), type: GraphQLInt, }, years: { description: build.wrapDescription("A quantity of years.", "field"), type: GraphQLInt, }, }; }; const GQLInterval = newWithHooks( GraphQLObjectType, { name: inflection.builtin("Interval"), description: build.wrapDescription( "An interval of time that has passed where the smallest distinct unit is a second.", "type" ), fields: makeIntervalFields(), }, { isIntervalType: true, } ); addType(GQLInterval, "graphile-build-pg built-in"); const GQLIntervalInput = newWithHooks( GraphQLInputObjectType, { name: inflection.inputType(inflection.builtin("Interval")), description: build.wrapDescription( "An interval of time that has passed where the smallest distinct unit is a second.", "type" ), fields: makeIntervalFields(), }, { isIntervalInputType: true, } ); addType(GQLIntervalInput, "graphile-build-pg built-in"); const stringType = (name, description, coerce) => new GraphQLScalarType({ name, description, serialize: value => String(value), parseValue: coerce ? value => coerce(String(value)) : value => String(value), parseLiteral: ast => { if (ast.kind !== Kind.STRING) { throw new Error("Can only parse string values"); } if (coerce) { return coerce(ast.value); } else { return ast.value; } }, }); const BigFloat = stringType( inflection.builtin("BigFloat"), build.wrapDescription( "A floating point number that requires more precision than IEEE 754 binary 64", "type" ) ); const BitString = stringType( inflection.builtin("BitString"), build.wrapDescription( "A string representing a series of binary bits", "type" ) ); addType(BigFloat, "graphile-build-pg built-in"); addType(BitString, "graphile-build-pg built-in"); const rawTypes = [ 1186, // interval 1082, // date 1114, // timestamp 1184, // timestamptz 1083, // time 1266, // timetz ]; const tweakToJson = fragment => fragment; // Since everything is to_json'd now, just pass through const tweakToJsonArray = fragment => fragment; const tweakToText = fragment => sql.fragment`(${fragment})::text`; const tweakToTextArray = fragment => sql.fragment`(${fragment})::text[]`; const tweakToNumericText = fragment => sql.fragment`(${fragment})::numeric::text`; const tweakToNumericTextArray = fragment => sql.fragment`(${fragment})::numeric[]::text[]`; const pgTweaksByTypeIdAndModifer = {}; const pgTweaksByTypeId = { // '::text' rawTypes ...rawTypes.reduce((memo, typeId) => { memo[typeId] = tweakToText; return memo; }, {}), // cast numbers above our ken to strings to avoid loss of precision 20: tweakToText, 1700: tweakToText, // to_json all dates to make them ISO (overrides rawTypes above) 1082: tweakToJson, 1114: tweakToJson, 1184: tweakToJson, 1083: tweakToJson, 1266: tweakToJson, 790: tweakToNumericText, }; const pgTweakFragmentForTypeAndModifier = ( fragment, type, typeModifier = null, resolveData ) => { const typeModifierKey = typeModifier != null ? typeModifier : -1; const tweaker = (pgTweaksByTypeIdAndModifer[type.id] && pgTweaksByTypeIdAndModifer[type.id][typeModifierKey]) || pgTweaksByTypeId[type.id]; if (tweaker) { return tweaker(fragment, resolveData); } else if (type.domainBaseType) { if (type.domainBaseType.isPgArray) { // If we have a domain that's for example an `int8[]`, we must // process it into a `text[]` otherwise we risk loss of accuracy // when taking PostgreSQL's JSON into Node.js. const arrayItemType = type.domainBaseType.arrayItemType; const domainBaseTypeModifierKey = type.domainBaseTypeModifier != null ? type.domainBaseTypeModifier : -1; const arrayItemTweaker = (pgTweaksByTypeIdAndModifer[arrayItemType.id] && pgTweaksByTypeIdAndModifer[arrayItemType.id][ domainBaseTypeModifierKey ]) || pgTweaksByTypeId[arrayItemType.id]; // If it's a domain over a known type array (e.g. `bigint[]`), use // the Array version of the tweaker. switch (arrayItemTweaker) { case tweakToText: return tweakToTextArray(fragment); case tweakToNumericText: return tweakToNumericTextArray(fragment); case tweakToJson: return tweakToJsonArray(fragment); } // If we get here, it's not a simple type, so use our // infrastructure to figure out what tweaks to apply to the array // item. const sqlVal = sql.fragment`val`; const innerFragment = pgTweakFragmentForTypeAndModifier( sqlVal, arrayItemType, type.domainBaseTypeModifier, resolveData ); if (innerFragment === sqlVal) { // There was no tweak applied to the fragment, no change // necessary. return fragment; } else { // Tweaking was necessary, process each item in the array in this // way, and then return the resulting array, being careful that // nulls are preserved. return sql.fragment`(case when ${fragment} is null then null else array(select ${innerFragment} from unnest(${fragment}) as unnest(${sqlVal})) end)`; } } else { // TODO: check that domains don't support atttypemod return pgTweakFragmentForTypeAndModifier( fragment, type.domainBaseType, type.domainBaseTypeModifier, resolveData ); } } else if (type.isPgArray) { const error = new Error( `Internal graphile-build-pg error: should not attempt to tweak an array, please process array before tweaking (type: "${type.namespaceName}.${type.name}")` ); if (process.env.NODE_ENV === "test") { // This is to ensure that Graphile core does not introduce these problems throw error; } // eslint-disable-next-line no-console console.error(error); return fragment; } else { return fragment; } }; /* Determined by running: select oid, typname, typarray, typcategory, typtype from pg_catalog.pg_type where typtype = 'b' order by oid; We only need to add oidLookups for types that don't have the correct fallback */ const SimpleDate = stringType( inflection.builtin("Date"), build.wrapDescription("The day, does not include a time.", "type") ); const SimpleDatetime = stringType( inflection.builtin("Datetime"), build.wrapDescription( "A point in time as described by the [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) standard. May or may not include a timezone.", "type" ) ); const SimpleTime = stringType( inflection.builtin("Time"), build.wrapDescription( "The exact time of day, does not include the date. May or may not have a timezone offset.", "type" ) ); const SimpleJSON = stringType( inflection.builtin("JSON"), build.wrapDescription( "A JavaScript object encoded in the JSON format as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).", "type" ) ); const SimpleUUID = stringType( inflection.builtin("UUID"), build.wrapDescription( "A universally unique identifier as defined by [RFC 4122](https://tools.ietf.org/html/rfc4122).", "type" ), string => { if ( !/^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$/i.test( string ) ) { throw new Error( "Invalid UUID, expected 32 hexadecimal characters, optionally with hyphens" ); } return string; } ); const InetType = stringType( inflection.builtin("InternetAddress"), build.wrapDescription( "An IPv4 or IPv6 host address, and optionally its subnet.", "type" ) ); const RegProcType = stringType( inflection.builtin("RegProc"), build.wrapDescription( "A builtin object identifier type for a function name", "type" ) ); const RegProcedureType = stringType( inflection.builtin("RegProcedure"), build.wrapDescription( "A builtin object identifier type for a function with argument types", "type" ) ); const RegOperType = stringType( inflection.builtin("RegOper"), build.wrapDescription( "A builtin object identifier type for an operator", "type" ) ); const RegOperatorType = stringType( inflection.builtin("RegOperator"), build.wrapDescription( "A builtin object identifier type for an operator with argument types", "type" ) ); const RegClassType = stringType( inflection.builtin("RegClass"), build.wrapDescription( "A builtin object identifier type for a relation name", "type" ) ); const RegTypeType = stringType( inflection.builtin("RegType"), build.wrapDescription( "A builtin object identifier type for a data type name", "type" ) ); const RegRoleType = stringType( inflection.builtin("RegRole"), build.wrapDescription( "A builtin object identifier type for a role name", "type" ) ); const RegNamespaceType = stringType( inflection.builtin("RegNamespace"), build.wrapDescription( "A builtin object identifier type for a namespace name", "type" ) ); const RegConfigType = stringType( inflection.builtin("RegConfig"), build.wrapDescription( "A builtin object identifier type for a text search configuration", "type" ) ); const RegDictionaryType = stringType( inflection.builtin("RegDictionary"), build.wrapDescription( "A builtin object identifier type for a text search dictionary", "type" ) ); const CidrType = pgUseCustomNetworkScalars ? stringType( inflection.builtin("CidrAddress"), build.wrapDescription("An IPv4 or IPv6 CIDR address.", "type") ) : GraphQLString; const MacAddrType = pgUseCustomNetworkScalars ? stringType( inflection.builtin("MacAddress"), build.wrapDescription("A 6-byte MAC address.", "type") ) : GraphQLString; const MacAddr8Type = pgUseCustomNetworkScalars ? stringType( inflection.builtin("MacAddress8"), build.wrapDescription("An 8-byte MAC address.", "type") ) : GraphQLString; // pgExtendedTypes might change what types we use for things const JSONType = pgExtendedTypes ? makeGraphQLJSONType(graphql, inflection.builtin("JSON")) : SimpleJSON; const UUIDType = SimpleUUID; // GraphQLUUID const DateType = SimpleDate; // GraphQLDate const DateTimeType = SimpleDatetime; // GraphQLDateTime const TimeType = SimpleTime; // GraphQLTime // 'point' in PostgreSQL is a 16-byte type that's comprised of two 8-byte floats. const Point = newWithHooks( GraphQLObjectType, { name: inflection.builtin("Point"), fields: { x: { type: new GraphQLNonNull(GraphQLFloat), }, y: { type: new GraphQLNonNull(GraphQLFloat), }, }, }, { isPointType: true, } ); const PointInput = newWithHooks( GraphQLInputObjectType, { name: inflection.inputType(inflection.builtin("Point")), fields: { x: { type: new GraphQLNonNull(GraphQLFloat), }, y: { type: new GraphQLNonNull(GraphQLFloat), }, }, }, { isPointInputType: true, } ); // Other plugins might want to use JSON addType(JSONType, "graphile-build-pg built-in"); addType(UUIDType, "graphile-build-pg built-in"); addType(DateType, "graphile-build-pg built-in"); addType(DateTimeType, "graphile-build-pg built-in"); addType(TimeType, "graphile-build-pg built-in"); addType(RegProcType, "graphile-build-pg built-in"); addType(RegProcedureType, "graphile-build-pg built-in"); addType(RegOperType, "graphile-build-pg built-in"); addType(RegOperatorType, "graphile-build-pg built-in"); addType(RegClassType, "graphile-build-pg built-in"); addType(RegTypeType, "graphile-build-pg built-in"); addType(RegRoleType, "graphile-build-pg built-in"); addType(RegNamespaceType, "graphile-build-pg built-in"); addType(RegConfigType, "graphile-build-pg built-in"); addType(RegDictionaryType, "graphile-build-pg built-in"); const oidLookup = { 20: stringType( inflection.builtin("BigInt"), build.wrapDescription( "A signed eight-byte integer. The upper big integer values are greater than the max value for a JavaScript number. Therefore all big integers will be output as strings and not numbers.", "type" ) ), // bigint - even though this is int8, it's too big for JS int, so cast to string. 21: GraphQLInt, // int2 23: GraphQLInt, // int4 700: GraphQLFloat, // float4 701: GraphQLFloat, // float8 1700: BigFloat, // numeric 790: GraphQLFloat, // money 1186: GQLInterval, // interval 1082: DateType, // date 1114: DateTimeType, // timestamp 1184: DateTimeType, // timestamptz 1083: TimeType, // time 1266: TimeType, // timetz 114: JSONType, // json 3802: JSONType, // jsonb 2950: UUIDType, // uuid 1560: BitString, // bit 1562: BitString, // varbit 18: GraphQLString, // char 25: GraphQLString, // text 1043: GraphQLString, // varchar 600: Point, // point 869: InetType, 650: CidrType, 829: MacAddrType, 774: MacAddr8Type, 24: RegProcType, 2202: RegProcedureType, 2203: RegOperType, 2204: RegOperatorType, 2205: RegClassType, 2206: RegTypeType, 4096: RegRoleType, 4089: RegNamespaceType, 3734: RegConfigType, 3769: RegDictionaryType, }; const oidInputLookup = { 1186: GQLIntervalInput, // interval 600: PointInput, // point }; const jsonStringify = o => JSON.stringify(o); if (pgExtendedTypes) { pg2GqlMapper[114] = { map: identity, unmap: o => sql.value(jsonStringify(o)), }; } else { pg2GqlMapper[114] = { map: jsonStringify, unmap: str => sql.value(str), }; } pg2GqlMapper[3802] = pg2GqlMapper[114]; // jsonb // interval pg2GqlMapper[1186] = { map: str => parseInterval(str), unmap: o => { const keys = [ "seconds", "minutes", "hours", "days", "months", "years", ]; const parts = []; for (const key of keys) { if (o[key]) { parts.push(`${o[key]} ${key}`); } } return sql.value(parts.join(" ") || "0 seconds"); }, }; pg2GqlMapper[790] = { map: _ => _, unmap: val => sql.fragment`(${sql.value(val)})::money`, }; // point pg2GqlMapper[600] = { map: f => { if (f[0] === "(" && f[f.length - 1] === ")") { const [x, y] = f .slice(1, -1) .split(",") .map(f => parseFloat(f)); return { x, y }; } }, unmap: o => sql.fragment`point(${sql.value(o.x)}, ${sql.value(o.y)})`, }; // TODO: add more support for geometric types let depth = 0; /* * Enforce: this is the fallback when we can't find a specific GraphQL type * for a specific PG type. Use the generators from * `pgRegisterGqlTypeByTypeId` first, this is a last resort. */ const enforceGqlTypeByPgTypeId = (typeId, typeModifier) => { const type = introspectionResultsByKind.type.find(t => t.id === typeId); depth++; if (depth > 50) { throw new Error("Type enforcement went too deep - infinite loop?"); } try { return reallyEnforceGqlTypeByPgTypeAndModifier(type, typeModifier); } catch (e) { const error = new Error( `Error occurred when processing database type '${ type.namespaceName }.${type.name}' (type=${type.type}):\n${indent(e.message)}` ); // $FlowFixMe error.originalError = e; throw error; } finally { depth--; } }; const reallyEnforceGqlTypeByPgTypeAndModifier = (type, typeModifier) => { if (!type.id) { throw new Error( `Invalid argument to enforceGqlTypeByPgTypeId - expected a full type, received '${type}'` ); } if (!gqlTypeByTypeIdAndModifier[type.id]) { gqlTypeByTypeIdAndModifier[type.id] = {}; } if (!gqlInputTypeByTypeIdAndModifier[type.id]) { gqlInputTypeByTypeIdAndModifier[type.id] = {}; } const typeModifierKey = typeModifier != null ? typeModifier : -1; // Explicit overrides if (!gqlTypeByTypeIdAndModifier[type.id][typeModifierKey]) { const gqlType = oidLookup[type.id]; if (gqlType) { gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] = gqlType; } } if (!gqlInputTypeByTypeIdAndModifier[type.id][typeModifierKey]) { const gqlInputType = oidInputLookup[type.id]; if (gqlInputType) { gqlInputTypeByTypeIdAndModifier[type.id][typeModifierKey] = gqlInputType; } } // cf https://github.com/graphile/graphile-engine/pull/748#discussion_r650828353 let shouldSkipAddType = false; // Enums if ( !gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] && type.type === "e" ) { gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] = newWithHooks( GraphQLEnumType, { name: inflection.enumType(type), description: type.description, values: type.enumVariants.reduce((memo, value, i) => { memo[inflection.enumName(value)] = { description: type.enumDescriptions ? type.enumDescriptions[i] : null, value: value, }; return memo; }, {}), }, { pgIntrospection: type, isPgEnumType: true, } ); } // Ranges if ( !gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] && type.type === "r" ) { const subtype = introspectionResultsByKind.typeById[type.rangeSubTypeId]; const gqlRangeSubType = getGqlTypeByTypeIdAndModifier( subtype.id, typeModifier ); const gqlRangeInputSubType = getGqlInputTypeByTypeIdAndModifier( subtype.id, typeModifier ); if (!gqlRangeSubType) { throw new Error("Range of unsupported"); } if (!gqlRangeInputSubType) { throw new Error("Range of unsupported input type"); } let Range = getTypeByName(inflection.rangeType(gqlRangeSubType.name)); let RangeInput; if (!Range) { const RangeBound = newWithHooks( GraphQLObjectType, { name: inflection.rangeBoundType(gqlRangeSubType.name), description: build.wrapDescription( "The value at one end of a range. A range can either include this value, or not.", "type" ), fields: { value: { description: build.wrapDescription( "The value at one end of our range.", "field" ), type: new GraphQLNonNull(gqlRangeSubType), }, inclusive: { description: build.wrapDescription( "Whether or not the value of this bound is included in the range.", "field" ), type: new GraphQLNonNull(GraphQLBoolean), }, }, }, { isPgRangeBoundType: true, pgIntrospection: type, pgSubtypeIntrospection: subtype, pgTypeModifier: typeModifier, } ); const RangeBoundInput = newWithHooks( GraphQLInputObjectType, { name: inflection.inputType(RangeBound.name), description: build.wrapDescription( "The value at one end of a range. A range can either include this value, or not.", "type" ), fields: { value: { description: build.wrapDescription( "The value at one end of our range.", "field" ), type: new GraphQLNonNull(gqlRangeInputSubType), }, inclusive: { description: build.wrapDescription( "Whether or not the value of this bound is included in the range.", "field" ), type: new GraphQLNonNull(GraphQLBoolean), }, }, }, { isPgRangeBoundInputType: true, pgIntrospection: type, pgSubtypeIntrospection: subtype, pgTypeModifier: typeModifier, } ); Range = newWithHooks( GraphQLObjectType, { name: inflection.rangeType(gqlRangeSubType.name), description: build.wrapDescription( `A range of \`${gqlRangeSubType.name}\`.`, "type" ), fields: { start: { description: build.wrapDescription( "The starting bound of our range.", "field" ), type: RangeBound, }, end: { description: build.wrapDescription( "The ending bound of our range.", "field" ), type: RangeBound, }, }, }, { isPgRangeType: true, pgIntrospection: type, pgSubtypeIntrospection: subtype, pgTypeModifier: typeModifier, } ); RangeInput = newWithHooks( GraphQLInputObjectType, { name: inflection.inputType(Range.name), description: build.wrapDescription( `A range of \`${gqlRangeSubType.name}\`.`, "type" ), fields: { start: { description: build.wrapDescription( "The starting bound of our range.", "field" ), type: RangeBoundInput, }, end: { description: build.wrapDescription( "The ending bound of our range.", "field" ), type: RangeBoundInput, }, }, }, { isPgRangeInputType: true, pgIntrospection: type, pgSubtypeIntrospection: subtype, pgTypeModifier: typeModifier, } ); addType(Range, "graphile-build-pg built-in"); addType(RangeInput, "graphile-build-pg built-in"); } else { RangeInput = getTypeByName(inflection.inputType(Range.name)); } gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] = Range; gqlInputTypeByTypeIdAndModifier[type.id][typeModifierKey] = RangeInput; if (pgTweaksByTypeIdAndModifer[type.id] === undefined) { pgTweaksByTypeIdAndModifer[type.id] = {}; } pgTweaksByTypeIdAndModifer[type.id][typeModifierKey] = fragment => sql.fragment`\ case when (${fragment}) is null then null else json_build_object( 'start', case when lower(${fragment}) is null then null else json_build_object('value', ${pgTweakFragmentForTypeAndModifier( sql.fragment`lower(${fragment})`, subtype, typeModifier, {} )}, 'inclusive', lower_inc(${fragment})) end, 'end', case when upper(${fragment}) is null then null else json_build_object('value', ${pgTweakFragmentForTypeAndModifier( sql.fragment`upper(${fragment})`, subtype, typeModifier, {} )}, 'inclusive', upper_inc(${fragment})) end ) end`; pg2GqlMapper[type.id] = { map: identity, unmap: ({ start, end }) => { // Ref: https://www.postgresql.org/docs/9.6/static/rangetypes.html#RANGETYPES-CONSTRUCT const lower = (start && gql2pg(start.value, subtype, null)) || sql.null; const upper = (end && gql2pg(end.value, subtype, null)) || sql.null; const lowerInclusive = start && !start.inclusive ? "(" : "["; const upperInclusive = end && !end.inclusive ? ")" : "]"; return sql.fragment`${sql.identifier( type.namespaceName, type.name )}(${lower}, ${upper}, ${sql.literal( lowerInclusive + upperInclusive )})`; }, }; } // Domains if ( !gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] && type.type === "d" && type.domainBaseTypeId ) { // might be used as an enum alias: https://github.com/graphile/postgraphile/issues/1500 const tagEnumName = type.tags && typeof type.tags.enum === "string" ? type.tags.enum : null; const truncatedTypeName = !tagEnumName && type.name.endsWith(ENUM_DOMAIN_SUFFIX) ? type.name.slice(0, type.name.length - ENUM_DOMAIN_SUFFIX.length) : null; const underlyingEnumTypeName = tagEnumName || truncatedTypeName; if (underlyingEnumTypeName !== null) { const baseTypeId = getPgFakeEnumIdentifier( type.namespaceName, underlyingEnumTypeName ); const baseType = getGqlTypeByTypeIdAndModifier( baseTypeId, typeModifierKey ); const baseInputType = getGqlInputTypeByTypeIdAndModifier( baseTypeId, typeModifierKey ); if (!baseType || !baseInputType) { if (tagEnumName) { throw new Error( `The domain ${type.name} uses '@enum ${tagEnumName}' to references its underlying enum type, but no type named ${tagEnumName} could be found.` ); } else { throw new Error( `The domain ${type.name} references the enum type ${underlyingEnumTypeName}, but no such type could be found.` ); } } gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] = baseType; gqlInputTypeByTypeIdAndModifier[type.id][typeModifierKey] = baseInputType; shouldSkipAddType = true; } else { const baseType = getGqlTypeByTypeIdAndModifier( type.domainBaseTypeId, typeModifier ); const baseInputType = getGqlInputTypeByTypeIdAndModifier( type.domainBaseTypeId, typeModifier ); // Hack stolen from: https://github.com/graphile/postgraphile/blob/ade728ed8f8e3ecdc5fdad7d770c67aa573578eb/src/graphql/schema/type/aliasGqlType.ts#L16 gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] = Object.assign(Object.create(baseType), { name: inflection.domainType(type), description: type.description, }); if (baseInputType && baseInputType !== baseType) { gqlInputTypeByTypeIdAndModifier[type.id][typeModifierKey] = Object.assign(Object.create(baseInputType), { name: inflection.inputType( gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] ), description: type.description, }); } } } // Arrays if ( !gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] && type.category === "A" ) { const arrayEntryOutputType = getGqlTypeByTypeIdAndModifier( type.arrayItemTypeId, typeModifier ); gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] = new GraphQLList(arrayEntryOutputType); if (!disableIssue390Fix) { const arrayEntryInputType = getGqlInputTypeByTypeIdAndModifier( type.arrayItemTypeId, typeModifier ); if (arrayEntryInputType) { gqlInputTypeByTypeIdAndModifier[type.id][typeModifierKey] = new GraphQLList(arrayEntryInputType); } } } // Booleans if ( !gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] && type.category === "B" ) { gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] = GraphQLBoolean; } // Numbers may be too large for GraphQL/JS to handle, so stringify by // default. if ( !gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] && type.category === "N" ) { pgTweaksByTypeId[type.id] = tweakToText; gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] = BigFloat; } // Nothing else worked; pass through as string! if (!gqlTypeByTypeIdAndModifier[type.id][typeModifierKey]) { // XXX: consider using stringType(upperFirst(camelCase(`fallback_${type.name}`)), type.description)? gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] = GraphQLString; } // Now for input types, fall back to output types if possible if (!gqlInputTypeByTypeIdAndModifier[type.id][typeModifierKey]) { if ( isInputType(gqlTypeByTypeIdAndModifier[type.id][typeModifierKey]) ) { gqlInputTypeByTypeIdAndModifier[type.id][typeModifierKey] = gqlTypeByTypeIdAndModifier[type.id][typeModifierKey]; } } // if its a domain type substituting for an enum table, // adding it creates a duplicate if (!shouldSkipAddType) { addType( getNamedType(gqlTypeByTypeIdAndModifier[type.id][typeModifierKey]) ); } return gqlTypeByTypeIdAndModifier[type.id][typeModifierKey]; }; function getGqlTypeByTypeIdAndModifier( typeId, typeModifier = null, useFallback = true ) { const typeModifierKey = typeModifier != null ? typeModifier : -1; if (!gqlTypeByTypeIdAndModifier[typeId]) { gqlTypeByTypeIdAndModifier[typeId] = {}; } if (!gqlInputTypeByTypeIdAndModifier[typeId]) { gqlInputTypeByTypeIdAndModifier[typeId] = {}; } if (!gqlTypeByTypeIdAndModifier[typeId][typeModifierKey]) { const type = introspectionResultsByKind.type.find( t => t.id === typeId ); if (!type) { throw new Error( `Type '${typeId}' not present in introspection results` ); } const gen = gqlTypeByTypeIdGenerator[type.id]; if (gen) { const set = Type => { gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] = Type; }; const result = gen(set, typeModifier); if (result) { if ( gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] && gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] !== result ) { throw new Error( `Callback and return types differ when defining type for '${type.id}'` ); } gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] = result; } } } if ( !gqlTypeByTypeIdAndModifier[typeId][typeModifierKey] && typeModifierKey > -1 ) { // Fall back to `null` modifier, but if that still doesn't work, we // still want to pass the modifier to enforceGqlTypeByPgTypeId. const fallback = getGqlTypeByTypeIdAndModifier(typeId, null, false); if (fallback) { return fallback; } } if ( useFallback && !gqlTypeByTypeIdAndModifier[typeId][typeModifierKey] ) { return enforceGqlTypeByPgTypeId(typeId, typeModifier); } return gqlTypeByTypeIdAndModifier[typeId][typeModifierKey]; } function getGqlInputTypeByTypeIdAndModifier(typeId, typeModifier = null) { // First, load the OUTPUT type (it might register an input type) getGqlTypeByTypeIdAndModifier(typeId, typeModifier); const typeModifierKey = typeModifier != null ? typeModifier : -1; if (!gqlInputTypeByTypeIdAndModifier[typeId]) { gqlInputTypeByTypeIdAndModifier[typeId] = {}; } if (!gqlInputTypeByTypeIdAndModifier[typeId][typeModifierKey]) { const type = introspectionResultsByKind.typeById[typeId]; if (!type) { throw new Error( `Type '${typeId}' not present in introspection results` ); } const gen = gqlInputTypeByTypeIdGenerator[type.id]; if (gen) { const set = Type => { gqlInputTypeByTypeIdAndModifier[type.id][typeModifierKey] = Type; }; const result = gen(set, typeModifier); if (result) { if ( gqlInputTypeByTypeIdAndModifier[type.id][typeModifierKey] && gqlInputTypeByTypeIdAndModifier[type.id][typeModifierKey] !== result ) { throw new Error( `Callback and return types differ when defining type for '${type.id}'` ); } gqlInputTypeByTypeIdAndModifier[type.id][typeModifierKey] = result; } } } // Use the same type as the output type if it's valid input if ( !gqlInputTypeByTypeIdAndModifier[typeId][typeModifierKey] && gqlTypeByTypeIdAndModifier[typeId] && gqlTypeByTypeIdAndModifier[typeId][typeModifierKey] && isInputType(gqlTypeByTypeIdAndModifier[typeId][typeModifierKey]) ) { gqlInputTypeByTypeIdAndModifier[typeId][typeModifierKey] = gqlTypeByTypeIdAndModifier[typeId][typeModifierKey]; } if ( !gqlInputTypeByTypeIdAndModifier[typeId][typeModifierKey] && typeModifierKey > -1 ) { // Fall back to default return getGqlInputTypeByTypeIdAndModifier(typeId, null); } return gqlInputTypeByTypeIdAndModifier[typeId][typeModifierKey]; } function registerGqlTypeByTypeId(typeId, gen, yieldToExisting = false) { if (gqlTypeByTypeIdGenerator[typeId]) { if (yieldToExisting) { return; } throw new Error( `There's already a type generator registered for '${typeId}'` ); } gqlTypeByTypeIdGenerator[typeId] = gen; } function registerGqlInputTypeByTypeId( typeId, gen, yieldToExisting = false ) { if (gqlInputTypeByTypeIdGenerator[typeId]) { if (yieldToExisting) { return; } throw new Error( `There's already an input type generator registered for '${typeId}'` ); } gqlInputTypeByTypeIdGenerator[typeId] = gen; } // DEPRECATIONS! function getGqlTypeByTypeId(typeId, typeModifier) { if (typeModifier === undefined) { // eslint-disable-next-line no-console console.warn( "DEPRECATION WARNING: getGqlTypeByTypeId should not be used - for some columns we also require typeModifier to be specified. Please update your code ASAP to pass `attribute.typeModifier` through as the second parameter (or null if it's not available)." ); } return getGqlTypeByTypeIdAndModifier(typeId, typeModifier); } function getGqlInputTypeByTypeId(typeId, typeModifier) { if (typeModifier === undefined) { // eslint-disable-next-line no-console console.warn( "DEPRECATION WARNING: getGqlInputTypeByTypeId should not be used - for some columns we also require typeModifier to be specified. Please update your code ASAP to pass `attribute.typeModifier` through as the second parameter (or null if it's not available)." ); } return getGqlInputTypeByTypeIdAndModifier(typeId, typeModifier); } function pgTweakFragmentForType( fragment, type, typeModifier, resolveData ) { if (typeModifier === undefined) { // eslint-disable-next-line no-console console.warn( "DEPRECATION WARNING: pgTweakFragmentForType should not be used - for some columns we also require typeModifier to be specified. Please update your code ASAP to pass `attribute.typeModifier` through as the third parameter (or null if it's not available)." ); } return pgTweakFragmentForTypeAndModifier( fragment, type, typeModifier, resolveData ); } // END OF DEPRECATIONS! return build.extend(build, { pgRegisterGqlTypeByTypeId: registerGqlTypeByTypeId, pgRegisterGqlInputTypeByTypeId: registerGqlInputTypeByTypeId, pgGetGqlTypeByTypeIdAndModifier: getGqlTypeByTypeIdAndModifier, pgGetGqlInputTypeByTypeIdAndModifier: getGqlInputTypeByTypeIdAndModifier, pg2GqlMapper, pg2gql, pg2gqlForType, gql2pg, pgTweakFragmentForTypeAndModifier, pgTweaksByTypeId, pgTweaksByTypeIdAndModifer, // DEPRECATED METHODS: pgGetGqlTypeByTypeId: getGqlTypeByTypeId, // DEPRECATED, replaced by getGqlTypeByTypeIdAndModifier pgGetGqlInputTypeByTypeId: getGqlInputTypeByTypeId, // DEPRECATED, replaced by getGqlInputTypeByTypeIdAndModifier pgTweakFragmentForType, // DEPRECATED, replaced by pgTweakFragmentForTypeAndModifier }); }, ["PgTypes"], [], ["PgIntrospection", "StandardTypes"] ); /* Start of hstore type */ builder.hook( "inflection", (inflection, build) => { // This hook allows you to append a plugin which renames the KeyValueHash // (hstore) type name. if (pgSkipHstore) return build; return build.extend(inflection, { hstoreType() { return "KeyValueHash"; }, }); }, ["PgTypesHstore"] ); builder.hook( "build", build => { // This hook tells graphile-build-pg about the hstore database type so it // knows how to express it in input/output. if (pgSkipHstore) return build; const { pgIntrospectionResultsByKind: introspectionResultsByKind, pgRegisterGqlTypeByTypeId, pgRegisterGqlInputTypeByTypeId, pg2GqlMapper, pgSql: sql, } = build; // Check we have the hstore extension const hstoreExtension = introspectionResultsByKind.extension.find( e => e.name === "hstore" ); if (!hstoreExtension) { return build; } // Get the 'hstore' type itself: const hstoreType = introspectionResultsByKind.type.find( t => t.name === "hstore" && t.namespaceId === hstoreExtension.namespaceId ); if (!hstoreType) { return build; } const hstoreTypeName = build.inflection.hstoreType(); // We're going to use our own special HStore type for this so that we get // better validation; but you could just as easily use JSON directly if you // wanted to. const GraphQLHStoreType = makeGraphQLHstoreType(build, hstoreTypeName); // Now register the hstore type with the type system for both output and input. pgRegisterGqlTypeByTypeId(hstoreType.id, () => GraphQLHStoreType); pgRegisterGqlInputTypeByTypeId(hstoreType.id, () => GraphQLHStoreType); // Finally we must tell the system how to translate the data between PG-land and JS-land: pg2GqlMapper[hstoreType.id] = { // node-postgres parses hstore for us, no action required on map