|
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */ |
|
import debugFactory from 'debug'; |
|
import { Context, SchemaBuilder } from 'postgraphile'; |
|
import { PgAttribute, PgClass, PgConstraint, QueryBuilder, SQL, inflections } from 'graphile-build-pg'; |
|
import { GraphQLType } from 'graphql'; |
|
|
|
declare module 'graphile-build-pg' { |
|
// eslint-disable-next-line @typescript-eslint/interface-name-prefix |
|
interface PgConstraint { |
|
uncoveredPrimaryAttributes: PgAttribute[] | undefined; |
|
} |
|
} |
|
|
|
const debug = debugFactory('graphile-build-pg'); |
|
|
|
function uncoveredPrimaryAttributes(fkeyConstraint: PgConstraint, cls: PgClass): PgAttribute[] | undefined { |
|
const pkeyConstraint = cls.primaryKeyConstraint; |
|
if (!pkeyConstraint) return; // might be a composite type, not a table |
|
if (fkeyConstraint.keyAttributes.every(attr => pkeyConstraint.keyAttributes.includes(attr))) { |
|
return pkeyConstraint.keyAttributes.filter(attr => !fkeyConstraint.keyAttributes.includes(attr)); |
|
} |
|
} |
|
export default function(builder: SchemaBuilder, { |
|
subscriptions, |
|
}: {subscriptions: boolean}): void { |
|
builder.hook('inflection', inflections => ({ |
|
...inflections, |
|
singleRelationByUniqueKeysBackwards( |
|
detailedKeys: PgAttribute[], |
|
table: PgClass, |
|
_foreignTable: PgClass, |
|
constraint: PgConstraint |
|
) { |
|
if (constraint.tags.foreignSingleFieldName) { |
|
return constraint.tags.foreignSingleFieldName; |
|
} |
|
if (constraint.tags.foreignFieldName) { |
|
return this.singularize(constraint.tags.foreignFieldName); |
|
} |
|
return this.rowByUniqueKeys( |
|
detailedKeys, |
|
table, |
|
table.primaryKeyConstraint |
|
); |
|
} |
|
})); |
|
builder.hook('build', build => { |
|
for (const constraint of build.pgIntrospectionResultsByKind.constraint as PgConstraint[]) { |
|
if (constraint.type == 'f') |
|
constraint.uncoveredPrimaryAttributes = uncoveredPrimaryAttributes(constraint, constraint.class); |
|
// console.log(constraint.class.name, constraint.keyAttributes.map(a => a.name), ' => ', constraint.foreignClass!.name, constraint.foreignKeyAttributes.map(a => a.name)); |
|
} |
|
|
|
for (const cls of build.pgIntrospectionResultsByKind.class as PgClass[]) { |
|
if (!cls.namespace) continue; |
|
const pkeyConstraint = cls.primaryKeyConstraint; |
|
if (pkeyConstraint) { // might be a composite type, not a table |
|
if (cls.constraints.some(constraint => !!constraint.uncoveredPrimaryAttributes)) { |
|
/* console.log(cls.constraints.filter(c => c.type == 'f').map(constraint => |
|
`${constraint.class.name}(${constraint.keyAttributes.map(a => a.name)}) => ${constraint.foreignClass!.name}(${constraint.foreignKeyAttributes.map(a => a.name)})` |
|
), cls.primaryKeyConstraint && cls.primaryKeyConstraint.keyAttributes.map(a => a.name)); */ |
|
if (pkeyConstraint.tags.omit) { |
|
debug(`primary key constraint on ${cls.namespace.name}.${cls.name} has @omit ${pkeyConstraint.tags.omit} but would be omitted automatically due to foreign key constraints`); |
|
} else { |
|
debug(`omitting primary key constraint on ${cls.namespace.name}.${cls.name} because [${pkeyConstraint.keyAttributes.map(a => a.name)}] have some foreign key constraints`); |
|
pkeyConstraint.tags.omit = true; |
|
} |
|
} |
|
} |
|
} |
|
return build; |
|
}, ['PgBackwardByUniqueConstraints']); |
|
|
|
// Apart from the selection of constraints and columns, the code is basically a mix between the PgBackwardRelationPlugin.js and PgRowByUniqueConstraint.js plugins |
|
builder.hook('GraphQLObjectType:fields', (fields, build, context) => { |
|
const { |
|
extend, |
|
getSafeAliasFromAlias, |
|
getSafeAliasFromResolveInfo, |
|
gql2pg, |
|
graphql, |
|
inflection, |
|
pgGetGqlTypeByTypeIdAndModifier, |
|
pgOmit: omit, |
|
pgQueryFromResolveData: queryFromResolveData, |
|
pgSql: sql, |
|
} = build; |
|
const { |
|
scope: { isPgRowType, pgIntrospection: foreignTable }, |
|
fieldWithHooks, |
|
Self, |
|
} = context as Context<any> & { |
|
scope: { isPgRowType: boolean; pgIntrospection: PgClass }; |
|
}; |
|
if (!isPgRowType || !foreignTable || foreignTable.kind !== 'class') return fields; |
|
// This is a relation in which WE are foreign |
|
const relevantConstraints = foreignTable.foreignConstraints.filter(({uncoveredPrimaryAttributes: upa}) => upa != null && upa.length); |
|
const foreignTableTypeName = inflection.tableType(foreignTable); |
|
const gqlForeignTableType = pgGetGqlTypeByTypeIdAndModifier(foreignTable.type.id, null); |
|
if (!gqlForeignTableType) { |
|
debug(`Could not determine type for foreign table with id ${foreignTable.type.id}`); |
|
return fields; |
|
} |
|
console.assert(Self == gqlForeignTableType, 'expected', Self, 'to be the same as', gqlForeignTableType); |
|
|
|
return extend(fields, relevantConstraints.reduce((fields, constraint) => { |
|
const table = constraint.class; |
|
const tableTypeName = inflection.tableType(table); |
|
const keys = constraint.keyAttributes; |
|
const foreignKeys = constraint.foreignKeyAttributes; |
|
const uncoveredPrimaryKeys = constraint.uncoveredPrimaryAttributes!; |
|
const gqlTableType = pgGetGqlTypeByTypeIdAndModifier(table.type.id, null); |
|
|
|
if (omit(constraint, 'single')) return fields; |
|
if (!table.isSelectable || omit(table, 'read')) return fields; |
|
if (!keys.length || !foreignKeys.length) return fields; |
|
if ([...keys, ...foreignKeys, ...uncoveredPrimaryKeys].some(key => omit(key, 'read'))) return fields; |
|
|
|
const singleRelationFieldName = inflection.singleRelationByUniqueKeysBackwards( |
|
uncoveredPrimaryKeys, |
|
table, |
|
foreignTable, |
|
constraint |
|
); |
|
|
|
const parameterKeys = uncoveredPrimaryKeys.map(key => ({ |
|
...key, |
|
sqlIdentifier: sql.identifier(key.name), |
|
paramName: inflection.column(key), // inflection.argument(key.name, i) |
|
})); |
|
fields[singleRelationFieldName] = fieldWithHooks(singleRelationFieldName, ({ |
|
getDataFromParsedResolveInfoFragment, |
|
addDataGenerator, |
|
addArgDataGenerator, |
|
}) => { |
|
addArgDataGenerator(function idArgumentsGenerator(args: {[key: string]: any}) { |
|
return { |
|
pgQuery(queryBuilder: QueryBuilder) { |
|
const sqlTableAlias = queryBuilder.getTableAlias(); |
|
for (const key of parameterKeys) |
|
queryBuilder.where(sql.fragment`${sqlTableAlias}.${key.sqlIdentifier} = ${gql2pg( |
|
args[key.paramName], |
|
key.type, |
|
key.typeModifier |
|
)}`); |
|
} |
|
}; |
|
}); |
|
addDataGenerator(parsedResolveInfoFragment => { |
|
return { |
|
pgQuery(queryBuilder: QueryBuilder) { |
|
queryBuilder.select(() => { |
|
const resolveData = getDataFromParsedResolveInfoFragment( |
|
parsedResolveInfoFragment, |
|
gqlTableType |
|
); |
|
const tableAlias = sql.identifier(Symbol()); |
|
const foreignTableAlias = queryBuilder.getTableAlias(); |
|
const query = queryFromResolveData( |
|
sql.identifier(table.namespace.name, table.name), |
|
tableAlias, |
|
resolveData, |
|
{ |
|
useAsterisk: false, |
|
asJson: true, |
|
addNullCase: true, |
|
withPagination: false, |
|
}, |
|
(innerQueryBuilder: QueryBuilder) => { |
|
innerQueryBuilder.parentQueryBuilder = queryBuilder; |
|
if (subscriptions) { |
|
innerQueryBuilder.selectIdentifiers(table); |
|
} |
|
keys.forEach((key, i) => { |
|
innerQueryBuilder.where( |
|
sql.fragment`${tableAlias}.${sql.identifier( |
|
key.name |
|
)} = ${foreignTableAlias}.${sql.identifier( |
|
foreignKeys[i].name |
|
)}` |
|
); |
|
}); |
|
}, |
|
queryBuilder.context, |
|
queryBuilder.rootValue |
|
); |
|
return sql.fragment`(${query})`; |
|
}, getSafeAliasFromAlias(parsedResolveInfoFragment.alias)); |
|
}, |
|
}; |
|
}); |
|
return { |
|
description: |
|
constraint.tags.backwardDescription || |
|
`Select a single \`${tableTypeName}\` that is related to this \`${foreignTableTypeName}\`.`, |
|
type: gqlTableType, |
|
args: parameterKeys.reduce((memo, key) => { |
|
memo[key.paramName] = { |
|
type: new graphql.GraphQLNonNull(pgGetGqlTypeByTypeIdAndModifier(key.typeId, key.typeModifier)), |
|
}; |
|
return memo; |
|
}, {} as {[key: string]: { type: GraphQLType }}), |
|
resolve(data, _args, resolveContext, resolveInfo) { |
|
const safeAlias = getSafeAliasFromResolveInfo(resolveInfo); |
|
const record = data[safeAlias]; |
|
const liveRecord = resolveInfo.rootValue && resolveInfo.rootValue.liveRecord; |
|
if (record && liveRecord) { |
|
liveRecord('pg', table, record.__identifiers); |
|
} |
|
return record; |
|
}, |
|
}; |
|
}, { |
|
pgFieldIntrospection: table, |
|
isPgBackwardSingleRelationField: true, |
|
}); |
|
|
|
return fields; |
|
}, {} as typeof fields), `Adding backwards unique relations for ${Self.name}`); |
|
}); |
|
} |