import { z } from 'zod'; import { ModelDef, ModelShape, OrderBy, PaginationConfig, QueryContextMeta, SelectAst } from './types'; import { normalizeWhere, Where } from './where-dsl'; import { orderObjectToArray } from './query-ast'; export interface QueryExecutor { executeSelect>(ast: SelectAst, ctx?: QueryContextMeta): Promise; } export class QueryBuilder> { private _selection: Partial> | undefined; private _where: Where> | undefined; private _orderBy: OrderBy> | undefined; private _limit: number | undefined; private _offset: number | undefined; private _pagination: PaginationConfig | undefined; constructor(private readonly model: TModel, private readonly executor: QueryExecutor) {} select(sel?: Partial & string, boolean>>): this { this._selection = (sel as any) || undefined; return this; } where(filter?: Where>): this { this._where = filter; return this; } orderBy(order?: OrderBy>): this { this._orderBy = order; return this; } limit(n: number): this { this._limit = n; return this; } offset(n: number): this { this._offset = n; return this; } paginate(cfg: PaginationConfig): this { this._pagination = cfg; return this; } private buildAst(): SelectAst { const normalized = normalizeWhere(this.model, this._where as any); return { kind: 'select', model: this.model, selection: this._selection as any, where: normalized.ast, orderBy: orderObjectToArray(this._orderBy), limit: this._limit, offset: this._offset, }; } async getMany(ctx?: QueryContextMeta): Promise>> { const ast = this.buildAst(); const rows = await this.executor.executeSelect(ast, ctx); const arr = Array.isArray(rows) ? rows : []; const selectionKeys = this._selection ? Object.keys(this._selection).filter(k => (this._selection as any)[k]) : Object.keys(this.model.fields); const parsedSchema = deriveSchemaForSelection(this.model.schema, selectionKeys as string[]); return arr.map((r) => { const mapped = mapRecordToModelShape(r as any, this.model, selectionKeys as string[]); return parsedSchema.parse(mapped) as ModelShape; }); } async getOne(ctx?: QueryContextMeta): Promise | null> { const rows = await this.limit(1).getMany(ctx); return rows[0] ?? null; } } export class ORMQueryAPI> { constructor(private readonly model: TModel, private readonly executor: QueryExecutor) {} select(sel?: Partial & string, boolean>>) { return new QueryBuilder(this.model, this.executor).select(sel); } where(filter?: Where>) { return new QueryBuilder(this.model, this.executor).where(filter); } orderBy(order?: OrderBy>) { return new QueryBuilder(this.model, this.executor).orderBy(order); } limit(n: number) { return new QueryBuilder(this.model, this.executor).limit(n); } offset(n: number) { return new QueryBuilder(this.model, this.executor).offset(n); } paginate(cfg: PaginationConfig) { return new QueryBuilder(this.model, this.executor).paginate(cfg); } } function mapRecordToModelShape(record: Record, model: ModelDef, selectionKeys: string[]): Record { const out: Record = {}; for (const key of selectionKeys) { const field = model.fields[key]; if (!field) continue; out[key] = getByPath(record, field.path); } return out; } function getByPath(obj: any, path: string): any { const parts = path.split('.'); let cur: any = obj; for (const p of parts) { if (cur == null) return undefined; // Salesforce nested relationship in records often use relationship names without ".Name" flattening. cur = cur[p] ?? cur[p.replace(/__r$/, '')]; } return cur; } function deriveSchemaForSelection(schema: z.ZodTypeAny, keys: string[]): z.ZodTypeAny { // If the source is an object schema, pick and make the rest optional if (schema instanceof z.ZodObject) { const obj = schema as z.ZodObject; const picked = obj.pick(keys.reduce((acc: any, k) => { acc[k] = true; return acc; }, {})); // Allow partial because partial selections omit other keys return picked; } return schema; }