@rawsql-ts/sql-contract 0.2.0 → 0.3.1

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.
@@ -1,910 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.__internal = exports.Mapper = exports.EntityMapping = exports.RowMapping = exports.mapperPresets = void 0;
4
- exports.rowMapping = rowMapping;
5
- exports.entity = entity;
6
- exports.columnMapFromPrefix = columnMapFromPrefix;
7
- exports.createMapper = createMapper;
8
- exports.createMapperFromExecutor = createMapperFromExecutor;
9
- exports.createReader = createReader;
10
- exports.toRowsExecutor = toRowsExecutor;
11
- exports.mapRows = mapRows;
12
- exports.mapSimpleRows = mapSimpleRows;
13
- const internal_1 = require("./internal");
14
- /**
15
- * Named presets for simple mapping that avoid implicit inference.
16
- */
17
- exports.mapperPresets = {
18
- safe() {
19
- return {
20
- keyTransform: 'none',
21
- coerceDates: false,
22
- };
23
- },
24
- appLike() {
25
- return {
26
- keyTransform: 'snake_to_camel',
27
- coerceDates: true,
28
- };
29
- },
30
- };
31
- /**
32
- * Builds a row mapping that can be consumed by {@link Mapper#query} or {@link mapRows}.
33
- */
34
- const isKeyExtractor = (value) => typeof value === 'function';
35
- const isKeyColumnArray = (value) => Array.isArray(value);
36
- class RowMapping {
37
- constructor(options) {
38
- var _a, _b, _c;
39
- this.parents = [];
40
- this.name = options.name;
41
- this.key = options.key;
42
- this.prefix = (_a = options.prefix) !== null && _a !== void 0 ? _a : '';
43
- this.columnMap = {};
44
- this.overrideLookup = new Map();
45
- if (options.columnMap) {
46
- for (const [property, column] of Object.entries(options.columnMap)) {
47
- if (typeof column !== 'string') {
48
- throw new Error(`RowMapping "${this.name}" columnMap["${property}"] must be a string.`);
49
- }
50
- this.columnMap[property] = column;
51
- this.overrideLookup.set(column.toLowerCase(), property);
52
- }
53
- }
54
- if (!this.prefix && this.overrideLookup.size === 0) {
55
- throw new Error(`RowMapping "${this.name}" must define either "prefix" or "columnMap".`);
56
- }
57
- this.prefixNormalized = this.prefix.toLowerCase();
58
- this.prefixLength = this.prefixNormalized.length;
59
- this.shouldCoerce = (_b = options.coerce) !== null && _b !== void 0 ? _b : true;
60
- this.coerceFn = (_c = options.coerceFn) !== null && _c !== void 0 ? _c : coerceColumnValue;
61
- const keyInput = options.key;
62
- if (isKeyExtractor(keyInput)) {
63
- this.keyDescriptor = 'derived key function';
64
- this.keyExtractor = (ctx) => keyInput(this.buildMappedRowForKey(ctx));
65
- }
66
- else if (isKeyColumnArray(keyInput)) {
67
- const columns = keyInput;
68
- if (columns.length === 0) {
69
- throw new Error(`RowMapping "${this.name}" composite key must include at least one column.`);
70
- }
71
- const resolvedColumns = columns.map((column) => this.resolveColumnName(column));
72
- this.keyColumns = resolvedColumns;
73
- this.keyDescriptor = `columns ${columns.join(', ')}`;
74
- this.keyExtractor = (ctx) => resolvedColumns.map((column) => {
75
- const { found, value } = lookupRowColumn(ctx, column);
76
- if (!found) {
77
- throwMissingKeyColumn(this, column, ctx);
78
- }
79
- return value;
80
- });
81
- }
82
- else {
83
- const propertyName = keyInput;
84
- const columnName = this.resolveColumnName(propertyName);
85
- this.keyColumns = [columnName];
86
- this.keyDescriptor = `property "${String(keyInput)}"`;
87
- this.keyExtractor = (ctx) => (() => {
88
- const { found, value } = lookupRowColumn(ctx, columnName);
89
- if (!found) {
90
- throwMissingKeyColumn(this, columnName, ctx);
91
- }
92
- return value;
93
- })();
94
- }
95
- }
96
- /**
97
- * Registers a parent relationship that will be attached after the current row is mapped.
98
- */
99
- belongsTo(propertyName, parent, localKey, options) {
100
- var _a;
101
- const optional = (_a = options === null || options === void 0 ? void 0 : options.optional) !== null && _a !== void 0 ? _a : false;
102
- this.parents.push({
103
- propertyName: String(propertyName),
104
- parent,
105
- localKey,
106
- optional,
107
- });
108
- return this;
109
- }
110
- /**
111
- * Registers a parent relationship with an explicit local key.
112
- */
113
- belongsToWithLocalKey(propertyName, parent, localKey) {
114
- return this.belongsTo(propertyName, parent, localKey);
115
- }
116
- /**
117
- * Registers an optional parent relationship with an explicit local key.
118
- */
119
- belongsToOptional(propertyName, parent, localKey) {
120
- if (localKey == null) {
121
- throw new Error(`localKey is required when declaring optional relation "${String(propertyName)}" on "${this.name}"`);
122
- }
123
- return this.belongsTo(propertyName, parent, localKey, { optional: true });
124
- }
125
- matchColumn(columnName) {
126
- const normalized = columnName.toLowerCase();
127
- const override = this.overrideLookup.get(normalized);
128
- if (override) {
129
- return override;
130
- }
131
- if (!this.prefixNormalized) {
132
- // When no prefix is provided we rely on explicit column overrides.
133
- return undefined;
134
- }
135
- if (!normalized.startsWith(this.prefixNormalized)) {
136
- return undefined;
137
- }
138
- // prefix is expected to include trailing '_' (e.g. 'item_') so remainder begins with the column part.
139
- // Prefix matching is case-insensitive and purely string-based.
140
- // If the prefix lacks '_', remainder may begin mid-token; prefer "item_" style prefixes.
141
- const remainder = normalized.slice(this.prefixLength);
142
- return remainder ? toCamelCase(remainder) : undefined;
143
- }
144
- resolveColumnName(propertyName) {
145
- if (this.columnMap[propertyName]) {
146
- return this.columnMap[propertyName];
147
- }
148
- if (!this.prefix) {
149
- return propertyName;
150
- }
151
- if (propertyName.toLowerCase().startsWith(this.prefixNormalized)) {
152
- return propertyName;
153
- }
154
- return `${this.prefix}${toSnakeCase(propertyName)}`;
155
- }
156
- readKeyValue(ctx) {
157
- return this.keyExtractor(ctx);
158
- }
159
- /**
160
- * Returns a human-readable description of the key for error reporting.
161
- */
162
- get keyDescription() {
163
- return this.keyDescriptor;
164
- }
165
- /**
166
- * Lists the resolved key column names, if the key is not derived.
167
- */
168
- get keyColumnNames() {
169
- return this.keyColumns;
170
- }
171
- assignFields(target, ctx) {
172
- for (const column of Object.keys(ctx.row)) {
173
- const propertyName = this.matchColumn(column);
174
- if (!propertyName) {
175
- continue;
176
- }
177
- target[propertyName] = this.normalizeColumnValue(ctx.row[column]);
178
- }
179
- }
180
- normalizeColumnValue(value) {
181
- if (!this.shouldCoerce) {
182
- return value;
183
- }
184
- return this.coerceFn(value);
185
- }
186
- buildMappedRowForKey(ctx) {
187
- const target = {};
188
- for (const column of Object.keys(ctx.row)) {
189
- const propertyName = this.matchColumn(column);
190
- if (!propertyName) {
191
- continue;
192
- }
193
- target[propertyName] = ctx.row[column];
194
- }
195
- return target;
196
- }
197
- }
198
- exports.RowMapping = RowMapping;
199
- exports.EntityMapping = RowMapping;
200
- /**
201
- * Creates a new row mapping from the provided options.
202
- */
203
- function rowMapping(options) {
204
- return new RowMapping(options);
205
- }
206
- /**
207
- * @deprecated Use {@link rowMapping} instead.
208
- */
209
- function entity(options) {
210
- return rowMapping(options);
211
- }
212
- /**
213
- * Builds a column map by prefixing each property with the provided prefix and
214
- * converting property names to snake_case.
215
- */
216
- function columnMapFromPrefix(prefix, properties) {
217
- const columnMap = {};
218
- for (const property of properties) {
219
- columnMap[property] = `${prefix}${toSnakeCase(String(property))}`;
220
- }
221
- return columnMap;
222
- }
223
- /**
224
- * Executes SQL via the provided executor and maps the rows using the supplied mapping.
225
- */
226
- class Mapper {
227
- constructor(executor, defaults = undefined) {
228
- this.executor = executor;
229
- this.defaults = defaults;
230
- }
231
- async query(sql, params = [], mappingOrOptions) {
232
- const rows = await this.executor(sql, params);
233
- if (mappingOrOptions instanceof RowMapping) {
234
- return mapRows(rows, mappingOrOptions);
235
- }
236
- return mapSimpleRows(rows, mergeMapperOptions(this.defaults, mappingOrOptions));
237
- }
238
- async queryOne(sql, params = [], mappingOrOptions) {
239
- // Narrow mappingOrOptions before invoking the overload so the compiler can
240
- // select the expected signature.
241
- if (mappingOrOptions instanceof RowMapping) {
242
- const rows = await this.query(sql, params, mappingOrOptions);
243
- return rows[0];
244
- }
245
- const rows = await this.query(sql, params, mappingOrOptions);
246
- return rows[0];
247
- }
248
- /**
249
- * Binds a structured row mapping to a reader that exposes `list` and `one`.
250
- */
251
- bind(mapping) {
252
- const createReader = (currentValidator) => {
253
- const validateRows = (rows) => applyRowValidator(rows, currentValidator);
254
- const validateValue = (value) => applyScalarValidator(value, currentValidator);
255
- const validator = (nextValidator) => {
256
- const normalizedNext = normalizeReaderValidator(nextValidator);
257
- const composed = composeValidators(currentValidator, normalizedNext);
258
- return createReader(composed);
259
- };
260
- return {
261
- list: async (sql, params = []) => {
262
- const rows = await this.query(sql, params, mapping);
263
- return validateRows(rows);
264
- },
265
- one: async (sql, params = []) => {
266
- const rows = await this.query(sql, params, mapping);
267
- const row = expectExactlyOneRow(rows);
268
- return validateValue(row);
269
- },
270
- scalar: async (sql, params = []) => {
271
- const value = await readScalarValue(this.executor, sql, params);
272
- return value;
273
- },
274
- validator,
275
- };
276
- };
277
- return createReader();
278
- }
279
- }
280
- exports.Mapper = Mapper;
281
- /**
282
- * This package maps rows and does not manage DB drivers.
283
- * Inject a query executor rather than wiring connections inside the mapper.
284
- */
285
- function createMapper(executor, defaults) {
286
- return new Mapper(executor, defaults);
287
- }
288
- /**
289
- * Creates a mapper using the supplied executor and user defaults.
290
- * This helper is the recommended entry point when wiring an executor because
291
- * it clearly signals where defaults are configured.
292
- */
293
- function createMapperFromExecutor(executor, defaults) {
294
- return createMapper(executor, defaults);
295
- }
296
- /**
297
- * Creates a reader-bound mapper using the supplied executor.
298
- * When no overrides are provided, the app-like preset is applied so snake_case
299
- * columns are normalized to camelCase keys by default.
300
- */
301
- function createReader(executor, options) {
302
- const resolvedOptions = {
303
- ...exports.mapperPresets.appLike(),
304
- ...options,
305
- };
306
- if ((options === null || options === void 0 ? void 0 : options.idKeysAsString) === undefined) {
307
- resolvedOptions.idKeysAsString = false;
308
- }
309
- return createMapperFromExecutor(executor, resolvedOptions);
310
- }
311
- /**
312
- * Normalizes an executor returning `{ rows }` so it can be consumed by the mapper.
313
- */
314
- function toRowsExecutor(executorOrTarget, methodName) {
315
- if (typeof executorOrTarget === 'function') {
316
- return async (sql, params) => {
317
- const result = await executorOrTarget(sql, params);
318
- if (Array.isArray(result)) {
319
- return result;
320
- }
321
- if ('rows' in result) {
322
- return result.rows;
323
- }
324
- return [];
325
- };
326
- }
327
- const executor = async (sql, params) => {
328
- if (!methodName) {
329
- throw new Error('Method name is required when passing an object/key pair');
330
- }
331
- const method = executorOrTarget[methodName];
332
- if (typeof method !== 'function') {
333
- throw new Error(`Method "${methodName}" not found on target`);
334
- }
335
- const result = await method.call(executorOrTarget, sql, params);
336
- if (Array.isArray(result)) {
337
- return result;
338
- }
339
- if (result && typeof result === 'object' && 'rows' in result) {
340
- return result.rows;
341
- }
342
- return [];
343
- };
344
- return executor;
345
- }
346
- /**
347
- * Maps a pre-fetched row array into typed objects defined by a row mapping.
348
- * Row values remain `unknown`, and the mapper only applies the general-purpose
349
- * coercion rules declared in `coerceColumnValue`.
350
- */
351
- function mapRows(rows, mapping) {
352
- const cache = new Map();
353
- const roots = new Map();
354
- // Deduplicate root entities by key so joined rows map back to the same object.
355
- for (const row of rows) {
356
- const ctx = createRowContext(row);
357
- const keyString = normalizeRowKey(mapping.readKeyValue(ctx), mapping, ctx);
358
- const entity = buildEntity(ctx, mapping, cache, new Set(), [], undefined);
359
- // Always hydrate parents per row; cache reuses existing entity references.
360
- if (!roots.has(keyString)) {
361
- roots.set(keyString, entity);
362
- }
363
- }
364
- return Array.from(roots.values());
365
- }
366
- function expectExactlyOneRow(rows) {
367
- if (rows.length === 0) {
368
- throw new Error('expected exactly one row but received none.');
369
- }
370
- if (rows.length > 1) {
371
- throw new Error(`expected exactly one row but received ${rows.length}.`);
372
- }
373
- return rows[0];
374
- }
375
- function readScalarValue(executor, sql, params) {
376
- return executor(sql, params).then((rows) => extractScalar(rows));
377
- }
378
- function extractScalar(rows) {
379
- const row = expectExactlyOneRow(rows);
380
- const columns = Object.keys(row);
381
- if (columns.length !== 1) {
382
- throw new Error(`expected exactly one column but received ${columns.length}.`);
383
- }
384
- return row[columns[0]];
385
- }
386
- function applyRowValidator(rows, validator) {
387
- if (!validator) {
388
- return rows;
389
- }
390
- return rows.map((row) => validator(row));
391
- }
392
- function applyScalarValidator(value, validator) {
393
- if (!validator) {
394
- return value;
395
- }
396
- return validator(value);
397
- }
398
- function isReaderSchemaLike(value) {
399
- if (typeof value !== 'object' || value === null) {
400
- return false;
401
- }
402
- const candidate = value;
403
- if ('parse' in candidate && typeof candidate.parse === 'function') {
404
- return true;
405
- }
406
- if ('assert' in candidate && typeof candidate.assert === 'function') {
407
- return true;
408
- }
409
- return false;
410
- }
411
- function hasParse(value) {
412
- return typeof value.parse === 'function';
413
- }
414
- function hasAssert(value) {
415
- return typeof value.assert === 'function';
416
- }
417
- const readerValidatorSchemaError = 'reader.validator expects a function or an object with parse/assert methods.';
418
- function normalizeReaderValidator(validator) {
419
- if (!validator) {
420
- return undefined;
421
- }
422
- if (typeof validator === 'function') {
423
- return validator;
424
- }
425
- if (!isReaderSchemaLike(validator)) {
426
- throw new Error(readerValidatorSchemaError);
427
- }
428
- if (hasParse(validator)) {
429
- const parser = validator.parse;
430
- return (value) => parser(value);
431
- }
432
- if (hasAssert(validator)) {
433
- const assertFn = validator.assert;
434
- return (value) => {
435
- const asserted = assertFn(value);
436
- if (asserted !== undefined) {
437
- return asserted;
438
- }
439
- return value;
440
- };
441
- }
442
- throw new Error(readerValidatorSchemaError);
443
- }
444
- function composeValidators(first, second) {
445
- if (!first) {
446
- return second;
447
- }
448
- if (!second) {
449
- return first;
450
- }
451
- return (value) => second(first(value));
452
- }
453
- const builtinMapperOptions = {
454
- keyTransform: 'snake_to_camel',
455
- idKeysAsString: true,
456
- };
457
- function mergeTypeHints(defaults, overrides) {
458
- if (!defaults && !overrides) {
459
- return undefined;
460
- }
461
- return {
462
- ...(defaults !== null && defaults !== void 0 ? defaults : {}),
463
- ...(overrides !== null && overrides !== void 0 ? overrides : {}),
464
- };
465
- }
466
- function mergeMapperOptions(defaults, overrides) {
467
- var _a, _b, _c, _d, _e, _f;
468
- const keyTransform = (_b = (_a = overrides === null || overrides === void 0 ? void 0 : overrides.keyTransform) !== null && _a !== void 0 ? _a : defaults === null || defaults === void 0 ? void 0 : defaults.keyTransform) !== null && _b !== void 0 ? _b : builtinMapperOptions.keyTransform;
469
- const coerceDates = (_c = overrides === null || overrides === void 0 ? void 0 : overrides.coerceDates) !== null && _c !== void 0 ? _c : defaults === null || defaults === void 0 ? void 0 : defaults.coerceDates;
470
- const coerceFn = (_d = overrides === null || overrides === void 0 ? void 0 : overrides.coerceFn) !== null && _d !== void 0 ? _d : defaults === null || defaults === void 0 ? void 0 : defaults.coerceFn;
471
- const typeHints = mergeTypeHints(defaults === null || defaults === void 0 ? void 0 : defaults.typeHints, overrides === null || overrides === void 0 ? void 0 : overrides.typeHints);
472
- const idKeysAsString = (_f = (_e = overrides === null || overrides === void 0 ? void 0 : overrides.idKeysAsString) !== null && _e !== void 0 ? _e : defaults === null || defaults === void 0 ? void 0 : defaults.idKeysAsString) !== null && _f !== void 0 ? _f : builtinMapperOptions.idKeysAsString;
473
- return {
474
- keyTransform,
475
- coerceDates,
476
- coerceFn,
477
- typeHints,
478
- idKeysAsString,
479
- };
480
- }
481
- function createKeyTransformFn(transform) {
482
- if (!transform || transform === 'snake_to_camel') {
483
- return snakeToCamel;
484
- }
485
- if (transform === 'none') {
486
- return (column) => column;
487
- }
488
- if (typeof transform === 'function') {
489
- return transform;
490
- }
491
- return snakeToCamel;
492
- }
493
- /**
494
- * Maps pre-fetched rows into typed DTOs using the simple map preset, honoring key transforms, type hints, and optional coercion settings.
495
- *
496
- * @template T Target DTO shape.
497
- * @param rows Rows produced by the SQL executor.
498
- * @param options Optional overrides that control key normalization, coercion, and type hints.
499
- * @returns An array of `T` instances synthesized from `rows`.
500
- */
501
- function mapSimpleRows(rows, options) {
502
- var _a, _b, _c;
503
- const coerceFn = options === null || options === void 0 ? void 0 : options.coerceFn;
504
- const keyTransform = (_a = options === null || options === void 0 ? void 0 : options.keyTransform) !== null && _a !== void 0 ? _a : builtinMapperOptions.keyTransform;
505
- const keyTransformFn = createKeyTransformFn(keyTransform);
506
- const shouldCoerceDates = (_b = options === null || options === void 0 ? void 0 : options.coerceDates) !== null && _b !== void 0 ? _b : false;
507
- const typeHints = options === null || options === void 0 ? void 0 : options.typeHints;
508
- const idKeysAsString = (_c = options === null || options === void 0 ? void 0 : options.idKeysAsString) !== null && _c !== void 0 ? _c : builtinMapperOptions.idKeysAsString;
509
- return rows.map((row) => {
510
- var _a;
511
- const dto = {};
512
- const seen = new Map();
513
- // Map each column to a camelCase key while detecting naming collisions.
514
- for (const [column, rawValue] of Object.entries(row)) {
515
- const propertyName = keyTransformFn(column);
516
- if (!propertyName) {
517
- continue;
518
- }
519
- const existing = seen.get(propertyName);
520
- if (existing && existing !== column) {
521
- throw new Error(`Column "${column}" conflicts with "${existing}" after camelCase normalization ("${propertyName}").`);
522
- }
523
- seen.set(propertyName, column);
524
- const columnHint = typeHints === null || typeHints === void 0 ? void 0 : typeHints[propertyName];
525
- let normalizedValue = rawValue;
526
- const shouldStringifyIdentifier = !columnHint && idKeysAsString && isIdentifierProperty(propertyName);
527
- if (columnHint) {
528
- normalizedValue = applyTypeHint(normalizedValue, columnHint, propertyName);
529
- }
530
- else if (shouldCoerceDates && typeof normalizedValue === 'string') {
531
- normalizedValue = coerceDateValue(normalizedValue);
532
- }
533
- if (shouldStringifyIdentifier) {
534
- normalizedValue = stringifyIdentifierValue(normalizedValue);
535
- }
536
- const coercedValue = (_a = coerceFn === null || coerceFn === void 0 ? void 0 : coerceFn({
537
- key: propertyName,
538
- sourceKey: column,
539
- value: normalizedValue,
540
- })) !== null && _a !== void 0 ? _a : normalizedValue;
541
- dto[propertyName] = shouldStringifyIdentifier
542
- ? stringifyIdentifierValue(coercedValue)
543
- : coercedValue;
544
- }
545
- return dto;
546
- });
547
- }
548
- /**
549
- * Date coercion helper that mirrors the ISO-with-timezone restriction used by the
550
- * structured mapper. Only strings already matching the ISO 8601 timestamp-with-offset
551
- * pattern are converted to Date.
552
- */
553
- function coerceDateValue(value) {
554
- const trimmed = value.trim();
555
- let normalized = trimmed.includes(' ')
556
- ? trimmed.replace(' ', 'T')
557
- : trimmed;
558
- if (/[+-]\d{2}$/.test(normalized)) {
559
- normalized = `${normalized}:00`;
560
- }
561
- if (isoDateTimeRegex.test(normalized)) {
562
- const parsed = Date.parse(normalized);
563
- if (!Number.isNaN(parsed)) {
564
- return new Date(parsed);
565
- }
566
- }
567
- return value;
568
- }
569
- function applyTypeHint(value, hint, propertyName) {
570
- if (value === undefined || value === null) {
571
- return value;
572
- }
573
- switch (hint) {
574
- case 'string':
575
- if (typeof value === 'string') {
576
- return value;
577
- }
578
- if (typeof value === 'number' || typeof value === 'bigint') {
579
- return String(value);
580
- }
581
- return value;
582
- case 'number':
583
- if (typeof value === 'number') {
584
- return value;
585
- }
586
- if (typeof value === 'string') {
587
- const parsed = Number(value);
588
- if (!Number.isNaN(parsed)) {
589
- return parsed;
590
- }
591
- }
592
- return value;
593
- case 'boolean':
594
- if (typeof value === 'boolean') {
595
- return value;
596
- }
597
- if (typeof value === 'string') {
598
- const normalized = value.trim().toLowerCase();
599
- if (normalized === 'true') {
600
- return true;
601
- }
602
- if (normalized === 'false') {
603
- return false;
604
- }
605
- }
606
- return value;
607
- case 'date':
608
- if (value instanceof Date) {
609
- return value;
610
- }
611
- if (typeof value === 'string') {
612
- const coerced = coerceDateValue(value);
613
- if (coerced instanceof Date) {
614
- return coerced;
615
- }
616
- }
617
- return value;
618
- case 'bigint':
619
- if (typeof value === 'bigint') {
620
- return value;
621
- }
622
- if (typeof value === 'number') {
623
- return BigInt(value);
624
- }
625
- if (typeof value === 'string') {
626
- try {
627
- return BigInt(value);
628
- }
629
- catch {
630
- throw new Error(`Type hint 'bigint' failed for "${propertyName !== null && propertyName !== void 0 ? propertyName : 'value'}": "${value}" is not a valid bigint.`);
631
- }
632
- }
633
- return value;
634
- }
635
- }
636
- function isIdentifierProperty(propertyName) {
637
- if (propertyName === 'id') {
638
- return true;
639
- }
640
- if (!propertyName.endsWith('Id')) {
641
- return false;
642
- }
643
- const firstChar = propertyName.charAt(0);
644
- if (firstChar !== firstChar.toLowerCase()) {
645
- return false;
646
- }
647
- // Only treat camelCase names ending in 'Id' (uppercase I, lowercase d) as identifiers.
648
- return true;
649
- }
650
- function stringifyIdentifierValue(value) {
651
- if (value === undefined || value === null) {
652
- return value;
653
- }
654
- if (typeof value === 'string') {
655
- return value;
656
- }
657
- if (typeof value === 'number' || typeof value === 'bigint') {
658
- return String(value);
659
- }
660
- return value;
661
- }
662
- function buildEntity(ctx, mapping, cache, visited, stack, relation) {
663
- const { entity, isNew, keyString } = getOrCreateEntity(ctx, mapping, cache);
664
- const visitKey = `${mapping.name}:${keyString}`;
665
- const currentFrame = {
666
- entity: mapping.name,
667
- relation,
668
- key: keyString,
669
- };
670
- if (visited.has(visitKey)) {
671
- const cyclePath = [...stack, currentFrame]
672
- .map((frame) => formatFrame(frame))
673
- .join(' -> ');
674
- throw new Error(`Circular row mapping detected: ${cyclePath}`);
675
- }
676
- visited.add(visitKey);
677
- stack.push(currentFrame);
678
- try {
679
- if (isNew) {
680
- mapping.assignFields(entity, ctx);
681
- }
682
- hydrateParents(entity, ctx, mapping, cache, visited, stack);
683
- return entity;
684
- }
685
- finally {
686
- visited.delete(visitKey);
687
- stack.pop();
688
- }
689
- }
690
- function getOrCreateEntity(ctx, mapping, cache) {
691
- const keyString = normalizeRowKey(mapping.readKeyValue(ctx), mapping, ctx);
692
- let entitySet = cache.get(mapping);
693
- if (!entitySet) {
694
- entitySet = new Map();
695
- cache.set(mapping, entitySet);
696
- }
697
- const existing = entitySet.get(keyString);
698
- if (existing) {
699
- return { entity: existing, isNew: false, keyString };
700
- }
701
- const newEntity = {};
702
- entitySet.set(keyString, newEntity);
703
- return { entity: newEntity, isNew: true, keyString };
704
- }
705
- function hydrateParents(entity, ctx, mapping, cache, visited, stack) {
706
- for (const parent of mapping.parents) {
707
- const localColumn = mapping.resolveColumnName(parent.localKey);
708
- const normalizedLocalColumn = localColumn.toLowerCase();
709
- if (!ctx.normalizedColumns.has(normalizedLocalColumn)) {
710
- missingLocalKey(mapping.name, parent.propertyName, localColumn, parent.parent.name);
711
- }
712
- const localKeyValue = getRowValue(ctx, localColumn);
713
- if (localKeyValue === undefined || localKeyValue === null) {
714
- if (parent.optional) {
715
- continue;
716
- }
717
- localKeyIsNull(mapping.name, parent.propertyName, localColumn, parent.parent.name);
718
- }
719
- const parentKeyColumns = resolveParentKeyColumns(parent.parent);
720
- for (const parentKeyColumn of parentKeyColumns) {
721
- const normalizedParentKeyColumn = parentKeyColumn.toLowerCase();
722
- if (!ctx.normalizedColumns.has(normalizedParentKeyColumn)) {
723
- missingParentKeyColumn(mapping.name, parent.propertyName, parent.parent.name, parentKeyColumn);
724
- }
725
- }
726
- const missingValueColumn = parentKeyColumns.find((parentKeyColumn) => {
727
- const parentKeyValue = getRowValue(ctx, parentKeyColumn);
728
- return parentKeyValue === undefined || parentKeyValue === null;
729
- });
730
- if (missingValueColumn) {
731
- if (parent.optional) {
732
- continue;
733
- }
734
- throw new Error(`Missing key column "${missingValueColumn}" for parent mapping "${parent.parent.name}"`);
735
- }
736
- const parentEntity = buildEntity(ctx, parent.parent, cache, visited, stack, parent.propertyName);
737
- entity[parent.propertyName] = parentEntity;
738
- }
739
- }
740
- function resolveParentKeyColumns(mapping) {
741
- const explicitColumns = mapping.keyColumnNames;
742
- if (explicitColumns && explicitColumns.length > 0) {
743
- return explicitColumns;
744
- }
745
- const descriptor = mapping.key;
746
- if (isKeyExtractor(descriptor)) {
747
- throw new Error(`RowMapping "${mapping.name}" exposes a derived key function that cannot be referenced by belongsTo relations.`);
748
- }
749
- if (Array.isArray(descriptor)) {
750
- if (descriptor.length === 0) {
751
- throw new Error(`RowMapping "${mapping.name}" composite key must include at least one column.`);
752
- }
753
- return descriptor.map((column) => mapping.resolveColumnName(column));
754
- }
755
- if (typeof descriptor !== 'string') {
756
- throw new Error(`RowMapping "${mapping.name}" key descriptor must be a property name when not derived or composite.`);
757
- }
758
- return [mapping.resolveColumnName(descriptor)];
759
- }
760
- function createRowContext(row) {
761
- const normalized = new Map();
762
- for (const column of Object.keys(row)) {
763
- normalized.set(column.toLowerCase(), column);
764
- }
765
- return { row, normalizedColumns: normalized };
766
- }
767
- function getRowValue(ctx, columnName) {
768
- const actual = ctx.normalizedColumns.get(columnName.toLowerCase());
769
- if (!actual) {
770
- return undefined;
771
- }
772
- return ctx.row[actual];
773
- }
774
- function lookupRowColumn(ctx, columnName) {
775
- const normalizedKey = columnName.toLowerCase();
776
- const actual = ctx.normalizedColumns.get(normalizedKey);
777
- if (actual) {
778
- return { found: true, value: ctx.row[actual] };
779
- }
780
- for (const column of Object.keys(ctx.row)) {
781
- if (column.toLowerCase() === normalizedKey) {
782
- return { found: true, value: ctx.row[column] };
783
- }
784
- }
785
- return { found: false, value: undefined };
786
- }
787
- function coerceColumnValue(value) {
788
- if (typeof value !== 'string') {
789
- return value;
790
- }
791
- const trimmed = value.trim();
792
- if (!trimmed) {
793
- return value;
794
- }
795
- if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
796
- const numeric = Number(trimmed);
797
- if (!Number.isNaN(numeric)) {
798
- return numeric;
799
- }
800
- }
801
- const lower = trimmed.toLowerCase();
802
- if (lower === 'true' || lower === 'false') {
803
- return lower === 'true';
804
- }
805
- // Mapper should stay DBMS-agnostic; Date coercion is intentionally limited to ISO 8601 datetime strings that include a timezone designator.
806
- const isIsoDateTime = isoDateTimeRegex.test(trimmed);
807
- if (isIsoDateTime) {
808
- const parsed = Date.parse(trimmed);
809
- if (!Number.isNaN(parsed)) {
810
- return new Date(parsed);
811
- }
812
- }
813
- return value;
814
- }
815
- const isoDateTimeRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(?:\.\d{1,9})?)?(Z|[+-]\d{2}:?\d{2})$/;
816
- function normalizeRowKey(value, mapping, ctx) {
817
- var _a;
818
- if (value === undefined || value === null) {
819
- throwMissingKeyValue(mapping, ctx);
820
- }
821
- if (!Array.isArray(value)) {
822
- return serializeKeyComponent(value, mapping, ctx, 0, (_a = mapping.keyColumnNames) === null || _a === void 0 ? void 0 : _a[0]);
823
- }
824
- if (value.length === 0) {
825
- throw new Error(`Composite key for mapping "${mapping.name}" (${mapping.keyDescription}) must include at least one value.`);
826
- }
827
- return value
828
- .map((component, index) => {
829
- var _a;
830
- return serializeKeyComponent(component, mapping, ctx, index, (_a = mapping.keyColumnNames) === null || _a === void 0 ? void 0 : _a[index]);
831
- })
832
- .join('|');
833
- }
834
- function serializeKeyComponent(component, mapping, ctx, index, columnName) {
835
- var _a;
836
- if (component === undefined || component === null) {
837
- throwMissingKeyValue(mapping, ctx, columnName !== null && columnName !== void 0 ? columnName : (_a = mapping.keyColumnNames) === null || _a === void 0 ? void 0 : _a[index]);
838
- }
839
- if (typeof component === 'string') {
840
- return `s:${component.length}:${component}`;
841
- }
842
- if (typeof component === 'number') {
843
- if (!Number.isFinite(component)) {
844
- throw new Error(`Row mapping "${mapping.name}" key component ${index + 1} must be a finite number.`);
845
- }
846
- return `n:${component}`;
847
- }
848
- if (typeof component === 'bigint') {
849
- return `b:${component}`;
850
- }
851
- throw new Error(`Row mapping "${mapping.name}" key component ${index + 1} must be a string, number, or bigint.`);
852
- }
853
- function throwMissingKeyColumn(mapping, columnName, ctx) {
854
- const descriptor = mapping.keyDescription;
855
- throw new Error(`${mapping.name}: Missing key column "${columnName}" (${descriptor}) in row ${JSON.stringify(ctx.row)}`);
856
- }
857
- function throwMissingKeyValue(mapping, ctx, columnName) {
858
- const descriptor = mapping.keyDescription;
859
- const columnInfo = columnName
860
- ? ` for column "${columnName}"`
861
- : '';
862
- throw new Error(`${mapping.name}: Missing key value${columnInfo} (${descriptor}) in row ${JSON.stringify(ctx.row)}`);
863
- }
864
- function missingLocalKey(mappingName, propertyName, localColumn, parentName) {
865
- throw new Error(`Missing local key column "${localColumn}" for relation "${propertyName}" on ${mappingName} (parent ${parentName})`);
866
- }
867
- function missingParentKeyColumn(mappingName, propertyName, parentName, parentKeyColumn) {
868
- throw new Error(`Missing key column "${parentKeyColumn}" for parent "${parentName}" relation "${propertyName}" on ${mappingName}`);
869
- }
870
- function localKeyIsNull(mappingName, propertyName, localColumn, parentName) {
871
- throw new Error(`Local key column "${localColumn}" is null for relation "${propertyName}" on ${mappingName} (parent ${parentName})`);
872
- }
873
- function formatFrame(frame) {
874
- const relationSuffix = frame.relation ? `.${frame.relation}` : '';
875
- return `${frame.entity}${relationSuffix}(${frame.key})`;
876
- }
877
- function snakeToCamel(raw) {
878
- const trimmed = raw.trim();
879
- if (!trimmed) {
880
- return '';
881
- }
882
- if (trimmed.includes('_')) {
883
- return toCamelCase(trimmed);
884
- }
885
- if (trimmed === trimmed.toUpperCase()) {
886
- return trimmed.toLowerCase();
887
- }
888
- return `${trimmed.charAt(0).toLowerCase()}${trimmed.slice(1)}`;
889
- }
890
- function toCamelCase(value) {
891
- return value
892
- .split('_')
893
- .filter(Boolean)
894
- .map((segment, index) => index === 0
895
- ? segment.toLowerCase()
896
- : `${segment.charAt(0).toUpperCase()}${segment.slice(1).toLowerCase()}`)
897
- .join('');
898
- }
899
- function toSnakeCase(value) {
900
- return value
901
- .replace(/([A-Z])/g, '_$1')
902
- .replace(/__+/g, '_')
903
- .toLowerCase()
904
- .replace(/^_+/, '');
905
- }
906
- exports.__internal = {
907
- normalizeKeyValue: internal_1.normalizeKeyValue,
908
- normalizeKeyFromRow: internal_1.normalizeKeyFromRow,
909
- };
910
- //# sourceMappingURL=index.js.map