import { type AggregationEntry } from "./aggregations.js"; import { type FilterSpecNode, type SearchFilterShape } from "./filters.js"; import type { ResolvedProjection, ResolvedProjectionField, TopLevelProjection } from "./projection.js"; export type PipelineFunctionDataSource = "OPENSEARCH" | "NONE"; export interface EmittedPipelineFunction { name: string; fileName: string; content: string; dataSource: PipelineFunctionDataSource; } export type ResolverEmissionMode = "monolithic" | "pipeline"; export interface EmittedResolverFile { queryFieldName: string; /** * `monolithic` — `content` carries the full UNIT resolver (request + * response inline; reads OS via `operation: "GET"` directly). `functions` * is empty. * `pipeline` — `content` is the resolver-level after-mapping; `functions` * holds the prepare (NONE) and search (OPENSEARCH) pipeline functions. */ mode: ResolverEmissionMode; /** Resolver-level file. UNIT body for monolithic; before/after for pipeline. */ fileName: string; content: string; /** * Pipeline functions in execution order. Consumers wire these as AppSync * Functions and reference them on a PIPELINE Resolver. Splitting the work * across functions keeps each file's APPSYNC_JS code under the 32 KB * per-function cap, which a single-resolver shape would exceed on wide * @searchInfer projections (issue #105). Empty for `mode === "monolithic"`. */ functions: EmittedPipelineFunction[]; } export interface ResolverOptions { defaultPageSize: number; maxPageSize: number; trackTotalHitsUpTo: number; /** * Byte threshold above which a projection's monolithic shape is rejected * and the pipeline shape is emitted instead. Suggested 28,000 (32K cap * minus headroom). Measured against the rendered monolithic content. */ monolithicThresholdBytes?: number; /** * `buckets` for the `auto_date_histogram` emitted when a date_histogram * aggregation declares no bounds. See DEFAULT_AUTO_DATE_HISTOGRAM_BUCKETS. */ autoDateHistogramBuckets?: number; } /** * AppSync's hard per-function code limit. `CreateFunction` rejects any resolver * function whose code exceeds this — a deploy-time `BadRequestException: Code * must be 32768 bytes or less`. The emitter measures every function it emits * against the (smaller) split threshold so the output ships with headroom; the * emitter host also hard-errors compile on any generated function that still * lands over this cap, so the over-limit case can never pass compile silently * (issue #173). */ export declare const APPSYNC_FUNCTION_BYTE_LIMIT = 32768; /** * AppSync's hard limit on functions attached to one pipeline resolver. The * recursive split (issue #173) stops subdividing here: a projection whose fully * split pipeline still needs more functions than this cannot be emitted, and * the emitter host raises a compile diagnostic rather than emit an * undeployable pipeline. */ export declare const MAX_PIPELINE_FUNCTIONS = 10; /** * Per-histogram `buckets` ceiling for a bounds-less `auto_date_histogram` * (issue #150). This is the most any single histogram gets; the emitted * `buildAggs` lowers it when a request selects several histograms so their sum * stays under the per-request budget (issue #155). * * `auto_date_histogram` returns at most this many buckets, picking the finest * interval at or above `minimum_interval` that fits. The value therefore sets * how wide a range still keeps the author's declared interval: at 10,000 that * is 833 years of monthly buckets, 27 years of daily, and 1.1 years of hourly * — past any real corpus, so declared intervals survive. A 9999-12-31 sentinel * (~96,000 months) does not fit and steps down to yearly (~8,000 buckets), * which renders instead of failing. */ export declare const DEFAULT_AUTO_DATE_HISTOGRAM_BUCKETS = 10000; /** * Explicit `size` on every emitted `terms` aggregation. OpenSearch defaults an * absent `size` to 10, so relying on that default already bounded the bucket * count — but the emitter's invariant is that no bucketing aggregation ships * without a hard ceiling visible in the emitted body, so the default is made * explicit. Set to OpenSearch's own default so the emitted queries return the * same buckets they always have. */ export declare const DEFAULT_TERMS_SIZE = 10; /** * Soft per-request bucket budget: a third of OpenSearch's default * `search.max_buckets` (65,535). That cap counts every bucket in the whole * request, not one aggregation, so the emitted `buildAggs` divides this budget * across the `auto_date_histogram` aggregations a request actually selects * (issue #155). A request that reaches the hard cap is already a 503; a third * leaves room for the terms and metric buckets sharing the request. */ export declare const PER_REQUEST_BUCKET_BUDGET = 21845; /** * Lower bound on the per-histogram bucket count after the budget is divided, so * a request selecting many histograms still renders a legible chart instead of * a handful of buckets. Below ~256 buckets a time series stops being readable; * this floor only binds past ~85 selected histograms. */ export declare const MIN_AUTO_DATE_HISTOGRAM_BUCKETS = 256; export declare function emitGraphQLResolver(projection: TopLevelProjection, options: ResolverOptions): Promise; /** * Groups aggregation entries by their top-level nested path so a level-3 aggs * split partitions along the same nested boundaries the query split uses. Flat * aggregations (no nested path) group under the root key; nested aggregations * group by the first path segment, keeping every aggregation that shares a * `_` wrapper in one partition so the wrapper is assembled by a single * function. */ declare function partitionAggregationsByTopPath(aggregations: AggregationEntry[]): AggregationEntry[][]; /** * Free-text fields grouped by the query clause they belong in. `flat` holds * root-document paths — top-level fields plus `object`-mapped sub-projection * fields, which live in the same Lucene document and so are reachable by a * dotted path. `nested` holds one group per `@nested` sub-projection that * carries at least one `@searchable` text field; those are separate hidden * documents, reachable only through a `nested` query naming their path. */ interface NestedTextGroup { path: string; fields: string[]; } interface TextFieldCollection { flat: string[]; nested: NestedTextGroup[]; } /** * One step from a document level to the level below it. The boolean records * whether the schema declares that step a list, resolved when the spec is * built so the emitted walker never has to ask at runtime. */ export type DocumentPathSegment = [string, boolean]; /** * The non-null fields of one document level, addressed by the segment path * that reaches that level from the document root. Lists and values are split * because they degrade differently: a list is fillable, a value is not. */ export interface DocumentLevelSpec { path: DocumentPathSegment[]; lists: string[]; values: string[]; } /** * The non-null response shape the SDL declares, level by level. Only * `searchable` fields reach the SDL, so filter-only and aggregatable-only * fields are absent here too. Levels are collected outermost-first, so the * emitted walker fills a parent list before descending into it. */ declare function collectDocumentSpec(projection: ResolvedProjection): DocumentLevelSpec[]; declare function hasTextType(field: ResolvedProjectionField): boolean; declare function renderMonolithicResolver(textFields: TextFieldCollection, keywordFields: string[], textSortFields: string[], aggregations: AggregationEntry[], searchFilterShape: SearchFilterShape | undefined, indexName: string, documentSpec: DocumentLevelSpec[], options: ResolverOptions): string; /** * Pipeline resolver "before/after" code. The `request` exports here become the * pipeline's before-mapping; `response` is the after-mapping that runs after * all functions complete. The OS response lives at `ctx.prev.result` after * the OS-datasource function in the pipeline returns. */ declare function renderResolver(aggregations: AggregationEntry[], documentSpec: DocumentLevelSpec[], options: ResolverOptions): string; /** * Pipeline function on a NONE datasource. Builds the OS query body from * `ctx.args` (FILTER_SPEC walk + aggs assembly) and stashes it for the next * function to send. Holds the bulk of the request-side code: keeping it in * its own function keeps the resolver-level after-mapping (response shape + * aggregation mapping) under the 32 KB per-file APPSYNC_JS cap (issue #105). */ declare function renderPrepareFunction(textFields: TextFieldCollection, keywordFields: string[], textSortFields: string[], aggregations: AggregationEntry[], searchFilterShape: SearchFilterShape | undefined, options: ResolverOptions): string; /** * Pipeline function on the OPENSEARCH datasource. Reads the pre-built body * from `ctx.stash.queryBody` (set by the prepare function) and issues the * OS HTTP request. Tiny on purpose — the heavy filter/aggs construction * lives in the prepare function where it has its own 32 KB budget. * * The response handler must fail loudly (issue #150): returning `ctx.result` * unchecked lets a failed search through as `undefined`, which the resolver * after-mapping then dereferences — the AppSync JS runtime reports that as * `ReferenceError: parsedBody is not defined`, naming neither the status code * nor the OpenSearch reason. */ declare function renderSearchFunction(indexName: string): string; /** * Query-side NONE function for the level-2/3 split (issue #173). Builds its * slice of the `must`/`filter`/`must_not` clause arrays and appends them to the * shared `ctx.stash` arrays the OPENSEARCH `search` function later folds into a * bool query. The clauses are commutative, so a projection's filter nodes can * be spread across several of these without changing the assembled query. * * `isRoot` marks the one function that also owns the request-wide work: page * size, `search_after`, `buildSort`, the free-text `must`, and the keyword-term * `filter`s. Non-root functions carry only a slice of the top-level nested / * object filter nodes and the walker that translates them. */ declare function renderQueryFunction(nodes: FilterSpecNode[], isRoot: boolean, textFields: TextFieldCollection, keywordFields: string[], textSortFields: string[], options: ResolverOptions): string; /** * Aggs-side NONE function for the level-2/3 split (issue #173). Projects the * caller's selection onto its slice of AGG_SPEC and merges the result into the * shared `ctx.stash.aggs` object the `search` function attaches to the body. * * `allAggNames` is the projection's full set of aggregation names, not just * this partition's. The issue-#150 alias detection reads any first path segment * naming no declared aggregation as an alias and widens to send everything; a * partition that only knew its own slice would misread a sibling partition's * aggregation as an alias, so every partition carries the full list. * * The per-request histogram bucket budget (issue #155) is applied once, in * `search`, across the assembled aggs — a partition cannot divide the budget * correctly because it sees only its own histograms. */ declare function renderAggsFunction(aggregations: AggregationEntry[], allAggNames: string[]): string; /** * OPENSEARCH function for the level-2/3 split (issue #173). The query and aggs * NONE functions ahead of it have written their contributions to `ctx.stash`; * this function folds them into one OpenSearch request body and issues the * single round-trip. It also owns the per-request histogram bucket budget * (issue #155), applied here because only the assembled aggs see every selected * histogram at once. Response handling matches the level-1 `search` function. */ declare function renderSplitSearchFunction(indexName: string, aggregations: AggregationEntry[], options: ResolverOptions): string; /** * Builds the `[{n:"byTagName",a:{...}}, {n:"byStatus",g:"_tags",p:"tags",a:{...}}]` * literal that `buildAggs` projects the caller's selection onto. * * AGG_SPEC entries use single-letter keys to keep wide projections under * AppSync's 32 KB per-function code cap (issue #99, #105). The reader is * buildAggs inside the emitted prepare function; keys must match there: * n = GraphQL aggregation field name, a = OpenSearch agg body, * p = nested path, g = nested agg group key. * * Flat aggregations come first, then nested ones grouped by path, so the * assembled `body.aggs` key order matches the projection's declaration order. * * Aggregations carry a per-projection-unique `aggName` (e.g. `byCounterpartyId`). * If the same aggName appears more than once (which can happen when a * projection spreads the same field/aggregation twice), the duplicate would * overwrite the first at assembly time. Dedupe here, first-wins, matching the * response-side mapping. */ declare function renderAggSpecLiteral(aggregations: AggregationEntry[]): string; /** * Emits the `buildAggs` runtime helper, which projects the caller's selection * onto AGG_SPEC. APPSYNC_JS exposes `ctx.info.selectionSetList` as an array of * slash-paths into the selection set; an aggregation is requested exactly when * `aggregations/` is present. * * Only requested aggregations reach OpenSearch, and a nested wrapper is built * only for groups with at least one requested child (issue #150). Assembling * at runtime from a compact spec keeps the emitted code size flat regardless * of how many aggregations the projection declares — the alternative, emitting * a per-selection object literal, does not fit the 32 KB per-function cap. * * `selectionSetList` names an aliased field by its alias only — the schema * field name is absent — so `aggregations { s: bySpecies { key } }` yields * `aggregations/s` and nothing that identifies `bySpecies`. The alias target is * not recoverable, so buildAggs detects the alias instead: the valid children of * `aggregations` are the AGG_SPEC names plus `__typename`, which Apollo, Amplify * and Relay inject into every object selection set; any other first segment is * read as an alias. Such a selection falls back to sending every aggregation, * which is what the caller received before issue #150 narrowed the block. * * Known false negative: an alias that happens to name another declared * aggregation (`byAlias: bySpecies`) reads as declared, so no fallback fires and * the wrong aggregation is sent — undetectable from `selectionSetList` alone. * * Each selected `auto_date_histogram` (marked `h:1`) has its `buckets` set from * a per-request budget divided across the histograms actually sent, so their * sum stays under OpenSearch's `search.max_buckets` however many are selected * (issue #155). The alias fallback selects every aggregation, so its histograms * count toward the same budget. * * Returns "" when the projection has no aggregations at all. */ declare function renderBuildAggsFunction(aggregations: AggregationEntry[], options: ResolverOptions): string; /** * Emits the request-side block that assigns `body.aggs` to the aggregations the * caller selected, and leaves the key off entirely when they selected none. * * Sending only the requested aggregations keeps OpenSearch from executing every * aggregation the doc type declares on a `searchX(first: 0) { totalCount }` * style probe, and isolates each aggregation's failures to the queries that ask * for it. Returns "" when the projection has no aggregations at all. */ declare function renderAggsAssignment(aggregations: AggregationEntry[], indent: string): string; declare function renderResponseAggregations(aggregations: AggregationEntry[], documentSpec: DocumentLevelSpec[]): string; export declare const __test: { hasTextType: typeof hasTextType; renderResolver: typeof renderResolver; renderPrepareFunction: typeof renderPrepareFunction; renderSearchFunction: typeof renderSearchFunction; renderMonolithicResolver: typeof renderMonolithicResolver; renderQueryFunction: typeof renderQueryFunction; renderAggsFunction: typeof renderAggsFunction; renderSplitSearchFunction: typeof renderSplitSearchFunction; renderAggsAssignment: typeof renderAggsAssignment; renderAggSpecLiteral: typeof renderAggSpecLiteral; renderBuildAggsFunction: typeof renderBuildAggsFunction; renderResponseAggregations: typeof renderResponseAggregations; collectDocumentSpec: typeof collectDocumentSpec; partitionAggregationsByTopPath: typeof partitionAggregationsByTopPath; DEFAULT_MONOLITHIC_THRESHOLD_BYTES: number; }; export {}; //# sourceMappingURL=emit-graphql-resolver.d.ts.map