UNPKG

ponder-client

Version:
359 lines (330 loc) 9.58 kB
import type { Hex } from 'viem'; type Scalar = 'string' | 'int' | 'float' | 'boolean' | 'hex' | 'bigint'; type BaseColumn< TType extends Scalar = Scalar, TReferences extends `${string}.id` | undefined | unknown = unknown, TOptional extends boolean | unknown = unknown, TList extends boolean | unknown = unknown, > = { _type: 'b'; type: TType; references: TReferences; optional: TOptional; list: TList; }; type ReferenceColumn< TType extends Scalar = Scalar, TReferences extends `${string}.id` = `${string}.id`, TOptional extends boolean = boolean, > = BaseColumn<TType, TReferences, TOptional, false>; type NonReferenceColumn< TType extends Scalar = Scalar, TOptional extends boolean = boolean, TList extends boolean = boolean, > = BaseColumn<TType, undefined, TOptional, TList>; type EnumColumn< TType extends string | unknown = unknown, TOptional extends boolean | unknown = unknown, TList extends boolean | unknown = unknown, > = { _type: 'e'; type: TType; optional: TOptional; list: TList; }; type ManyColumn<T extends `${string}.${string}` | unknown = unknown> = T extends `${infer TTableName extends string}.${infer TColumnName extends string}` ? { _type: 'm'; referenceTable: TTableName; referenceColumn: TColumnName; } : { _type: 'm' }; type OneColumn<T extends string | unknown = unknown> = T extends string ? { _type: 'o'; referenceColumn: T; } : { _type: 'o' }; type Columns = Record< string, NonReferenceColumn | ReferenceColumn | EnumColumn | ManyColumn | OneColumn >; /** * Recover raw typescript types from the intermediate representation */ type RecoverScalarType<TScalar extends Scalar> = TScalar extends 'string' ? string : TScalar extends 'int' ? number : TScalar extends 'float' ? number : TScalar extends 'boolean' ? boolean : TScalar extends 'hex' ? Hex : TScalar extends 'bigint' ? bigint : never; type RecoverColumnType< TColumn extends | NonReferenceColumn | ReferenceColumn | EnumColumn | ManyColumn | OneColumn, > = TColumn extends { type: infer _type extends Scalar; list: infer _list extends boolean; } ? _list extends false ? RecoverScalarType<_type> : RecoverScalarType<_type>[] : never; type RecoverPageInfoType<TKey extends string | number | symbol> = TKey extends 'hasNextPage' ? boolean : TKey extends 'hasPreviousPage' ? boolean : TKey extends 'startCursor' ? string : TKey extends 'endCursor' ? string : never; type ManyFilterWhere<TColumns extends Columns> = { [column in keyof TColumns]?: RecoverColumnType<TColumns[column]>; }; interface ManyFilter<TColumns extends Columns> { limit?: number; orderBy?: keyof TColumns; orderDirection?: 'asc' | 'desc'; before?: string; after?: string; where?: ManyFilterWhere<TColumns>; } type SingleFilter< // eslint-disable-next-line @typescript-eslint/no-unused-vars TColumns extends Columns, > = { id: string; }; type Selection<TColumns extends Columns> = { [column in keyof TColumns]?: boolean; }; interface Pagination { hasNextPage?: boolean; hasPreviousPage?: boolean; startCursor?: boolean; endCursor?: boolean; } type QueryPart< TTableName extends string, TColumns extends Columns, TSelection extends Selection<TColumns> = Selection<TColumns>, TPagination extends Pagination = Pagination, > = | { type: 'many'; table: TTableName; filter: ManyFilter<TColumns>; columns: TSelection; pagination: TPagination; } | { type: 'one'; table: TTableName; filter: SingleFilter<TColumns>; columns: TSelection; pagination: TPagination; }; type Query< TTableName extends string = string, TColumns extends Columns = Columns, TSelection extends Record<string, Selection<TColumns>> = Record< string, Selection<TColumns> >, > = Record<string, QueryPart<TTableName, TColumns, TSelection[string]>>; // eslint-disable-next-line @typescript-eslint/no-explicit-any type Data<Q extends Query<any, any, any>> = // eslint-disable-next-line @typescript-eslint/no-unused-vars Q extends Query<infer TTableName, infer TColumns> ? { [QueryName in keyof Q]: Q[QueryName]['type'] extends 'many' ? { items: { [column in keyof Q[QueryName]['columns']]: RecoverColumnType< TColumns[column] >; }[]; pageInfo: { [key in keyof Q[QueryName]['pagination']]: RecoverPageInfoType<key>; }; } : { [column in keyof Q[QueryName]['columns']]: RecoverColumnType< TColumns[column] >; }; } : never; function many< TTableName extends string, TColumns extends Columns, TSelection extends Selection<TColumns> = Selection<TColumns>, TPagination extends Pagination = Pagination, >(table: TTableName) { // eslint-disable-next-line @typescript-eslint/explicit-function-return-type return function ( filter: ManyFilter<TColumns>, columns: TSelection, pagination: TPagination, ) { return { type: 'many', table, filter, columns, pagination, } satisfies QueryPart<TTableName, TColumns>; }; } function one< TTableName extends string, TColumns extends Columns, TSelection extends Selection<TColumns> = Selection<TColumns>, >(table: TTableName) { // eslint-disable-next-line @typescript-eslint/explicit-function-return-type return function (filter: SingleFilter<TColumns>, columns: TSelection) { return { type: 'one', table, filter, columns, pagination: {}, } satisfies QueryPart<TTableName, TColumns>; }; } function serializeTableName<TTableName extends string>( tableName: TTableName, ): string { const tableNameString = String(tableName); return tableNameString.charAt(0).toLowerCase() + tableNameString.slice(1); } function serializeWhereFilterValue< TColumns extends Columns, T extends RecoverColumnType<TColumns[string]>, >(value: T | undefined): string { switch (typeof value) { case 'string': return `"${value}"`; case 'number': return `${value}`; case 'bigint': return `"${value.toString()}"`; case 'boolean': return `${value}`; default: throw new Error(`Unsupported type: ${typeof value}`); } } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type function serializeWhereFilter<TColumns extends Columns>( where: ManyFilterWhere<TColumns>, ) { const filterString = Object.entries(where) .filter(([, v]) => v !== undefined) .map(([k, v]) => `${k}: ${serializeWhereFilterValue(v)}`) .join(', '); return `{ ${filterString} }`; } function serializePart< TTableName extends string, TColumns extends Columns, TSelection extends Selection<TColumns> = Selection<TColumns>, >(name: TTableName, part: QueryPart<TTableName, TColumns, TSelection>): string { switch (part.type) { case 'many': return ` ${String(name)}: ${serializeTableName(part.table)}s( ${part.filter.limit ? `limit: ${part.filter.limit}` : ''} ${ part.filter.orderBy ? `orderBy: "${String(part.filter.orderBy)}"` : '' } ${ part.filter.orderDirection ? `orderDirection: "${part.filter.orderDirection}"` : '' } ${part.filter.before ? `before: "${part.filter.before}"` : ''} ${part.filter.after ? `after: "${part.filter.after}"` : ''} ${ part.filter.where ? `where: ${serializeWhereFilter(part.filter.where)}` : '' } ) { items { ${Object.entries(part.columns) .filter(([, v]) => v) .map(([k]) => `${k}`) .join('\n')} } pageInfo { ${Object.entries(part.pagination) .filter(([, v]) => v) .map(([k]) => `${k}`) .join('\n')} } } `; case 'one': return ` ${String(name)}: ${serializeTableName(part.table)}( id: "${part.filter.id}" ) { ${Object.entries(part.columns) .filter(([, v]) => v) .map(([k]) => `${k}`) .join('\n')} } `; } } function serialize<TTableName extends string, TColumns extends Columns>( q: Query<TTableName, TColumns>, ): string { const qString = `{ ${Object.entries(q) .map(([name, part]) => { const partName = name as TTableName; return serializePart(partName, part); }) .join('\n')} }`; return qString; } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type async function query< TTableName extends string, TColumns extends Columns = Columns, >(endpoint: string, q: Query<TTableName, TColumns>) { const res = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ query: serialize(q), }), }); const json = (await res.json()) as { // eslint-disable-next-line @typescript-eslint/no-explicit-any data: any; }; return json.data; } export { one, many, query }; export type { Data, Query, QueryPart };