metal-orm 1.0.16 → 1.0.17
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.
- package/README.md +33 -37
- package/dist/decorators/index.cjs +152 -23
- package/dist/decorators/index.cjs.map +1 -1
- package/dist/decorators/index.d.cts +1 -1
- package/dist/decorators/index.d.ts +1 -1
- package/dist/decorators/index.js +152 -23
- package/dist/decorators/index.js.map +1 -1
- package/dist/index.cjs +322 -115
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +53 -4
- package/dist/index.d.ts +53 -4
- package/dist/index.js +316 -115
- package/dist/index.js.map +1 -1
- package/dist/{select-BKZrMRCQ.d.cts → select-BPCn6MOH.d.cts} +183 -64
- package/dist/{select-BKZrMRCQ.d.ts → select-BPCn6MOH.d.ts} +183 -64
- package/package.json +2 -1
- package/src/core/ast/aggregate-functions.ts +50 -4
- package/src/core/ast/expression-builders.ts +22 -15
- package/src/core/ast/expression-nodes.ts +6 -0
- package/src/core/ddl/introspect/functions/postgres.ts +2 -6
- package/src/core/dialect/abstract.ts +12 -8
- package/src/core/dialect/mssql/functions.ts +24 -15
- package/src/core/dialect/postgres/functions.ts +33 -24
- package/src/core/dialect/sqlite/functions.ts +19 -12
- package/src/core/functions/datetime.ts +2 -1
- package/src/core/functions/numeric.ts +2 -1
- package/src/core/functions/standard-strategy.ts +52 -12
- package/src/core/functions/text.ts +2 -1
- package/src/core/functions/types.ts +8 -8
- package/src/index.ts +5 -4
- package/src/orm/domain-event-bus.ts +43 -25
- package/src/orm/entity-meta.ts +40 -0
- package/src/orm/execution-context.ts +6 -0
- package/src/orm/hydration-context.ts +6 -4
- package/src/orm/orm-session.ts +35 -24
- package/src/orm/orm.ts +10 -10
- package/src/orm/query-logger.ts +15 -0
- package/src/orm/runtime-types.ts +60 -2
- package/src/orm/transaction-runner.ts +7 -0
- package/src/orm/unit-of-work.ts +1 -0
- package/src/query-builder/insert-query-state.ts +13 -3
- package/src/query-builder/select-helpers.ts +50 -0
- package/src/query-builder/select.ts +122 -30
- package/src/query-builder/update-query-state.ts +31 -9
- package/src/schema/types.ts +16 -6
package/README.md
CHANGED
|
@@ -325,16 +325,12 @@ const orm = new Orm({
|
|
|
325
325
|
const session = new OrmSession({ orm, executor });
|
|
326
326
|
|
|
327
327
|
// 2) Load entities with lazy relations
|
|
328
|
-
const [user] = await new SelectQueryBuilder(users)
|
|
329
|
-
.
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
.includeLazy('posts') // HasMany as a lazy collection
|
|
335
|
-
.includeLazy('roles') // BelongsToMany as a lazy collection
|
|
336
|
-
.where(eq(users.columns.id, 1))
|
|
337
|
-
.execute(session);
|
|
328
|
+
const [user] = await new SelectQueryBuilder(users)
|
|
329
|
+
.selectColumns('id', 'name', 'email')
|
|
330
|
+
.includeLazy('posts') // HasMany as a lazy collection
|
|
331
|
+
.includeLazy('roles') // BelongsToMany as a lazy collection
|
|
332
|
+
.where(eq(users.columns.id, 1))
|
|
333
|
+
.execute(session);
|
|
338
334
|
|
|
339
335
|
// user is an Entity<typeof users>
|
|
340
336
|
// scalar props are normal:
|
|
@@ -355,10 +351,11 @@ await session.commit();
|
|
|
355
351
|
What the runtime gives you:
|
|
356
352
|
|
|
357
353
|
- [Identity map](https://en.wikipedia.org/wiki/Identity_map_pattern) (per context).
|
|
358
|
-
- [Unit of Work](https://en.wikipedia.org/wiki/Unit_of_work) style change tracking on scalar properties.
|
|
359
|
-
- Relation tracking (add/remove/sync on collections).
|
|
360
|
-
- Cascades on relations: `'all' | 'persist' | 'remove' | 'link'`.
|
|
361
|
-
- Single flush: `session.commit()` figures out inserts, updates, deletes, and pivot changes.
|
|
354
|
+
- [Unit of Work](https://en.wikipedia.org/wiki/Unit_of_work) style change tracking on scalar properties.
|
|
355
|
+
- Relation tracking (add/remove/sync on collections).
|
|
356
|
+
- Cascades on relations: `'all' | 'persist' | 'remove' | 'link'`.
|
|
357
|
+
- Single flush: `session.commit()` figures out inserts, updates, deletes, and pivot changes.
|
|
358
|
+
- Column pickers to stay DRY: `selectColumns` on the root table, `selectRelationColumns` / `includePick` on relations, and `selectColumnsDeep` or the `sel`/`esel` helpers to build typed selection maps without repeating `table.columns.*`.
|
|
362
359
|
|
|
363
360
|
<a id="level-3"></a>
|
|
364
361
|
### Level 3: Decorator entities ✨
|
|
@@ -370,11 +367,11 @@ Finally, you can describe your models with decorators and still use the same run
|
|
|
370
367
|
```ts
|
|
371
368
|
import mysql from 'mysql2/promise';
|
|
372
369
|
import { Orm, OrmSession, MySqlDialect, col, createMysqlExecutor } from 'metal-orm';
|
|
373
|
-
import {
|
|
374
|
-
Entity,
|
|
375
|
-
Column,
|
|
376
|
-
PrimaryKey,
|
|
377
|
-
HasMany,
|
|
370
|
+
import {
|
|
371
|
+
Entity,
|
|
372
|
+
Column,
|
|
373
|
+
PrimaryKey,
|
|
374
|
+
HasMany,
|
|
378
375
|
BelongsTo,
|
|
379
376
|
bootstrapEntities,
|
|
380
377
|
selectFromEntity,
|
|
@@ -432,24 +429,23 @@ const orm = new Orm({
|
|
|
432
429
|
});
|
|
433
430
|
const session = new OrmSession({ orm, executor });
|
|
434
431
|
|
|
435
|
-
// 3) Query starting from the entity class
|
|
436
|
-
const [user] = await selectFromEntity(User)
|
|
437
|
-
.
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
- You
|
|
452
|
-
- You like decorators for explicit mapping but still want AST-first SQL and a disciplined runtime.
|
|
432
|
+
// 3) Query starting from the entity class
|
|
433
|
+
const [user] = await selectFromEntity(User)
|
|
434
|
+
.selectColumns('id', 'name')
|
|
435
|
+
.includeLazy('posts')
|
|
436
|
+
.where(/* same eq()/and() API as before */)
|
|
437
|
+
.execute(session);
|
|
438
|
+
|
|
439
|
+
user.posts.add({ title: 'From decorators' });
|
|
440
|
+
await session.commit();
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
Tip: to keep selections terse, use `selectColumns`/`selectRelationColumns` or the `sel`/`esel` helpers instead of spelling `table.columns.*` over and over.
|
|
444
|
+
|
|
445
|
+
This level is nice when:
|
|
446
|
+
|
|
447
|
+
- You want classes as your domain model, but don't want a separate schema DSL.
|
|
448
|
+
- You like decorators for explicit mapping but still want AST-first SQL and a disciplined runtime.
|
|
453
449
|
|
|
454
450
|
---
|
|
455
451
|
|
|
@@ -332,6 +332,15 @@ var isWindowFunctionNode = (node) => node?.type === "WindowFunction";
|
|
|
332
332
|
var isExpressionSelectionNode = (node) => isFunctionNode(node) || isCaseExpressionNode(node) || isWindowFunctionNode(node);
|
|
333
333
|
|
|
334
334
|
// src/core/ast/expression-builders.ts
|
|
335
|
+
var valueToOperand = (value) => {
|
|
336
|
+
if (isOperandNode(value)) {
|
|
337
|
+
return value;
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
type: "Literal",
|
|
341
|
+
value
|
|
342
|
+
};
|
|
343
|
+
};
|
|
335
344
|
var toNode = (col) => {
|
|
336
345
|
if (isOperandNode(col)) return col;
|
|
337
346
|
const def = col;
|
|
@@ -341,10 +350,10 @@ var toLiteralNode = (value) => ({
|
|
|
341
350
|
type: "Literal",
|
|
342
351
|
value
|
|
343
352
|
});
|
|
353
|
+
var isLiteralValue = (value) => value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
344
354
|
var toOperand = (val) => {
|
|
345
|
-
if (val
|
|
346
|
-
|
|
347
|
-
return { type: "Literal", value: val };
|
|
355
|
+
if (isLiteralValue(val)) {
|
|
356
|
+
return valueToOperand(val);
|
|
348
357
|
}
|
|
349
358
|
return toNode(val);
|
|
350
359
|
};
|
|
@@ -385,6 +394,24 @@ var notExists = (subquery) => ({
|
|
|
385
394
|
subquery
|
|
386
395
|
});
|
|
387
396
|
|
|
397
|
+
// src/core/sql/sql.ts
|
|
398
|
+
var JOIN_KINDS = {
|
|
399
|
+
/** INNER JOIN type */
|
|
400
|
+
INNER: "INNER",
|
|
401
|
+
/** LEFT JOIN type */
|
|
402
|
+
LEFT: "LEFT",
|
|
403
|
+
/** RIGHT JOIN type */
|
|
404
|
+
RIGHT: "RIGHT",
|
|
405
|
+
/** CROSS JOIN type */
|
|
406
|
+
CROSS: "CROSS"
|
|
407
|
+
};
|
|
408
|
+
var ORDER_DIRECTIONS = {
|
|
409
|
+
/** Ascending order */
|
|
410
|
+
ASC: "ASC",
|
|
411
|
+
/** Descending order */
|
|
412
|
+
DESC: "DESC"
|
|
413
|
+
};
|
|
414
|
+
|
|
388
415
|
// src/core/ast/aggregate-functions.ts
|
|
389
416
|
var buildAggregate = (name) => (col) => ({
|
|
390
417
|
type: "Function",
|
|
@@ -394,14 +421,21 @@ var buildAggregate = (name) => (col) => ({
|
|
|
394
421
|
var count = buildAggregate("COUNT");
|
|
395
422
|
var sum = buildAggregate("SUM");
|
|
396
423
|
var avg = buildAggregate("AVG");
|
|
424
|
+
var min = buildAggregate("MIN");
|
|
425
|
+
var max = buildAggregate("MAX");
|
|
397
426
|
|
|
398
427
|
// src/core/functions/standard-strategy.ts
|
|
399
|
-
var StandardFunctionStrategy = class {
|
|
428
|
+
var StandardFunctionStrategy = class _StandardFunctionStrategy {
|
|
400
429
|
constructor() {
|
|
401
430
|
this.renderers = /* @__PURE__ */ new Map();
|
|
402
431
|
this.registerStandard();
|
|
403
432
|
}
|
|
404
433
|
registerStandard() {
|
|
434
|
+
this.add("COUNT", ({ compiledArgs }) => `COUNT(${compiledArgs.join(", ")})`);
|
|
435
|
+
this.add("SUM", ({ compiledArgs }) => `SUM(${compiledArgs[0]})`);
|
|
436
|
+
this.add("AVG", ({ compiledArgs }) => `AVG(${compiledArgs[0]})`);
|
|
437
|
+
this.add("MIN", ({ compiledArgs }) => `MIN(${compiledArgs[0]})`);
|
|
438
|
+
this.add("MAX", ({ compiledArgs }) => `MAX(${compiledArgs[0]})`);
|
|
405
439
|
this.add("ABS", ({ compiledArgs }) => `ABS(${compiledArgs[0]})`);
|
|
406
440
|
this.add("UPPER", ({ compiledArgs }) => `UPPER(${compiledArgs[0]})`);
|
|
407
441
|
this.add("LOWER", ({ compiledArgs }) => `LOWER(${compiledArgs[0]})`);
|
|
@@ -428,6 +462,7 @@ var StandardFunctionStrategy = class {
|
|
|
428
462
|
this.add("DAY_OF_WEEK", ({ compiledArgs }) => `DAYOFWEEK(${compiledArgs[0]})`);
|
|
429
463
|
this.add("WEEK_OF_YEAR", ({ compiledArgs }) => `WEEKOFYEAR(${compiledArgs[0]})`);
|
|
430
464
|
this.add("DATE_TRUNC", ({ compiledArgs }) => `DATE_TRUNC(${compiledArgs[0]}, ${compiledArgs[1]})`);
|
|
465
|
+
this.add("GROUP_CONCAT", (ctx) => this.renderGroupConcat(ctx));
|
|
431
466
|
}
|
|
432
467
|
add(name, renderer) {
|
|
433
468
|
this.renderers.set(name, renderer);
|
|
@@ -435,6 +470,36 @@ var StandardFunctionStrategy = class {
|
|
|
435
470
|
getRenderer(name) {
|
|
436
471
|
return this.renderers.get(name);
|
|
437
472
|
}
|
|
473
|
+
renderGroupConcat(ctx) {
|
|
474
|
+
const arg = ctx.compiledArgs[0];
|
|
475
|
+
const orderClause = this.buildOrderByExpression(ctx);
|
|
476
|
+
const orderSegment = orderClause ? ` ${orderClause}` : "";
|
|
477
|
+
const separatorClause = this.formatGroupConcatSeparator(ctx);
|
|
478
|
+
return `GROUP_CONCAT(${arg}${orderSegment}${separatorClause})`;
|
|
479
|
+
}
|
|
480
|
+
buildOrderByExpression(ctx) {
|
|
481
|
+
const orderBy = ctx.node.orderBy;
|
|
482
|
+
if (!orderBy || orderBy.length === 0) {
|
|
483
|
+
return "";
|
|
484
|
+
}
|
|
485
|
+
const parts = orderBy.map((order) => `${ctx.compileOperand(order.column)} ${order.direction}`);
|
|
486
|
+
return `ORDER BY ${parts.join(", ")}`;
|
|
487
|
+
}
|
|
488
|
+
formatGroupConcatSeparator(ctx) {
|
|
489
|
+
if (!ctx.node.separator) {
|
|
490
|
+
return "";
|
|
491
|
+
}
|
|
492
|
+
return ` SEPARATOR ${ctx.compileOperand(ctx.node.separator)}`;
|
|
493
|
+
}
|
|
494
|
+
getGroupConcatSeparatorOperand(ctx) {
|
|
495
|
+
return ctx.node.separator ?? _StandardFunctionStrategy.DEFAULT_GROUP_CONCAT_SEPARATOR;
|
|
496
|
+
}
|
|
497
|
+
static {
|
|
498
|
+
this.DEFAULT_GROUP_CONCAT_SEPARATOR = {
|
|
499
|
+
type: "Literal",
|
|
500
|
+
value: ","
|
|
501
|
+
};
|
|
502
|
+
}
|
|
438
503
|
};
|
|
439
504
|
|
|
440
505
|
// src/core/dialect/abstract.ts
|
|
@@ -781,7 +846,11 @@ var Dialect = class _Dialect {
|
|
|
781
846
|
const compiledArgs = fnNode.args.map((arg) => this.compileOperand(arg, ctx));
|
|
782
847
|
const renderer = this.functionStrategy.getRenderer(fnNode.name);
|
|
783
848
|
if (renderer) {
|
|
784
|
-
return renderer({
|
|
849
|
+
return renderer({
|
|
850
|
+
node: fnNode,
|
|
851
|
+
compiledArgs,
|
|
852
|
+
compileOperand: (operand) => this.compileOperand(operand, ctx)
|
|
853
|
+
});
|
|
785
854
|
}
|
|
786
855
|
return `${fnNode.name}(${compiledArgs.join(", ")})`;
|
|
787
856
|
}
|
|
@@ -1204,6 +1273,14 @@ var PostgresFunctionStrategy = class extends StandardFunctionStrategy {
|
|
|
1204
1273
|
const partClean = String(partArg.value).replace(/['"]/g, "").toLowerCase();
|
|
1205
1274
|
return `DATE_TRUNC('${partClean}', ${date})`;
|
|
1206
1275
|
});
|
|
1276
|
+
this.add("GROUP_CONCAT", (ctx) => {
|
|
1277
|
+
const arg = ctx.compiledArgs[0];
|
|
1278
|
+
const orderClause = this.buildOrderByExpression(ctx);
|
|
1279
|
+
const orderSegment = orderClause ? ` ${orderClause}` : "";
|
|
1280
|
+
const separatorOperand = this.getGroupConcatSeparatorOperand(ctx);
|
|
1281
|
+
const separator = ctx.compileOperand(separatorOperand);
|
|
1282
|
+
return `STRING_AGG(${arg}, ${separator}${orderSegment})`;
|
|
1283
|
+
});
|
|
1207
1284
|
}
|
|
1208
1285
|
};
|
|
1209
1286
|
|
|
@@ -1448,6 +1525,12 @@ var SqliteFunctionStrategy = class extends StandardFunctionStrategy {
|
|
|
1448
1525
|
}
|
|
1449
1526
|
return `date(${date}, 'start of ${partClean}')`;
|
|
1450
1527
|
});
|
|
1528
|
+
this.add("GROUP_CONCAT", (ctx) => {
|
|
1529
|
+
const arg = ctx.compiledArgs[0];
|
|
1530
|
+
const separatorOperand = this.getGroupConcatSeparatorOperand(ctx);
|
|
1531
|
+
const separator = ctx.compileOperand(separatorOperand);
|
|
1532
|
+
return `GROUP_CONCAT(${arg}, ${separator})`;
|
|
1533
|
+
});
|
|
1451
1534
|
}
|
|
1452
1535
|
};
|
|
1453
1536
|
|
|
@@ -1564,6 +1647,14 @@ var MssqlFunctionStrategy = class extends StandardFunctionStrategy {
|
|
|
1564
1647
|
const partClean = String(partArg.value).replace(/['"]/g, "").toLowerCase();
|
|
1565
1648
|
return `DATETRUNC(${partClean}, ${date})`;
|
|
1566
1649
|
});
|
|
1650
|
+
this.add("GROUP_CONCAT", (ctx) => {
|
|
1651
|
+
const arg = ctx.compiledArgs[0];
|
|
1652
|
+
const separatorOperand = this.getGroupConcatSeparatorOperand(ctx);
|
|
1653
|
+
const separator = ctx.compileOperand(separatorOperand);
|
|
1654
|
+
const orderClause = this.buildOrderByExpression(ctx);
|
|
1655
|
+
const withinGroup = orderClause ? ` WITHIN GROUP (${orderClause})` : "";
|
|
1656
|
+
return `STRING_AGG(${arg}, ${separator})${withinGroup}`;
|
|
1657
|
+
});
|
|
1567
1658
|
}
|
|
1568
1659
|
};
|
|
1569
1660
|
|
|
@@ -1933,24 +2024,6 @@ var createJoinNode = (kind, tableName, condition, relationName) => ({
|
|
|
1933
2024
|
meta: relationName ? { relationName } : void 0
|
|
1934
2025
|
});
|
|
1935
2026
|
|
|
1936
|
-
// src/core/sql/sql.ts
|
|
1937
|
-
var JOIN_KINDS = {
|
|
1938
|
-
/** INNER JOIN type */
|
|
1939
|
-
INNER: "INNER",
|
|
1940
|
-
/** LEFT JOIN type */
|
|
1941
|
-
LEFT: "LEFT",
|
|
1942
|
-
/** RIGHT JOIN type */
|
|
1943
|
-
RIGHT: "RIGHT",
|
|
1944
|
-
/** CROSS JOIN type */
|
|
1945
|
-
CROSS: "CROSS"
|
|
1946
|
-
};
|
|
1947
|
-
var ORDER_DIRECTIONS = {
|
|
1948
|
-
/** Ascending order */
|
|
1949
|
-
ASC: "ASC",
|
|
1950
|
-
/** Descending order */
|
|
1951
|
-
DESC: "DESC"
|
|
1952
|
-
};
|
|
1953
|
-
|
|
1954
2027
|
// src/query-builder/hydration-manager.ts
|
|
1955
2028
|
var HydrationManager = class _HydrationManager {
|
|
1956
2029
|
/**
|
|
@@ -4059,6 +4132,21 @@ var SelectQueryBuilder = class _SelectQueryBuilder {
|
|
|
4059
4132
|
select(columns) {
|
|
4060
4133
|
return this.clone(this.columnSelector.select(this.context, columns));
|
|
4061
4134
|
}
|
|
4135
|
+
/**
|
|
4136
|
+
* Selects columns from the root table by name (typed).
|
|
4137
|
+
* @param cols - Column names on the root table
|
|
4138
|
+
*/
|
|
4139
|
+
selectColumns(...cols) {
|
|
4140
|
+
const selection = {};
|
|
4141
|
+
for (const key of cols) {
|
|
4142
|
+
const col = this.env.table.columns[key];
|
|
4143
|
+
if (!col) {
|
|
4144
|
+
throw new Error(`Column '${key}' not found on table '${this.env.table.name}'`);
|
|
4145
|
+
}
|
|
4146
|
+
selection[key] = col;
|
|
4147
|
+
}
|
|
4148
|
+
return this.select(selection);
|
|
4149
|
+
}
|
|
4062
4150
|
/**
|
|
4063
4151
|
|
|
4064
4152
|
* Selects raw column expressions
|
|
@@ -4219,6 +4307,47 @@ var SelectQueryBuilder = class _SelectQueryBuilder {
|
|
|
4219
4307
|
nextLazy.add(relationName);
|
|
4220
4308
|
return this.clone(this.context, nextLazy);
|
|
4221
4309
|
}
|
|
4310
|
+
/**
|
|
4311
|
+
* Selects columns for a related table in a single hop.
|
|
4312
|
+
*/
|
|
4313
|
+
selectRelationColumns(relationName, ...cols) {
|
|
4314
|
+
const relation = this.env.table.relations[relationName];
|
|
4315
|
+
if (!relation) {
|
|
4316
|
+
throw new Error(`Relation '${relationName}' not found on table '${this.env.table.name}'`);
|
|
4317
|
+
}
|
|
4318
|
+
const target = relation.target;
|
|
4319
|
+
for (const col of cols) {
|
|
4320
|
+
if (!target.columns[col]) {
|
|
4321
|
+
throw new Error(
|
|
4322
|
+
`Column '${col}' not found on related table '${target.name}' for relation '${relationName}'`
|
|
4323
|
+
);
|
|
4324
|
+
}
|
|
4325
|
+
}
|
|
4326
|
+
return this.include(relationName, { columns: cols });
|
|
4327
|
+
}
|
|
4328
|
+
/**
|
|
4329
|
+
* Convenience alias for selecting specific columns from a relation.
|
|
4330
|
+
*/
|
|
4331
|
+
includePick(relationName, cols) {
|
|
4332
|
+
return this.selectRelationColumns(relationName, ...cols);
|
|
4333
|
+
}
|
|
4334
|
+
/**
|
|
4335
|
+
* Selects columns for the root table and relations from a single config object.
|
|
4336
|
+
*/
|
|
4337
|
+
selectColumnsDeep(config) {
|
|
4338
|
+
let qb = this;
|
|
4339
|
+
if (config.root?.length) {
|
|
4340
|
+
qb = qb.selectColumns(...config.root);
|
|
4341
|
+
}
|
|
4342
|
+
for (const key of Object.keys(config)) {
|
|
4343
|
+
if (key === "root") continue;
|
|
4344
|
+
const relName = key;
|
|
4345
|
+
const cols = config[relName];
|
|
4346
|
+
if (!cols || !cols.length) continue;
|
|
4347
|
+
qb = qb.selectRelationColumns(relName, ...cols);
|
|
4348
|
+
}
|
|
4349
|
+
return qb;
|
|
4350
|
+
}
|
|
4222
4351
|
getLazyRelations() {
|
|
4223
4352
|
return Array.from(this.lazyRelations);
|
|
4224
4353
|
}
|