ponder-client
Version:
Type-safe, lightweight Ponder client
359 lines (330 loc) • 9.58 kB
text/typescript
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 };