Skip to content

Instantly share code, notes, and snippets.

@ab-pm
Last active November 5, 2019 18:51
Show Gist options
  • Save ab-pm/0f0b021db2c541008875b581f91404ee to your computer and use it in GitHub Desktop.
Save ab-pm/0f0b021db2c541008875b581f91404ee to your computer and use it in GitHub Desktop.
Local entities

Assuming the folling schema:

CREATE TABLE person (
	id serial PRIMARY KEY,
	name text,
	email text
);
CREATE TABLE team (
 	id serial PRIMARY KEY,
	name text
);
CREATE TABLE membership (
	team integer REFERENCES team,
	person integer REFERENCES person,
	active boolean,
	PRIMARY KEY (team, person)
);

You would no longer be able to query an individual membership from the global Query type like in

query {
  team(id: 7) {
    name
  }
  membership(team: 7, person: 13) {
    active
  }
}

But rather you can now write the query as

query {
  team(id: 7) {
    name
    membership(person: 13) {
      active
    }
  }
}

also in reverse

  person(id: 13) {
    name
    membership(team: 7) {
      active
    }
  }
}
/* 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}`);
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment