import { AfterClauseWithNoOrderError, QueryClauseFormattingError, } from '../errors.js'; import { isFilterStatement, isIdFilter, isWhereFilter } from '../filters.js'; import { CollectionQuery, FilterStatement, ModelFilterStatement, OrderStatement, QueryOrder, QueryResultCardinality, QuerySelectionFromQuery, QueryWhere, RefCollectionName, RefSubquery, RelationshipRef, RelationSubquery, SchemaQuery, ValueCursor, WithInclusion, WithInclusionRaw, WithSelection, } from './types/index.js'; import { CollectionNameFromModels, Models } from '../schema/index.js'; import { ValuePointer } from '../utils/value-pointer.js'; import { Writeable } from '../types.js'; export function queryBuilder< M extends Models = Models, CN extends CollectionNameFromModels = CollectionNameFromModels, >(collectionName: CN) { return new QueryBuilder(collectionName); } export class QueryBuilder< M extends Models = Models, CN extends CollectionNameFromModels = CollectionNameFromModels, // When you init the builder, default to no inclusions Q extends CollectionQuery = WithInclusion, {}>, > implements CollectionQuery { collectionName: CN; select: Q['select'] = undefined; where: Q['where'] = undefined; limit: Q['limit'] = undefined; order: Q['order'] = undefined; include: Q['include'] = undefined; after: Q['after'] = undefined; vars: Q['vars'] = undefined; constructor(collectionName: CN, query?: Omit) { this.collectionName = collectionName; if (query) { this.select = query.select; this.where = query.where; this.limit = query.limit; this.order = query.order; this.include = query.include; this.after = query.after; this.vars = query.vars; } } Select>( value: ReadonlyArray | undefined ): QueryBuilder> { const select = value; return new QueryBuilder>( this.collectionName, // @ts-expect-error { ...this, select, } ); } Where(...args: FilterInput) { const where = QUERY_INPUT_TRANSFORMERS().where(this, ...args); return new QueryBuilder( this.collectionName, // @ts-expect-error { ...this, where, } ); } Id(value: string) { const where = this.where ? this.where.filter((f) => !isIdFilter(f)) : []; where.push( // @ts-expect-error ['id', '=', value] ); return new QueryBuilder( this.collectionName, // @ts-expect-error { ...this, where, } ); } Limit(value: number) { const limit = value; return new QueryBuilder( this.collectionName, // @ts-expect-error { ...this, limit, } ); } Order(...args: OrderInput) { const order = QUERY_INPUT_TRANSFORMERS().order(this, ...args); return new QueryBuilder( this.collectionName, // @ts-expect-error { ...this, order, } ); } After(value: AfterInput, inclusive?: boolean) { const after = QUERY_INPUT_TRANSFORMERS().after( this, value, inclusive ); return new QueryBuilder( this.collectionName, // @ts-expect-error { ...this, after, } ); } Vars(value: Record) { const vars = value; return new QueryBuilder( this.collectionName, // @ts-expect-error { ...this, vars, } ); } Include< Alias extends RelationshipRef, RQ extends RefSubquery, >( alias: Alias, queryExt: RQ ): QueryBuilder< M, CN, WithInclusion< Q, // @ts-expect-error Q['include'] & { [K in Alias]: RQ } > >; Include>( alias: Alias, builder: ( rel: >( ref: Ref ) => RelationBuilder ) => RQ ): QueryBuilder< M, CN, WithInclusion< Q, // @ts-expect-error Q['include'] & { [K in Alias]: RQ } > >; Include>( alias: Alias ): QueryBuilder< M, CN, WithInclusion< Q, // @ts-expect-error Q['include'] & { [K in Alias]: null } > >; Include(alias: any, queryExt?: any): any { if (typeof queryExt === 'function') { queryExt = queryExt(relationBuilder); } const include = QUERY_INPUT_TRANSFORMERS().include( this, alias, queryExt ); return new QueryBuilder( this.collectionName, // @ts-expect-error { ...this, include, } ); } SubqueryOne>( alias: Alias, subquery: SQ ): QueryBuilder< M, CN, WithInclusion< Q, // @ts-expect-error Q['include'] & { [K in Alias]: RelationSubquery } > >; SubqueryOne>( alias: Alias, subquery: ( sub: >( collectionName: CName ) => QueryBuilder ) => SQ ): QueryBuilder< M, CN, WithInclusion< Q, // @ts-expect-error Q['include'] & { [K in Alias]: RelationSubquery } > >; SubqueryOne(alias: any, subquery: any): any { if (typeof subquery === 'function') { subquery = subquery(queryBuilder); } const include = QUERY_INPUT_TRANSFORMERS().include(this, alias, { subquery, cardinality: 'one', }); return new QueryBuilder( this.collectionName, // @ts-expect-error { ...this, include, } ); } SubqueryMany>( alias: Alias, subquery: SQ ): QueryBuilder< M, CN, WithInclusion< Q, // @ts-expect-error Q['include'] & { [K in Alias]: RelationSubquery } > >; SubqueryMany>( alias: Alias, subquery: ( sub: >( collectionName: CName ) => QueryBuilder ) => SQ ): QueryBuilder< M, CN, WithInclusion< Q, // @ts-expect-error Q['include'] & { [K in Alias]: RelationSubquery } > >; SubqueryMany(alias: any, subquery: any): any { if (typeof subquery === 'function') { subquery = subquery(queryBuilder); } const include = QUERY_INPUT_TRANSFORMERS().include(this, alias, { subquery, cardinality: 'many', }); return new QueryBuilder( this.collectionName, // @ts-expect-error { ...this, include, } ); } } export function relationBuilder< M extends Models, CN extends CollectionNameFromModels, Ref extends RelationshipRef, >(ref: Ref) { return new RelationBuilder(ref); } export class RelationBuilder< M extends Models = Models, CN extends CollectionNameFromModels = CollectionNameFromModels, Ref extends RelationshipRef = RelationshipRef, RQ extends RefSubquery = WithInclusionRaw< RefSubquery, {} >, > implements RefSubquery { _extends: Ref; select: RQ['select'] = undefined; where: RQ['where'] = undefined; limit: RQ['limit'] = undefined; order: RQ['order'] = undefined; include: RQ['include'] = undefined; constructor(ref: Ref, query?: RQ) { this._extends = ref; if (query) { this.select = query.select; this.where = query.where; this.limit = query.limit; this.order = query.order; this.include = query.include; } } // TODO: add back include // TOOD: fixup types once you have included return types set up Select[number]>( value: Selection[] ): RelationBuilder< M, CN, Ref, WithSelection< // @ts-expect-error RQ, Selection > > { const select = value; return new RelationBuilder< M, CN, Ref, WithSelection< // @ts-expect-error RQ, Selection > >( this._extends, // @ts-expect-error { ...this, select, } ); } Where(...args: FilterInput>) { const where = QUERY_INPUT_TRANSFORMERS< M, RefCollectionName >().where(this, ...args); return new RelationBuilder( this._extends, // @ts-expect-error { ...this, where, } ); } Id(id: string) { const where = this.where ? this.where.filter((f) => !isIdFilter(f)) : []; where.push( // @ts-expect-error ['id', '=', id] ); return new RelationBuilder( this._extends, // @ts-expect-error { ...this, where, } ); } Limit(value: number) { const limit = value; return new RelationBuilder( this._extends, // @ts-expect-error { ...this, limit, } ); } Order(...args: OrderInput>) { const order = QUERY_INPUT_TRANSFORMERS< M, RefCollectionName >().order(this, ...args); return new RelationBuilder( this._extends, // @ts-expect-error { ...this, order, } ); } Include< Alias extends string, NextRQ extends RefSubquery>, >( alias: Alias, queryExt: NextRQ ): RelationBuilder< M, CN, Ref, WithInclusionRaw >; Include< Alias extends string, NextRQ extends RefSubquery>, >( alias: Alias, builder: ( rel: >>( ref: NextRef ) => RelationBuilder, NextRef> ) => NextRQ ): RelationBuilder< M, CN, Ref, WithInclusionRaw >; Include>>( alias: Alias ): RelationBuilder< M, CN, Ref, WithInclusionRaw >; Include(alias: any, queryExt?: any): any { if (typeof queryExt === 'function') { queryExt = queryExt(relationBuilder); } const include = QUERY_INPUT_TRANSFORMERS().include( this, alias, queryExt ); return new RelationBuilder>( this._extends, // @ts-expect-error { ...this, include, } ); } SubqueryOne>( alias: Alias, subquery: SQ ): RelationBuilder< M, CN, Ref, WithInclusionRaw< RQ, RQ['include'] & { [K in Alias]: RelationSubquery } > >; SubqueryOne>( alias: Alias, subquery: ( sub: >( collectionName: CName ) => QueryBuilder ) => SQ ): RelationBuilder< M, CN, Ref, WithInclusionRaw< RQ, RQ['include'] & { [K in Alias]: RelationSubquery } > >; SubqueryOne(alias: any, subquery: any): any { if (typeof subquery === 'function') { subquery = subquery(queryBuilder); } const include = QUERY_INPUT_TRANSFORMERS< M, RefCollectionName >().include(this, alias, { subquery, cardinality: 'one', }); return new RelationBuilder(this._extends, { ...this, include, }); } SubqueryMany>( alias: Alias, subquery: SQ ): RelationBuilder< M, CN, Ref, WithInclusionRaw< RQ, RQ['include'] & { [K in Alias]: RelationSubquery } > >; SubqueryMany>( alias: Alias, subquery: ( sub: >( collectionName: CName ) => QueryBuilder ) => SQ ): RelationBuilder< M, CN, Ref, WithInclusionRaw< RQ, RQ['include'] & { [K in Alias]: RelationSubquery } > >; SubqueryMany(alias: any, subquery: any): any { if (typeof subquery === 'function') { subquery = subquery(queryBuilder); } const include = QUERY_INPUT_TRANSFORMERS< M, RefCollectionName >().include(this, alias, { subquery, cardinality: 'many', }); return new RelationBuilder(this._extends, { ...this, include, }); } } type RelationshipRefFromQuery> = Q extends CollectionQuery ? RelationshipRef : never; /** * Input for builder where() clauses */ export type FilterInput< M extends Models, CN extends CollectionNameFromModels, > = // .Where(undefined) | [typeof undefined] // .Where("id", "=", "123") // For some weird TS reason, unioning readonly tuples makes it hard to parse the parameters in our tests (so using Writable, but this is safe because the type will be spread) | Writeable> // .Where(["id", "=", "123"], ["name", "=", "foo"]) | QueryWhere // .Where(ternary ? ["id", "=", "123"] : undefined) | [QueryWhere[number] | undefined] // .Where([["id", "=", "123"], ["name", "=", "foo"]]) | [QueryWhere]; /** * Input for builder order() clauses */ export type OrderInput< M extends Models, CN extends CollectionNameFromModels, > = | [typeof undefined] | OrderStatement | QueryOrder // Handle ternary | [QueryOrder[number] | undefined] | [QueryOrder]; /** * Input for builder after() clauses */ export type AfterInput< M extends Models, CN extends CollectionNameFromModels, > = ValueCursor | undefined; // FetchResultEntity> // /** // * Helper type to extract the subquery information from a relation name based on include() inputs // */ // export type InclusionByRName< // M extends Models, // CN extends CollectionNameFromModels, // RName extends RelationAttributes, // > = RelationSubquery< // M, // ToQuery>, // Ref['cardinality'] // >; // /** // * A collection query with just allowed params for a subquery in an include() clause // */ // export type IncludeSubquery< // M extends Models, // CN extends CollectionNameFromModels, // > = Pick< // CollectionQueryDefault, // 'select' | 'order' | 'where' | 'limit' | 'include' // >; /** * E.g. where(undefined) */ function isInputNoOp(args: any): args is [undefined] { return Array.isArray(args) && args[0] === undefined; } /** * E.g. where("id", "=", "123") */ function isInputSpreadFilter< M extends Models, CN extends CollectionNameFromModels, >(args: any): args is FilterStatement { return isFilterStatement(args); } /** * E.g. where(["id", "=", "123"], more filters) */ function isInputSpreadClauses< M extends Models, CN extends CollectionNameFromModels, >(args: any): args is QueryWhere { return Array.isArray(args) && args.every((arg: any) => isWhereFilter(arg)); } /** * E.g. where([["id", "=", "123"], ["name", "=", "foo"]]) */ function isInputClauseGroup< M extends Models, CN extends CollectionNameFromModels, >(args: any): args is [QueryWhere] { return args.length === 1 && isInputSpreadClauses(args[0]); } export const QUERY_INPUT_TRANSFORMERS = < M extends Models, CN extends CollectionNameFromModels, >() => ({ where: >( q: Pick, 'where'>, ...args: A ): QueryWhere => { let newWhere: QueryWhere = []; if (isInputNoOp(args)) return q.where ?? []; if (isInputSpreadFilter(args)) { newWhere = [args]; } else if (isInputSpreadClauses(args)) { newWhere = args; } else if (isInputClauseGroup(args)) { newWhere = args[0]; } else { throw new QueryClauseFormattingError('where', args); } return [...(q.where ?? []), ...newWhere]; }, order: ( q: Pick, 'order'>, ...args: OrderInput ): QueryOrder | undefined => { if (!args[0]) return q.order ?? []; let newOrder: QueryOrder = []; /** * E.g. order("id", "ASC") */ if ( args.length === 2 && (args as any[]).every((arg) => typeof arg === 'string') ) { newOrder = [[...args] as OrderStatement]; } else if ( /** * E.g. order([["id", "ASC"], ["name", "DESC"]]) */ args.length === 1 && args[0] instanceof Array && args[0].every((arg) => arg instanceof Array) ) { newOrder = args[0] as NonNullable>; } else if (args.every((arg) => arg instanceof Array)) { /** * E.g. order(["id", "ASC"], ["name", "DESC"]) */ newOrder = args as NonNullable>; } else { throw new QueryClauseFormattingError('order', args); } return [...(q.order ?? []), ...newOrder]; }, include( q: Pick, 'include'>, alias: Alias, query?: any ): Record { // TODO: include should be typed as a set of subqueries return { ...q.include, // Set to null so the inclusion of the key can be serialized [alias]: query ?? null, }; }, after( q: Pick, 'after' | 'order'>, after: AfterInput, inclusive?: boolean ): [ValueCursor, boolean] | undefined { if (!after) return undefined; if (!q.order) throw new AfterClauseWithNoOrderError(after); const orderAttributes = q.order.map((o) => o[0]); if (after instanceof Array) return [after, inclusive ?? false]; if (typeof after === 'object') { return [ // Maybe even sunset this and only use ValueCursor format orderAttributes.map((attr) => ValuePointer.Get(after as any, attr) ) as ValueCursor, inclusive ?? false, ]; } throw new QueryClauseFormattingError('after', after); }, });