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,175 lines (1,137 loc) 57.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _GraphQLJSON = _interopRequireDefault(require("../GraphQLJSON")); var _postgresInterval = require("../postgresInterval"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const ENUM_DOMAIN_SUFFIX = "_enum_domain"; function indent(str) { return " " + str.replace(/\n/g, "\n "); } function identity(value) { return value; } var PgTypesPlugin = 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 ? (0, _GraphQLJSON.default)(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 => (0, _postgresInterval.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 map: identity, // When unmapping we need to convert back to hstore unmap: o => sql.fragment`(${sql.value(hstoreStringify(o))}::${sql.identifier(hstoreType.namespaceName, hstoreType.name)})` }; return build; }, ["PgTypesHstore"], [], ["PgTypes"]); /* End of hstore type */ /* Geometric types */ 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 (!pgGeometricTypes) return build; const { pgRegisterGqlTypeByTypeId, pgRegisterGqlInputTypeByTypeId, pgGetGqlTypeByTypeIdAndModifier, pgGetGqlInputTypeByTypeIdAndModifier, pg2GqlMapper, pgSql: sql, graphql: { GraphQLObjectType, GraphQLInputObjectType, GraphQLList, GraphQLBoolean, GraphQLFloat }, inflection } = build; // Check we have the hstore extension const LINE = 628; const LSEG = 601; const BOX = 603; const PATH = 602; const POLYGON = 604; const CIRCLE = 718; pgRegisterGqlTypeByTypeId(LINE, () => { const Point = pgGetGqlTypeByTypeIdAndModifier("600", null); if (!Point) { throw new Error("Need point type"); } return new GraphQLObjectType({ name: inflection.builtin("Line"), description: build.wrapDescription("An infinite line that passes through points 'a' and 'b'.", "type"), fields: { a: { type: Point }, b: { type: Point } } }); }); pgRegisterGqlInputTypeByTypeId(LINE, () => { const PointInput = pgGetGqlInputTypeByTypeIdAndModifier("600", null); return new GraphQLInputObjectType({ name: inflection.builtin("LineInput"), description: build.wrapDescription("An infinite line that passes through points 'a' and 'b'.", "type"), fields: { a: { type: PointInput }, b: { type: PointInput } } }); }); pg2GqlMapper[LINE] = { map: f => { if (f[0] === "{" && f[f.length - 1] === "}") { const [A, B, C] = f.slice(1, -1).split(",").map(f => parseFloat(f)); // Lines have the form Ax + By + C = 0. // So if y = 0, Ax + C = 0; x = -C/A. // If x = 0, By + C = 0; y = -C/B. return { a: { x: -C / A, y: 0 }, b: { x: 0, y: -C / B } }; } }, unmap: o => sql.fragment`line(point(${sql.value(o.a.x)}, ${sql.value(o.a.y)}), point(${sql.value(o.b.x)}, ${sql.value(o.b.y)}))` }; pgRegisterGqlTypeByTypeId(LSEG, () => { const Point = pgGetGqlTypeByTypeIdAndModifier("600", null); return new GraphQLObjectType({ name: inflection.builtin("LineSegment"), description: build.wrapDescription("An finite line between points 'a' and 'b'.", "type"), fields: { a: { type: Point }, b: { type: Point } } }); }); pgRegisterGqlInputTypeByTypeId(LSEG, () => { const PointInput = pgGetGqlInputTypeByTypeIdAndModifier("600", null); return new GraphQLInputObjectType({ name: inflection.builtin("LineSegmentInput"), description: build.wrapDescription("An finite line between points 'a' and 'b'.", "type"), fields: { a: { type: PointInput }, b: { type: PointInput } } }); }); pg2GqlMapper[LSEG] = { map: f => { if (f[0] === "[" && f[f.length - 1] === "]") { const [x1, y1, x2, y2] = f.slice(1, -1).replace(/[()]/g, "").split(",").map(f => parseFloat(f)); return { a: { x: x1, y: y1 }, b: { x: x2, y: y2 } }; } }, unmap: o => sql.fragment`lseg(point(${sql.value(o.a.x)}, ${sql.value(o.a.y)}), point(${sql.value(o.b.x)}, ${sql.value(o.b.y)}))` }; pgRegisterGqlTypeByTypeId(BOX, () => { const Point = pgGetGqlTypeByTypeIdAndModifier("600", null); return new GraphQLObjectType({ name: inflection.builtin("Box"), description: build.wrapDescription("A rectangular box defined by two opposite corners 'a' and 'b'", "type"), fields: { a: { type: Point }, b: { type: Point } } }); }); pgRegisterGqlInputTypeByTypeId(BOX, () => { const PointInput = pgGetGqlInputTypeByTypeIdAndModifier("600", null); return new GraphQLInputObjectType({ name: inflection.builtin("BoxInput"), description: build.wrapDescription("A rectangular box defined by two opposite corners 'a' and 'b'", "type"), fields: { a: { type: PointInput }, b: { type: PointInput } } }); }); pg2GqlMapper[BOX] = { map: f => { if (f[0] === "(" && f[f.length - 1] === ")") { const [x1, y1, x2, y2] = f.slice(1, -1).replace(/[()]/g, "").split(",").map(f => parseFloat(f)); return { a: { x: x1, y: y1 }, b: { x: x2, y: y2 } }; } }, unmap: o => sql.fragment`box(point(${sql.value(o.a.x)}, ${sql.value(o.a.y)}), point(${sql.value(o.b.x)}, ${sql.value(o.b.y)}))` }; pgRegisterGqlTypeByTypeId(PATH, () => { const Point = pgGetGqlTypeByTypeIdAndModifier("600", null); return new GraphQLObjectType({ name: inflection.builtin("Path"), description: build.wrapDescription("A path (open or closed) made up of points", "type"), fields: { points: { type: new GraphQLList(Point) }, isOpen: { description: build.wrapDescription("True if this is a closed path (similar to a polygon), false otherwise.", "field"), type: GraphQLBoolean } } }); }); pgRegisterGqlInputTypeByTypeId(PATH, () => { const PointInput = pgGetGqlInputTypeByTypeIdAndModifier("600", null); return new GraphQLInputObjectType({ name: inflection.builtin("PathInput"), description: build.wrapDescription("A path (open or closed) made up of points", "type"), fields: { points: { type: new GraphQLList(PointInput) }, isOpen: { description: build.wrapDescription("True if this is a closed path (similar to a polygon), false otherwise.", "field"), type: GraphQLBoolean } } }); }); pg2GqlMapper[PATH] = { map: f => { let isOpen = null; if (f[0] === "(" && f[f.length -