import { Value } from './value'; import { SmartList } from './smart-list'; import { retainQueryDiagnosticOrigin, snapshotQuery } from './query-snapshot'; import type { QueryOperation } from './request-intent'; export enum SortDirection { Asc = 'Asc', Desc = 'Desc' } export class OrderBy { constructor( public field: string, public expr: any | null, public direction: SortDirection ) {} static new(field: string, direction: SortDirection): OrderBy { return new OrderBy(field, null, direction); } static expr(expr: any, direction: SortDirection): OrderBy { return new OrderBy('', expr, direction); } static asc(field: string): OrderBy { return OrderBy.new(field, SortDirection.Asc); } static desc(field: string): OrderBy { return OrderBy.new(field, SortDirection.Desc); } } export class AggregationCacheOptions { constructor( public enabledValue: boolean, public cacheExpiredMillis: number, public propagateValue: boolean, public propagateCacheExpiredMillis: number ) {} static enabled(cacheExpiredMillis: number): AggregationCacheOptions { return new AggregationCacheOptions(true, cacheExpiredMillis, false, 0); } propagate(cacheExpiredMillis: number): AggregationCacheOptions { this.propagateValue = true; this.propagateCacheExpiredMillis = cacheExpiredMillis; return this; } } export interface TeaQLPage { data: SmartList; totalCount: number; offset: number; limit: number; } export interface QuerySelection { toQuery(): SelectQuery; } export interface FacetRequest { facetName: string; relationName: string; query: SelectQuery; includeAllFacets: boolean; operation?: QueryOperation; } export interface RelationAggregate { relationName: string; alias: string; query: SelectQuery; singleResult: boolean; operation?: QueryOperation; } export class SelectQuery { private hardLimitValue: number = 10_000; private continuousPageFetchOptions?: { namespace: string; ttlSeconds: number }; private continuousPageRuntimeContext?: any; private idSetPaginationOptions?: { namespace: string; ttlSeconds: number; maxIds: number }; private idSetPaginationRuntimeContext?: any; private topNProbeThreshold?: number; public entity: string; public filterCondition: any | null = null; public limitValue: number = 0; public offsetValue: number = 0; public orderItems: OrderBy[] = []; public selectItems: string[] = []; public properties: string[] = []; public joins: any[] = []; public groupByItems: string[] = []; public aggregateItems: any[] = []; public aggregationCache?: AggregationCacheOptions; public facets: FacetRequest[] = []; public relations: Array<{ name: string; query?: SelectQuery; localKey?: string; foreignKey?: string; many?: boolean; operation?: QueryOperation; }> = []; public relationAggregates: RelationAggregate[] = []; public commentText?: string; public purposeText?: string; constructor(entity: string) { this.entity = entity; // Local runtime policy must never cross the federation JSON boundary. Object.defineProperty(this, 'hardLimitValue', { enumerable: false, writable: true, value: 10_000 }); Object.defineProperty(this, 'continuousPageFetchOptions', { enumerable: false, writable: true, value: undefined }); Object.defineProperty(this, 'continuousPageRuntimeContext', { enumerable: false, writable: true, value: undefined }); Object.defineProperty(this, 'idSetPaginationOptions', { enumerable: false, writable: true, value: undefined }); Object.defineProperty(this, 'idSetPaginationRuntimeContext', { enumerable: false, writable: true, value: undefined }); Object.defineProperty(this, 'topNProbeThreshold', { enumerable: false, writable: true, value: undefined }); } comment(text: string): this { this.commentText = text; return this; } purpose(text: string): this { this.purposeText = text; return this; } facetBy( facetName: string, relationName: string, request: QuerySelection, includeAllFacets = true, operation?: QueryOperation, ): this { this.facets.push({ facetName, relationName, query: request.toQuery(), includeAllFacets, operation, }); return this; } clone(): SelectQuery { const copy = snapshotQuery(this); // Cloning an execution snapshot produces a mutable builder. The request // envelope still owns immutable intent and reapplies it on derivation. for (const field of ['commentText', 'purposeText', '_comment', '_purpose']) { const descriptor = Object.getOwnPropertyDescriptor(copy, field); if (descriptor && 'value' in descriptor && descriptor.configurable) { Object.defineProperty(copy, field, { ...descriptor, writable: true }); } } return copy; } filter(condition: any): this { this.filterCondition = condition; return this; } limit(limit: number): this { if (!Number.isSafeInteger(limit) || limit < 1) { throw new Error('QUERY_INVALID_LIMIT: limit must be a positive safe integer'); } this.limitValue = limit; return this; } optimizeForContinuousPageFetch(): this { return this.optimizeForContinuousPageFetchWith('default', 600); } optimizeForContinuousPageFetchWith(namespace: string, ttlSeconds: number): this { if (!namespace?.trim()) throw new Error('continuous page namespace must not be empty'); if (!Number.isSafeInteger(ttlSeconds) || ttlSeconds <= 0) throw new Error('continuous page ttlSeconds must be a positive integer'); this.continuousPageFetchOptions = { namespace, ttlSeconds }; return this; } bindContinuousPageRuntime(runtime: any): this { this.continuousPageRuntimeContext = runtime; return this; } localContinuousPageOptions(): { namespace: string; ttlSeconds: number } | undefined { return this.continuousPageFetchOptions; } localContinuousPageRuntime(): any { return this.continuousPageRuntimeContext; } clearContinuousPageRuntime(): this { this.continuousPageRuntimeContext = undefined; return this; } optimizePaginationWithIdSet(): this { return this.optimizePaginationWithIdSetConfig('default', 600, 3_000_000); } optimizePaginationWithIdSetConfig(namespace: string, ttlSeconds: number, maxIds: number): this { if (!namespace?.trim()) throw new Error('ID set namespace must not be empty'); if (!Number.isSafeInteger(ttlSeconds) || ttlSeconds <= 0) throw new Error('ID set ttlSeconds must be a positive integer'); if (!Number.isSafeInteger(maxIds) || maxIds <= 0) throw new Error('ID set maxIds must be a positive integer'); this.idSetPaginationOptions = { namespace, ttlSeconds, maxIds }; return this; } bindIdSetPaginationRuntime(runtime: any): this { this.idSetPaginationRuntimeContext = runtime; return this; } localIdSetPaginationOptions(): { namespace: string; ttlSeconds: number; maxIds: number } | undefined { return this.idSetPaginationOptions; } localIdSetPaginationRuntime(): any { return this.idSetPaginationRuntimeContext; } clearIdSetPaginationRuntime(): this { this.idSetPaginationRuntimeContext = undefined; return this; } topNProbeParentThreshold(threshold: number): this { if (!Number.isSafeInteger(threshold) || threshold < 0) { throw new Error('topNProbeParentThreshold must be a non-negative safe integer'); } this.topNProbeThreshold = threshold; return this; } localTopNProbeParentThreshold(): number | undefined { return this.topNProbeThreshold; } prepareForList(): this { if (!Number.isSafeInteger(this.offsetValue) || this.offsetValue < 0) { throw new Error('QUERY_INVALID_OFFSET: offset must be a non-negative safe integer'); } if (this.limitValue && (!Number.isSafeInteger(this.limitValue) || this.limitValue < 1)) { throw new Error('QUERY_INVALID_LIMIT: limit must be a positive safe integer'); } this.applyListLimit(this.hardLimitValue); return this; } forExactCount(alias = '__teaql_total'): SelectQuery { const count = new SelectQuery(this.entity); count.filterCondition = snapshotQuery(this.filterCondition); count.commentText = this.commentText; count.purposeText = this.purposeText; count.aggregate('Count', 'id', alias); retainQueryDiagnosticOrigin(this, count); return count; } private applyListLimit(ceiling: number): void { if (!this.limitValue) this.limitValue = ceiling; if (this.limitValue > ceiling) { throw new Error(`QUERY_HARD_LIMIT_EXCEEDED: requested limit ${this.limitValue} exceeds hard limit ${ceiling}`); } for (const relation of this.relations) { relation.query?.applyListLimit(10_000); } } offset(offset: number): this { if (!Number.isSafeInteger(offset) || offset < 0) { throw new Error('QUERY_INVALID_OFFSET: offset must be a non-negative safe integer'); } this.offsetValue = offset; return this; } relation(name: string): this { this.relations.push({ name }); return this; } relationQuery( name: string, query: SelectQuery, localKey = 'id', foreignKey = 'id', many = true, operation?: QueryOperation, ): this { this.relations.push({ name, query, localKey, foreignKey, many, operation }); return this; } relationAggregate( relationName: string, alias: string, query: SelectQuery, singleResult = true, operation?: QueryOperation, ): this { this.relationAggregates.push({ relationName, alias, query, singleResult, operation }); return this; } order(orderBy: OrderBy): this { this.orderItems.push(orderBy); return this; } select(items: string[]): this { this.selectItems.push(...items); return this; } enableAggregationCacheFor(cacheExpiredMillis: number): this { this.aggregationCache = AggregationCacheOptions.enabled(cacheExpiredMillis); return this; } propagateAggregationCache(cacheExpiredMillis: number): this { if (!this.aggregationCache) { this.aggregationCache = AggregationCacheOptions.enabled(0); } this.aggregationCache.propagate(cacheExpiredMillis); return this; } aggregate(functionName: string, field: string, alias: string): this { this.aggregateItems.push({ function: functionName, field, alias }); return this; } groupBy(item: string): this { this.groupByItems.push(item); return this; } } export class MutationQuery { constructor( public entity: string, public action: "Create" | "Update" | "Delete", public payload: any, public id?: any, public comment?: string, public expectedVersion?: number, ) {} }