@ensnode/ponder-subgraph
Version:
A Hono middleware for generating Subgraph-compatible GraphQL schema.
309 lines (305 loc) • 9.04 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/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