import type { DBQueryConfigOrderByCallback, DBQueryConfigOrderByObject, SQL, TableFilterCommons, } from "../orm/index.ts" import { and, lt, gt, desc, asc } from "../orm/index.ts" import type { AnyTableSchema, TableSchemaToSelect } from "./infer.ts" import type { AnyPgColumn } from "./pgcore.ts" export type Cursor = string | null export interface WithCursorBasedPaginationOptions { limit: number cursor: Cursor } export interface WithCursorBasedPaginationResult { nextCursor: Cursor totalCount: number } interface OrderByOption { key: Key order: "asc" | "desc" } export interface EncodeCursorOptions { data: Data orderBy: Array> } export type EncodeCursorResult = string export const encodeCursor = (options: EncodeCursorOptions): EncodeCursorResult => { const { data, orderBy } = options const encoded = orderBy .map((item) => { const { key } = item if (data[key] === undefined) { throw new Error(`data[${String(key)}] is undefined`) } return data[key] }) .map((value) => { if (value instanceof Date) { return `Date(${value.getTime()})` } return String(value) }) .join(":") return btoa(encoded) } export interface DecodeCursorOptions { dataForType: Data cursor: Cursor orderBy: Array> } export type DecodedCursor = { [K in Key]: Data[K] extends Date ? Date : Data[K] } export type DecodeCursorResult = DecodedCursor< Data, Key > | null export const decodeCursor = ( options: DecodeCursorOptions, ): DecodeCursorResult => { const { cursor, orderBy } = options if (cursor === null) { return null } const decoded = atob(cursor) .split(":") .map((value) => { if (value.startsWith("Date(")) { return new Date(Number.parseInt(value.slice(5, -1), 10)) } return value }) .reduce((acc, value, index) => { const key = orderBy[index]!.key return { ...acc, [key]: value } }, {}) return decoded as DecodedCursor } export interface GetNextCursorOptions { list: Data[] limit: number encodeOptions: Omit, "data"> } export type GetNextCursorResult = Cursor export const getNextCursor = (options: GetNextCursorOptions): GetNextCursorResult => { const { list, limit, encodeOptions } = options if (list.length === 0) { return null } if (list.length < limit) { return null } const lastData = list.at(-1)! const cursor = encodeCursor({ data: lastData, ...encodeOptions, }) return cursor } export interface BuildWhereSqlOptions< TableSchema extends AnyTableSchema, Key extends keyof TableSchemaToSelect, > { tableSchema: TableSchema orderBy: Array> decodeCursorResult: DecodeCursorResult, Key> } export type BuildWhereSqlResult = SQL | undefined export const buildWhereSql = < TableSchema extends AnyTableSchema, Key extends keyof TableSchemaToSelect, >( options: BuildWhereSqlOptions, ): BuildWhereSqlResult => { const { tableSchema, decodeCursorResult, orderBy } = options if (decodeCursorResult === null) { return undefined } if (orderBy.length === 0) { return undefined } const conditionSqlList = orderBy.map((item) => { const { key, order } = item if (order === "asc") { return gt( tableSchema[key as keyof TableSchema] as unknown as AnyPgColumn, decodeCursorResult[key], ) } if (order === "desc") { return lt( tableSchema[key as keyof TableSchema] as unknown as AnyPgColumn, decodeCursorResult[key], ) } throw new Error(`Invalid order: ${String(order)}`) }) return and(...conditionSqlList) } export interface BuildWhereQueryOptions< TableSchema extends AnyTableSchema, Key extends keyof TableSchemaToSelect, > { tableSchemaForType: TableSchema orderBy: Array> decodeCursorResult: DecodeCursorResult, Key> } /** * @description 此处只支持表内字段的条件构建,不支持关联表字段的条件构建。 * 这样做的原因是为了保持类型参数简单且一致,若引入关联表字段条件构建, * 则需要引入完整的 Relations 类型作为类型参数(截至 drizzle-orm 1.0.0-beta)。 */ export type BuildWhereQueryResult = TableFilterCommons export const buildWhereQuery = < TableSchema extends AnyTableSchema, Key extends keyof TableSchemaToSelect, >( options: BuildWhereQueryOptions, ): BuildWhereQueryResult => { const { decodeCursorResult, orderBy } = options if (decodeCursorResult === null) { return {} } if (orderBy.length === 0) { return {} } const conditionList = orderBy.map((item) => { const { key, order } = item if (order === "asc") { return { [key]: { gt: decodeCursorResult[key], }, } } if (order === "desc") { return { [key]: { lt: decodeCursorResult[key], }, } } throw new Error(`Invalid order: ${String(order)}`) }) if (conditionList.length === 1) { return conditionList[0]! } return { AND: conditionList, } as BuildWhereQueryResult } export interface BuildOrderBySql< TableSchema extends AnyTableSchema, Key extends keyof TableSchemaToSelect, > { tableSchema: TableSchema orderBy: Array> } export type BuildOrderBySqlResult = DBQueryConfigOrderByCallback export const buildOrderBySql = < TableSchema extends AnyTableSchema, Key extends keyof TableSchemaToSelect, >( options: BuildOrderBySql, ): BuildOrderBySqlResult => { const { tableSchema, orderBy } = options const sqlList = orderBy.map((item) => { const { key, order } = item if (order === "asc") { return asc(tableSchema[key as keyof TableSchema] as unknown as AnyPgColumn) } if (order === "desc") { return desc(tableSchema[key as keyof TableSchema] as unknown as AnyPgColumn) } throw new Error(`Invalid order: ${String(order)}`) }) return () => sqlList } export interface BuildOrderByQueryOptions< TableSchema extends AnyTableSchema, Key extends keyof TableSchemaToSelect, > { tableSchemaForType: TableSchema orderBy: Array> } export type BuildOrderByQueryResult = DBQueryConfigOrderByObject export const buildOrderByQuery = < TableSchema extends AnyTableSchema, Key extends keyof TableSchemaToSelect, >( options: BuildOrderByQueryOptions, ): BuildOrderByQueryResult => { const { orderBy } = options const orderByObject = orderBy.reduce>((acc, item) => { const { key, order } = item return Object.assign(acc, { [key]: order }) }, {}) return orderByObject } export interface BuildCursorBasedPaginationOptions< TableSchema extends AnyTableSchema, Key extends keyof TableSchemaToSelect, > { tableSchema: TableSchema orderBy: Array> cursor: Cursor limit: number } export interface BuildCursorBasedPaginationResult< TableSchema extends AnyTableSchema, Key extends keyof TableSchemaToSelect, > { decodeCursorResult: DecodeCursorResult, Key> whereSql: BuildWhereSqlResult whereSqlAnd: (sql: BuildWhereSqlResult) => BuildWhereQueryResult whereQuery: BuildWhereQueryResult whereQueryAnd: (query: BuildWhereQueryResult) => BuildWhereQueryResult orderBySql: BuildOrderBySqlResult orderByQuery: BuildOrderByQueryResult limit: number getNextCursor: (list: Array>) => Cursor } export const buildCursorBasedPagination = < TableSchema extends AnyTableSchema, Key extends keyof TableSchemaToSelect, >( options: BuildCursorBasedPaginationOptions, ): BuildCursorBasedPaginationResult => { const { tableSchema, orderBy, limit, cursor } = options type Data = TableSchemaToSelect const decodeCursorResult = decodeCursor({ dataForType: {} as Data, cursor, orderBy, }) const whereSql = buildWhereSql({ tableSchema, decodeCursorResult, orderBy, }) const whereSqlAnd = (sql: BuildWhereSqlResult): BuildWhereQueryResult => { return { RAW: and(whereSql, sql), } } const whereQuery = buildWhereQuery({ tableSchemaForType: tableSchema, decodeCursorResult, orderBy, }) const whereQueryAnd = ( query: BuildWhereQueryResult, ): BuildWhereQueryResult => { return { AND: [whereQuery, query], } as BuildWhereQueryResult } const orderBySql = buildOrderBySql({ tableSchema, orderBy, }) const orderByQuery = buildOrderByQuery({ tableSchemaForType: tableSchema, orderBy, }) const _getNextCursor = (list: Data[]): Cursor => { return getNextCursor({ list, limit, encodeOptions: { orderBy, }, }) } return { decodeCursorResult, whereSql, whereSqlAnd, whereQuery, whereQueryAnd, orderBySql, orderByQuery, limit, getNextCursor: _getNextCursor, } }