// @flow import type { Plugin } from "graphile-build"; const nullableIf = (GraphQLNonNull, condition, Type) => condition ? Type : new GraphQLNonNull(Type); export default (function PgColumnsPlugin(builder) { builder.hook( "build", build => { const { pgSql: sql, pgTweakFragmentForTypeAndModifier, pgQueryFromResolveData: queryFromResolveData, } = build; const getSelectValueForFieldAndTypeAndModifier = ( ReturnType, fieldScope, parsedResolveInfoFragment, sqlFullName, type, typeModifier ) => { const { getDataFromParsedResolveInfoFragment } = fieldScope; if (type.isPgArray) { const ident = sql.identifier(Symbol()); return sql.fragment`(\ case when ${sqlFullName} is null then null when coalesce(array_length(${sqlFullName}, 1), 0) = 0 then '[]'::json else ( select json_agg(${getSelectValueForFieldAndTypeAndModifier( ReturnType, fieldScope, parsedResolveInfoFragment, ident, type.arrayItemType, typeModifier )}) from unnest(${sqlFullName}) as ${ident} ) end )`; } else { const resolveData = getDataFromParsedResolveInfoFragment( parsedResolveInfoFragment, ReturnType ); if (type.type === "c") { const isDefinitelyNotATable = type.class && !type.class.isSelectable; const jsonBuildObject = queryFromResolveData( sql.identifier(Symbol()), // Ignore! sqlFullName, resolveData, { onlyJsonField: true, addNullCase: !isDefinitelyNotATable, addNotDistinctFromNullCase: isDefinitelyNotATable, } ); return jsonBuildObject; } else { return pgTweakFragmentForTypeAndModifier( sqlFullName, type, typeModifier, resolveData ); } } }; return build.extend(build, { pgGetSelectValueForFieldAndTypeAndModifier: getSelectValueForFieldAndTypeAndModifier, }); }, ["PgColumns"], [], ["PgTypes"] ); builder.hook( "GraphQLObjectType:fields", (fields, build, context) => { const { extend, pgGetGqlTypeByTypeIdAndModifier, pgSql: sql, pg2gqlForType, graphql: { GraphQLString, GraphQLNonNull }, pgColumnFilter, inflection, pgOmit: omit, pgGetSelectValueForFieldAndTypeAndModifier: getSelectValueForFieldAndTypeAndModifier, describePgEntity, sqlCommentByAddingTags, } = build; const { scope: { isPgRowType, isPgCompoundType, pgIntrospection: table }, fieldWithHooks, } = context; if ( !(isPgRowType || isPgCompoundType) || !table || table.kind !== "class" ) { return fields; } return extend( fields, table.attributes.reduce((memo, attr) => { // PERFORMANCE: These used to be .filter(...) calls if (!pgColumnFilter(attr, build, context)) return memo; if (omit(attr, "read")) return memo; const fieldName = inflection.column(attr); if (memo[fieldName]) { throw new Error( `Two columns produce the same GraphQL field name '${fieldName}' on class '${table.namespaceName}.${table.name}'; one of them is '${attr.name}'` ); } memo = extend( memo, { [fieldName]: fieldWithHooks( fieldName, fieldContext => { const { type, typeModifier } = attr; const sqlColumn = sql.identifier(attr.name); const { addDataGenerator } = fieldContext; const ReturnType = pgGetGqlTypeByTypeIdAndModifier( attr.typeId, attr.typeModifier ) || GraphQLString; addDataGenerator(parsedResolveInfoFragment => { return { pgQuery: queryBuilder => { queryBuilder.select( getSelectValueForFieldAndTypeAndModifier( ReturnType, fieldContext, parsedResolveInfoFragment, sql.fragment`(${queryBuilder.getTableAlias()}.${sqlColumn})`, // The brackets are necessary to stop the parser getting confused, ref: https://www.postgresql.org/docs/9.6/static/rowtypes.html#ROWTYPES-ACCESSING type, typeModifier ), fieldName ); }, }; }); const convertFromPg = pg2gqlForType(type); return { description: attr.description, type: nullableIf( GraphQLNonNull, !attr.isNotNull && !attr.type.domainIsNotNull && !attr.tags.notNull, ReturnType ), resolve: (data, _args, _context, _resolveInfo) => { return convertFromPg(data[fieldName]); }, }; }, { pgFieldIntrospection: attr } ), }, `Adding field for ${describePgEntity( attr )}. You can rename this field with a 'Smart Comment':\n\n ${sqlCommentByAddingTags( attr, { name: "newNameHere", } )}` ); return memo; }, {}), `Adding columns to '${describePgEntity(table)}'` ); }, ["PgColumns"] ); builder.hook( "GraphQLInputObjectType:fields", (fields, build, context) => { const { extend, pgGetGqlInputTypeByTypeIdAndModifier, graphql: { GraphQLString, GraphQLNonNull }, pgColumnFilter, inflection, pgOmit: omit, describePgEntity, sqlCommentByAddingTags, } = build; const { scope: { isPgRowType, isPgCompoundType, isPgPatch, isPgBaseInput, pgIntrospection: table, pgAddSubfield, }, fieldWithHooks, } = context; if ( !(isPgRowType || isPgCompoundType) || !table || table.kind !== "class" ) { return fields; } return extend( fields, table.attributes.reduce((memo, attr) => { // PERFORMANCE: These used to be .filter(...) calls if (!pgColumnFilter(attr, build, context)) return memo; const action = isPgBaseInput ? "base" : isPgPatch ? "update" : "create"; if (omit(attr, action)) return memo; if (attr.identity === "a") return memo; const fieldName = inflection.column(attr); if (memo[fieldName]) { throw new Error( `Two columns produce the same GraphQL field name '${fieldName}' on input class '${table.namespaceName}.${table.name}'; one of them is '${attr.name}'` ); } memo = extend( memo, { [fieldName]: fieldWithHooks( fieldName, pgAddSubfield( fieldName, attr.name, attr.type, { description: attr.description, type: nullableIf( GraphQLNonNull, isPgBaseInput || isPgPatch || (!attr.isNotNull && (!attr.type.domainIsNotNull || attr.type.domainHasDefault) && !attr.tags.notNull) || attr.hasDefault || attr.tags.hasDefault || attr.identity === "d", pgGetGqlInputTypeByTypeIdAndModifier( attr.typeId, attr.typeModifier ) || GraphQLString ), }, attr.typeModifier ), { pgFieldIntrospection: attr } ), }, `Adding input object field for ${describePgEntity( attr )}. You can rename this field with a 'Smart Comment':\n\n ${sqlCommentByAddingTags( attr, { name: "newNameHere", } )}` ); return memo; }, {}), `Adding columns to input object for ${describePgEntity(table)}` ); }, ["PgColumns"] ); }: Plugin);