@nestjs-odata/typeorm 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,705 @@
1
+ import { IncomingMessage } from "http";
2
+ import { DataSource, EntityManager, EntityMetadata, ObjectLiteral, Repository, SelectQueryBuilder } from "typeorm";
3
+ import { ApplyNode, BatchResponsePart, BinaryExprNode, EdmEntityConfig, EdmEntityType, EdmRegistry, EntityClass, FilterNode, FilterVisitor, FunctionCallNode, IETagProvider, IEdmDeriver, IQueryTranslator, ISearchProvider, LambdaExprNode, LiteralNode, ODataModuleOptions, ODataModuleResolvedOptions, ODataQuery, ODataQueryResult, OrderByItem, PropertyAccessNode, SearchNode, SelectNode, UnaryExprNode, UnmappedTypeStrategy } from "@nestjs-odata/core";
4
+ import { DynamicModule, OnModuleInit } from "@nestjs/common";
5
+ import { EntityClassOrSchema } from "@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type.js";
6
+
7
+ //#region src/batch/batch-controller.d.ts
8
+ /**
9
+ * Minimal interface for the subset of Express Request used by BatchController.
10
+ * Avoids a direct dependency on @types/express in the typeorm adapter package.
11
+ */
12
+ interface BatchRequest extends IncomingMessage {
13
+ body?: unknown;
14
+ rawBody?: Buffer | string;
15
+ headers: Record<string, string | string[] | undefined>;
16
+ }
17
+ /**
18
+ * Minimal interface for the subset of Express Response used by BatchController.
19
+ */
20
+ interface BatchResponse {
21
+ status(code: number): this;
22
+ set(field: string, value: string): this;
23
+ send(body: string): this;
24
+ json(body: unknown): this;
25
+ }
26
+ /**
27
+ * BatchController handles POST /$batch requests.
28
+ *
29
+ * The service root prefix is set dynamically when registering this controller
30
+ * in ODataTypeOrmModule.forFeature().
31
+ */
32
+ declare class BatchController {
33
+ private readonly dataSource;
34
+ private readonly edmRegistry;
35
+ private readonly options;
36
+ private readonly entityClasses;
37
+ constructor(dataSource: DataSource, edmRegistry: EdmRegistry, options: ODataModuleResolvedOptions, entityClasses: EntityClass[]);
38
+ /**
39
+ * POST {serviceRoot}/$batch
40
+ *
41
+ * Reads raw body (must be pre-configured with body-parser for text/plain or rawBody),
42
+ * parses multipart/mixed, dispatches sub-requests, builds multipart response.
43
+ */
44
+ handleBatch(req: BatchRequest, res: BatchResponse): Promise<void>;
45
+ /**
46
+ * Extract the raw body string from the request.
47
+ *
48
+ * Tries multiple strategies in order:
49
+ * 1. req.body as string (if a text/plain body parser ran first)
50
+ * 2. req.rawBody Buffer (NestJS rawBody: true option)
51
+ * 3. Read directly from the Node.js request stream (multipart/mixed fallback)
52
+ * This works because $batch sends multipart/mixed which no standard body
53
+ * parser processes, leaving the stream unconsumed.
54
+ */
55
+ private extractRawBody;
56
+ /**
57
+ * Dispatch a single individual request (outside a changeset).
58
+ * Failures are caught and returned as per-operation error responses (BATCH-03).
59
+ */
60
+ private dispatchIndividualRequest;
61
+ /**
62
+ * Execute all operations in a changeset within a single QueryRunner transaction.
63
+ * If any operation fails, all are rolled back (BATCH-02).
64
+ *
65
+ * Per D-02: full changeset rollback on any failure.
66
+ * Per WRITE-03 / T-10-08: contentIdMap is local to each changeset call — never leaks across changesets.
67
+ */
68
+ private executeChangeset;
69
+ /**
70
+ * Resolve Content-ID references ($N) in a BatchRequestPart's URL and body.
71
+ *
72
+ * Per WRITE-03: $N in a URL or body is substituted with the location URL recorded
73
+ * for content ID N from a prior 201 response in the same changeset.
74
+ *
75
+ * Fast path: returns the original part unchanged when contentIdMap is empty or
76
+ * no $N patterns match (avoids unnecessary object allocation).
77
+ *
78
+ * Per immutability rules: never mutates the readonly BatchRequestPart — returns a new object.
79
+ * Per T-10-07: substitution uses only URLs already in contentIdMap (populated by our own
80
+ * 201 responses) — no code evaluation, no cross-changeset leak.
81
+ */
82
+ private resolveContentIdReferences;
83
+ /**
84
+ * Dispatch a parsed sub-request to the appropriate handler using the given EntityManager.
85
+ * Uses the EntityManager so changesets can use a transaction-scoped manager.
86
+ *
87
+ * Per T-05-03: URL parsing is regex-based, entity operations go through TypeORM parameterized queries.
88
+ */
89
+ private dispatchWithManager;
90
+ /** GET /EntitySet(key) */
91
+ private dispatchGetByKey;
92
+ /** GET /EntitySet (collection — basic support) */
93
+ private dispatchGetCollection;
94
+ /** POST /EntitySet */
95
+ private dispatchCreate;
96
+ /** PATCH /EntitySet(key) */
97
+ private dispatchUpdate;
98
+ /**
99
+ * PUT /EntitySet(key) — full entity replacement.
100
+ *
101
+ * Per OData v4 spec (D-01): all entity properties replaced.
102
+ * Unspecified fields reset to column defaults (null for nullable, default value otherwise).
103
+ * Distinct from PATCH merge semantics used by dispatchUpdate().
104
+ *
105
+ * Uses repo.metadata.columns for column introspection — same pattern as handleReplace().
106
+ */
107
+ private dispatchReplace;
108
+ /** DELETE /EntitySet(key) */
109
+ private dispatchDelete;
110
+ /**
111
+ * Build an OData error response part from an exception.
112
+ * Never exposes stack traces (T-05-05).
113
+ */
114
+ private buildErrorResponse;
115
+ /**
116
+ * Resolve a TypeORM entity class from an EDM entity type name.
117
+ * Matches by checking DataSource metadata for each registered entity class.
118
+ */
119
+ private resolveEntityClass;
120
+ }
121
+ //#endregion
122
+ //#region src/batch/batch-response-builder.d.ts
123
+ /**
124
+ * Build a multipart/mixed batch response from individual operation results.
125
+ *
126
+ * @param parts - Array of per-operation response descriptors
127
+ * @returns Object with `contentType` (for Content-Type header) and `body` (response body string)
128
+ */
129
+ declare function buildBatchResponse(parts: BatchResponsePart[]): {
130
+ contentType: string;
131
+ body: string;
132
+ };
133
+ //#endregion
134
+ //#region src/odata-typeorm.module.d.ts
135
+ /**
136
+ * DI injection token for the array of TypeORM entity classes
137
+ * registered via ODataTypeOrmModule.forFeature().
138
+ */
139
+ declare const TYPEORM_ODATA_ENTITIES: unique symbol;
140
+ /**
141
+ * TypeOrmEdmInitializer — runs at module init to derive EDM from TypeORM entities
142
+ * and register them in the EdmRegistry.
143
+ *
144
+ * Per D-06, D-16, EDM-06: EDM derivation happens after DataSource is ready (onModuleInit).
145
+ */
146
+ declare class TypeOrmEdmInitializer implements OnModuleInit {
147
+ private readonly dataSource;
148
+ private readonly edmRegistry;
149
+ private readonly options;
150
+ private readonly entityClasses;
151
+ constructor(dataSource: DataSource, edmRegistry: EdmRegistry, options: ODataModuleOptions, entityClasses: EntityClass[]);
152
+ onModuleInit(): void;
153
+ }
154
+ /**
155
+ * TypeORM adapter module for @nestjs-odata/core.
156
+ *
157
+ * Wraps TypeOrmModule.forFeature() and registers the entity classes
158
+ * under the TYPEORM_ODATA_ENTITIES token. TypeOrmEdmInitializer runs
159
+ * at onModuleInit to derive and register OData EDM metadata from TypeORM.
160
+ *
161
+ * ODataModule.forRoot() must be imported in the application root module —
162
+ * EdmRegistry is @Global() so it is available here without an explicit import.
163
+ *
164
+ * Usage:
165
+ * ODataTypeOrmModule.forFeature([Product, Category])
166
+ */
167
+ declare class ODataTypeOrmModule {
168
+ /**
169
+ * Register TypeORM entity classes for OData auto-derivation.
170
+ *
171
+ * @param entities - Array of TypeORM entity classes (decorated with @Entity())
172
+ * @returns DynamicModule that imports TypeOrmModule.forFeature, provides TYPEORM_ODATA_ENTITIES,
173
+ * and wires TypeOrmEdmInitializer to populate the EdmRegistry at onModuleInit
174
+ */
175
+ static forFeature(entities: EntityClassOrSchema[], options?: {
176
+ serviceRoot?: string;
177
+ }): DynamicModule;
178
+ }
179
+ //#endregion
180
+ //#region src/deriver/typeorm-edm-deriver.d.ts
181
+ /**
182
+ * TypeOrmEdmDeriver — derives OData v4 EdmEntityConfig[] from TypeORM EntityMetadata.
183
+ *
184
+ * Respects OData decorators: @EdmType, @ODataExclude, @ODataEntitySet.
185
+ * Detects ViewEntity and marks them read-only.
186
+ * Navigation property types are always namespace-qualified (e.g., 'Default.Category').
187
+ *
188
+ * CRITICAL: Never produces Edm.DateTime — all datetime-like types produce Edm.DateTimeOffset.
189
+ *
190
+ * Usage (runtime):
191
+ * const deriver = new TypeOrmEdmDeriver('Default', 'skip')
192
+ * // When called by the module, metadatas come from the registered DataSource
193
+ *
194
+ * Usage (testing):
195
+ * deriver.deriveEntityTypes([Product], dataSource.entityMetadatas)
196
+ */
197
+ declare class TypeOrmEdmDeriver implements IEdmDeriver {
198
+ private readonly namespace;
199
+ private readonly unmappedTypeStrategy;
200
+ constructor(namespace: string, unmappedTypeStrategy: UnmappedTypeStrategy);
201
+ /**
202
+ * Implement IEdmDeriver — called by the core module.
203
+ * When entity metadatas are not provided, returns empty (requires integration with DataSource).
204
+ */
205
+ deriveEntityTypes(entityClasses: EntityClass[]): EdmEntityConfig[];
206
+ /**
207
+ * Overload for direct use with TypeORM EntityMetadata (used in tests and adapter integration).
208
+ */
209
+ deriveEntityTypes(entityClasses: EntityClass[], entityMetadatas: EntityMetadata[]): EdmEntityConfig[];
210
+ private deriveEntity;
211
+ private deriveProperties;
212
+ private deriveNavigationProperties;
213
+ }
214
+ //#endregion
215
+ //#region src/translator/filter-visitor.d.ts
216
+ /**
217
+ * TypeOrmFilterVisitor — translates an OData filter AST into TypeORM
218
+ * SelectQueryBuilder.andWhere() calls with named parameters.
219
+ *
220
+ * All literal values are bound via named parameters (:p1, :p2, ...) — zero
221
+ * string interpolation in WHERE clauses (T-03-04 mitigation).
222
+ *
223
+ * LIKE special characters (%, _) are escaped before use in contains/startswith/endswith
224
+ * to prevent wildcard injection (T-03-05 mitigation).
225
+ *
226
+ * Per SEC-04: Tracks filter AST nesting depth and throws ODataValidationError when
227
+ * depth exceeds maxFilterDepth. Prevents pathological filter expressions.
228
+ */
229
+ declare class TypeOrmFilterVisitor implements FilterVisitor<void> {
230
+ private readonly qb;
231
+ private readonly alias;
232
+ private readonly entityType;
233
+ private readonly maxFilterDepth;
234
+ private readonly dialect;
235
+ private readonly repo?;
236
+ /** Shared param counter — passed between sibling visitors for unique names */
237
+ paramCount: number;
238
+ /** Current nesting depth — propagated to sub-visitors for or branches */
239
+ currentDepth: number;
240
+ /** Accumulated params from resolveExpression (e.g. arithmetic operands) to merge into andWhere */
241
+ private pendingParams;
242
+ constructor(qb: SelectQueryBuilder<ObjectLiteral>, alias: string, entityType: EdmEntityType, maxFilterDepth?: number, dialect?: 'sqlite' | 'postgres' | 'ansi', repo?: Repository<ObjectLiteral> | undefined);
243
+ /** Entry point: dispatch the root FilterNode to the appropriate visit method */
244
+ visit(node: FilterNode): void;
245
+ visitBinaryExpr(node: BinaryExprNode): void;
246
+ visitUnaryExpr(node: UnaryExprNode): void;
247
+ visitFunctionCall(node: FunctionCallNode): void;
248
+ visitLambdaExpr(node: LambdaExprNode): void;
249
+ private buildAnyClause;
250
+ private buildAllClause;
251
+ visitPropertyAccess(_node: PropertyAccessNode): void;
252
+ visitLiteral(_node: LiteralNode): void;
253
+ private nextParam;
254
+ /**
255
+ * Resolve a FilterNode to an SQL expression string (left side of comparison).
256
+ * Handles PropertyAccessNode (column ref) and scalar FunctionCallNode (LENGTH, LOWER, etc.).
257
+ */
258
+ private resolveExpression;
259
+ /** Resolve date/time function to dialect-correct SQL. Returns null if not a date function. */
260
+ private resolveDateFunction;
261
+ /** Bind an arithmetic operand: Literal nodes become named params; others recurse. */
262
+ private resolveArithOperand;
263
+ /** Resolve indexof/substring/concat to parameterized SQL. Returns null if not a string fn. */
264
+ private resolveStringFunction;
265
+ /** Extract the raw JS value from a LiteralNode */
266
+ private extractLiteralValue;
267
+ /** Apply a LIKE function (contains, startswith, endswith) with escaped value */
268
+ private applyLikeFunction;
269
+ /**
270
+ * Escape LIKE special characters to prevent wildcard injection (T-03-05).
271
+ * % -> \%, _ -> \_
272
+ */
273
+ private escapeLike;
274
+ }
275
+ //#endregion
276
+ //#region src/translator/select-visitor.d.ts
277
+ /**
278
+ * TypeOrmSelectVisitor — translates an OData $select node into a TypeORM
279
+ * SelectQueryBuilder.select() call.
280
+ *
281
+ * Key properties (primary keys) are always included in the projection even if
282
+ * not in the $select list (T-03-06 mitigation — avoids identity column gaps).
283
+ */
284
+ declare class TypeOrmSelectVisitor {
285
+ private readonly qb;
286
+ private readonly alias;
287
+ private readonly entityType;
288
+ constructor(qb: SelectQueryBuilder<ObjectLiteral>, alias: string, entityType: EdmEntityType);
289
+ /**
290
+ * Apply the $select projection to the QueryBuilder.
291
+ *
292
+ * - If select.all is true or select.items is absent/empty, do nothing (return all columns).
293
+ * - Otherwise, build a deduplicated column list and call qb.select().
294
+ */
295
+ apply(select: SelectNode): void;
296
+ }
297
+ //#endregion
298
+ //#region src/translator/orderby-visitor.d.ts
299
+ /**
300
+ * TypeOrmOrderByVisitor — translates OData $orderby items into TypeORM
301
+ * QueryBuilder.orderBy() / addOrderBy() calls.
302
+ */
303
+ declare class TypeOrmOrderByVisitor {
304
+ private readonly qb;
305
+ private readonly alias;
306
+ constructor(qb: SelectQueryBuilder<ObjectLiteral>, alias: string);
307
+ /**
308
+ * Apply $orderby items to the QueryBuilder.
309
+ * First item uses orderBy(), subsequent items use addOrderBy().
310
+ */
311
+ apply(items: OrderByItem[]): void;
312
+ private resolvePropertyName;
313
+ }
314
+ //#endregion
315
+ //#region src/translator/pagination-visitor.d.ts
316
+ /**
317
+ * TypeOrmPaginationVisitor — applies OData $top/$skip pagination to a TypeORM
318
+ * SelectQueryBuilder via take() and skip().
319
+ */
320
+ declare class TypeOrmPaginationVisitor {
321
+ private readonly qb;
322
+ constructor(qb: SelectQueryBuilder<ObjectLiteral>);
323
+ /**
324
+ * Apply pagination to the QueryBuilder.
325
+ *
326
+ * - top=0 is valid (returns empty result set) — applies take(0)
327
+ * - undefined values are skipped (no call made)
328
+ */
329
+ paginate(top: number | undefined, skip: number | undefined): void;
330
+ }
331
+ //#endregion
332
+ //#region src/translator/typeorm-query-translator.d.ts
333
+ /**
334
+ * Translation result from translate() — includes the QueryBuilder and the
335
+ * expandPaginationMap needed for post-JOIN in-memory slicing (D-13).
336
+ * When $apply is present, applyProperties contains the projected column names.
337
+ */
338
+ interface TranslateResult {
339
+ readonly qb: SelectQueryBuilder<ObjectLiteral>;
340
+ readonly expandPaginationMap: ReadonlyMap<string, {
341
+ skip?: number;
342
+ top?: number;
343
+ }>;
344
+ readonly applyProperties?: string[];
345
+ }
346
+ /**
347
+ * TypeOrmQueryTranslator — orchestrates all visitor classes to translate
348
+ * an ODataQuery into a TypeORM SelectQueryBuilder, then executes it.
349
+ *
350
+ * Implements IQueryTranslator<SelectQueryBuilder<ObjectLiteral>> — the adapter seam
351
+ * between @nestjs-odata/core query types and TypeORM query building.
352
+ *
353
+ * Visitor application order (deterministic):
354
+ * 1. filter — WHERE clause (must be first, affects result set)
355
+ * 1.5 search — $search LIKE conditions (additional WHERE)
356
+ * 2. apply — $apply aggregation pipeline (skips select/orderby/expand)
357
+ * 3. select — column projection (skipped when $apply present)
358
+ * 4. orderby — sorting (skipped when $apply present)
359
+ * 5. pagination — take/skip
360
+ * 6. expand — JOINs for navigation properties (skipped when $apply present)
361
+ */
362
+ declare class TypeOrmQueryTranslator implements IQueryTranslator<TranslateResult> {
363
+ private readonly repo;
364
+ private readonly edmRegistry;
365
+ private readonly options;
366
+ private readonly searchProvider?;
367
+ constructor(repo: Repository<ObjectLiteral>, edmRegistry: EdmRegistry, options: ODataModuleResolvedOptions, searchProvider?: ISearchProvider | undefined);
368
+ /**
369
+ * Translate an ODataQuery AST into a TypeORM SelectQueryBuilder.
370
+ * Also returns the expandPaginationMap for post-JOIN slicing (D-13).
371
+ * Does not execute the query — call execute() for DB access.
372
+ */
373
+ translate(query: ODataQuery, entityType: EdmEntityType): TranslateResult;
374
+ /**
375
+ * Execute the translated SelectQueryBuilder and return structured results.
376
+ * Applies post-JOIN in-memory expand pagination after getMany() (D-13).
377
+ * When $apply is present, uses getRawMany() to preserve aggregate aliases.
378
+ *
379
+ * @param translateResult - The result from translate() (qb + expandPaginationMap)
380
+ * @param includeCount - If true, uses getManyAndCount() to include total count
381
+ */
382
+ execute(translateResult: TranslateResult | SelectQueryBuilder<ObjectLiteral>, includeCount: boolean): Promise<ODataQueryResult>;
383
+ }
384
+ //#endregion
385
+ //#region src/translator/typeorm-auto-handler.d.ts
386
+ /**
387
+ * TypeOrmAutoHandler — handles OData GET and $count requests automatically
388
+ * using the TypeORM repository and the query translator.
389
+ *
390
+ * Provides zero-boilerplate OData endpoint handling:
391
+ * - handleGet(): translate + execute OData query, detect next page via top+1 trick
392
+ * - handleCount(): strip pagination/select/orderby, return total count for filter
393
+ *
394
+ * Per D-13: effectiveTop is clamped by maxTop (T-03-11).
395
+ * Per D-15: handleCount() strips $top/$skip to avoid Pitfall 3.
396
+ * Per Pitfall 2: fetches top+1 items to detect next page without a separate count query.
397
+ *
398
+ * The EdmEntityType is resolved at request time via EdmRegistry so the provider
399
+ * can be registered during module compile time before EDM derivation runs at onModuleInit.
400
+ */
401
+ declare class TypeOrmAutoHandler {
402
+ private readonly translator;
403
+ private readonly edmRegistry;
404
+ private readonly options;
405
+ private readonly repo;
406
+ private readonly etagProvider?;
407
+ private readonly dataSource?;
408
+ constructor(translator: TypeOrmQueryTranslator, edmRegistry: EdmRegistry, options: ODataModuleResolvedOptions, repo: Repository<ObjectLiteral>, etagProvider?: IETagProvider | undefined, dataSource?: DataSource | undefined);
409
+ /**
410
+ * Resolve EdmEntityType from the registry at request time.
411
+ * Throws if the entity set is not registered.
412
+ */
413
+ private resolveEntityType;
414
+ /**
415
+ * Handle an OData GET collection request.
416
+ *
417
+ * Strategy:
418
+ * 1. Resolve EdmEntityType via EdmRegistry
419
+ * 2. Determine effectiveTop (query.top ?? maxTop), clamped to maxTop
420
+ * 3. Build a modified query with top=effectiveTop+1 to detect next page
421
+ * 4. Translate to SelectQueryBuilder
422
+ * 5. Execute (with or without count per query.count)
423
+ * 6. Check if items.length > effectiveTop (has more pages)
424
+ * 7. If yes: slice to effectiveTop, build nextLink; otherwise: no nextLink
425
+ *
426
+ * @param query - Validated ODataQuery from ODataQueryPipe
427
+ * @param requestUrl - Base URL of the request for nextLink construction
428
+ */
429
+ handleGet(query: ODataQuery, requestUrl: string): Promise<ODataQueryResult>;
430
+ /**
431
+ * Handle an OData $count route.
432
+ *
433
+ * Per Pitfall 3: strips $top/$skip/$orderby/$select from the query so count
434
+ * returns total matching rows for the filter, not just the current page count.
435
+ *
436
+ * @param query - Validated ODataQuery from ODataQueryPipe
437
+ */
438
+ handleCount(query: ODataQuery): Promise<number>;
439
+ /**
440
+ * Build an OData nextLink URL with updated $skip and $top parameters.
441
+ *
442
+ * @param requestUrl - The current request URL (may contain existing query params)
443
+ * @param newSkip - The new $skip value for the next page
444
+ * @param top - The page size ($top value)
445
+ */
446
+ buildNextLink(requestUrl: string, newSkip: number, top: number): string;
447
+ /**
448
+ * Handle OData GET by key — single entity retrieval.
449
+ *
450
+ * Per D-05: returns single entity, throws NotFoundException if not found.
451
+ * Per D-02: key is parsed from parenthetical format.
452
+ * Per T-04-08: parseODataKey returns typed values used in parameterized where clause.
453
+ * Per T-09-05: If-None-Match header supported — returns { __notModified, etag } signal when matched.
454
+ */
455
+ handleGetByKey(keyStr: string, entitySetName: string, ifNoneMatchHeader?: string): Promise<unknown>;
456
+ /**
457
+ * Handle OData POST — create entity.
458
+ *
459
+ * Per D-03: returns 201 + Location header + created entity.
460
+ * Returns { entity, locationUrl } for the interceptor to set the Location header.
461
+ * Per T-04-07: TypeORM repo.create() only maps declared columns — unknown fields ignored.
462
+ * Per T-04-11: entity class acts as whitelist, preventing mass assignment.
463
+ */
464
+ handleCreate(body: Record<string, unknown>, entitySetName: string): Promise<{
465
+ entity: unknown;
466
+ locationUrl: string;
467
+ }>;
468
+ /**
469
+ * Handle OData POST with deep insert — create parent and nested children atomically.
470
+ *
471
+ * Per D-02 (deep insert): navigation property keys in the body identify child collections.
472
+ * All saves occur within the provided EntityManager (transaction scope).
473
+ *
474
+ * Per T-10-05: depth is checked before recursion — throws 400 if depth >= maxDeepInsertDepth.
475
+ * Per T-10-06: TypeORM repo.create() only maps declared columns — mass assignment safe.
476
+ *
477
+ * @param body - Request body (scalar fields + optional navigation property arrays)
478
+ * @param entitySetName - OData entity set name (e.g. 'Orders')
479
+ * @param manager - Transaction-scoped EntityManager
480
+ * @param depth - Current recursion depth (default 0)
481
+ */
482
+ handleDeepCreate(body: Record<string, unknown>, entitySetName: string, manager: EntityManager, depth?: number): Promise<{
483
+ entity: unknown;
484
+ locationUrl: string;
485
+ }>;
486
+ /**
487
+ * Resolve a TypeORM entity class from an EDM entity type name.
488
+ * Requires DataSource to be provided.
489
+ */
490
+ private resolveEntityClassFromDataSource;
491
+ /**
492
+ * Handle OData PATCH — partial update (merge-patch semantics).
493
+ *
494
+ * Per D-01: send only changed fields, server merges with existing entity.
495
+ * Per Pitfall 4: checks preload result for undefined (entity not found case).
496
+ * Per T-04-08: parseODataKey returns typed values for safe parameterized queries.
497
+ * Per T-09-04: If-Match header enforces optimistic concurrency — 412 on stale ETag.
498
+ */
499
+ handleUpdate(keyStr: string, body: Record<string, unknown>, entitySetName: string, ifMatchHeader?: string): Promise<unknown>;
500
+ /**
501
+ * Handle OData PUT — full entity replacement.
502
+ *
503
+ * Per D-01 (OData v4 spec): PUT replaces ALL entity properties.
504
+ * Unspecified fields reset to column defaults (null for nullable, default value otherwise).
505
+ * This is distinct from PATCH merge semantics.
506
+ *
507
+ * Implementation:
508
+ * 1. Parse key from URL
509
+ * 2. Validate body key matches URL key (reject 400 on mismatch)
510
+ * 3. Find existing entity — throw 404 if not found
511
+ * 4. Validate ETag If-Match (412 on mismatch)
512
+ * 5. Build full replacement using repo.metadata.columns:
513
+ * - Skip isPrimary, isCreateDate, isUpdateDate, isVersion columns
514
+ * - body value > column default > null (if nullable) > absent
515
+ * 6. Save and return
516
+ *
517
+ * Per T-10-01: body key mismatch rejected before DB write.
518
+ * Per T-10-02: repo.create() only maps declared entity columns — mass assignment safe.
519
+ * Per T-10-03: managed columns (PK, timestamps, version) skipped.
520
+ * Per T-10-04: ETag If-Match enforcement identical to handleUpdate().
521
+ */
522
+ handleReplace(keyStr: string, body: Record<string, unknown>, entitySetName: string, ifMatchHeader?: string): Promise<unknown>;
523
+ /**
524
+ * Handle OData DELETE — remove entity.
525
+ *
526
+ * Per D-04: returns 204 No Content (void return value).
527
+ * Per T-04-08: parseODataKey returns typed values for safe parameterized queries.
528
+ * Per T-09-04: If-Match header enforces optimistic concurrency — 412 on stale ETag.
529
+ */
530
+ handleDelete(keyStr: string, entitySetName: string, ifMatchHeader?: string): Promise<void>;
531
+ }
532
+ //#endregion
533
+ //#region src/translator/expand-pagination.d.ts
534
+ /**
535
+ * Apply post-JOIN pagination to expanded collections.
536
+ *
537
+ * TypeORM hydrates relations as JavaScript arrays. After getMany(),
538
+ * this function slices each expanded collection by the per-expand-item
539
+ * $top/$skip values recorded during ExpandVisitor traversal.
540
+ *
541
+ * Per D-13: In-memory slicing is the v1 approach. Acceptable for
542
+ * single-level expand pagination. Total result set is already bounded
543
+ * by maxTop and maxExpandDepth before slicing occurs (T-05-10 accept).
544
+ *
545
+ * Mutates items in-place — the hydrated array is replaced with the sliced
546
+ * version. This avoids allocating a new top-level array.
547
+ */
548
+ declare function applyExpandPagination(items: ObjectLiteral[], paginationMap: ReadonlyMap<string, {
549
+ readonly skip?: number;
550
+ readonly top?: number;
551
+ }>): void;
552
+ //#endregion
553
+ //#region src/translator/search-provider.d.ts
554
+ /**
555
+ * TypeORM implementation of ISearchProvider.
556
+ *
557
+ * Builds parameterized LIKE conditions for $search queries.
558
+ * Uses @ODataSearchable()-decorated properties to determine which fields to search.
559
+ *
560
+ * Search terms are parameterized (never interpolated) and LIKE special characters
561
+ * (%, _) are escaped before use — mitigates T-11-04 SQL injection.
562
+ *
563
+ * If no @ODataSearchable fields are found, throws ODataValidationError with a
564
+ * clear message instructing the developer to add the decorator.
565
+ */
566
+ declare class TypeOrmSearchProvider implements ISearchProvider {
567
+ private readonly dataSource;
568
+ private readonly edmRegistry;
569
+ constructor(dataSource: DataSource, edmRegistry: EdmRegistry);
570
+ /**
571
+ * Build a parameterized WHERE condition from a parsed $search AST node.
572
+ *
573
+ * @param searchNode - Parsed $search expression
574
+ * @param entitySetName - OData entity set name being searched
575
+ * @param alias - TypeORM QueryBuilder alias for the entity table
576
+ * @returns Condition string + named params, or null if unable to build
577
+ */
578
+ buildSearchCondition(searchNode: SearchNode, entitySetName: string, alias: string): {
579
+ condition: string;
580
+ params: Record<string, unknown>;
581
+ } | null;
582
+ /**
583
+ * Resolve the entity class from the EdmRegistry and DataSource metadata.
584
+ */
585
+ private resolveEntityClass;
586
+ /**
587
+ * Build a single LIKE condition for a search term across all searchable fields.
588
+ * Fields are OR'd together: (alias.f1 LIKE :p OR alias.f2 LIKE :p)
589
+ * Negated terms are wrapped: NOT (...)
590
+ */
591
+ private buildTermCondition;
592
+ /**
593
+ * Build a binary AND/OR condition combining two recursively-built conditions.
594
+ */
595
+ private buildBinaryCondition;
596
+ /**
597
+ * Escape LIKE special characters to prevent wildcard injection (T-11-04).
598
+ * % -> \%, _ -> \_
599
+ */
600
+ private escapeLike;
601
+ }
602
+ //#endregion
603
+ //#region src/translator/apply-visitor.d.ts
604
+ /**
605
+ * TypeOrmApplyVisitor — translates an OData $apply pipeline AST into
606
+ * TypeORM SelectQueryBuilder GROUP BY / aggregate SQL.
607
+ *
608
+ * Handles three step types in pipeline order:
609
+ * - ApplyFilter: delegates to TypeOrmFilterVisitor (adds WHERE clause)
610
+ * - ApplyGroupBy: adds GROUP BY columns + aggregate SELECT expressions
611
+ * - ApplyAggregate: adds aggregate SELECT expressions (no GROUP BY)
612
+ *
613
+ * Security: aggregate property names validated against AggregateExpression.method
614
+ * enum (constrained by parser); aliases validated by parser regex before SQL use.
615
+ * No user input is directly interpolated into SQL — property/alias names come
616
+ * from the validated AST (T-11-05 mitigation).
617
+ *
618
+ * Returns projected column names (for @odata.context URL construction).
619
+ */
620
+ declare class TypeOrmApplyVisitor {
621
+ private readonly qb;
622
+ private readonly alias;
623
+ private readonly entityType;
624
+ private readonly maxFilterDepth;
625
+ private readonly dialect;
626
+ private readonly repo;
627
+ constructor(qb: SelectQueryBuilder<ObjectLiteral>, alias: string, entityType: EdmEntityType, maxFilterDepth: number | undefined, dialect: "sqlite" | "postgres" | "ansi" | undefined, repo: Repository<ObjectLiteral>);
628
+ /**
629
+ * Process all steps in an $apply pipeline in order.
630
+ * Returns all projected column names for @odata.context construction.
631
+ */
632
+ apply(applyNode: ApplyNode): string[];
633
+ private applyStep;
634
+ /**
635
+ * ApplyFilter step — delegates to TypeOrmFilterVisitor.
636
+ * Adds WHERE clause to the query. Returns no projected columns.
637
+ */
638
+ private applyFilterStep;
639
+ /**
640
+ * ApplyGroupBy step — adds GROUP BY columns and optional aggregate expressions.
641
+ * Clears existing SELECT first (replaces entity.* with explicit projections).
642
+ */
643
+ private applyGroupByStep;
644
+ /**
645
+ * ApplyAggregate step (no GROUP BY) — adds aggregate SELECT expressions.
646
+ * Clears existing SELECT first (replaces entity.* with explicit projections).
647
+ */
648
+ private applyAggregateStep;
649
+ /**
650
+ * Build the SQL aggregate function expression for a single AggregateExpression.
651
+ *
652
+ * Method mapping:
653
+ * - count with $count property => COUNT(*)
654
+ * - count with field => COUNT(alias.field)
655
+ * - sum => SUM(alias.field)
656
+ * - avg => AVG(alias.field)
657
+ * - min => MIN(alias.field)
658
+ * - max => MAX(alias.field)
659
+ * - countdistinct => COUNT(DISTINCT alias.field)
660
+ */
661
+ private buildAggregateSql;
662
+ }
663
+ //#endregion
664
+ //#region src/etag/typeorm-etag.provider.d.ts
665
+ /**
666
+ * TypeORM implementation of IETagProvider.
667
+ *
668
+ * Discovers the ETag source column per entity set in priority order:
669
+ * 1. @ODataETag() decorator (explicit opt-in takes precedence)
670
+ * 2. TypeORM @UpdateDateColumn (automatic fallback via isUpdateDate)
671
+ *
672
+ * ETag format: W/"<base64-encoded column value>"
673
+ * This is deterministic and changes whenever the column value changes.
674
+ *
675
+ * Caches column name resolution per entity set for performance.
676
+ */
677
+ declare class TypeOrmETagProvider implements IETagProvider {
678
+ private readonly dataSource;
679
+ private readonly edmRegistry;
680
+ private readonly cache;
681
+ constructor(dataSource: DataSource, edmRegistry: EdmRegistry);
682
+ /**
683
+ * Get the ETag column name for an entity set.
684
+ * Returns undefined if neither @ODataETag nor @UpdateDateColumn is found.
685
+ * Results are cached to avoid repeated metadata reflection.
686
+ */
687
+ getETagColumn(entitySetName: string): string | undefined;
688
+ /**
689
+ * Compute a weak ETag string from an entity's ETag column value.
690
+ * Format: W/"<base64-encoded string value>"
691
+ */
692
+ computeETag(entity: Record<string, unknown>, etagColumn: string): string;
693
+ /**
694
+ * Validate an If-Match header value against the current entity.
695
+ * Returns true if the ETag matches (safe to proceed with mutation).
696
+ */
697
+ validateIfMatch(ifMatchValue: string, entity: Record<string, unknown>, etagColumn: string): boolean;
698
+ private resolveETagColumn;
699
+ }
700
+ //#endregion
701
+ //#region src/index.d.ts
702
+ declare const VERSION = "0.0.1";
703
+ //#endregion
704
+ export { BatchController, BatchRequest, BatchResponse, ODataTypeOrmModule, TYPEORM_ODATA_ENTITIES, type TranslateResult, TypeOrmApplyVisitor, TypeOrmAutoHandler, TypeOrmETagProvider, TypeOrmEdmDeriver, TypeOrmEdmInitializer, TypeOrmFilterVisitor, TypeOrmOrderByVisitor, TypeOrmPaginationVisitor, TypeOrmQueryTranslator, TypeOrmSearchProvider, TypeOrmSelectVisitor, VERSION, applyExpandPagination, buildBatchResponse };
705
+ //# sourceMappingURL=index.d.cts.map