Created
August 27, 2019 11:12
-
-
Save singingwolfboy/74a249b17d690922cc72bea26b28b4b5 to your computer and use it in GitHub Desktop.
Based on https://github.com/graphile-contrib/pg-many-to-many, but generates two connections/edges per many-to-many relation so that values on the junction table can be exposed on the connection edges. Not fully functional.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Given a `leftTable`, trace through the foreign key relations | |
// and identify a `junctionTable` and `rightTable`. | |
// Returns a list of data objects for these many-to-many relations. | |
function manyToManyRelations(leftTable, build) { | |
const { | |
pgIntrospectionResultsByKind: introspectionResultsByKind, | |
pgOmit: omit, | |
} = build; | |
return leftTable.foreignConstraints | |
.filter(con => con.type === "f" && !omit(con, "read")) | |
.reduce((relationInfos, junctionLeftConstraint) => { | |
const junctionTable = | |
introspectionResultsByKind.classById[junctionLeftConstraint.classId]; | |
if (!junctionTable) { | |
throw new Error( | |
`Could not find the table that referenced us (constraint: ${ | |
junctionLeftConstraint.name | |
})` | |
); | |
} | |
const junctionRightConstraint = junctionTable.constraints | |
.filter(con => con.type === "f") | |
.find(con => con.foreignClassId !== leftTable.id); | |
if (!junctionRightConstraint) { | |
return relationInfos; | |
} | |
const rightTable = junctionRightConstraint.foreignClass; | |
const leftKeyAttributes = junctionLeftConstraint.foreignKeyAttributes; | |
const junctionLeftKeyAttributes = junctionLeftConstraint.keyAttributes; | |
const junctionRightKeyAttributes = junctionRightConstraint.keyAttributes; | |
const rightKeyAttributes = junctionRightConstraint.foreignKeyAttributes; | |
// Ensure keys were found | |
if ( | |
!leftKeyAttributes.every(_ => _) || | |
!junctionLeftKeyAttributes.every(_ => _) || | |
!junctionRightKeyAttributes.every(_ => _) || | |
!rightKeyAttributes.every(_ => _) | |
) { | |
throw new Error("Could not find key columns!"); | |
} | |
// Ensure keys can be read | |
if ( | |
leftKeyAttributes.some(attr => omit(attr, "read")) || | |
junctionLeftKeyAttributes.some(attr => omit(attr, "read")) || | |
junctionRightKeyAttributes.some(attr => omit(attr, "read")) || | |
rightKeyAttributes.some(attr => omit(attr, "read")) | |
) { | |
return relationInfos; | |
} | |
// Ensure both constraints are single-column | |
// TODO: handle multi-column | |
if (leftKeyAttributes.length > 1 || rightKeyAttributes.length > 1) { | |
return relationInfos; | |
} | |
// Ensure junction constraint keys are not unique (which would result in a one-to-one relation) | |
const junctionLeftConstraintIsUnique = !!junctionTable.constraints.find( | |
c => | |
(c.type === "p" || c.type === "u") && | |
c.keyAttributeNums.length === junctionLeftKeyAttributes.length && | |
c.keyAttributeNums.every( | |
(n, i) => junctionLeftKeyAttributes[i].num === n | |
) | |
); | |
const junctionRightConstraintIsUnique = !!junctionTable.constraints.find( | |
c => | |
(c.type === "p" || c.type === "u") && | |
c.keyAttributeNums.length === junctionRightKeyAttributes.length && | |
c.keyAttributeNums.every( | |
(n, i) => junctionRightKeyAttributes[i].num === n | |
) | |
); | |
if (junctionLeftConstraintIsUnique || junctionRightConstraintIsUnique) { | |
return relationInfos; | |
} | |
relationInfos.push({ | |
leftKeyAttributes, | |
junctionLeftKeyAttributes, | |
junctionRightKeyAttributes, | |
rightKeyAttributes, | |
junctionTable, | |
rightTable, | |
junctionLeftConstraint, | |
junctionRightConstraint, | |
}); | |
return relationInfos; | |
}, []); | |
} | |
const hasNonNullKey = row => { | |
if ( | |
Array.isArray(row.__identifiers) && | |
row.__identifiers.every(i => i != null) | |
) { | |
return true; | |
} | |
for (const k in row) { | |
if (row.hasOwnProperty(k)) { | |
if ((k[0] !== "_" || k[1] !== "_") && row[k] !== null) { | |
return true; | |
} | |
} | |
} | |
return false; | |
}; | |
function makeConnectionTypeViaJunction( | |
table, // rightTable | |
junctionTable, | |
junctionLeftKeyAttributes, | |
junctionRightKeyAttributes, | |
build, | |
context | |
) { | |
const { | |
extend, | |
newWithHooks, | |
inflection, | |
graphql: { GraphQLObjectType, GraphQLNonNull, GraphQLList }, | |
pgColumnFilter, | |
pgOmit: omit, | |
pgSql: sql, | |
pg2gql, | |
getTypeByName, | |
pgGetGqlTypeByTypeIdAndModifier, | |
describePgEntity, | |
sqlCommentByAddingTags, | |
pgField, | |
pgGetSelectValueForFieldAndTypeAndModifier: getSelectValueForFieldAndTypeAndModifier, | |
getSafeAliasFromResolveInfo, | |
options: { | |
pgForbidSetofFunctionsToReturnNull = false, | |
subscriptions = false, | |
}, | |
} = build; | |
const nullableIf = (condition, Type) => | |
condition ? Type : new GraphQLNonNull(Type); | |
const Cursor = getTypeByName("Cursor"); | |
const handleNullRow = pgForbidSetofFunctionsToReturnNull | |
? (row, _identifiers) => row | |
: (row, identifiers) => { | |
if ((identifiers && hasNonNullKey(identifiers)) || hasNonNullKey(row)) { | |
return row; | |
} else { | |
return null; | |
} | |
}; | |
const TableType = pgGetGqlTypeByTypeIdAndModifier(table.type.id, null); | |
if (!TableType) { | |
throw new Error( | |
`Could not determine type for table with id ${table.type.id}` | |
); | |
} | |
const primaryKeyConstraint = table.primaryKeyConstraint; | |
const primaryKeys = | |
primaryKeyConstraint && primaryKeyConstraint.keyAttributes; | |
const junctionAttributes = junctionTable.attributes.filter( | |
attr => | |
pgColumnFilter(attr, build, context) && | |
!omit(attr, "filter") && | |
!junctionLeftKeyAttributes.includes(attr) && | |
!junctionRightKeyAttributes.includes(attr) | |
); | |
const junctionTypeName = inflection.tableType(junctionTable); | |
const EdgeType = newWithHooks( | |
GraphQLObjectType, | |
{ | |
description: `A \`${ | |
TableType.name | |
}\` edge in the connection, with data from \`${junctionTypeName}\`.`, | |
name: inflection.edgeViaJunction(TableType.name, junctionTypeName), | |
fields: ({ fieldWithHooks }) => { | |
const edgeFields = { | |
cursor: fieldWithHooks( | |
"cursor", | |
({ addDataGenerator }) => { | |
addDataGenerator(() => ({ | |
usesCursor: [true], | |
pgQuery: queryBuilder => { | |
if (primaryKeys) { | |
queryBuilder.selectIdentifiers(table); | |
} | |
}, | |
})); | |
return { | |
description: "A cursor for use in pagination.", | |
type: Cursor, | |
resolve(data) { | |
return data.__cursor && base64(JSON.stringify(data.__cursor)); | |
}, | |
}; | |
}, | |
{ | |
isCursorField: true, | |
} | |
), | |
node: pgField( | |
build, | |
fieldWithHooks, | |
"node", | |
{ | |
description: `The \`${TableType.name}\` at the end of the edge.`, | |
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); | |
} | |
}, | |
} | |
), | |
}; | |
junctionAttributes.reduce((memo, attr) => { | |
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 => { | |
const junctionTableAlias = | |
queryBuilder.parentQueryBuilder.junctionTableAlias; | |
if (!junctionTableAlias) { | |
throw new Error( | |
"Missing junctionTableAlias on queryBuilder" | |
); | |
} | |
queryBuilder.select( | |
getSelectValueForFieldAndTypeAndModifier( | |
ReturnType, | |
fieldContext, | |
parsedResolveInfoFragment, | |
sql.fragment`(${junctionTableAlias}.${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( | |
!attr.isNotNull && | |
!attr.type.domainIsNotNull && | |
!attr.tags.notNull, | |
ReturnType | |
), | |
resolve: (data, _args, _context, _resolveInfo) => { | |
// return convertFromPg(data[fieldName]); | |
return pg2gql(data[fieldName], type); | |
}, | |
}; | |
}, | |
{ pgFieldIntrospection: attr } | |
), | |
}, | |
`Adding field for ${describePgEntity( | |
attr | |
)}. You can rename this field with:\n\n ${sqlCommentByAddingTags( | |
attr, | |
{ | |
name: "newNameHere", | |
} | |
)}` | |
); | |
return memo; | |
}, edgeFields); | |
return edgeFields; | |
}, | |
}, | |
{ | |
isEdgeType: true, | |
isPgRowEdgeType: true, | |
nodeType: TableType, | |
} | |
); | |
const PageInfo = getTypeByName(inflection.builtin("PageInfo")); | |
newWithHooks( | |
GraphQLObjectType, | |
{ | |
description: `A connection to a list of \`${ | |
TableType.name | |
}\` values, with data from \`${junctionTypeName}\`.`, | |
name: inflection.connectionViaJunction(TableType.name, junctionTypeName), | |
fields: ({ recurseDataGeneratorsForField, fieldWithHooks }) => { | |
recurseDataGeneratorsForField("pageInfo", true); | |
return { | |
nodes: pgField( | |
build, | |
fieldWithHooks, | |
"nodes", | |
{ | |
description: `A list of \`${TableType.name}\` objects.`, | |
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: `A list of edges which contains the \`${ | |
TableType.name | |
}\`, info from the \`${junctionTypeName}\`, and the cursor to aid in pagination.`, | |
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: "Information to aid in pagination.", | |
type: new GraphQLNonNull(PageInfo), | |
resolve(data) { | |
return data; | |
}, | |
}, | |
}; | |
}, | |
}, | |
{ | |
isConnectionType: true, | |
isPgRowConnectionType: true, | |
edgeType: EdgeType, | |
nodeType: TableType, | |
} | |
); | |
} | |
module.exports = function PgEnhancedManyToManyRelationPlugin( | |
builder, | |
{ pgSimpleCollections } | |
) { | |
builder.hook("inflection", inflection => { | |
return Object.assign(inflection, { | |
manyToManyRelationByKeys( | |
_leftKeyAttributes, | |
junctionLeftKeyAttributes, | |
junctionRightKeyAttributes, | |
_rightKeyAttributes, | |
junctionTable, | |
rightTable, | |
_junctionLeftConstraint, | |
junctionRightConstraint | |
) { | |
if (junctionRightConstraint.tags.manyToManyFieldName) { | |
return junctionRightConstraint.tags.manyToManyFieldName; | |
} | |
return this.camelCase( | |
`${this.pluralize( | |
this._singularizedTableName(rightTable) | |
)}-by-${this._singularizedTableName(junctionTable)}-${[ | |
...junctionLeftKeyAttributes, | |
...junctionRightKeyAttributes, | |
] | |
.map(attr => this.column(attr)) | |
.join("-and-")}` | |
); | |
}, | |
manyToManyRelationByKeysSimple( | |
_leftKeyAttributes, | |
junctionLeftKeyAttributes, | |
junctionRightKeyAttributes, | |
_rightKeyAttributes, | |
junctionTable, | |
rightTable, | |
_junctionLeftConstraint, | |
junctionRightConstraint | |
) { | |
if (junctionRightConstraint.tags.manyToManySimpleFieldName) { | |
return junctionRightConstraint.tags.manyToManySimpleFieldName; | |
} | |
return this.camelCase( | |
`${this.pluralize( | |
this._singularizedTableName(rightTable) | |
)}-by-${this._singularizedTableName(junctionTable)}-${[ | |
...junctionLeftKeyAttributes, | |
...junctionRightKeyAttributes, | |
] | |
.map(attr => this.column(attr)) | |
.join("-and-")}-list` | |
); | |
}, | |
edgeViaJunction(nodeTypeName, junctionTypeName) { | |
return this.upperCamelCase( | |
`${this.pluralize(nodeTypeName)}-via-${junctionTypeName}-edge` | |
); | |
}, | |
connectionViaJunction(nodeTypeName, junctionTypeName) { | |
return this.upperCamelCase( | |
`${this.pluralize(nodeTypeName)}-via-${junctionTypeName}-connection` | |
); | |
}, | |
}); | |
}); | |
builder.hook( | |
"init", | |
(_, build, context) => { | |
const { | |
newWithHooks, | |
pgIntrospectionResultsByKind: introspectionResultsByKind, | |
pgGetGqlTypeByTypeIdAndModifier, | |
pgGetGqlInputTypeByTypeIdAndModifier, | |
graphql: { GraphQLInputObjectType, GraphQLString }, | |
pgColumnFilter, | |
inflection, | |
pgOmit: omit, | |
describePgEntity, | |
sqlCommentByAddingTags, | |
pgField, | |
} = build; | |
introspectionResultsByKind.class.forEach(leftTable => { | |
// PERFORMANCE: These used to be .filter(...) calls | |
if (!leftTable.isSelectable || omit(leftTable, "filter")) return; | |
if (!leftTable.namespace) return; | |
const manyToManyRelationsInfo = manyToManyRelations(leftTable, build); | |
manyToManyRelationsInfo.forEach( | |
({ | |
leftKeyAttributes, | |
junctionLeftKeyAttributes, | |
junctionRightKeyAttributes, | |
rightKeyAttributes, | |
junctionTable, | |
rightTable, | |
junctionLeftConstraint, | |
junctionRightConstraint, | |
}) => { | |
const relationName = inflection.manyToManyRelationByKeys( | |
leftKeyAttributes, | |
junctionLeftKeyAttributes, | |
junctionRightKeyAttributes, | |
rightKeyAttributes, | |
junctionTable, | |
rightTable, | |
junctionLeftConstraint, | |
junctionRightConstraint | |
); | |
newWithHooks( | |
GraphQLInputObjectType, | |
{ | |
description: `A condition to be used against \`${relationName}\` object types. All fields are tested for equality and combined with a logical ‘and.’`, | |
name: inflection.conditionType(relationName), | |
fields: context => { | |
const { fieldWithHooks } = context; | |
return junctionTable.attributes.reduce((memo, attr) => { | |
// PERFORMANCE: These used to be .filter(...) calls | |
if (!pgColumnFilter(attr, build, context)) return memo; | |
if (omit(attr, "filter")) return memo; | |
if (junctionLeftKeyAttributes.includes(attr)) return memo; | |
if (junctionRightKeyAttributes.includes(attr)) return memo; | |
const fieldName = inflection.column(attr); | |
memo = build.extend( | |
memo, | |
{ | |
[fieldName]: fieldWithHooks( | |
fieldName, | |
{ | |
description: `Checks for equality with the \`${fieldName}\` field in the junction table.`, | |
type: | |
pgGetGqlInputTypeByTypeIdAndModifier( | |
attr.typeId, | |
attr.typeModifier | |
) || GraphQLString, | |
}, | |
{ | |
isPgConnectionConditionInputField: true, | |
} | |
), | |
}, | |
`Adding condition argument for ${describePgEntity(attr)}` | |
); | |
return memo; | |
}, {}); | |
}, | |
}, | |
{ | |
__origin: `Adding condition type for ${describePgEntity( | |
leftTable | |
)}. You can rename the table's GraphQL type via:\n\n ${sqlCommentByAddingTags( | |
leftTable, | |
{ | |
name: "newNameHere", | |
} | |
)}`, | |
pgIntrospection: leftTable, | |
isPgCondition: true, | |
}, | |
true // Conditions might all be filtered | |
); | |
makeConnectionTypeViaJunction( | |
rightTable, | |
junctionTable, | |
junctionLeftKeyAttributes, | |
junctionRightKeyAttributes, | |
build, | |
context | |
); | |
} | |
); | |
}); | |
return _; | |
}, | |
["PgEnhancedManyToManyConnection"], | |
[], | |
["PgTypes"] | |
); | |
builder.hook("GraphQLObjectType:fields", (fields, build, context) => { | |
const { | |
extend, | |
getTypeByName, | |
pgGetGqlTypeByTypeIdAndModifier, | |
pgSql: sql, | |
getSafeAliasFromResolveInfo, | |
getSafeAliasFromAlias, | |
graphql: { GraphQLNonNull, GraphQLList }, | |
inflection, | |
pgQueryFromResolveData: queryFromResolveData, | |
pgAddStartEndCursor: addStartEndCursor, | |
describePgEntity, | |
} = build; | |
const { | |
scope: { isPgRowType, pgIntrospection: leftTable }, | |
fieldWithHooks, | |
Self, | |
} = context; | |
if (!isPgRowType || !leftTable || leftTable.kind !== "class") { | |
return fields; | |
} | |
const manyToManyRelationsInfo = manyToManyRelations(leftTable, build); | |
return extend( | |
fields, | |
manyToManyRelationsInfo.reduce( | |
( | |
memo, | |
{ | |
leftKeyAttributes, | |
junctionLeftKeyAttributes, | |
junctionRightKeyAttributes, | |
rightKeyAttributes, | |
junctionTable, | |
rightTable, | |
junctionLeftConstraint, | |
junctionRightConstraint, | |
} | |
) => { | |
const RightTableType = pgGetGqlTypeByTypeIdAndModifier( | |
rightTable.type.id, | |
null | |
); | |
if (!RightTableType) { | |
throw new Error( | |
`Could not determine type for table with id ${ | |
junctionRightConstraint.classId | |
}` | |
); | |
} | |
const junctionTypeName = inflection.tableType(junctionTable); | |
const RightTableConnectionType = getTypeByName( | |
inflection.connectionViaJunction( | |
RightTableType.name, | |
junctionTypeName | |
) | |
); | |
// Since we're ignoring multi-column keys, we can simplify here | |
const leftKeyAttribute = leftKeyAttributes[0]; | |
const junctionLeftKeyAttribute = junctionLeftKeyAttributes[0]; | |
const junctionRightKeyAttribute = junctionRightKeyAttributes[0]; | |
const rightKeyAttribute = rightKeyAttributes[0]; | |
function makeFields(isConnection) { | |
const manyRelationFieldName = isConnection | |
? inflection.manyToManyRelationByKeys( | |
leftKeyAttributes, | |
junctionLeftKeyAttributes, | |
junctionRightKeyAttributes, | |
rightKeyAttributes, | |
junctionTable, | |
rightTable, | |
junctionLeftConstraint, | |
junctionRightConstraint | |
) | |
: inflection.manyToManyRelationByKeysSimple( | |
leftKeyAttributes, | |
junctionLeftKeyAttributes, | |
junctionRightKeyAttributes, | |
rightKeyAttributes, | |
junctionTable, | |
rightTable, | |
junctionLeftConstraint, | |
junctionRightConstraint | |
); | |
memo = extend( | |
memo, | |
{ | |
[manyRelationFieldName]: fieldWithHooks( | |
manyRelationFieldName, | |
({ | |
getDataFromParsedResolveInfoFragment, | |
addDataGenerator, | |
}) => { | |
addDataGenerator(parsedResolveInfoFragment => { | |
return { | |
pgQuery: queryBuilder => { | |
queryBuilder.select(() => { | |
const resolveData = getDataFromParsedResolveInfoFragment( | |
parsedResolveInfoFragment, | |
isConnection | |
? RightTableConnectionType | |
: RightTableType | |
); | |
const rightTableAlias = sql.identifier(Symbol()); | |
const junctionTableAlias = sql.identifier(Symbol()); | |
const leftTableAlias = queryBuilder.getTableAlias(); | |
const query = queryFromResolveData( | |
sql.identifier( | |
rightTable.namespace.name, | |
rightTable.name | |
), | |
rightTableAlias, | |
resolveData, | |
{ | |
withPagination: isConnection, | |
withPaginationAsFields: false, | |
asJsonAggregate: !isConnection, | |
}, | |
innerQueryBuilder => { | |
innerQueryBuilder.parentQueryBuilder = queryBuilder; | |
innerQueryBuilder.junctionTableAlias = junctionTableAlias; | |
const rightPrimaryKeyConstraint = | |
rightTable.primaryKeyConstraint; | |
const rightPrimaryKeyAttributes = | |
rightPrimaryKeyConstraint && | |
rightPrimaryKeyConstraint.keyAttributes; | |
if (rightPrimaryKeyAttributes) { | |
innerQueryBuilder.beforeLock( | |
"orderBy", | |
() => { | |
// append order by primary key to the list of orders | |
if ( | |
!innerQueryBuilder.isOrderUnique(false) | |
) { | |
innerQueryBuilder.data.cursorPrefix = [ | |
"primary_key_asc", | |
]; | |
rightPrimaryKeyAttributes.forEach( | |
attr => { | |
innerQueryBuilder.orderBy( | |
sql.fragment`${innerQueryBuilder.getTableAlias()}.${sql.identifier( | |
attr.name | |
)}`, | |
true | |
); | |
} | |
); | |
innerQueryBuilder.setOrderIsUnique(); | |
} | |
} | |
); | |
} | |
// I would use `innerQueryBuilder.join()` if that existed, but it doesn't, | |
// so I have to reach into `data`. :-( | |
innerQueryBuilder.data.join.push( | |
sql.fragment`INNER JOIN ${sql.identifier( | |
junctionTable.namespace.name, | |
junctionTable.name | |
)} AS ${junctionTableAlias} ON (${rightTableAlias}.${sql.identifier( | |
rightKeyAttribute.name | |
)} = ${junctionTableAlias}.${sql.identifier( | |
junctionRightKeyAttribute.name | |
)})` | |
); | |
innerQueryBuilder.data.join.push( | |
sql.fragment`INNER JOIN ${sql.identifier( | |
leftTable.namespace.name, | |
leftTable.name | |
)} AS ${leftTableAlias} ON (${leftTableAlias}.${sql.identifier( | |
leftKeyAttribute.name | |
)} = ${junctionTableAlias}.${sql.identifier( | |
junctionLeftKeyAttribute.name | |
)})` | |
); | |
} | |
); | |
return sql.fragment`(${query})`; | |
}, getSafeAliasFromAlias(parsedResolveInfoFragment.alias)); | |
}, | |
}; | |
}); | |
const rightTableTypeName = inflection.tableType(rightTable); | |
return { | |
description: `Reads and enables pagination through a set of \`${rightTableTypeName}\`.`, | |
type: isConnection | |
? new GraphQLNonNull(RightTableConnectionType) | |
: new GraphQLNonNull( | |
new GraphQLList(new GraphQLNonNull(RightTableType)) | |
), | |
args: {}, | |
resolve: (data, _args, _context, resolveInfo) => { | |
const safeAlias = getSafeAliasFromResolveInfo( | |
resolveInfo | |
); | |
if (isConnection) { | |
return addStartEndCursor(data[safeAlias]); | |
} else { | |
return data[safeAlias]; | |
} | |
}, | |
}; | |
}, | |
{ | |
isPgFieldConnection: isConnection, | |
isPgFieldSimpleCollection: !isConnection, | |
isPgManyToManyRelationField: true, | |
pgFieldIntrospection: rightTable, | |
// ADDED | |
pgManyToManyLeftTable: leftTable, | |
pgManyToManyLeftKeyAttributes: leftKeyAttributes, | |
pgManyToManyRightTable: rightTable, | |
pgManyToManyRightKeyAttributes: rightKeyAttributes, | |
pgManyToManyJunctionTable: junctionTable, | |
pgManyToManyJunctionLeftConstraint: junctionLeftConstraint, | |
pgManyToManyJunctionRightConstraint: junctionRightConstraint, | |
pgManyToManyJunctionLeftKeyAttributes: junctionLeftKeyAttributes, | |
pgManyToManyJunctionRightKeyAttributes: junctionRightKeyAttributes, | |
} | |
), | |
}, | |
`Many-to-many relation (${ | |
isConnection ? "connection" : "simple collection" | |
}) for ${describePgEntity( | |
junctionLeftConstraint | |
)} and ${describePgEntity(junctionRightConstraint)}.` | |
); | |
} | |
const simpleCollections = | |
junctionRightConstraint.tags.simpleCollections || | |
rightTable.tags.simpleCollections || | |
pgSimpleCollections; | |
const hasConnections = simpleCollections !== "only"; | |
const hasSimpleCollections = | |
simpleCollections === "only" || simpleCollections === "both"; | |
if (hasConnections) { | |
makeFields(true); | |
} | |
if (hasSimpleCollections) { | |
makeFields(false); | |
} | |
return memo; | |
}, | |
{} | |
), | |
`Adding many-to-many relations for ${Self.name}` | |
); | |
}); | |
builder.hook( | |
"GraphQLObjectType:fields:field:args", | |
(args, build, context) => { | |
const { | |
pgSql: sql, | |
gql2pg, | |
extend, | |
getTypeByName, | |
pgColumnFilter, | |
inflection, | |
pgOmit: omit, | |
} = build; | |
const { | |
scope: { | |
fieldName, | |
isPgManyToManyRelationField, | |
isPgFieldConnection, | |
isPgFieldSimpleCollection, | |
pgManyToManyLeftKeyAttributes: leftKeyAttributes, | |
pgManyToManyRightTable: rightTable, | |
pgManyToManyRightKeyAttributes: rightKeyAttributes, | |
pgManyToManyJunctionTable: junctionTable, | |
pgManyToManyJunctionLeftConstraint: junctionLeftConstraint, | |
pgManyToManyJunctionRightConstraint: junctionRightConstraint, | |
pgManyToManyJunctionLeftKeyAttributes: junctionLeftKeyAttributes, | |
pgManyToManyJunctionRightKeyAttributes: junctionRightKeyAttributes, | |
}, | |
addArgDataGenerator, | |
Self, | |
field, | |
} = context; | |
if (!isPgManyToManyRelationField) return args; | |
const shouldAddCondition = | |
isPgFieldConnection || isPgFieldSimpleCollection; | |
if (!shouldAddCondition) return args; | |
const TableConditionType = getTypeByName( | |
inflection.conditionType( | |
inflection.manyToManyRelationByKeys( | |
leftKeyAttributes, | |
junctionLeftKeyAttributes, | |
junctionRightKeyAttributes, | |
rightKeyAttributes, | |
junctionTable, | |
rightTable, | |
junctionLeftConstraint, | |
junctionRightConstraint | |
) | |
) | |
); | |
if (!TableConditionType) { | |
return args; | |
} | |
const relevantAttributes = junctionTable.attributes.filter( | |
attr => | |
pgColumnFilter(attr, build, context) && | |
!omit(attr, "filter") && | |
!junctionLeftKeyAttributes.includes(attr) && | |
!junctionRightKeyAttributes.includes(attr) | |
); | |
addArgDataGenerator(function connectionCondition({ condition }) { | |
return { | |
pgQuery: queryBuilder => { | |
if (condition != null) { | |
// This is a bit precarious, and it doesn't yet work for live queries... | |
const junctionTableAlias = queryBuilder.junctionTableAlias; | |
if (!junctionTableAlias) { | |
throw new Error("Missing junctionTableAlias on queryBuilder"); | |
} | |
relevantAttributes.forEach(attr => { | |
const fieldName = inflection.column(attr); | |
const val = condition[fieldName]; | |
if (val != null) { | |
// queryBuilder.addLiveCondition(() => record => | |
// record[attr.name] === val | |
// ); | |
queryBuilder.where( | |
sql.fragment`${junctionTableAlias}.${sql.identifier( | |
attr.name | |
)} = ${gql2pg(val, attr.type, attr.typeModifier)}` | |
); | |
} else if (val === null) { | |
// queryBuilder.addLiveCondition(() => record => | |
// record[attr.name] == null | |
// ); | |
queryBuilder.where( | |
sql.fragment`${junctionTableAlias}.${sql.identifier( | |
attr.name | |
)} IS NULL` | |
); | |
} | |
}); | |
} | |
}, | |
}; | |
}); | |
// The `PgConnectionArgCondition` plugin adds a `condition` argument, | |
// but it doesn't do what we want. So we'll delete that argument, | |
// and set our own instead. | |
delete args.condition; | |
return extend( | |
args, | |
{ | |
condition: { | |
description: | |
"A condition to be used in determining which values should be returned by the collection.", | |
type: TableConditionType, | |
}, | |
}, | |
`Adding condition to connection field '${fieldName || | |
field.type}' of '${Self.name}'` | |
); | |
}, | |
["PgEnhancedManyToManyRelationArgCondition"] | |
); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment