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
JavaScript
"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 -