@metaobjectsdev/runtime-ts 0.6.0 → 0.7.0-rc.10

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.
@@ -13,7 +13,10 @@ import {
13
13
  resolvePkFields, compileFilter,
14
14
  type Filter, type QueryOpts,
15
15
  } from "./query-builder.js";
16
- import { buildNameMap, resolveTableName, type EntityNameMap } from "@metaobjectsdev/metadata";
16
+ import {
17
+ buildNameMap, resolveTableName, DEFAULT_COLUMN_NAMING_STRATEGY,
18
+ type ColumnNamingStrategy, type EntityNameMap,
19
+ } from "@metaobjectsdev/metadata";
17
20
  import { coerceRowOnRead, coerceRowOnWrite } from "./type-coercer.js";
18
21
  import { decodeRef, encodeRef } from "./ref-codec.js";
19
22
  import { runValidators } from "./validator-runner.js";
@@ -37,6 +40,12 @@ import { VALID_ENTITY_NAME, DEFAULT_IF_MISSING } from "./constants.js";
37
40
  export interface ObjectManagerOptions {
38
41
  metadata: MetaData;
39
42
  driver: PersistenceDriver;
43
+ /**
44
+ * Column-naming strategy for fields with no `@column` override. Defaults to
45
+ * `"snake_case"`. Must agree with the codegen / migration config that
46
+ * created the schema (a mismatch yields columns the runtime can't address).
47
+ */
48
+ columnNamingStrategy?: ColumnNamingStrategy;
40
49
  }
41
50
 
