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
298 lines (289 loc) • 11 kB
Flow
// @flow
import type { Plugin } from "graphile-build";
import debugFactory from "debug";
const debug = debugFactory("graphile-build-pg");
export default (function PgMutationCreatePlugin(
builder,
{ pgDisableDefaultMutations }
) {
if (pgDisableDefaultMutations) {
return;
}
builder.hook(
"GraphQLObjectType:fields",
(fields, build, context) => {
const {
extend,
newWithHooks,
parseResolveInfo,
pgIntrospectionResultsByKind,
pgGetGqlTypeByTypeIdAndModifier,
pgGetGqlInputTypeByTypeIdAndModifier,
pgSql: sql,
gql2pg,
graphql: {
GraphQLObjectType,
GraphQLInputObjectType,
GraphQLNonNull,
GraphQLString,
},
pgColumnFilter,
inflection,
pgQueryFromResolveData: queryFromResolveData,
pgOmit: omit,
pgViaTemporaryTable: viaTemporaryTable,
describePgEntity,
sqlCommentByAddingTags,
pgField,
} = build;
const {
scope: { isRootMutation },
fieldWithHooks,
} = context;
if (!isRootMutation) {
return fields;
}
return extend(
fields,
pgIntrospectionResultsByKind.class.reduce((memo, table) => {
// PERFORMANCE: These used to be .filter(...) calls
if (!table.namespace) return memo;
if (!table.isSelectable) return memo;
if (!table.isInsertable || omit(table, "create")) return memo;
const Table = pgGetGqlTypeByTypeIdAndModifier(table.type.id, null);
if (!Table) {
debug(
`There was no table type for table '${table.namespace.name}.${table.name}', so we're not generating a create mutation for it.`
);
return memo;
}
const TableInput = pgGetGqlInputTypeByTypeIdAndModifier(
table.type.id,
null
);
if (!TableInput) {
debug(
`There was no input type for table '${table.namespace.name}.${table.name}', so we're going to omit it from the create mutation.`
);
}
const tableTypeName = inflection.tableType(table);
const InputType = newWithHooks(
GraphQLInputObjectType,
{
name: inflection.createInputType(table),
description: build.wrapDescription(
`All input for the create \`${tableTypeName}\` mutation.`,
"type"
),
fields: {
clientMutationId: {
description: build.wrapDescription(
"An arbitrary string value with no semantic meaning. Will be included in the payload verbatim. May be used to track mutations by the client.",
"field"
),
type: GraphQLString,
},
...(TableInput
? {
[inflection.tableFieldName(table)]: {
description: build.wrapDescription(
`The \`${tableTypeName}\` to be created by this mutation.`,
"field"
),
type: new GraphQLNonNull(TableInput),
},
}
: null),
},
},
{
__origin: `Adding table create input type for ${describePgEntity(
table
)}. You can rename the table's GraphQL type via a 'Smart Comment':\n\n ${sqlCommentByAddingTags(
table,
{
name: "newNameHere",
}
)}`,
isPgCreateInputType: true,
pgInflection: table, // TODO:v5: remove - TYPO!
pgIntrospection: table,
}
);
const PayloadType = newWithHooks(
GraphQLObjectType,
{
name: inflection.createPayloadType(table),
description: build.wrapDescription(
`The output of our create \`${tableTypeName}\` mutation.`,
"type"
),
fields: ({ fieldWithHooks }) => {
const tableName = inflection.tableFieldName(table);
return {
clientMutationId: {
description: build.wrapDescription(
"The exact same `clientMutationId` that was provided in the mutation input, unchanged and unused. May be used by a client to track mutations.",
"field"
),
type: GraphQLString,
},
[tableName]: pgField(
build,
fieldWithHooks,
tableName,
{
description: build.wrapDescription(
`The \`${tableTypeName}\` that was created by this mutation.`,
"field"
),
type: Table,
},
{
isPgCreatePayloadResultField: true,
pgFieldIntrospection: table,
}
),
};
},
},
{
__origin: `Adding table create payload type for ${describePgEntity(
table
)}. You can rename the table's GraphQL type via a 'Smart Comment':\n\n ${sqlCommentByAddingTags(
table,
{
name: "newNameHere",
}
)}\n\nor disable the built-in create mutation via:\n\n ${sqlCommentByAddingTags(
table,
{ omit: "create" }
)}`,
isMutationPayload: true,
isPgCreatePayloadType: true,
pgIntrospection: table,
}
);
const fieldName = inflection.createField(table);
memo = build.extend(
memo,
{
[fieldName]: fieldWithHooks(
fieldName,
context => {
const { getDataFromParsedResolveInfoFragment } = context;
const relevantAttributes = table.attributes.filter(
attr =>
pgColumnFilter(attr, build, context) &&
!omit(attr, "create")
);
return {
description: build.wrapDescription(
`Creates a single \`${tableTypeName}\`.`,
"field"
),
type: PayloadType,
args: {
input: {
type: new GraphQLNonNull(InputType),
},
},
async resolve(data, args, resolveContext, resolveInfo) {
const { input } = args;
const { pgClient } = resolveContext;
const parsedResolveInfoFragment =
parseResolveInfo(resolveInfo);
parsedResolveInfoFragment.args = args; // Allow overriding via makeWrapResolversPlugin
const resolveData = getDataFromParsedResolveInfoFragment(
parsedResolveInfoFragment,
PayloadType
);
const insertedRowAlias = sql.identifier(Symbol());
const query = queryFromResolveData(
insertedRowAlias,
insertedRowAlias,
resolveData,
{},
null,
resolveContext,
resolveInfo.rootValue
);
const sqlColumns = [];
const sqlValues = [];
const inputData = input[inflection.tableFieldName(table)];
relevantAttributes.forEach(attr => {
const fieldName = inflection.column(attr);
const val = inputData[fieldName];
if (
Object.prototype.hasOwnProperty.call(
inputData,
fieldName
)
) {
sqlColumns.push(sql.identifier(attr.name));
sqlValues.push(
gql2pg(val, attr.type, attr.typeModifier)
);
}
});
const mutationQuery = sql.query`\
insert into ${sql.identifier(table.namespace.name, table.name)} ${
sqlColumns.length
? sql.fragment`(${sql.join(
sqlColumns,
", "
)}) values(${sql.join(sqlValues, ", ")})`
: sql.fragment`default values`
} returning *`;
let row;
try {
await pgClient.query("SAVEPOINT graphql_mutation");
const rows = await viaTemporaryTable(
pgClient,
sql.identifier(table.namespace.name, table.name),
mutationQuery,
insertedRowAlias,
query
);
row = rows[0];
await pgClient.query(
"RELEASE SAVEPOINT graphql_mutation"
);
} catch (e) {
await pgClient.query(
"ROLLBACK TO SAVEPOINT graphql_mutation"
);
throw e;
}
return {
clientMutationId: input.clientMutationId,
data: row,
};
},
};
},
{
pgFieldIntrospection: table,
isPgCreateMutationField: true,
}
),
},
`Adding create mutation for ${describePgEntity(
table
)}. You can omit this default mutation with a 'Smart Comment':\n\n ${sqlCommentByAddingTags(
table,
{
omit: "create",
}
)}`
);
return memo;
}, {}),
`Adding default 'create' mutation to root mutation`
);
},
["PgMutationCreate"],
[],
["PgTables"]
);
}: Plugin);