UNPKG

@ensnode/ponder-subgraph

Version:

A Hono middleware for generating Subgraph-compatible GraphQL schema.

309 lines (305 loc) • 9.04 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/graphql.ts import DataLoader from "dataloader"; import { Many, One, and, arrayContained, arrayContains, asc, createTableRelationsHelpers, desc, eq, extractTablesRelationalConfig, getTableColumns, getTableUniqueName, gt, gte, inArray, is, isNotNull, isNull, like, lt, lte, ne, not, notInArray, notLike, or, relations, sql } from "drizzle-orm"; import { toSnakeCase } from "drizzle-orm/casing"; import { PgDialect, PgEnumColumn, PgInteger, PgSerial, isPgEnum, 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/serialize.ts function deserialize(value) { return JSON.parse( value, (_, value_) => (value_ == null ? void 0 : value_.__type) === "bigint" ? BigInt(value_.value) : value_ ); } // src/graphql.ts var onchain = Symbol.for("ponder:onchain"); var OrderDirectionEnum = new GraphQLEnumType({ name: "OrderDirection", values: { asc: { value: "asc" }, desc: { value: "desc" } } }); 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 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 buildDataLoaderCache({ drizzle }) { const dataLoaderMap = /* @__PURE__ */ new Map(); return ({ table }) => { const baseQuery = drizzle.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 decodeRowFragment(encodedRowFragment) { return deserialize(Buffer.from(encodedRowFragment, "base64").toString()); } // src/middleware.ts var graphql = ({ db, graphqlSchema }, { 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 yoga = createYoga({ graphqlEndpoint: "*", // Disable built-in route validation, use Hono routing instead schema: graphqlSchema, context: () => { const getDataLoader = buildDataLoaderCache({ drizzle: db }); return { drizzle: db, getDataLoader }; }, maskedErrors: process.env.NODE_ENV === "production" ? true : { maskError(error) { console.error(error.originalError); return 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); response.status = 200; response.statusText = "OK"; return response; }); }; export { graphql }; //# sourceMappingURL=middleware.js.map