42
51
  export interface ReadOpts extends QueryOpts {
@@ -54,17 +63,19 @@ export interface WriteOpts {
54
63
  export class ObjectManager {
55
64
  private readonly metadata: MetaData;
56
65
  private readonly driver: PersistenceDriver;
66
+ private readonly columnNamingStrategy: ColumnNamingStrategy;
57
67
  private readonly nameMapCache = new Map<string, EntityNameMap>();
58
68
 
59
69
  constructor(opts: ObjectManagerOptions) {
60
70
  this.metadata = opts.metadata;
61
71
  this.driver = opts.driver;
72
+ this.columnNamingStrategy = opts.columnNamingStrategy ?? DEFAULT_COLUMN_NAMING_STRATEGY;
62
73
  }
63
74
 
64
75
  private nameMap(entity: MetaData): EntityNameMap {
65
76
  let m = this.nameMapCache.get(entity.name);
66
77
  if (!m) {
67
- m = buildNameMap(entity);
78
+ m = buildNameMap(entity, this.columnNamingStrategy);
68
79
  this.nameMapCache.set(entity.name, m);
69
80
  }
70
81
  return m;
@@ -79,7 +90,7 @@ export class ObjectManager {
79
90
  async findFirst(entityName: string, filter: Filter, opts: ReadOpts = {}): Promise<Row | null> {
80
91
  const entity = this.requireEntity(entityName);
81
92
  const driver = opts.tx ?? this.driver;
82
- const spec = buildSelectSpec(entity, filter, { ...opts, limit: 1 });
93
+ const spec = buildSelectSpec(entity, filter, { ...opts, limit: 1 }, undefined, this.columnNamingStrategy);
83
94
  const row = await driver.selectOne(spec);
84
95
  if (row === null) return null;
85
96
  const jsRow = this.toJsRow(entity, row);
@@ -92,7 +103,7 @@ export class ObjectManager {
92
103
  async findMany(entityName: string, filter?: Filter, opts: ReadOpts = {}): Promise<Row[]> {
93
104
  const entity = this.requireEntity(entityName);
94
105
  const driver = opts.tx ?? this.driver;
95
- const spec = buildSelectSpec(entity, filter, opts);
106
+ const spec = buildSelectSpec(entity, filter, opts, undefined, this.columnNamingStrategy);
96
107
  const rows = (await driver.selectMany(spec)).map((r) => this.toJsRow(entity, r));
97
108
  if (opts.include && opts.include.length > 0) {
98
109
  await this.attachIncludes(entity, rows, opts.include, driver);
@@ -103,7 +114,7 @@ export class ObjectManager {
103
114
  async count(entityName: string, filter?: Filter, opts: Pick<ReadOpts, "tx"> = {}): Promise<number> {
104
115
  const entity = this.requireEntity(entityName);
105
116
  const driver = opts.tx ?? this.driver;
106
- return driver.count(buildCountSpec(entity, filter));
117
+ return driver.count(buildCountSpec(entity, filter, this.columnNamingStrategy));
107
118
  }
108
119
 
109
120
  async load(refString: string): Promise<Row | null> {
@@ -144,7 +155,7 @@ export class ObjectManager {
144
155
  }
145
156
 
146
157
  const coerced = coerceRowOnWrite(entity, merged, driver.dialect);
147
- const spec = buildInsertSpec(entity, coerced);
158
+ const spec = buildInsertSpec(entity, coerced, this.columnNamingStrategy);
148
159
  const dbRow = await driver.insert(spec);
149
160
  return this.toJsRow(entity, dbRow);
150
161
  }
@@ -162,7 +173,7 @@ export class ObjectManager {
162
173
  }
163
174
 
164
175
  const coerced = coerceRowOnWrite(entity, restricted, driver.dialect);
165
- const spec = buildUpdateSpec(entity, coerced, id);
176
+ const spec = buildUpdateSpec(entity, coerced, id, this.columnNamingStrategy);
166
177
  const dbRow = await driver.update(spec);
167
178
  if (dbRow === null) {
168
179
  const mode = opts.ifMissing ?? DEFAULT_IF_MISSING;
@@ -175,7 +186,7 @@ export class ObjectManager {
175
186
  async delete(entityName: string, id: unknown, opts: WriteOpts = {}): Promise<boolean> {
176
187
  const entity = this.requireEntity(entityName);
177
188
  const driver = opts.tx ?? this.driver;
178
- const spec = buildDeleteSpec(entity, id);
189
+ const spec = buildDeleteSpec(entity, id, this.columnNamingStrategy);
179
190
  const n = await driver.delete(spec);
180
191
  if (n === 0) {
181
192
  const mode = opts.ifMissing ?? DEFAULT_IF_MISSING;
@@ -228,7 +239,7 @@ export class ObjectManager {
228
239
  const spec: UpdateManySpec = {
229
240
  table: resolveTableName(entity),
230
241
  values: this.toDbRow(entity, coerced),
231
- where: requireNonEmptyFilter(entity, filter, "updateMany"),
242
+ where: requireNonEmptyFilter(entity, filter, "updateMany", this.columnNamingStrategy),
232
243
  };
233
244
  return driver.updateMany(spec);
234
245
  }
@@ -238,14 +249,16 @@ export class ObjectManager {
238
249
  const driver = opts.tx ?? this.driver;
239
250
  const spec: DeleteManySpec = {
240
251
  table: resolveTableName(entity),
241
- where: requireNonEmptyFilter(entity, filter, "deleteMany"),
252
+ where: requireNonEmptyFilter(entity, filter, "deleteMany", this.columnNamingStrategy),
242
253
  };
243
254
  return driver.deleteMany(spec);
244
255
  }
245
256
 
246
257
  async transaction<T>(fn: (txOm: ObjectManager) => Promise<T>): Promise<T> {
247
258
  return this.driver.transaction(async (txDriver) => {
248
- const txOm = new ObjectManager({ metadata: this.metadata, driver: txDriver });
259
+ const txOm = new ObjectManager({
260
+ metadata: this.metadata, driver: txDriver, columnNamingStrategy: this.columnNamingStrategy,
261
+ });
249
262
  return fn(txOm);
250
263
  });
251
264
  }
@@ -258,7 +271,7 @@ export class ObjectManager {
258
271
  const n2m = resolveN2mDescriptor(entity, relationName, this.metadata);
259
272
  if (n2m !== null) {
260
273
  const target = this.requireEntity(n2m.targetEntityName);
261
- const { joinSpec, makeTargetSpec } = buildN2mLazySpecs(n2m, record, this.metadata);
274
+ const { joinSpec, makeTargetSpec } = buildN2mLazySpecs(n2m, record, this.metadata, this.columnNamingStrategy);
262
275
  const joinRows = await driver.selectMany(joinSpec);
263
276
  const targetSpec = makeTargetSpec(joinRows);
264
277
  if (targetSpec === null) return [];
@@ -268,7 +281,7 @@ export class ObjectManager {
268
281
 
269
282
  const desc = resolveRelationDescriptor(entity, relationName, this.metadata);
270
283
  const target = this.requireEntity(desc.targetEntityName);
271
- const spec = buildLazyRelateSpec(desc, record, this.metadata);
284
+ const spec = buildLazyRelateSpec(desc, record, this.metadata, this.columnNamingStrategy);
272
285
  if (spec === null) return desc.cardinality === "one" ? null : [];
273
286
  if (desc.cardinality === "one") {
274
287
  const row = await driver.selectOne(spec);
@@ -311,15 +324,15 @@ export class ObjectManager {
311
324
  const n2m = resolveN2mDescriptor(entity, inc, this.metadata);
312
325
  if (n2m !== null) {
313
326
  const target = this.requireEntity(n2m.targetEntityName);
314
- const { joinSpec, makeTargetSpec } = buildN2mBatchSpecs(n2m, records, this.metadata);
327
+ const { joinSpec, makeTargetSpec } = buildN2mBatchSpecs(n2m, records, this.metadata, this.columnNamingStrategy);
315
328
  const joinRows = await driver.selectMany(joinSpec);
316
329
  const targetSpec = makeTargetSpec(joinRows);
317
330
  const targetRows = targetSpec === null ? [] : (await driver.selectMany(targetSpec)).map((r) => this.toJsRow(target, r));
318
331
 
319
332
  const sourcePk = resolvePkFields(entity)[0]!;
320
333
  const joinEntity = this.requireEntity(n2m.joinEntityName);
321
- const sourceJoinDbCol = resolveJoinColumnName(joinEntity, n2m.sourceJoinField);
322
- const targetJoinDbCol = resolveJoinColumnName(joinEntity, n2m.targetJoinField);
334
+ const sourceJoinDbCol = resolveJoinColumnName(joinEntity, n2m.sourceJoinField, this.columnNamingStrategy);
335
+ const targetJoinDbCol = resolveJoinColumnName(joinEntity, n2m.targetJoinField, this.columnNamingStrategy);
323
336
  const targetPk = resolvePkFields(target)[0]!;
324
337
  const targetById = new Map(targetRows.map((r) => [r[targetPk], r]));
325
338
  const grouped = new Map<unknown, Row[]>();
@@ -336,7 +349,7 @@ export class ObjectManager {
336
349
 
337
350
  const desc = resolveRelationDescriptor(entity, inc, this.metadata);
338
351
  const target = this.requireEntity(desc.targetEntityName);
339
- const spec = buildIncludeBatchSpec(desc, records, this.metadata);
352
+ const spec = buildIncludeBatchSpec(desc, records, this.metadata, this.columnNamingStrategy);
340
353
  const targetRows = spec === null ? [] : (await driver.selectMany(spec)).map((r) => this.toJsRow(target, r));
341
354
 
342
355
  if (desc.cardinality === "one") {
@@ -412,8 +425,10 @@ export class ObjectManager {
412
425
 
413
426
  // updateMany / deleteMany with an empty filter would silently affect every row.
414
427
  // Force callers to be explicit (use $or-style or a tautology if they really mean "all").
415
- function requireNonEmptyFilter(entity: MetaData, filter: Filter, op: string): WhereClause {
416
- const where = compileFilter(entity, filter);
428
+ function requireNonEmptyFilter(
429
+ entity: MetaData, filter: Filter, op: string, strategy: ColumnNamingStrategy,
430
+ ): WhereClause {
431
+ const where = compileFilter(entity, filter, strategy);
417
432
  if (where === null) {
418
433
  throw new MetadataError(
419
434
  `${op} on '${entity.name}' requires a non-empty filter — pass an explicit condition or use a per-row loop`,
@@ -1,8 +1,9 @@
1
- import type { MetaData } from "@metaobjectsdev/metadata";
1
+ import type { ColumnNamingStrategy, MetaData } from "@metaobjectsdev/metadata";
2
2
  import {
3
3
  TYPE_FIELD, TYPE_IDENTITY,
4
4
  IDENTITY_SUBTYPE_PRIMARY,
5
5
  IDENTITY_ATTR_FIELDS,
6
+ DEFAULT_COLUMN_NAMING_STRATEGY,
6
7
  resolveTableName, resolveColumnName,
7
8
  } from "@metaobjectsdev/metadata";
8
9
  import { MetadataError } from "./errors.js";
@@ -69,11 +70,11 @@ function getField(entity: MetaData, fieldName: string): MetaData {
69
70
  return f;
70
71
  }
71
72
 
72
- function rowToColumns(entity: MetaData, data: Row): Row {
73
+ function rowToColumns(entity: MetaData, data: Row, strategy: ColumnNamingStrategy): Row {
73
74
  const out: Row = {};
74
75
  for (const [k, v] of Object.entries(data)) {
75
76
  const field = getField(entity, k);
76
- out[resolveColumnName(field)] = v;
77
+ out[resolveColumnName(field, strategy)] = v;
77
78
  }
78
79
  return out;
79
80
  }
@@ -82,29 +83,37 @@ function rowToColumns(entity: MetaData, data: Row): Row {
82
83
  * Returns null when `filter` has no clauses ({} or { $and: [] }) — meaning "match all"
83
84
  * (callers should treat null the same as omitting the where clause entirely).
84
85
  */
85
- export function compileFilter(entity: MetaData, filter: Filter): WhereClause | null {
86
+ export function compileFilter(
87
+ entity: MetaData,
88
+ filter: Filter,
89
+ strategy: ColumnNamingStrategy = DEFAULT_COLUMN_NAMING_STRATEGY,
90
+ ): WhereClause | null {
86
91
  if ("$and" in filter && Array.isArray(filter.$and)) {
87
92
  // TS can't narrow $and away from the Record branch, so the runtime check above
88
93
  // proves it's a Filter[] but the element type still needs a bridge cast.
89
94
  const andFilters = filter.$and as Filter[];
90
- const clauses = andFilters.map((f) => compileFilter(entity, f)).filter((c): c is WhereClause => c !== null);
95
+ const clauses = andFilters
96
+ .map((f) => compileFilter(entity, f, strategy))
97
+ .filter((c): c is WhereClause => c !== null);
91
98
  if (clauses.length === 0) return null;
92
99
  return clauses.length === 1 ? clauses[0]! : { kind: "and", clauses };
93
100
  }
94
101
  const entries = Object.entries(filter);
95
102
  if (entries.length === 0) return null;
96
103
  if (entries.length === 1) {
97
- return compileEntry(entity, entries[0]![0], entries[0]![1] as FilterValue);
104
+ return compileEntry(entity, entries[0]![0], entries[0]![1] as FilterValue, strategy);
98
105
  }
99
106
  return {
100
107
  kind: "and",
101
- clauses: entries.map(([k, v]) => compileEntry(entity, k, v as FilterValue)),
108
+ clauses: entries.map(([k, v]) => compileEntry(entity, k, v as FilterValue, strategy)),
102
109
  };
103
110
  }
104
111
 
105
- function compileEntry(entity: MetaData, fieldName: string, value: FilterValue): WhereClause {
112
+ function compileEntry(
113
+ entity: MetaData, fieldName: string, value: FilterValue, strategy: ColumnNamingStrategy,
114
+ ): WhereClause {
106
115
  const field = getField(entity, fieldName);
107
- const column = resolveColumnName(field);
116
+ const column = resolveColumnName(field, strategy);
108
117
 
109
118
  if (value === null) return { kind: "isNull", column, not: false };
110
119
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
@@ -137,12 +146,14 @@ function compileEntry(entity: MetaData, fieldName: string, value: FilterValue):
137
146
  throw new MetadataError(`No recognized operator on filter for field '${fieldName}'`);
138
147
  }
139
148
 
140
- function normalizeOrderBy(input: QueryOpts["orderBy"], entity: MetaData): OrderBy[] | undefined {
149
+ function normalizeOrderBy(
150
+ input: QueryOpts["orderBy"], entity: MetaData, strategy: ColumnNamingStrategy,
151
+ ): OrderBy[] | undefined {
141
152
  if (input === undefined) return undefined;
142
153
  const arr = Array.isArray(input[0]) ? (input as [string, "asc" | "desc"][]) : [input as [string, "asc" | "desc"]];
143
154
  return arr.map(([fieldName, dir]) => {
144
155
  const field = getField(entity, fieldName);
145
- return { column: resolveColumnName(field), direction: dir };
156
+ return { column: resolveColumnName(field, strategy), direction: dir };
146
157
  });
147
158
  }
148
159
 
@@ -152,20 +163,21 @@ export function buildSelectSpec(
152
163
  filter: Filter | undefined,
153
164
  opts: QueryOpts,
154
165
  projectedFields?: string[],
166
+ strategy: ColumnNamingStrategy = DEFAULT_COLUMN_NAMING_STRATEGY,
155
167
  ): SelectSpec {
156
168
  const allFields = projectedFields ?? listFieldNames(entity);
157
169
  const pkFields = resolvePkFields(entity);
158
170
  const fieldSet = new Set<string>(allFields);
159
171
  for (const pk of pkFields) fieldSet.add(pk);
160
172
 
161
- const columns = [...fieldSet].map((f) => resolveColumnName(getField(entity, f)));
162
- const orderBy = normalizeOrderBy(opts.orderBy, entity);
173
+ const columns = [...fieldSet].map((f) => resolveColumnName(getField(entity, f), strategy));
174
+ const orderBy = normalizeOrderBy(opts.orderBy, entity, strategy);
163
175
 
164
176
  const spec: SelectSpec = {
165
177
  table: resolveTableName(entity),
166
178
  columns,
167
179
  };
168
- const where = filter !== undefined ? compileFilter(entity, filter) : null;
180
+ const where = filter !== undefined ? compileFilter(entity, filter, strategy) : null;
169
181
  if (where !== null) spec.where = where;
170
182
  if (orderBy !== undefined) spec.orderBy = orderBy;
171
183
  if (opts.limit !== undefined) spec.limit = opts.limit;
@@ -173,25 +185,34 @@ export function buildSelectSpec(
173
185
  return spec;
174
186
  }
175
187
 
176
- export function buildCountSpec(entity: MetaData, filter: Filter | undefined): CountSpec {
188
+ export function buildCountSpec(
189
+ entity: MetaData, filter: Filter | undefined,
190
+ strategy: ColumnNamingStrategy = DEFAULT_COLUMN_NAMING_STRATEGY,
191
+ ): CountSpec {
177
192
  const spec: CountSpec = { table: resolveTableName(entity) };
178
- const where = filter !== undefined ? compileFilter(entity, filter) : null;
193
+ const where = filter !== undefined ? compileFilter(entity, filter, strategy) : null;
179
194
  if (where !== null) spec.where = where;
180
195
  return spec;
181
196
  }
182
197
 
183
- export function buildInsertSpec(entity: MetaData, data: Row): InsertSpec {
184
- const values = rowToColumns(entity, data);
198
+ export function buildInsertSpec(
199
+ entity: MetaData, data: Row,
200
+ strategy: ColumnNamingStrategy = DEFAULT_COLUMN_NAMING_STRATEGY,
201
+ ): InsertSpec {
202
+ const values = rowToColumns(entity, data, strategy);
185
203
  const allFields = listFieldNames(entity);
186
204
  return {
187
205
  table: resolveTableName(entity),
188
206
  values,
189
- returning: allFields.map((f) => resolveColumnName(getField(entity, f))),
207
+ returning: allFields.map((f) => resolveColumnName(getField(entity, f), strategy)),
190
208
  };
191
209
  }
192
210
 
193
- export function buildUpdateSpec(entity: MetaData, data: Row, id: unknown): UpdateSpec {
194
- const values = rowToColumns(entity, data);
211
+ export function buildUpdateSpec(
212
+ entity: MetaData, data: Row, id: unknown,
213
+ strategy: ColumnNamingStrategy = DEFAULT_COLUMN_NAMING_STRATEGY,
214
+ ): UpdateSpec {
215
+ const values = rowToColumns(entity, data, strategy);
195
216
  const pkFields = resolvePkFields(entity);
196
217
  if (pkFields.length !== 1) {
197
218
  throw new MetadataError(
@@ -199,16 +220,19 @@ export function buildUpdateSpec(entity: MetaData, data: Row, id: unknown): Updat
199
220
  { entity: entity.name },
200
221
  );
201
222
  }
202
- const pkColumn = resolveColumnName(getField(entity, pkFields[0]!));
223
+ const pkColumn = resolveColumnName(getField(entity, pkFields[0]!), strategy);
203
224
  return {
204
225
  table: resolveTableName(entity),
205
226
  values,
206
227
  where: { kind: "eq", column: pkColumn, value: id as PrimitiveValue },
207
- returning: listFieldNames(entity).map((f) => resolveColumnName(getField(entity, f))),
228
+ returning: listFieldNames(entity).map((f) => resolveColumnName(getField(entity, f), strategy)),
208
229
  };
209
230
  }
210
231
 
211
- export function buildDeleteSpec(entity: MetaData, id: unknown): DeleteSpec {
232
+ export function buildDeleteSpec(
233
+ entity: MetaData, id: unknown,
234
+ strategy: ColumnNamingStrategy = DEFAULT_COLUMN_NAMING_STRATEGY,
235
+ ): DeleteSpec {
212
236
  const pkFields = resolvePkFields(entity);
213
237
  if (pkFields.length !== 1) {
214
238
  throw new MetadataError(
@@ -216,7 +240,7 @@ export function buildDeleteSpec(entity: MetaData, id: unknown): DeleteSpec {
216
240
  { entity: entity.name },
217
241
  );
218
242
  }
219
- const pkColumn = resolveColumnName(getField(entity, pkFields[0]!));
243
+ const pkColumn = resolveColumnName(getField(entity, pkFields[0]!), strategy);
220
244
  return {
221
245
  table: resolveTableName(entity),
222
246
  where: { kind: "eq", column: pkColumn, value: id as PrimitiveValue },
@@ -1,4 +1,4 @@
1
- import type { MetaData } from "@metaobjectsdev/metadata";
1
+ import type { ColumnNamingStrategy, MetaData } from "@metaobjectsdev/metadata";
2
2
  import {
3
3
  TYPE_OBJECT, TYPE_RELATIONSHIP, TYPE_IDENTITY,
4
4
  IDENTITY_SUBTYPE_REFERENCE,
@@ -6,6 +6,7 @@ import {
6
6
  IDENTITY_REFERENCE_ATTR_REFERENCES,
7
7
  RELATIONSHIP_ATTR_CARDINALITY, RELATIONSHIP_ATTR_OBJECT_REF,
8
8
  CARDINALITY_ONE, CARDINALITY_MANY,
9
+ DEFAULT_COLUMN_NAMING_STRATEGY,
9
10
  } from "@metaobjectsdev/metadata";
10
11
  import { MetadataError } from "./errors.js";
11
12
  import {
@@ -140,11 +141,12 @@ export function buildLazyRelateSpec(
140
141
  desc: RelationDescriptor,
141
142
  sourceRecord: Row,
142
143
  root: MetaData,
144
+ strategy: ColumnNamingStrategy = DEFAULT_COLUMN_NAMING_STRATEGY,
143
145
  ): SelectSpec | null {
144
146
  const lookup = sourceRecord[desc.sourceField];
145
147
  if (lookup === null || lookup === undefined) return null;
146
148
  const target = mustGetEntity(root, desc.targetEntityName);
147
- return buildSelectSpec(target, { [desc.targetField]: lookup as PrimitiveValue }, {});
149
+ return buildSelectSpec(target, { [desc.targetField]: lookup as PrimitiveValue }, {}, undefined, strategy);
148
150
  }
149
151
 
150
152
  /** Builds one batched IN(...) lookup. Returns null when there are no non-null source values. */
@@ -152,6 +154,7 @@ export function buildIncludeBatchSpec(
152
154
  desc: RelationDescriptor,
153
155
  sourceRecords: Row[],
154
156
  root: MetaData,
157
+ strategy: ColumnNamingStrategy = DEFAULT_COLUMN_NAMING_STRATEGY,
155
158
  ): SelectSpec | null {
156
159
  const seen = new Set<PrimitiveValue>();
157
160
  for (const rec of sourceRecords) {
@@ -161,7 +164,7 @@ export function buildIncludeBatchSpec(
161
164
  }
162
165
  if (seen.size === 0) return null;
163
166
  const target = mustGetEntity(root, desc.targetEntityName);
164
- return buildSelectSpec(target, { [desc.targetField]: [...seen] as (string | number)[] }, {});
167
+ return buildSelectSpec(target, { [desc.targetField]: [...seen] as (string | number)[] }, {}, undefined, strategy);
165
168
  }
166
169
 
167
170
  function mustGetEntity(root: MetaData, name: string): MetaData {