UNPKG

@ensnode/ponder-subgraph

Version:

A Hono middleware for generating Subgraph-compatible GraphQL schema.

930 lines (924 loc) • 34.4 kB
// src/middleware.ts import { maxAliasesPlugin } from "@escape.tech/graphql-armor-max-aliases"; import { maxDepthPlugin } from "@escape.tech/graphql-armor-max-depth"; import { maxTokensPlugin } from "@escape.tech/graphql-armor-max-tokens"; import { createYoga } from "graphql-yoga"; import { createMiddleware } from "hono/factory"; // src/drizzle.ts import { setDatabaseSchema } from "@ponder/client"; import { drizzle } from "drizzle-orm/node-postgres"; var makeDrizzle = ({ schema, databaseUrl, databaseSchema }) => { setDatabaseSchema(schema, databaseSchema); return drizzle(databaseUrl, { schema, casing: "snake_case" }); }; // src/graphql.ts import DataLoader from "dataloader"; import { and, arrayContained, arrayContains, asc, createTableRelationsHelpers, desc, eq, extractTablesRelationalConfig, getTableColumns, getTableUniqueName, gt, gte, inArray, is, isNotNull, isNull, like, lt, lte, Many, ne, not, notInArray, notLike, One, or, relations, sql } from "drizzle-orm"; import { toSnakeCase } from "drizzle-orm/casing"; import { isPgEnum, PgEnumColumn, PgInteger, PgSerial, pgTable } from "drizzle-orm/pg-core"; import { GraphQLBoolean, GraphQLEnumType, GraphQLFloat, GraphQLInputObjectType, GraphQLInt, GraphQLInterfaceType, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, GraphQLSchema, GraphQLString } from "graphql"; import { GraphQLJSON } from "graphql-scalars"; // src/helpers.ts var intersectionOf = (arrays) => arrays.reduce((a, b) => a.filter((c) => b.includes(c))); var capitalize = (str) => { if (!str) return str; return `${str.charAt(0).toUpperCase()}${str.slice(1)}`; }; // src/serialize.ts function serialize(value) { return JSON.stringify( value, (_, v) => typeof v === "bigint" ? { __type: "bigint", value: v.toString() } : v ); } function deserialize(value) { return JSON.parse( value, (_, value_) => (value_ == null ? void 0 : value_.__type) === "bigint" ? BigInt(value_.value) : value_ ); } // src/graphql.ts var DEFAULT_LIMIT = 100; var MAX_LIMIT = 1e3; var OrderDirectionEnum = new GraphQLEnumType({ name: "OrderDirection", values: { asc: { value: "asc" }, desc: { value: "desc" } } }); function buildGraphQLSchema({ schema: _schema, polymorphicConfig: _polymorphicConfig }) { const polymorphicConfig = _polymorphicConfig ?? { types: {}, fields: {} }; const schema = { ..._schema }; const _tablesConfig = extractTablesRelationalConfig(schema, createTableRelationsHelpers); const polymorphicTableConfigs = Object.fromEntries( Object.entries(polymorphicConfig.types).map(([interfaceTypeName, implementingTables]) => [ interfaceTypeName, implementingTables.map((table) => getTableUniqueName(table)).map((tableName) => _tablesConfig.tables[_tablesConfig.tableNamesMap[tableName]]) ]) ); Object.assign( schema, ...Object.keys(polymorphicConfig.types).map( (interfaceTypeName) => getIntersectionTableSchema(interfaceTypeName, polymorphicTableConfigs[interfaceTypeName]) ) ); const polymorphicFields = Object.entries(polymorphicConfig.fields).map(([fieldPath, interfaceTypeName]) => [ fieldPath.split("."), interfaceTypeName ]); const tablesConfig = extractTablesRelationalConfig(schema, createTableRelationsHelpers); const tables = Object.values(tablesConfig.tables); const enums = Object.entries(schema).filter( (el) => isPgEnum(el[1]) ); const enumTypes = {}; for (const [enumTsName, enumObject] of enums) { enumTypes[enumObject.enumName] = new GraphQLEnumType({ name: enumTsName, values: enumObject.enumValues.reduce( (acc, cur) => ({ ...acc, [cur]: {} }), {} ) }); } const entityOrderByEnums = {}; for (const table of tables) { const values = Object.keys(table.columns).reduce( (acc, columnName) => ({ ...acc, [columnName]: { value: columnName } }), {} ); entityOrderByEnums[table.tsName] = new GraphQLEnumType({ name: `${getSubgraphEntityName(table.tsName)}_orderBy`, values }); } const entityFilterTypes = {}; for (const table of tables) { const filterType = new GraphQLInputObjectType({ name: `${getSubgraphEntityName(table.tsName)}_filter`, fields: () => { const filterFields = { // Logical operators // NOTE: lower case and/or and: { type: new GraphQLList(filterType) }, or: { type: new GraphQLList(filterType) } }; for (const [columnName, column] of Object.entries(table.columns)) { const type = columnToGraphQLCore(column, enumTypes); if (type instanceof GraphQLList) { const baseType = innerType(type); conditionSuffixes.universal.forEach((suffix) => { filterFields[`${columnName}${suffix}`] = { type: new GraphQLList(baseType) }; }); conditionSuffixes.plural.forEach((suffix) => { filterFields[`${columnName}${suffix}`] = { type: baseType }; }); } if (type instanceof GraphQLScalarType || type instanceof GraphQLEnumType) { if (type.name === "JSON") continue; conditionSuffixes.universal.forEach((suffix) => { filterFields[`${columnName}${suffix}`] = { type }; }); conditionSuffixes.singular.forEach((suffix) => { filterFields[`${columnName}${suffix}`] = { type: new GraphQLList(type) }; }); if (["String", "ID"].includes(type.name)) { conditionSuffixes.string.forEach((suffix) => { filterFields[`${columnName}${suffix}`] = { type }; }); conditionSuffixes.numeric.forEach((suffix) => { filterFields[`${columnName}${suffix}`] = { type }; }); } if (["Int", "Float", "BigInt"].includes(type.name)) { conditionSuffixes.numeric.forEach((suffix) => { filterFields[`${columnName}${suffix}`] = { type }; }); } } } for (const [_relationName, relation] of Object.entries(table.relations)) { if (is(relation, One)) { conditionSuffixes.universal.forEach((suffix) => { filterFields[`${relation.fieldName}${suffix}`] = { type: GraphQLString }; }); } } return filterFields; } }); entityFilterTypes[table.tsName] = filterType; } const entityTypes = {}; const interfaceTypes = {}; const entityPageTypes = {}; for (const interfaceTypeName of Object.keys(polymorphicTableConfigs)) { const table = tablesConfig.tables[interfaceTypeName]; interfaceTypes[interfaceTypeName] = new GraphQLInterfaceType({ name: interfaceTypeName, fields: () => { const fieldConfigMap = {}; for (const [columnName, column] of Object.entries(table.columns)) { const type = columnToGraphQLCore(column, enumTypes); fieldConfigMap[columnName] = { type: column.notNull ? new GraphQLNonNull(type) : type }; } return fieldConfigMap; } }); } for (const table of tables) { if (isInterfaceType(polymorphicConfig, table.tsName)) continue; const entityTypeName = getSubgraphEntityName(table.tsName); entityTypes[table.tsName] = new GraphQLObjectType({ name: entityTypeName, interfaces: Object.entries(polymorphicTableConfigs).filter( ([, implementingTables]) => implementingTables.map((table2) => table2.tsName).includes(table.tsName) ).map(([interfaceTypeName]) => interfaceTypes[interfaceTypeName]), fields: () => { var _a, _b, _c, _d; const fieldConfigMap = {}; for (const [columnName, column] of Object.entries(table.columns)) { const type = columnToGraphQLCore(column, enumTypes); fieldConfigMap[columnName] = { type: column.notNull ? new GraphQLNonNull(type) : type }; } const relations2 = Object.entries(table.relations); for (const [relationName, relation] of relations2) { const referencedTable = tables.find( (table2) => table2.dbName === relation.referencedTableName ); if (!referencedTable) throw new Error( `Internal error: Referenced table "${relation.referencedTableName}" not found` ); const referencedEntityType = entityTypes[referencedTable.tsName]; const referencedEntityPageType = entityPageTypes[referencedTable.tsName]; const referencedEntityFilterType = entityFilterTypes[referencedTable.tsName]; if (referencedEntityType === void 0 || referencedEntityPageType === void 0 || referencedEntityFilterType === void 0) throw new Error( `Internal error: Referenced entity types not found for table "${referencedTable.tsName}" ` ); if (is(relation, One)) { const fields = ((_a = relation.config) == null ? void 0 : _a.fields) ?? []; const references = ((_b = relation.config) == null ? void 0 : _b.references) ?? []; if (fields.length !== references.length) { throw new Error( "Internal error: Fields and references arrays must be the same length" ); } fieldConfigMap[relationName] = { // Note: There is a `relation.isNullable` field here but it appears // to be internal / incorrect. Until we have support for foriegn // key constraints, all `one` relations must be nullable. type: referencedEntityType, resolve: (parent, _args, context) => { const loader = context.getDataLoader({ table: referencedTable }); const rowFragment = {}; for (let i = 0; i < references.length; i++) { const referenceColumn = references[i]; const fieldColumn = fields[i]; const fieldColumnTsName = getColumnTsName(fieldColumn); const referenceColumnTsName = getColumnTsName(referenceColumn); rowFragment[referenceColumnTsName] = parent[fieldColumnTsName]; } const encodedId = encodeRowFragment(rowFragment); return loader.load(encodedId); } }; } else if (is(relation, Many)) { let oneRelation; for (const _relation of Object.values(referencedTable.relations)) { if (is(_relation, One) && relation.relationName === _relation.relationName) { oneRelation = _relation; } } if (oneRelation === void 0) { for (const _relation of Object.values(referencedTable.relations)) { if (is(_relation, One) && table.dbName === _relation.referencedTableName) { oneRelation = _relation; } } } if (oneRelation === void 0) throw new Error( `Internal error: Relation "${relationName}" not found in table "${referencedTable.tsName}"` ); const fields = ((_c = oneRelation.config) == null ? void 0 : _c.fields) ?? []; const references = ((_d = oneRelation.config) == null ? void 0 : _d.references) ?? []; const referencedEntityOrderByType = entityOrderByEnums[referencedTable.tsName]; if (!referencedEntityOrderByType) throw new Error(`Entity_orderBy Enum not found for ${referencedTable.tsName}`); fieldConfigMap[relationName] = { type: referencedEntityPageType, args: { where: { type: referencedEntityFilterType }, orderBy: { type: referencedEntityOrderByType }, orderDirection: { type: OrderDirectionEnum }, first: { type: GraphQLInt }, skip: { type: GraphQLInt } }, resolve: (parent, args, context, _info) => { const relationalConditions = []; for (let i = 0; i < references.length; i++) { const column = fields[i]; const value = parent[references[i].name]; relationalConditions.push(eq(column, value)); } return executePluralQuery( referencedTable, schema[referencedTable.tsName], context.drizzle, args, relationalConditions ); } }; } else { throw new Error( `Internal error: Relation "${relationName}" is unsupported, expected One or Many` ); } } polymorphicFields.filter(([[parent]]) => parent === entityTypeName).forEach(([[, fieldName], interfaceTypeName]) => { fieldConfigMap[fieldName] = definePolymorphicPluralField({ schema, interfaceType: interfaceTypes[interfaceTypeName], filterType: entityFilterTypes[interfaceTypeName], orderByType: entityOrderByEnums[interfaceTypeName], intersectionTableConfig: tablesConfig.tables[interfaceTypeName], implementingTableConfigs: polymorphicTableConfigs[interfaceTypeName] }); }); return fieldConfigMap; } }); entityPageTypes[table.tsName] = new GraphQLNonNull( new GraphQLList(new GraphQLNonNull(entityTypes[table.tsName])) ); } const queryFields = {}; for (const table of tables) { if (isInterfaceType(polymorphicConfig, table.tsName)) continue; const entityType = entityTypes[table.tsName]; const entityPageType = entityPageTypes[table.tsName]; const entityFilterType = entityFilterTypes[table.tsName]; const singularFieldName = table.tsName.charAt(0).toLowerCase() + table.tsName.slice(1); const pluralFieldName = `${singularFieldName}s`; queryFields[singularFieldName] = { type: entityType, // Find the primary key columns and GraphQL core types and include them // as arguments to the singular query type. args: Object.fromEntries( table.primaryKey.map((column) => [ getColumnTsName(column), { type: new GraphQLNonNull(columnToGraphQLCore(column, enumTypes)) } ]) ), resolve: async (_parent, args, context) => { const loader = context.getDataLoader({ table }); const encodedId = encodeRowFragment(args); return loader.load(encodedId); } }; const entityOrderByType = entityOrderByEnums[table.tsName]; if (!entityOrderByType) throw new Error(`Entity_orderBy Enum not found for ${table.tsName}`); queryFields[pluralFieldName] = { type: entityPageType, args: { where: { type: entityFilterType }, orderBy: { type: entityOrderByType }, orderDirection: { type: OrderDirectionEnum }, first: { type: GraphQLInt }, skip: { type: GraphQLInt } }, resolve: async (_parent, args, context, _info) => { return executePluralQuery(table, schema[table.tsName], context.drizzle, args); } }; } polymorphicFields.filter(([[parent]]) => parent === "Query").forEach(([[, fieldName], interfaceTypeName]) => { queryFields[fieldName] = definePolymorphicPluralField({ schema, interfaceType: interfaceTypes[interfaceTypeName], filterType: entityFilterTypes[interfaceTypeName], orderByType: entityOrderByEnums[interfaceTypeName], intersectionTableConfig: tablesConfig.tables[interfaceTypeName], implementingTableConfigs: polymorphicTableConfigs[interfaceTypeName] }); }); queryFields._meta = { type: new GraphQLObjectType({ name: "_Meta_", fields: { block: { type: new GraphQLNonNull( new GraphQLObjectType({ name: "_Block_", description: "Information about a specific block.", fields: { hash: { type: GraphQLString }, parentHash: { type: GraphQLString }, number: { type: new GraphQLNonNull(GraphQLInt) }, timestamp: { type: new GraphQLNonNull(GraphQLInt) } } }) ) }, deployment: { type: new GraphQLNonNull(GraphQLString), description: "An ID representing this instance of ENSNode. It is composed of the ENSNode version (https://github.com/namehash/ensnode/releases) and the Ponder build_id (https://ponder.sh/docs/api-reference/database#instance-lifecycle)." }, hasIndexingErrors: { type: new GraphQLNonNull(GraphQLBoolean), description: "If true, ENSIndexer has reported an indexing error and is not actively indexing blocks." } } }), resolve: async (_source, _args, _context) => _context._meta ?? null }; return new GraphQLSchema({ // Include these here so they are listed first in the printed schema. types: [GraphQLJSON, GraphQLBigInt, GraphQLPageInfo], query: new GraphQLObjectType({ name: "Query", fields: queryFields }) }); } var GraphQLPageInfo = new GraphQLObjectType({ name: "PageInfo", fields: { hasNextPage: { type: new GraphQLNonNull(GraphQLBoolean) }, hasPreviousPage: { type: new GraphQLNonNull(GraphQLBoolean) }, startCursor: { type: GraphQLString }, endCursor: { type: GraphQLString } } }); var GraphQLBigInt = new GraphQLScalarType({ name: "BigInt", serialize: (value) => String(value), parseValue: (value) => BigInt(value), parseLiteral: (value) => { if (value.kind === "StringValue") { return BigInt(value.value); } else { throw new Error( `Invalid value kind provided for field of type BigInt: ${value.kind}. Expected: StringValue` ); } } }); var columnToGraphQLCore = (column, enumTypes) => { if (column.columnType === "PgEvmBigint") { return GraphQLBigInt; } if (column instanceof PgEnumColumn) { if (column.enum === void 0) { throw new Error( `Internal error: Expected enum column "${getColumnTsName(column)}" to have an "enum" property` ); } const enumType = enumTypes[column.enum.enumName]; if (enumType === void 0) { throw new Error( `Internal error: Expected to find a GraphQL enum named "${column.enum.enumName}"` ); } return enumType; } switch (column.dataType) { case "boolean": return GraphQLBoolean; case "json": return GraphQLJSON; case "date": return GraphQLString; case "string": return GraphQLString; case "bigint": return GraphQLString; case "number": return is(column, PgInteger) || is(column, PgSerial) ? GraphQLInt : GraphQLFloat; case "buffer": return new GraphQLList(new GraphQLNonNull(GraphQLInt)); case "array": { if (column.columnType === "PgVector") { return new GraphQLList(new GraphQLNonNull(GraphQLFloat)); } if (column.columnType === "PgGeometry") { return new GraphQLList(new GraphQLNonNull(GraphQLFloat)); } const innerType2 = columnToGraphQLCore(column.baseColumn, enumTypes); return new GraphQLList(new GraphQLNonNull(innerType2)); } default: throw new Error(`Type ${column.dataType} is not implemented`); } }; var innerType = (type) => { if (type instanceof GraphQLScalarType || type instanceof GraphQLEnumType) return type; if (type instanceof GraphQLList || type instanceof GraphQLNonNull) return innerType(type.ofType); throw new Error(`Type ${type.toString()} is not implemented`); }; async function executePluralQuery(table, from, drizzle2, args, extraConditions = []) { const limit = args.first ?? DEFAULT_LIMIT; if (limit > MAX_LIMIT) { throw new Error(`Invalid limit. Got ${limit}, expected <=${MAX_LIMIT}.`); } const skip = args.skip ?? 0; const orderBySchema = buildOrderBySchema(table, args); const orderBy = orderBySchema.map(([columnName, direction]) => { const column = table.columns[columnName]; if (column === void 0) { throw new Error(`Unknown column "${columnName}" used in orderBy argument`); } return direction === "asc" ? asc(column) : desc(column); }); const whereConditions = buildWhereConditions(args.where, table); const query = drizzle2.select().from(from).where(and(...whereConditions, ...extraConditions)).orderBy(...orderBy).limit(limit).offset(skip); const startTime = performance.now(); const rows = await query; const queryDurationSeconds = (performance.now() - startTime) / 1e3; const isSlowQuery = queryDurationSeconds > 2; if (isSlowQuery) { console.warn(`Slow Query Detected (${queryDurationSeconds.toFixed(4)}s)`); console.warn(query.toSQL().sql); console.log("\n"); } return rows; } var conditionSuffixes = { universal: ["", "_not"], singular: ["_in", "_not_in"], plural: ["_has", "_not_has"], numeric: ["_gt", "_lt", "_gte", "_lte"], string: [ "_contains", "_not_contains", "_starts_with", "_ends_with", "_not_starts_with", "_not_ends_with" ] }; var conditionSuffixesByLengthDesc = Object.values(conditionSuffixes).flat().sort((a, b) => b.length - a.length); function buildWhereConditions(where, table) { const conditions = []; if (where === void 0) return conditions; for (const [whereKey, rawValue] of Object.entries(where)) { if (whereKey === "and" || whereKey === "or") { if (!Array.isArray(rawValue)) { throw new Error( `Invalid query: Expected an array for the ${whereKey} operator. Got: ${rawValue}` ); } const nestedConditions = rawValue.flatMap( (subWhere) => buildWhereConditions(subWhere, table) ); if (nestedConditions.length > 0) { conditions.push(whereKey === "and" ? and(...nestedConditions) : or(...nestedConditions)); } continue; } const conditionSuffix = conditionSuffixesByLengthDesc.find((s) => whereKey.endsWith(s)); if (conditionSuffix === void 0) { throw new Error(`Invariant violation: Condition suffix not found for where key ${whereKey}`); } const columnName = whereKey.slice(0, whereKey.length - conditionSuffix.length); const column = columnName in table.relations ? ( // if the referenced name is a relation, the relevant column is this table's `${relationName}Id` table.columns[`${columnName}Id`] ) : ( // otherwise validate that the column name is present in the table table.columns[columnName] ); if (column === void 0) { throw new Error(`Invalid query: Where clause contains unknown column ${columnName}`); } switch (conditionSuffix) { case "": if (column.columnType === "PgArray") { conditions.push(and(arrayContains(column, rawValue), arrayContained(column, rawValue))); } else { if (rawValue === null) { conditions.push(isNull(column)); } else { conditions.push(eq(column, rawValue)); } } break; case "_not": if (column.columnType === "PgArray") { conditions.push( not(and(arrayContains(column, rawValue), arrayContained(column, rawValue))) ); } else { if (rawValue === null) { conditions.push(isNotNull(column)); } else { conditions.push(ne(column, rawValue)); } } break; case "_in": conditions.push(inArray(column, rawValue)); break; case "_not_in": conditions.push(notInArray(column, rawValue)); break; case "_has": conditions.push(arrayContains(column, [rawValue])); break; case "_not_has": conditions.push(not(arrayContains(column, [rawValue]))); break; case "_gt": conditions.push(gt(column, rawValue)); break; case "_lt": conditions.push(lt(column, rawValue)); break; case "_gte": conditions.push(gte(column, rawValue)); break; case "_lte": conditions.push(lte(column, rawValue)); break; case "_contains": conditions.push(like(column, `%${rawValue}%`)); break; case "_not_contains": conditions.push(notLike(column, `%${rawValue}%`)); break; case "_starts_with": conditions.push(like(column, `${rawValue}%`)); break; case "_ends_with": conditions.push(like(column, `%${rawValue}`)); break; case "_not_starts_with": conditions.push(notLike(column, `${rawValue}%`)); break; case "_not_ends_with": conditions.push(notLike(column, `%${rawValue}`)); break; default: throw new Error(`Invalid Condition Suffix ${conditionSuffix}`); } } return conditions; } function buildOrderBySchema(table, args) { const userDirection = args.orderDirection ?? "asc"; const userColumns = args.orderBy !== void 0 ? [[args.orderBy, userDirection]] : []; const pkColumns = table.primaryKey.map((column) => [getColumnTsName(column), userDirection]); const missingPkColumns = pkColumns.filter( (pkColumn) => !userColumns.some((userColumn) => userColumn[0] === pkColumn[0]) ); return [...userColumns, ...missingPkColumns]; } function buildDataLoaderCache({ drizzle: drizzle2 }) { const dataLoaderMap = /* @__PURE__ */ new Map(); return ({ table }) => { const baseQuery = drizzle2.query[table.tsName]; if (baseQuery === void 0) throw new Error(`Internal error: Unknown table "${table.tsName}" in data loader cache`); let dataLoader = dataLoaderMap.get(table); if (dataLoader === void 0) { dataLoader = new DataLoader( async (encodedIds) => { const decodedRowFragments = encodedIds.map(decodeRowFragment); const idConditions = decodedRowFragments.map( (decodedRowFragment) => and(...buildWhereConditions(decodedRowFragment, table)) ); const rows = await baseQuery.findMany({ where: or(...idConditions), limit: encodedIds.length }); return decodedRowFragments.map( (fragment) => rows.find( (row) => Object.entries(fragment).every(([col, val]) => row[col] === val) ) ); }, { maxBatchSize: 1e3 } ); dataLoaderMap.set(table, dataLoader); } return dataLoader; }; } function getColumnTsName(column) { const tableColumns = getTableColumns(column.table); return Object.entries(tableColumns).find(([_, c]) => c.name === column.name)[0]; } function encodeRowFragment(rowFragment) { return Buffer.from(serialize(rowFragment)).toString("base64"); } function decodeRowFragment(encodedRowFragment) { return deserialize(Buffer.from(encodedRowFragment, "base64").toString()); } function getSubgraphEntityName(tsName) { return capitalize(tsName); } function isInterfaceType(polymorphicConfig, columnName) { return columnName in polymorphicConfig.types; } function getIntersectionTableSchema(interfaceTypeName, tableConfigs) { if (tableConfigs.length === 0) throw new Error("Must have some tables to intersect"); const baseColumns = tableConfigs[0].columns; const baseRelations = tableConfigs[0].relations; const commonColumnNames = intersectionOf( tableConfigs.map((table) => Object.keys(table.columns)) // ); const commonRelationsNames = intersectionOf( tableConfigs.map((table) => Object.keys(table.relations)) ); const intersectionTable = pgTable( "intersection_table", (t) => commonColumnNames.reduce((memo, columnName) => { const column = baseColumns[columnName]; const sqlType = column.getSQLType(); const snakeCaseColumnName = toSnakeCase(column.name); let newColumn = sqlType === "numeric(78)" ? ( // special case for bigint whose sqltype is "numeric(78)" t.numeric(snakeCaseColumnName, { precision: 78 }) ) : ( // handle standard types, removing any parameters from the SQLType // @ts-expect-error we know the key is valid and this is callable t[sqlType.split("(")[0]](snakeCaseColumnName) ); newColumn = column.primary ? newColumn.primaryKey() : newColumn; newColumn = newColumn.notNull(column.notNull); memo[columnName] = newColumn; return memo; }, {}) ); const intersectionTableRelations = relations( intersectionTable, ({ one }) => commonRelationsNames.reduce((memo, relationName) => { const relation = baseRelations[relationName]; if (is(relation, One)) { memo[relationName] = one(relation.referencedTable, relation.config); } else if (is(relation, Many)) { } return memo; }, {}) ); return { [interfaceTypeName]: intersectionTable, [`${interfaceTypeName}Relations`]: intersectionTableRelations }; } function getColumnsUnion(tables) { return tables.reduce( (memo, table) => ({ ...memo, ...table.columns }), {} ); } function buildUnionAllQuery(drizzle2, schema, tables, where = {}) { const allColumns = getColumnsUnion(tables); const allColumnNames = Object.keys(allColumns).sort(); const subqueries = tables.map((table) => { const selectAllColumnsIncludingNulls = allColumnNames.reduce((memo, columnName) => { const column = allColumns[columnName]; const snakeCaseColumnName = toSnakeCase(column.name); const dbColumnName = table.columns[columnName] ? snakeCaseColumnName : "NULL"; return { ...memo, [columnName]: sql.raw(`${dbColumnName}::${column.getSQLType()}`).as(snakeCaseColumnName) }; }, {}); const relationalConditions = Object.entries(where).map( ([foreignKeyName, foreignKeyValue]) => eq(table.columns[foreignKeyName], foreignKeyValue) ); return drizzle2.select({ ...selectAllColumnsIncludingNulls, // inject __typename into each subquery __typename: sql.raw(`'${getSubgraphEntityName(table.tsName)}'`).as("__typename") }).from(schema[table.tsName]).where(and(...relationalConditions)).$dynamic(); }); return subqueries.reduce((memo, fragment, i) => i === 0 ? fragment : memo.unionAll(fragment)).as("intersection_table"); } function definePolymorphicPluralField({ schema, interfaceType, filterType, orderByType, intersectionTableConfig, implementingTableConfigs }) { return { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(interfaceType))), args: { where: { type: filterType }, orderBy: { type: orderByType }, orderDirection: { type: OrderDirectionEnum }, first: { type: GraphQLInt }, skip: { type: GraphQLInt } }, resolve: async (parent, args, { drizzle: drizzle2 }, info) => { const foreignKeyName = getForeignKeyFieldName(intersectionTableConfig, info.parentType.name); const relationalFilter = foreignKeyName ? { [foreignKeyName]: parent.id } : {}; const subquery = buildUnionAllQuery( drizzle2, schema, implementingTableConfigs, relationalFilter ); return executePluralQuery(intersectionTableConfig, subquery, drizzle2, args); } }; } function getForeignKeyFieldName(table, parentTypeName) { var _a, _b, _c; const relationName = Object.keys(table.relations).find( (relationName2) => getSubgraphEntityName(relationName2) === parentTypeName ); if (!relationName) return; const relation = table.relations[relationName]; if (!is(relation, One)) return; const fkEntry = Object.entries(((_c = (_b = (_a = relation.config) == null ? void 0 : _a.fields) == null ? void 0 : _b[0]) == null ? void 0 : _c.table) ?? {}).find( ([_, column]) => { var _a2, _b2, _c2; return column.name === ((_c2 = (_b2 = (_a2 = relation.config) == null ? void 0 : _a2.fields) == null ? void 0 : _b2[0]) == null ? void 0 : _c2.name); } ); return fkEntry == null ? void 0 : fkEntry[0]; } // src/middleware.ts function subgraphGraphQLMiddleware({ databaseUrl, databaseSchema, schema, polymorphicConfig }, { maxOperationTokens = 1e3, maxOperationDepth = 100, maxOperationAliases = 30 } = { // Default limits are from Apollo: // https://www.apollographql.com/blog/prevent-graph-misuse-with-operation-size-and-complexity-limit maxOperationTokens: 1e3, maxOperationDepth: 100, maxOperationAliases: 30 }) { const drizzle2 = makeDrizzle({ schema, databaseUrl, databaseSchema }); const graphqlSchema = buildGraphQLSchema({ schema, polymorphicConfig }); const yoga = createYoga({ graphqlEndpoint: "*", // Disable built-in route validation, use Hono routing instead schema: graphqlSchema, context: () => { const getDataLoader = buildDataLoaderCache({ drizzle: drizzle2 }); return { drizzle: drizzle2, getDataLoader }; }, maskedErrors: process.env.NODE_ENV === "production" ? true : { maskError(error) { console.error(error); if (error instanceof Error) return error; return new Error(`Internal Server Error`); } }, logging: false, graphiql: true, parserAndValidationCache: false, plugins: [ maxTokensPlugin({ n: maxOperationTokens }), maxDepthPlugin({ n: maxOperationDepth, ignoreIntrospection: false }), maxAliasesPlugin({ n: maxOperationAliases, allowList: [] }) ] }); return createMiddleware(async (c) => { const response = await yoga.handle(c.req.raw, c.var); response.status = 200; response.statusText = "OK"; return response; }); } export { subgraphGraphQLMiddleware }; //# sourceMappingURL=index.js.map