metal-orm 1.1.7 → 1.1.9

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.
@@ -6,8 +6,8 @@ import { UpdateQueryBuilder } from '../query-builder/update.js';
6
6
  import { DeleteQueryBuilder } from '../query-builder/delete.js';
7
7
  import { findPrimaryKey } from '../query-builder/hydration-planner.js';
8
8
  import type { TableDef, TableHooks } from '../schema/table.js';
9
- import { payloadResultSets } from '../core/execution/db-executor.js';
10
- import type { DbExecutor, QueryResult } from '../core/execution/db-executor.js';
9
+ import { payloadResultSets } from '../core/execution/db-executor.js';
10
+ import type { DbExecutor, QueryResult } from '../core/execution/db-executor.js';
11
11
  import { IdentityMap } from './identity-map.js';
12
12
  import { EntityStatus } from './runtime-types.js';
13
13
  import type { TrackedEntity } from './runtime-types.js';
@@ -323,10 +323,10 @@ export class UnitOfWork {
323
323
  * @param compiled - The compiled query
324
324
  * @returns Query results
325
325
  */
326
- private async executeCompiled(compiled: CompiledQuery): Promise<QueryResult[]> {
327
- const payload = await this.executor.executeSql(compiled.sql, compiled.params);
328
- return payloadResultSets(payload);
329
- }
326
+ private async executeCompiled(compiled: CompiledQuery): Promise<QueryResult[]> {
327
+ const payload = await this.executor.executeSql(compiled.sql, compiled.params);
328
+ return payloadResultSets(payload);
329
+ }
330
330
 
331
331
  /**
332
332
  * Gets columns for RETURNING clause.
@@ -0,0 +1,323 @@
1
+ import { TableDef } from '../../schema/table.js';
2
+ import { SelectQueryNode, OrderByNode } from '../../core/ast/query.js';
3
+ import {
4
+ ColumnNode,
5
+ LiteralNode,
6
+ ExpressionNode,
7
+ and,
8
+ or
9
+ } from '../../core/ast/expression.js';
10
+ import { OrmSession } from '../../orm/orm-session.js';
11
+ import { SelectQueryState } from '../select-query-state.js';
12
+ import type { SelectQueryBuilder } from '../select.js';
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Public types
16
+ // ---------------------------------------------------------------------------
17
+
18
+ export type CursorPageOptions = {
19
+ first?: number;
20
+ after?: string;
21
+ last?: number;
22
+ before?: string;
23
+ };
24
+
25
+ export type CursorPageInfo = {
26
+ hasNextPage: boolean;
27
+ hasPreviousPage: boolean;
28
+ startCursor: string | null;
29
+ endCursor: string | null;
30
+ };
31
+
32
+ export type CursorPageResult<T> = {
33
+ items: T[];
34
+ pageInfo: CursorPageInfo;
35
+ };
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Internal types
39
+ // ---------------------------------------------------------------------------
40
+
41
+ interface CursorOrderSpec {
42
+ table: string;
43
+ column: string;
44
+ valueKey: string;
45
+ direction: 'ASC' | 'DESC';
46
+ }
47
+
48
+ interface EncodedCursor {
49
+ v: 2;
50
+ values: unknown[];
51
+ orderSig: string;
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Helpers
56
+ // ---------------------------------------------------------------------------
57
+
58
+ export function encodeCursor(payload: EncodedCursor): string {
59
+ return Buffer.from(JSON.stringify(payload)).toString('base64url');
60
+ }
61
+
62
+ export function decodeCursor(cursor: string): EncodedCursor {
63
+ let parsed: unknown;
64
+ try {
65
+ parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
66
+ } catch {
67
+ throw new Error('executeCursor: invalid cursor format');
68
+ }
69
+ if (
70
+ typeof parsed !== 'object' || parsed === null ||
71
+ (parsed as EncodedCursor).v !== 2 ||
72
+ !Array.isArray((parsed as EncodedCursor).values) ||
73
+ typeof (parsed as EncodedCursor).orderSig !== 'string'
74
+ ) {
75
+ throw new Error('executeCursor: invalid cursor payload');
76
+ }
77
+ return parsed as EncodedCursor;
78
+ }
79
+
80
+ export function buildOrderSignature(specs: CursorOrderSpec[]): string {
81
+ return specs.map(s => `${s.table}.${s.column}:${s.direction}`).join(',');
82
+ }
83
+
84
+ function extractOrderSpecs(ast: SelectQueryNode): CursorOrderSpec[] {
85
+ if (!ast.orderBy || ast.orderBy.length === 0) {
86
+ throw new Error('executeCursor: ORDER BY is required for cursor pagination');
87
+ }
88
+
89
+ return ast.orderBy.map((ob: OrderByNode) => {
90
+ if (ob.nulls) {
91
+ throw new Error('executeCursor: NULLS FIRST/LAST is not supported for cursor pagination');
92
+ }
93
+ const term = ob.term;
94
+ if (!term || (term as ColumnNode).type !== 'Column') {
95
+ throw new Error(
96
+ 'executeCursor: only column references are supported in ORDER BY for cursor pagination'
97
+ );
98
+ }
99
+ const col = term as ColumnNode;
100
+ return {
101
+ table: col.table,
102
+ column: col.name,
103
+ valueKey: resolveOrderValueKey(ast, col),
104
+ direction: ob.direction
105
+ };
106
+ });
107
+ }
108
+
109
+ function resolveOrderValueKey(ast: SelectQueryNode, col: ColumnNode): string {
110
+ const projectedColumn = ast.columns.find((candidate): candidate is ColumnNode =>
111
+ candidate.type === 'Column' &&
112
+ candidate.table === col.table &&
113
+ candidate.name === col.name
114
+ );
115
+
116
+ return projectedColumn?.alias ?? projectedColumn?.name ?? col.alias ?? col.name;
117
+ }
118
+
119
+ export function buildKeysetPredicate(
120
+ specs: CursorOrderSpec[],
121
+ values: unknown[],
122
+ mode: 'after' | 'before'
123
+ ): ExpressionNode {
124
+ if (values.length !== specs.length) {
125
+ throw new Error('executeCursor: invalid cursor payload');
126
+ }
127
+
128
+ // For a multi-column keyset (c1 DESC, c2 DESC) with mode='after':
129
+ // (c1 < v1) OR (c1 = v1 AND c2 < v2)
130
+ // 'after' on DESC → use '<'; 'after' on ASC → use '>'
131
+ // 'before' inverts the operators.
132
+
133
+ const branches: ExpressionNode[] = [];
134
+
135
+ for (let i = 0; i < specs.length; i++) {
136
+ const spec = specs[i];
137
+ const colNode: ColumnNode = { type: 'Column', table: spec.table, name: spec.column };
138
+ const value = values[i];
139
+ if (value === null || value === undefined) {
140
+ throw new Error('executeCursor: invalid cursor payload');
141
+ }
142
+ const literal: LiteralNode = { type: 'Literal', value: value as LiteralNode['value'] };
143
+
144
+ // Determine the comparison operator for the "breaking" column
145
+ let operator: '>' | '<';
146
+ if (mode === 'after') {
147
+ operator = spec.direction === 'ASC' ? '>' : '<';
148
+ } else {
149
+ operator = spec.direction === 'ASC' ? '<' : '>';
150
+ }
151
+
152
+ // Build equality prefix: c0 = v0 AND c1 = v1 AND ... AND c(i-1) = v(i-1)
153
+ const eqParts: ExpressionNode[] = [];
154
+ for (let j = 0; j < i; j++) {
155
+ const prevSpec = specs[j];
156
+ const prevCol: ColumnNode = { type: 'Column', table: prevSpec.table, name: prevSpec.column };
157
+ const prevValue = values[j];
158
+ if (prevValue === null || prevValue === undefined) {
159
+ throw new Error('executeCursor: invalid cursor payload');
160
+ }
161
+ const prevVal: LiteralNode = { type: 'Literal', value: prevValue as LiteralNode['value'] };
162
+ eqParts.push({
163
+ type: 'BinaryExpression',
164
+ left: prevCol,
165
+ operator: '=',
166
+ right: prevVal
167
+ });
168
+ }
169
+
170
+ // The "breaking" comparison: ci <op> vi
171
+ const breakExpr: ExpressionNode = {
172
+ type: 'BinaryExpression',
173
+ left: colNode,
174
+ operator,
175
+ right: literal
176
+ };
177
+
178
+ if (eqParts.length === 0) {
179
+ branches.push(breakExpr);
180
+ } else {
181
+ branches.push(and(...eqParts, breakExpr));
182
+ }
183
+ }
184
+
185
+ return branches.length === 1 ? branches[0] : or(...branches);
186
+ }
187
+
188
+ function buildCursorFromRow(row: Record<string, unknown>, specs: CursorOrderSpec[]): string {
189
+ const values = specs.map(spec => {
190
+ const value = row[spec.valueKey];
191
+ if (value === null || value === undefined) {
192
+ throw new Error('executeCursor: cursor pagination requires non-null ORDER BY values');
193
+ }
194
+ return value;
195
+ });
196
+ return encodeCursor({ v: 2, values, orderSig: buildOrderSignature(specs) });
197
+ }
198
+
199
+ function reverseDirection(direction: 'ASC' | 'DESC'): 'ASC' | 'DESC' {
200
+ return direction === 'ASC' ? 'DESC' : 'ASC';
201
+ }
202
+
203
+ function createExecutionBuilder<T, TTable extends TableDef>(
204
+ builder: SelectQueryBuilder<T, TTable>,
205
+ options: {
206
+ predicate?: ExpressionNode;
207
+ limit: number;
208
+ reverseOrder: boolean;
209
+ }
210
+ ): SelectQueryBuilder<T, TTable> {
211
+ const internals = builder.getInternals();
212
+ const baseAst = internals.context.state.ast;
213
+
214
+ const orderBy = options.reverseOrder && baseAst.orderBy
215
+ ? baseAst.orderBy.map(order => ({
216
+ ...order,
217
+ direction: reverseDirection(order.direction)
218
+ }))
219
+ : baseAst.orderBy;
220
+
221
+ const nextAst: SelectQueryNode = {
222
+ ...baseAst,
223
+ where: options.predicate
224
+ ? (baseAst.where ? and(baseAst.where, options.predicate) : options.predicate)
225
+ : baseAst.where,
226
+ orderBy,
227
+ limit: options.limit
228
+ };
229
+
230
+ const nextContext = {
231
+ ...internals.context,
232
+ state: new SelectQueryState(builder.getTable(), nextAst)
233
+ };
234
+
235
+ return internals.clone(nextContext, internals.includeTree);
236
+ }
237
+
238
+ // ---------------------------------------------------------------------------
239
+ // Main executor
240
+ // ---------------------------------------------------------------------------
241
+
242
+ export async function executeCursorQuery<T, TTable extends TableDef>(
243
+ builder: SelectQueryBuilder<T, TTable>,
244
+ session: OrmSession,
245
+ options: CursorPageOptions
246
+ ): Promise<CursorPageResult<T>> {
247
+ const { first, after, last, before } = options;
248
+
249
+ // --- Validation ---
250
+ if (first != null && last != null) {
251
+ throw new Error('executeCursor: "first" and "last" cannot be used together');
252
+ }
253
+ if (after != null && before != null) {
254
+ throw new Error('executeCursor: "after" and "before" cannot be used together');
255
+ }
256
+ if (first == null && last == null) {
257
+ throw new Error('executeCursor: either "first" or "last" must be provided');
258
+ }
259
+ const limit = first ?? last!;
260
+ if (!Number.isInteger(limit) || limit < 1) {
261
+ throw new Error(`executeCursor: "${first != null ? 'first' : 'last'}" must be an integer >= 1`);
262
+ }
263
+ const isBackward = last != null;
264
+ const cursor = after ?? before;
265
+
266
+ // --- Extract order specs from builder AST ---
267
+ const ast = builder.getInternals().context.state.ast;
268
+ const specs = extractOrderSpecs(ast);
269
+
270
+ // --- Apply cursor predicate if present ---
271
+ let predicate: ExpressionNode | undefined;
272
+ if (cursor) {
273
+ const decoded = decodeCursor(cursor);
274
+ const expectedSig = buildOrderSignature(specs);
275
+ if (decoded.orderSig !== expectedSig) {
276
+ throw new Error(
277
+ 'executeCursor: cursor ORDER BY signature does not match the current query. ' +
278
+ 'The ORDER BY clause must remain the same between paginated requests.'
279
+ );
280
+ }
281
+ predicate = buildKeysetPredicate(specs, decoded.values, isBackward ? 'before' : 'after');
282
+ }
283
+
284
+ // --- Fetch limit + 1 to detect hasNextPage ---
285
+ const executionBuilder = createExecutionBuilder(builder, {
286
+ predicate,
287
+ limit: limit + 1,
288
+ reverseOrder: isBackward
289
+ });
290
+ const rows = await executionBuilder.execute(session);
291
+
292
+ const hasExtraItem = rows.length > limit;
293
+ if (hasExtraItem) {
294
+ rows.pop();
295
+ }
296
+
297
+ const orderedRows = isBackward ? rows.reverse() : rows;
298
+ const items = orderedRows as (T & Record<string, unknown>)[];
299
+ const hasItems = items.length > 0;
300
+ const hasNextPage = hasItems
301
+ ? (isBackward ? before != null : hasExtraItem)
302
+ : false;
303
+ const hasPreviousPage = hasItems
304
+ ? (isBackward ? hasExtraItem : after != null)
305
+ : false;
306
+
307
+ const startCursor = hasItems
308
+ ? buildCursorFromRow(items[0] as Record<string, unknown>, specs)
309
+ : null;
310
+ const endCursor = hasItems
311
+ ? buildCursorFromRow(items[items.length - 1] as Record<string, unknown>, specs)
312
+ : null;
313
+
314
+ return {
315
+ items,
316
+ pageInfo: {
317
+ hasNextPage,
318
+ hasPreviousPage,
319
+ startCursor,
320
+ endCursor
321
+ }
322
+ };
323
+ }
@@ -61,6 +61,12 @@ import {
61
61
  WhereHasOptions
62
62
  } from './select/select-operations.js';
63
63
  export type { PaginatedResult };
64
+ import {
65
+ executeCursorQuery,
66
+ CursorPageOptions,
67
+ CursorPageResult
68
+ } from './select/cursor-pagination.js';
69
+ export type { CursorPageOptions, CursorPageResult, CursorPageInfo } from './select/cursor-pagination.js';
64
70
  import { SelectFromFacet } from './select/from-facet.js';
65
71
  import { SelectJoinFacet } from './select/join-facet.js';
66
72
  import { SelectProjectionFacet } from './select/projection-facet.js';
@@ -946,7 +952,42 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
946
952
  }
947
953
 
948
954
  /**
949
- * Executes the query and returns an array of values for a single column.
955
+ * Executes the query using cursor-based (keyset) pagination.
956
+ * Requires a stable ORDER BY on selected, non-null columns.
957
+ * Cursor pagination currently supports simple column references only and
958
+ * the cursor token is opaque: it must be reused with the same ORDER BY signature.
959
+ *
960
+ * @param session - ORM session context
961
+ * @param options - Cursor pagination options (`first`/`after` or `last`/`before`)
962
+ * @returns Promise of cursor-paginated result with items and pageInfo
963
+ * @example
964
+ * const page1 = await selectFrom(users)
965
+ * .orderBy(users.columns.createdAt, 'DESC')
966
+ * .orderBy(users.columns.id, 'DESC')
967
+ * .executeCursor(session, { first: 20 });
968
+ *
969
+ * // Next page
970
+ * const page2 = await selectFrom(users)
971
+ * .orderBy(users.columns.createdAt, 'DESC')
972
+ * .orderBy(users.columns.id, 'DESC')
973
+ * .executeCursor(session, { first: 20, after: page1.pageInfo.endCursor });
974
+ *
975
+ * // Previous page from a known cursor
976
+ * const prevPage = await selectFrom(users)
977
+ * .orderBy(users.columns.createdAt, 'DESC')
978
+ * .orderBy(users.columns.id, 'DESC')
979
+ * .executeCursor(session, { last: 20, before: page2.pageInfo.startCursor });
980
+ */
981
+ async executeCursor(
982
+ session: OrmSession,
983
+ options: CursorPageOptions
984
+ ): Promise<CursorPageResult<T>> {
985
+ const builder = this.ensureDefaultSelection();
986
+ return executeCursorQuery(builder, session, options);
987
+ }
988
+
989
+ /**
990
+ * Executes the query and returns an array of values for a single column.
950
991
  * This is a convenience method to avoid manual `.map(r => r.column)`.
951
992
  *
952
993
  * @param column - The column name to extract
@@ -5,8 +5,16 @@
5
5
  * Integrates with MetalORM's decorator entity system.
6
6
  */
7
7
 
8
- import type { TreeConfig } from './tree-types.js';
9
- import { resolveTreeConfig } from './tree-types.js';
8
+ import type { TreeConfig } from './tree-types.js';
9
+ import { resolveTreeConfig } from './tree-types.js';
10
+ import { RelationKinds, belongsTo, hasMany } from '../schema/relation.js';
11
+ import { addRelationMetadata, getEntityMetadata, type EntityConstructor } from '../orm/entity-metadata.js';
12
+ import {
13
+ type DecoratorTreeMetadata,
14
+ getOrCreateMetadataBag,
15
+ readMetadataBag,
16
+ readMetadataBagFromConstructor
17
+ } from '../decorators/decorator-metadata.js';
10
18
 
11
19
  /**
12
20
  * Symbol key for storing tree metadata on entity classes.
@@ -73,18 +81,25 @@ export interface TreeDecoratorOptions {
73
81
  * }
74
82
  * ```
75
83
  */
76
- export function Tree(options: TreeDecoratorOptions = {}) {
77
- return function <T extends abstract new (...args: unknown[]) => object>(
78
- target: T
79
- ): T {
80
- const config = resolveTreeConfig(options);
81
- const metadata: TreeMetadata = { config };
82
-
83
- setTreeMetadataOnClass(target, metadata);
84
-
85
- return target;
86
- };
87
- }
84
+ export function Tree(options: TreeDecoratorOptions = {}) {
85
+ return function <T extends abstract new (...args: unknown[]) => object>(
86
+ target: T,
87
+ context: ClassDecoratorContext<T>
88
+ ): T {
89
+ const config = resolveTreeConfig(options);
90
+ const metadataBag = readMetadataBag(context) ?? readMetadataBagFromConstructor(target);
91
+ const metadata: TreeMetadata = {
92
+ config,
93
+ parentProperty: metadataBag?.tree?.parentProperty,
94
+ childrenProperty: metadataBag?.tree?.childrenProperty
95
+ };
96
+
97
+ setTreeMetadataOnClass(target, metadata);
98
+ registerTreeRelations(target as unknown as EntityConstructor, metadata);
99
+
100
+ return target;
101
+ };
102
+ }
88
103
 
89
104
  /**
90
105
  * Decorator to mark a property as the parent relation in a tree entity.
@@ -95,23 +110,22 @@ export function Tree(options: TreeDecoratorOptions = {}) {
95
110
  * parent?: Category;
96
111
  * ```
97
112
  */
98
- export function TreeParent() {
99
- return function <T, V>(
100
- _value: undefined,
101
- context: ClassFieldDecoratorContext<T, V>
102
- ): void {
103
- const propertyName = String(context.name);
104
-
105
- context.addInitializer(function (this: T) {
106
- const constructor = (this as object).constructor as new (...args: unknown[]) => object;
107
- const metadata = getTreeMetadata(constructor);
108
- if (metadata) {
109
- metadata.parentProperty = propertyName;
110
- setTreeMetadata(constructor, metadata);
111
- }
112
- });
113
- };
114
- }
113
+ export function TreeParent() {
114
+ return function <T, V>(
115
+ _value: undefined,
116
+ context: ClassFieldDecoratorContext<T, V>
117
+ ): void {
118
+ if (!context.name) {
119
+ throw new Error('TreeParent decorator requires a property name');
120
+ }
121
+ if (context.private) {
122
+ throw new Error('TreeParent decorator does not support private fields');
123
+ }
124
+ const propertyName = String(context.name);
125
+ const bag = getOrCreateMetadataBag(context);
126
+ bag.tree = { ...bag.tree, parentProperty: propertyName };
127
+ };
128
+ }
115
129
 
116
130
  /**
117
131
  * Decorator to mark a property as the children relation in a tree entity.
@@ -122,23 +136,54 @@ export function TreeParent() {
122
136
  * children?: Category[];
123
137
  * ```
124
138
  */
125
- export function TreeChildren() {
126
- return function <T, V>(
127
- _value: undefined,
128
- context: ClassFieldDecoratorContext<T, V>
129
- ): void {
130
- const propertyName = String(context.name);
131
-
132
- context.addInitializer(function (this: T) {
133
- const constructor = (this as object).constructor as new (...args: unknown[]) => object;
134
- const metadata = getTreeMetadata(constructor);
135
- if (metadata) {
136
- metadata.childrenProperty = propertyName;
137
- setTreeMetadata(constructor, metadata);
138
- }
139
- });
140
- };
141
- }
139
+ export function TreeChildren() {
140
+ return function <T, V>(
141
+ _value: undefined,
142
+ context: ClassFieldDecoratorContext<T, V>
143
+ ): void {
144
+ if (!context.name) {
145
+ throw new Error('TreeChildren decorator requires a property name');
146
+ }
147
+ if (context.private) {
148
+ throw new Error('TreeChildren decorator does not support private fields');
149
+ }
150
+ const propertyName = String(context.name);
151
+ const bag = getOrCreateMetadataBag(context);
152
+ bag.tree = { ...bag.tree, childrenProperty: propertyName };
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Synchronizes tree metadata + self-relations for entities using @Tree decorators.
158
+ * Safe to call multiple times (idempotent).
159
+ */
160
+ export function syncTreeEntityMetadata(
161
+ ctor: EntityConstructor,
162
+ treeMetadata?: DecoratorTreeMetadata
163
+ ): void {
164
+ const current = getTreeMetadata(ctor);
165
+ if (!current) return;
166
+ const metadataBag = readMetadataBagFromConstructor(ctor);
167
+
168
+ const nextParentProperty =
169
+ treeMetadata?.parentProperty ?? metadataBag?.tree?.parentProperty ?? current.parentProperty;
170
+ const nextChildrenProperty =
171
+ treeMetadata?.childrenProperty ?? metadataBag?.tree?.childrenProperty ?? current.childrenProperty;
172
+
173
+ if (nextParentProperty !== current.parentProperty || nextChildrenProperty !== current.childrenProperty) {
174
+ setTreeMetadata(ctor, {
175
+ ...current,
176
+ parentProperty: nextParentProperty,
177
+ childrenProperty: nextChildrenProperty
178
+ });
179
+ }
180
+
181
+ registerTreeRelations(ctor, {
182
+ ...current,
183
+ parentProperty: nextParentProperty,
184
+ childrenProperty: nextChildrenProperty
185
+ });
186
+ }
142
187
 
143
188
  /**
144
189
  * Gets tree metadata from an entity class.
@@ -152,12 +197,50 @@ export function getTreeMetadata(
152
197
  /**
153
198
  * Sets tree metadata on an entity class.
154
199
  */
155
- export function setTreeMetadata(
156
- target: new (...args: unknown[]) => object,
157
- metadata: TreeMetadata
158
- ): void {
159
- (target as unknown as Record<symbol, TreeMetadata>)[TREE_METADATA_KEY] = metadata;
160
- }
200
+ export function setTreeMetadata(
201
+ target: new (...args: unknown[]) => object,
202
+ metadata: TreeMetadata
203
+ ): void {
204
+ (target as unknown as Record<symbol, TreeMetadata>)[TREE_METADATA_KEY] = metadata;
205
+ }
206
+
207
+ const registerTreeRelations = (
208
+ target: EntityConstructor,
209
+ metadata: TreeMetadata
210
+ ): void => {
211
+ const entityMeta = getEntityMetadata(target);
212
+ if (!entityMeta) return;
213
+
214
+ if (metadata.parentProperty && !entityMeta.relations[metadata.parentProperty]) {
215
+ addRelationMetadata(target, metadata.parentProperty, {
216
+ kind: RelationKinds.BelongsTo,
217
+ propertyKey: metadata.parentProperty,
218
+ target: () => target,
219
+ foreignKey: metadata.config.parentKey
220
+ });
221
+ if (entityMeta.table && !entityMeta.table.relations[metadata.parentProperty]) {
222
+ entityMeta.table.relations[metadata.parentProperty] = belongsTo(
223
+ entityMeta.table,
224
+ metadata.config.parentKey
225
+ );
226
+ }
227
+ }
228
+
229
+ if (metadata.childrenProperty && !entityMeta.relations[metadata.childrenProperty]) {
230
+ addRelationMetadata(target, metadata.childrenProperty, {
231
+ kind: RelationKinds.HasMany,
232
+ propertyKey: metadata.childrenProperty,
233
+ target: () => target,
234
+ foreignKey: metadata.config.parentKey
235
+ });
236
+ if (entityMeta.table && !entityMeta.table.relations[metadata.childrenProperty]) {
237
+ entityMeta.table.relations[metadata.childrenProperty] = hasMany(
238
+ entityMeta.table,
239
+ metadata.config.parentKey
240
+ );
241
+ }
242
+ }
243
+ };
161
244
 
162
245
  /**
163
246
  * Sets tree metadata on an entity class (internal, handles abstract classes).