@ensnode/ponder-subgraph
Version:
A Hono middleware for generating Subgraph-compatible GraphQL schema.
930 lines (924 loc) • 34.4 kB
JavaScript
// 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