// deno-lint-ignore-file no-explicit-any import { AsyncResult, Keyof, Optional } from "@fabric/core"; import { AlreadyExistsError, FilterOptions, Model, type ModelSchema, NotFoundError, OrderByOptions, SelectableQuery, StoreLimitableQuery, StoreQuery, StoreQueryDefinition, StoreQueryError, StoreSortableQuery, } from "@fabric/domain"; import { filterToParams, filterToSQL } from "../sqlite/filter-to-sql.ts"; import { transformRow } from "../sqlite/sql-to-value.ts"; import { SQLiteDatabase } from "../sqlite/sqlite-database.ts"; export class QueryBuilder implements StoreQuery { constructor( private db: SQLiteDatabase, private schema: ModelSchema, private query: StoreQueryDefinition, ) {} where(where: FilterOptions): StoreSortableQuery { return new QueryBuilder(this.db, this.schema, { ...this.query, where, }); } orderBy(opts: OrderByOptions): StoreLimitableQuery { return new QueryBuilder(this.db, this.schema, { ...this.query, orderBy: opts, }); } limit(limit: number, offset?: number | undefined): SelectableQuery { return new QueryBuilder(this.db, this.schema, { ...this.query, limit, offset: offset ?? 0, }); } select(): AsyncResult; select>( keys: K[], ): AsyncResult[], StoreQueryError>; select>(keys?: K[]): AsyncResult { return AsyncResult.tryFrom( () => { const [sql, params] = getSelectStatement( this.schema[this.query.from]!, { ...this.query, keys: keys!, }, ); return this.db.allPrepared( sql, params, transformRow(this.schema[this.query.from]!), ); }, (err) => new StoreQueryError(err.message), ); } selectOne(): AsyncResult, StoreQueryError>; selectOne>( keys: K[], ): AsyncResult>, StoreQueryError>; selectOne>(keys?: K[]): AsyncResult { return AsyncResult.tryFrom( async () => { const [stmt, params] = getSelectStatement( this.schema[this.query.from]!, { ...this.query, keys: keys!, limit: 1, }, ); return await this.db.onePrepared( stmt, params, transformRow(this.schema[this.query.from]!), ); }, (err) => new StoreQueryError(err.message), ); } selectOneOrFail(): AsyncResult; selectOneOrFail>( keys: K[], ): AsyncResult, StoreQueryError | NotFoundError>; selectOneOrFail>( keys?: K[], ): AsyncResult { return AsyncResult.tryFrom( async () => { const [stmt, params] = getSelectStatement( this.schema[this.query.from]!, { ...this.query, keys: keys!, limit: 1, }, ); return await this.db.onePrepared( stmt, params, transformRow(this.schema[this.query.from]!), ); }, (err) => new StoreQueryError(err.message), ).flatMap((result) => { if (!result) { return AsyncResult.failWith(new NotFoundError()); } return AsyncResult.ok(result); }); } assertNone(): AsyncResult { return AsyncResult.tryFrom( async () => { const [stmt, params] = getSelectStatement( this.schema[this.query.from]!, { ...this.query, limit: 1, }, ); return await this.db.onePrepared( stmt, params, ); }, (err) => new StoreQueryError(err.message), ).flatMap((result) => { if (result) { return AsyncResult.failWith(new AlreadyExistsError()); } return AsyncResult.ok(); }); } } export function getSelectStatement( collection: Model, query: StoreQueryDefinition, ): [string, Record] { const selectFields = query.keys ? query.keys.join(", ") : "*"; const queryFilter = filterToSQL(query.where); const limit = query.limit ? `LIMIT ${query.limit}` : ""; const offset = query.offset ? `OFFSET ${query.offset}` : ""; const sql = [ `SELECT ${selectFields}`, `FROM ${query.from}`, queryFilter, limit, offset, ].join(" "); return [ sql, { ...filterToParams(collection, query.where), }, ]; }