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
428 lines (423 loc) • 18.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
const base64 = str => Buffer.from(String(str)).toString("base64");
const hasNonNullKey = row => {
if (Array.isArray(row.__identifiers) && row.__identifiers.every(i => i != null)) {
return true;
}
for (const k in row) {
if (Object.prototype.hasOwnProperty.call(row, k)) {
if ((k[0] !== "_" || k[1] !== "_") && row[k] !== null) {
return true;
}
}
}
return false;
};
var PgTablesPlugin = function PgTablesPlugin(builder, {
pgForbidSetofFunctionsToReturnNull = false,
subscriptions = false
}) {
const handleNullRow = pgForbidSetofFunctionsToReturnNull ? (row, _identifiers) => row : (row, identifiers) => {
if (identifiers && hasNonNullKey(identifiers) || hasNonNullKey(row)) {
return row;
} else {
return null;
}
};
builder.hook("init", (_, build) => {
const {
getNodeIdForTypeAndIdentifiers,
nodeIdFieldName,
newWithHooks,
getSafeAliasFromResolveInfo,
pgSql: sql,
pgIntrospectionResultsByKind: introspectionResultsByKind,
getTypeByName,
pgGetGqlTypeByTypeIdAndModifier,
pgGetGqlInputTypeByTypeIdAndModifier,
pgRegisterGqlTypeByTypeId,
pgRegisterGqlInputTypeByTypeId,
pg2GqlMapper,
gql2pg,
graphql: {
GraphQLObjectType,
GraphQLNonNull,
GraphQLID,
GraphQLList,
GraphQLInputObjectType
},
inflection,
describePgEntity,
sqlCommentByAddingTags,
pgField
} = build;
const nullableIf = (condition, Type) => condition ? Type : new GraphQLNonNull(Type);
const Cursor = getTypeByName("Cursor");
introspectionResultsByKind.class.forEach(table => {
if (table.tags.enum) {
return;
}
const tablePgType = table.type;
if (!tablePgType) {
throw new Error("Could not determine the type for this table");
}
const arrayTablePgType = tablePgType.arrayType;
const primaryKeyConstraint = table.primaryKeyConstraint;
const primaryKeys = primaryKeyConstraint && primaryKeyConstraint.keyAttributes;
const attributes = table.attributes;
const tableTypeName = inflection.tableType(table);
const shouldHaveNodeId = nodeIdFieldName && table.isSelectable && table.namespace && primaryKeys && primaryKeys.length ? true : false;
let TableType;
let TablePatchType;
let TableBaseInputType;
pgRegisterGqlTypeByTypeId(tablePgType.id, cb => {
if (TableType) {
return TableType;
}
if (pg2GqlMapper[tablePgType.id]) {
// Already handled
throw new Error(`Register was called but there's already a mapper in place for '${tablePgType.id}'!`);
}
TableType = newWithHooks(GraphQLObjectType, {
description: table.description || tablePgType.description,
name: tableTypeName,
interfaces: () => {
if (shouldHaveNodeId) {
return [getTypeByName(inflection.builtin("Node"))];
} else {
return [];
}
},
fields: ({
addDataGeneratorForField,
Self
}) => {
const fields = {};
if (shouldHaveNodeId) {
// Enable nodeId interface
addDataGeneratorForField(nodeIdFieldName, () => {
return {
pgQuery: queryBuilder => {
queryBuilder.selectIdentifiers(table);
}
};
});
fields[nodeIdFieldName] = {
description: build.wrapDescription("A globally unique identifier. Can be used in various places throughout the system to identify this single value.", "field"),
type: new GraphQLNonNull(GraphQLID),
resolve(data) {
const identifiers = data.__identifiers;
if (!identifiers) {
return null;
}
/*
* For bigint we want NodeIDs to be the same as int up
* to the limits of int, and only to be strings after
* that point.
*/
const finalIdentifiers = identifiers.map((identifier, idx) => {
const key = primaryKeys[idx];
const type = key.type.domainBaseType || key.type;
if (type.id === "20" /* bigint */) {
/*
* When migrating from 'int' to 'bigint' we want
* to maintain nodeIDs in the safe range before
* moving to strings for larger numbers. Since we
* can represent ints up to MAX_SAFE_INTEGER
* (2^53 - 1) fine, we're using that as the
* boundary.
*/
const int = parseInt(identifier, 10);
if (int >= -Number.MAX_SAFE_INTEGER && int <= Number.MAX_SAFE_INTEGER) {
return int;
}
}
return identifier;
});
return getNodeIdForTypeAndIdentifiers(Self, ...finalIdentifiers);
}
};
}
return fields;
}
}, {
__origin: `Adding table type for ${describePgEntity(table)}. You can rename the table's GraphQL type via a 'Smart Comment':\n\n ${sqlCommentByAddingTags(table, {
name: "newNameHere"
})}`,
pgIntrospection: table,
isPgRowType: table.isSelectable,
isPgCompoundType: !table.isSelectable,
// TODO:v5: remove - typo
isPgCompositeType: !table.isSelectable
});
cb(TableType);
const pgCreateInputFields = {};
const pgPatchInputFields = {};
const pgBaseInputFields = {};
newWithHooks(GraphQLInputObjectType, {
description: build.wrapDescription(`An input for mutations affecting \`${tableTypeName}\``, "type"),
name: inflection.inputType(TableType)
}, {
__origin: `Adding table input type for ${describePgEntity(table)}. You can rename the table's GraphQL type via a 'Smart Comment':\n\n ${sqlCommentByAddingTags(table, {
name: "newNameHere"
})}`,
pgIntrospection: table,
isInputType: true,
isPgRowType: table.isSelectable,
isPgCompoundType: !table.isSelectable,
pgAddSubfield(fieldName, attrName, pgType, spec, typeModifier) {
pgCreateInputFields[fieldName] = {
name: attrName,
type: pgType,
typeModifier
};
return spec;
}
}, true // If no fields, skip type automatically
);
if (table.isSelectable) {
// XXX: these don't belong here; but we have to keep them here
// because third-party code depends on `getTypeByName` to find
// them; so we have to register them ahead of time. A better
// approach is to use the modifier to specify the type you need,
// 'patch' or 'base', so they can be registered just in time.
TablePatchType = newWithHooks(GraphQLInputObjectType, {
description: build.wrapDescription(`Represents an update to a \`${tableTypeName}\`. Fields that are set will be updated.`, "type"),
name: inflection.patchType(TableType)
}, {
__origin: `Adding table patch type for ${describePgEntity(table)}. You can rename the table's GraphQL type via a 'Smart Comment':\n\n ${sqlCommentByAddingTags(table, {
name: "newNameHere"
})}`,
pgIntrospection: table,
isPgRowType: table.isSelectable,
isPgCompoundType: !table.isSelectable,
isPgPatch: true,
pgAddSubfield(fieldName, attrName, pgType, spec, typeModifier) {
pgPatchInputFields[fieldName] = {
name: attrName,
type: pgType,
typeModifier
};
return spec;
}
}, true // Safe to skip this if no fields support updating
);
TableBaseInputType = newWithHooks(GraphQLInputObjectType, {
description: build.wrapDescription(`An input representation of \`${tableTypeName}\` with nullable fields.`, "type"),
name: inflection.baseInputType(TableType)
}, {
__origin: `Adding table base input type for ${describePgEntity(table)}. You can rename the table's GraphQL type via a 'Smart Comment':\n\n ${sqlCommentByAddingTags(table, {
name: "newNameHere"
})}`,
pgIntrospection: table,
isPgRowType: table.isSelectable,
isPgCompoundType: !table.isSelectable,
isPgBaseInput: true,
pgAddSubfield(fieldName, attrName, pgType, spec, typeModifier) {
pgBaseInputFields[fieldName] = {
name: attrName,
type: pgType,
typeModifier
};
return spec;
}
});
}
pg2GqlMapper[tablePgType.id] = {
map: _ => _,
unmap: (obj, modifier) => {
let fieldLookup;
if (modifier === "patch") {
fieldLookup = pgPatchInputFields;
} else if (modifier === "base") {
fieldLookup = pgBaseInputFields;
} else {
fieldLookup = pgCreateInputFields;
}
const attr2sql = attr => {
// TODO: this should use `fieldInput[*].name` to find the attribute
const fieldName = inflection.column(attr);
const inputField = fieldLookup[fieldName];
const v = obj[fieldName];
if (inputField && v != null) {
const {
type,
typeModifier
} = inputField;
return sql.fragment`${gql2pg(v, type, typeModifier)}::${type.isFake ? sql.identifier("unknown") : sql.identifier(type.namespaceName, type.name)}`;
} else {
return sql.null; // TODO: return default instead.
}
};
return sql.fragment`row(${sql.join(attributes.map(attr2sql), ",")})::${tablePgType.isFake ? sql.identifier("unknown") : sql.identifier(tablePgType.namespaceName, tablePgType.name)}`;
}
};
const EdgeType = newWithHooks(GraphQLObjectType, {
description: build.wrapDescription(`A \`${tableTypeName}\` edge in the connection.`, "type"),
name: inflection.edge(TableType.name),
fields: ({
fieldWithHooks
}) => {
return {
cursor: fieldWithHooks("cursor", ({
addDataGenerator
}) => {
addDataGenerator(() => ({
usesCursor: [true],
pgQuery: queryBuilder => {
if (primaryKeys) {
queryBuilder.selectIdentifiers(table);
}
}
}));
return {
description: build.wrapDescription("A cursor for use in pagination.", "field"),
type: Cursor,
resolve(data) {
return data.__cursor && base64(JSON.stringify(data.__cursor));
}
};
}, {
isCursorField: true
}),
node: pgField(build, fieldWithHooks, "node", {
description: build.wrapDescription(`The \`${tableTypeName}\` at the end of the edge.`, "field"),
type: nullableIf(!pgForbidSetofFunctionsToReturnNull, TableType),
resolve(data, _args, resolveContext, resolveInfo) {
const safeAlias = getSafeAliasFromResolveInfo(resolveInfo);
const record = handleNullRow(data[safeAlias], data.__identifiers);
const liveRecord = resolveInfo.rootValue && resolveInfo.rootValue.liveRecord;
if (record && primaryKeys && liveRecord && data.__identifiers) {
liveRecord("pg", table, data.__identifiers);
}
return record;
}
}, {}, false, {
withQueryBuilder: queryBuilder => {
if (subscriptions) {
queryBuilder.selectIdentifiers(table);
}
}
})
};
}
}, {
__origin: `Adding table edge type for ${describePgEntity(table)}. You can rename the table's GraphQL type via a 'Smart Comment':\n\n ${sqlCommentByAddingTags(table, {
name: "newNameHere"
})}`,
isEdgeType: true,
isPgRowEdgeType: true,
nodeType: TableType,
pgIntrospection: table
});
const PageInfo = getTypeByName(inflection.builtin("PageInfo"));
/*const ConnectionType = */
newWithHooks(GraphQLObjectType, {
description: build.wrapDescription(`A connection to a list of \`${tableTypeName}\` values.`, "type"),
name: inflection.connection(TableType.name),
fields: ({
recurseDataGeneratorsForField,
fieldWithHooks
}) => {
recurseDataGeneratorsForField("pageInfo", true);
return {
nodes: pgField(build, fieldWithHooks, "nodes", {
description: build.wrapDescription(`A list of \`${tableTypeName}\` objects.`, "field"),
type: new GraphQLNonNull(new GraphQLList(nullableIf(!pgForbidSetofFunctionsToReturnNull, TableType))),
resolve(data, _args, resolveContext, resolveInfo) {
const safeAlias = getSafeAliasFromResolveInfo(resolveInfo);
const liveRecord = resolveInfo.rootValue && resolveInfo.rootValue.liveRecord;
return data.data.map(entry => {
const record = handleNullRow(entry[safeAlias], entry[safeAlias].__identifiers);
if (record && liveRecord && primaryKeys && entry[safeAlias].__identifiers) {
liveRecord("pg", table, entry[safeAlias].__identifiers);
}
return record;
});
}
}, {}, false, {
withQueryBuilder: queryBuilder => {
if (subscriptions) {
queryBuilder.selectIdentifiers(table);
}
}
}),
edges: pgField(build, fieldWithHooks, "edges", {
description: build.wrapDescription(`A list of edges which contains the \`${tableTypeName}\` and cursor to aid in pagination.`, "field"),
type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(EdgeType))),
resolve(data, _args, _context, resolveInfo) {
const safeAlias = getSafeAliasFromResolveInfo(resolveInfo);
return data.data.map(entry => ({
...entry,
...entry[safeAlias]
}));
}
}, {}, false, {
hoistCursor: true
}),
pageInfo: PageInfo && {
description: build.wrapDescription("Information to aid in pagination.", "field"),
type: new GraphQLNonNull(PageInfo),
resolve(data) {
return data;
}
}
};
}
}, {
__origin: `Adding table connection type for ${describePgEntity(table)}. You can rename the table's GraphQL type via a 'Smart Comment':\n\n ${sqlCommentByAddingTags(table, {
name: "newNameHere"
})}`,
isConnectionType: true,
isPgRowConnectionType: true,
edgeType: EdgeType,
nodeType: TableType,
pgIntrospection: table
});
}, true);
pgRegisterGqlInputTypeByTypeId(tablePgType.id, (_set, modifier) => {
// This must come first, it triggers creation of all the types
const TableType = pgGetGqlTypeByTypeIdAndModifier(tablePgType.id, null);
// This must come after the pgGetGqlTypeByTypeIdAndModifier call
if (modifier === "patch") {
// TODO: v5: move the definition from above down here
return TablePatchType;
}
if (modifier === "base") {
// TODO: v5: move the definition from above down here
return TableBaseInputType;
}
if (TableType) {
return getTypeByName(inflection.inputType(TableType));
}
return null;
}, true);
if (arrayTablePgType) {
// Note: these do not return
//
// `new GraphQLList(new GraphQLNonNull(...))`
//
// because it's possible to return null entries from postgresql
// functions. We should probably add a flag to instead export
// the non-null version as that's more typical.
pgRegisterGqlTypeByTypeId(arrayTablePgType.id, () => {
const TableType = pgGetGqlTypeByTypeIdAndModifier(tablePgType.id, null);
return new GraphQLList(TableType);
}, true);
pgRegisterGqlInputTypeByTypeId(arrayTablePgType.id, (_set, modifier) => {
const RelevantTableInputType = pgGetGqlInputTypeByTypeIdAndModifier(tablePgType.id, modifier);
if (RelevantTableInputType) {
return new GraphQLList(RelevantTableInputType);
}
}, true);
}
});
return _;
}, ["PgTables"], [], ["PgTypes"]);
};
exports.default = PgTablesPlugin;
//# sourceMappingURL=PgTablesPlugin.js.map