@ailnaf/zoddb 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +69 -0
- package/README.md +90 -5
- package/dist/db.d.ts +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +340 -16
- package/dist/db.js.map +1 -1
- package/dist/filter.d.ts +1 -0
- package/dist/filter.d.ts.map +1 -1
- package/dist/filter.js +16 -5
- package/dist/filter.js.map +1 -1
- package/dist/relations.d.ts +58 -7
- package/dist/relations.d.ts.map +1 -1
- package/dist/schema.d.ts +1 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +1 -0
- package/dist/schema.js.map +1 -1
- package/docs/api.md +98 -7
- package/package.json +1 -1
package/dist/db.js
CHANGED
|
@@ -49,10 +49,22 @@ async function trackQuery(ctx, meta, fn, countRows) {
|
|
|
49
49
|
throw error;
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
-
async function exec(ctx, text, params = [], meta = { operation: 'query' }) {
|
|
53
|
-
return trackQuery(ctx, { ...meta, sql: text, params }, () => ctx.raw.unsafe(text, params));
|
|
52
|
+
async function exec(ctx, text, params = [], meta = { operation: 'query' }, countRows) {
|
|
53
|
+
return trackQuery(ctx, { ...meta, sql: text, params }, () => ctx.raw.unsafe(text, params), countRows);
|
|
54
54
|
}
|
|
55
55
|
const readCtx = (ctx) => ctx.replica ? { ...ctx, raw: ctx.replica, onReplica: true } : ctx;
|
|
56
|
+
async function rawQueryImpl(ctx, text, params = []) {
|
|
57
|
+
assertParams(params);
|
|
58
|
+
try {
|
|
59
|
+
return await exec(ctx, text, params, { operation: 'raw' }, result => {
|
|
60
|
+
const count = result.count;
|
|
61
|
+
return typeof count === 'number' ? count : result.length;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
throw mapError(e);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
56
68
|
const quoteIdent = (s) => `"${s.replace(/"/g, '""')}"`;
|
|
57
69
|
/** 顶层用 begin;已在事务里时用 savepoint(postgres.js 的 TransactionSql 没有 begin) */
|
|
58
70
|
function beginOrSavepoint(conn, cb) {
|
|
@@ -412,6 +424,171 @@ async function applyLinkWrite(ctx, parent, table, key, def, value) {
|
|
|
412
424
|
await insertLinkRows(ctx, def, sourceValue, targets, `${operation}.connect`);
|
|
413
425
|
}
|
|
414
426
|
}
|
|
427
|
+
async function applyOneUpdate(ctx, parent, table, key, def, value) {
|
|
428
|
+
const operation = `update ${table.name}.${key}`;
|
|
429
|
+
if (!isPlainObject(value))
|
|
430
|
+
throw new Error(`${operation} 需要 { connect } 或 { disconnect }`);
|
|
431
|
+
const hasConnect = value.connect !== undefined;
|
|
432
|
+
const hasDisconnect = value.disconnect !== undefined;
|
|
433
|
+
for (const k of Object.keys(value)) {
|
|
434
|
+
if (k !== 'connect' && k !== 'disconnect')
|
|
435
|
+
throw new Error(`${operation} 不支持选项 ${k}`);
|
|
436
|
+
}
|
|
437
|
+
if (hasConnect === hasDisconnect)
|
|
438
|
+
throw new Error(`${operation} 需要 connect/disconnect 二选一`);
|
|
439
|
+
if (hasDisconnect && value.disconnect !== true)
|
|
440
|
+
throw new Error(`${operation}.disconnect 必须是 true`);
|
|
441
|
+
const from = resolveRelationColumn(def.from);
|
|
442
|
+
const to = resolveRelationColumn(def.to);
|
|
443
|
+
const parentPks = table.__plan.filter(p => p.pk);
|
|
444
|
+
if (parentPks.length !== 1)
|
|
445
|
+
throw new Error(`${operation}: 父表 ${table.name} 必须使用单列主键`);
|
|
446
|
+
const parentPk = parentPks[0];
|
|
447
|
+
const parentValue = parent[parentPk.outKey];
|
|
448
|
+
if (parentValue === undefined || parentValue === null)
|
|
449
|
+
throw new Error(`${operation}: ${table.name}.${parentPk.outKey} 不能为空`);
|
|
450
|
+
const fromPlan = table.__plan.find(p => p.outKey === from.outKey);
|
|
451
|
+
if (!fromPlan)
|
|
452
|
+
throw new Error(`${operation}: 找不到 ${table.name}.${from.outKey}`);
|
|
453
|
+
const setParentFk = async (targetValue) => {
|
|
454
|
+
const rows = await updateColumnsImpl(ctx, table, { [from.outKey]: targetValue }, {
|
|
455
|
+
[parentPk.outKey]: parentValue,
|
|
456
|
+
});
|
|
457
|
+
if (rows.length !== 1)
|
|
458
|
+
throw new Error(`${operation}: 父行 ${String(parentValue)} 不存在`);
|
|
459
|
+
};
|
|
460
|
+
if (hasDisconnect) {
|
|
461
|
+
if (!fromPlan.nullable)
|
|
462
|
+
throw new Error(`${operation}: ${table.name}.${from.outKey} 不是 nullable,不能 disconnect`);
|
|
463
|
+
await setParentFk(null);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (!isPlainObject(value.connect))
|
|
467
|
+
throw new Error(`${operation}.connect 必须是对象`);
|
|
468
|
+
const targetPks = def.table.__plan.filter(p => p.pk);
|
|
469
|
+
if (targetPks.length !== 1)
|
|
470
|
+
throw new Error(`${operation}: 目标表 ${def.table.name} 必须使用单列主键`);
|
|
471
|
+
const targetPk = targetPks[0];
|
|
472
|
+
const targetId = value.connect[targetPk.outKey];
|
|
473
|
+
if (targetId === undefined || targetId === null)
|
|
474
|
+
throw new Error(`${operation}.connect 需要 ${def.table.name}.${targetPk.outKey}`);
|
|
475
|
+
const target = await findOneImpl(ctx, def.table, { where: { [targetPk.outKey]: targetId } });
|
|
476
|
+
if (!target)
|
|
477
|
+
throw new Error(`${operation}: 找不到 ${def.table.name}.${String(targetId)}`);
|
|
478
|
+
const targetValue = target[to.outKey];
|
|
479
|
+
if (targetValue === undefined || targetValue === null)
|
|
480
|
+
throw new Error(`${operation}: ${def.table.name}.${to.outKey} 不能为空`);
|
|
481
|
+
await setParentFk(targetValue);
|
|
482
|
+
}
|
|
483
|
+
async function applyManyUpdate(ctx, parent, table, key, def, value) {
|
|
484
|
+
const operation = `update ${table.name}.${key}`;
|
|
485
|
+
if (!isPlainObject(value))
|
|
486
|
+
throw new Error(`${operation} 需要 { create, update, delete } 之一`);
|
|
487
|
+
const allowed = new Set(['create', 'update', 'delete']);
|
|
488
|
+
for (const k of Object.keys(value)) {
|
|
489
|
+
if (!allowed.has(k))
|
|
490
|
+
throw new Error(`${operation} 不支持选项 ${k}`);
|
|
491
|
+
}
|
|
492
|
+
const parseList = (name) => {
|
|
493
|
+
const raw = value[name];
|
|
494
|
+
if (raw === undefined)
|
|
495
|
+
return undefined;
|
|
496
|
+
if (!Array.isArray(raw))
|
|
497
|
+
throw new Error(`${operation}.${name} 必须是数组`);
|
|
498
|
+
return raw;
|
|
499
|
+
};
|
|
500
|
+
const creates = parseList('create');
|
|
501
|
+
const updates = parseList('update');
|
|
502
|
+
const deletes = parseList('delete');
|
|
503
|
+
if (creates === undefined && updates === undefined && deletes === undefined) {
|
|
504
|
+
throw new Error(`${operation} 需要 create/update/delete 之一`);
|
|
505
|
+
}
|
|
506
|
+
const from = resolveRelationColumn(def.from);
|
|
507
|
+
const to = resolveRelationColumn(def.to);
|
|
508
|
+
const parentValue = parent[from.outKey];
|
|
509
|
+
if (parentValue === undefined || parentValue === null)
|
|
510
|
+
throw new Error(`${operation}: ${table.name}.${from.outKey} 不能为空`);
|
|
511
|
+
const childPks = def.table.__plan.filter(p => p.pk);
|
|
512
|
+
if (childPks.length !== 1)
|
|
513
|
+
throw new Error(`${operation}: 目标表 ${def.table.name} 必须使用单列主键`);
|
|
514
|
+
const childPk = childPks[0];
|
|
515
|
+
const updateItems = [];
|
|
516
|
+
const updateIds = new Set();
|
|
517
|
+
if (updates) {
|
|
518
|
+
for (const [index, patch] of updates.entries()) {
|
|
519
|
+
if (!isPlainObject(patch))
|
|
520
|
+
throw new Error(`${operation}.update[${index}] 必须是对象`);
|
|
521
|
+
const id = patch[childPk.outKey];
|
|
522
|
+
if (id === undefined || id === null)
|
|
523
|
+
throw new Error(`${operation}.update[${index}] 需要 ${def.table.name}.${childPk.outKey}`);
|
|
524
|
+
if (updateIds.has(id))
|
|
525
|
+
throw new Error(`${operation}.update[${index}] 重复子行 ${String(id)}`);
|
|
526
|
+
updateIds.add(id);
|
|
527
|
+
const values = { ...patch };
|
|
528
|
+
delete values[childPk.outKey];
|
|
529
|
+
if (Object.keys(values).length === 0)
|
|
530
|
+
throw new Error(`${operation}.update[${index}] patch 至少需要一个字段`);
|
|
531
|
+
if (to.outKey in values)
|
|
532
|
+
throw new Error(`${operation}.update[${index}] 不能修改外键 ${def.table.name}.${to.outKey}`);
|
|
533
|
+
updateItems.push({ id, values });
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
const deleteItems = [];
|
|
537
|
+
const deleteIds = new Set();
|
|
538
|
+
if (deletes) {
|
|
539
|
+
for (const [index, spec] of deletes.entries()) {
|
|
540
|
+
if (!isPlainObject(spec))
|
|
541
|
+
throw new Error(`${operation}.delete[${index}] 必须是对象`);
|
|
542
|
+
const keys = Object.keys(spec);
|
|
543
|
+
if (keys.length !== 1 || keys[0] !== childPk.outKey) {
|
|
544
|
+
throw new Error(`${operation}.delete[${index}] 只允许 { ${childPk.outKey} }`);
|
|
545
|
+
}
|
|
546
|
+
const id = spec[childPk.outKey];
|
|
547
|
+
if (id === undefined || id === null)
|
|
548
|
+
throw new Error(`${operation}.delete[${index}] 需要 ${def.table.name}.${childPk.outKey}`);
|
|
549
|
+
if (deleteIds.has(id))
|
|
550
|
+
throw new Error(`${operation}.delete[${index}] 重复子行 ${String(id)}`);
|
|
551
|
+
deleteIds.add(id);
|
|
552
|
+
deleteItems.push(id);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
for (const id of updateIds) {
|
|
556
|
+
if (deleteIds.has(id))
|
|
557
|
+
throw new Error(`${operation}: 子行 ${String(id)} 不能同时 update 和 delete`);
|
|
558
|
+
}
|
|
559
|
+
if (creates) {
|
|
560
|
+
for (const [index, row] of creates.entries()) {
|
|
561
|
+
if (!isPlainObject(row))
|
|
562
|
+
throw new Error(`${operation}.create[${index}] 必须是对象`);
|
|
563
|
+
const id = row[childPk.outKey];
|
|
564
|
+
if (id !== undefined && id !== null && (updateIds.has(id) || deleteIds.has(id))) {
|
|
565
|
+
throw new Error(`${operation}.create[${index}]: 子行 ${String(id)} 与 update/delete 冲突`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (creates && creates.length > 0) {
|
|
570
|
+
const values = creates.map(row => ({ ...row, [to.outKey]: parentValue }));
|
|
571
|
+
await insertManyImpl(ctx, def.table, values);
|
|
572
|
+
}
|
|
573
|
+
for (const { id, values } of updateItems) {
|
|
574
|
+
const rows = await updateColumnsImpl(ctx, def.table, values, {
|
|
575
|
+
[childPk.outKey]: id,
|
|
576
|
+
[to.outKey]: parentValue,
|
|
577
|
+
});
|
|
578
|
+
if (rows.length !== 1) {
|
|
579
|
+
throw new Error(`${operation}: 子行 ${String(id)} 不存在或不属于父行 ${String(parentValue)}`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
for (const id of deleteItems) {
|
|
583
|
+
const deleted = await deleteImpl(ctx, def.table, {
|
|
584
|
+
[childPk.outKey]: id,
|
|
585
|
+
[to.outKey]: parentValue,
|
|
586
|
+
});
|
|
587
|
+
if (deleted !== 1) {
|
|
588
|
+
throw new Error(`${operation}: 子行 ${String(id)} 不存在或不属于父行 ${String(parentValue)}`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
415
592
|
async function createImpl(ctx, table, input) {
|
|
416
593
|
const rels = ctx.relations.find(r => r.table === table);
|
|
417
594
|
const columns = {};
|
|
@@ -550,6 +727,8 @@ async function updateImpl(ctx, table, values, where) {
|
|
|
550
727
|
const rels = ctx.relations.find(r => r.table === table);
|
|
551
728
|
const columns = {};
|
|
552
729
|
const links = [];
|
|
730
|
+
const manyUpdates = [];
|
|
731
|
+
const oneUpdates = [];
|
|
553
732
|
const unknown = [];
|
|
554
733
|
for (const [k, v] of Object.entries(values)) {
|
|
555
734
|
if (v === undefined)
|
|
@@ -563,8 +742,16 @@ async function updateImpl(ctx, table, values, where) {
|
|
|
563
742
|
links.push({ key: k, def, value: v });
|
|
564
743
|
continue;
|
|
565
744
|
}
|
|
745
|
+
if (def?._kind === 'many') {
|
|
746
|
+
manyUpdates.push({ key: k, def, value: v });
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
if (def?._kind === 'one') {
|
|
750
|
+
oneUpdates.push({ key: k, def, value: v });
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
566
753
|
if (def)
|
|
567
|
-
throw new Error(`update: ${table.name}.${k} 不支持关系写入(只有 M2M through 支持)`);
|
|
754
|
+
throw new Error(`update: ${table.name}.${k} 不支持关系写入(只有 M2M through / many / one 支持)`);
|
|
568
755
|
unknown.push(k);
|
|
569
756
|
}
|
|
570
757
|
// 保留既有 update 的 ZodError 语义:未知列先交给严格 schema 报错。
|
|
@@ -573,7 +760,20 @@ async function updateImpl(ctx, table, values, where) {
|
|
|
573
760
|
table.updateSchema.parse(values);
|
|
574
761
|
throw new Error(`update: 未知字段 ${table.name}.${unknown[0]}`);
|
|
575
762
|
}
|
|
576
|
-
|
|
763
|
+
const relationWrites = links.length > 0 || manyUpdates.length > 0 || oneUpdates.length > 0;
|
|
764
|
+
if (relationWrites) {
|
|
765
|
+
const pkKeys = table.__plan.filter(p => p.pk).map(p => p.outKey);
|
|
766
|
+
const changedPk = pkKeys.find(k => columns[k] !== undefined);
|
|
767
|
+
if (changedPk)
|
|
768
|
+
throw new Error(`update: 关系写入时不能修改父主键 ${table.name}.${changedPk}`);
|
|
769
|
+
for (const { key, def } of oneUpdates) {
|
|
770
|
+
const from = resolveRelationColumn(def.from);
|
|
771
|
+
if (columns[from.outKey] !== undefined) {
|
|
772
|
+
throw new Error(`update: ${table.name}.${key} 与父外键 ${table.name}.${from.outKey} 冲突`);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
if (!relationWrites)
|
|
577
777
|
return updateColumnsImpl(ctx, table, columns, where);
|
|
578
778
|
return beginOrSavepoint(ctx.raw, async (txSql) => {
|
|
579
779
|
const tx = { ...ctx, raw: txSql };
|
|
@@ -585,9 +785,15 @@ async function updateImpl(ctx, table, values, where) {
|
|
|
585
785
|
rows = await findManyImpl(tx, table, { where: where, limit: 2 });
|
|
586
786
|
}
|
|
587
787
|
if (rows.length !== 1) {
|
|
588
|
-
throw new Error(`update:
|
|
788
|
+
throw new Error(`update: 关系写入要求 where 命中恰好 1 行(命中 ${rows.length} 行)`);
|
|
589
789
|
}
|
|
590
790
|
const parent = rows[0];
|
|
791
|
+
for (const { key, def, value } of manyUpdates) {
|
|
792
|
+
await applyManyUpdate(tx, parent, table, key, def, value);
|
|
793
|
+
}
|
|
794
|
+
for (const { key, def, value } of oneUpdates) {
|
|
795
|
+
await applyOneUpdate(tx, parent, table, key, def, value);
|
|
796
|
+
}
|
|
591
797
|
for (const { key, def, value } of links) {
|
|
592
798
|
await applyLinkWrite(tx, parent, table, key, def, value);
|
|
593
799
|
}
|
|
@@ -645,7 +851,52 @@ async function countImpl(ctx, table, where) {
|
|
|
645
851
|
const rows = await exec(ctx, text, w?.params ?? [], { operation: 'count', table: table.name });
|
|
646
852
|
return rows[0]?.n ?? 0;
|
|
647
853
|
}
|
|
648
|
-
|
|
854
|
+
const AGGREGATE_NUMERIC = /^(int|int2|int4|int8|smallint|integer|bigint|numeric|decimal|real|double|float)/;
|
|
855
|
+
function isPlainObject(value) {
|
|
856
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof Date);
|
|
857
|
+
}
|
|
858
|
+
function parseAggregate(table, raw) {
|
|
859
|
+
if (!isPlainObject(raw))
|
|
860
|
+
throw new Error(`with: ${table.name}.aggregate 必须是对象`);
|
|
861
|
+
const allowed = new Set(['count', 'sum', 'max']);
|
|
862
|
+
for (const key of Object.keys(raw)) {
|
|
863
|
+
if (!allowed.has(key))
|
|
864
|
+
throw new Error(`with: ${table.name}.aggregate 不支持 ${key}`);
|
|
865
|
+
}
|
|
866
|
+
if (raw.count !== undefined && raw.count !== true)
|
|
867
|
+
throw new Error(`with: ${table.name}.aggregate.count 必须是 true`);
|
|
868
|
+
const count = raw.count === true;
|
|
869
|
+
const parseColumns = (name) => {
|
|
870
|
+
const value = raw[name];
|
|
871
|
+
if (value === undefined)
|
|
872
|
+
return [];
|
|
873
|
+
if (!isPlainObject(value))
|
|
874
|
+
throw new Error(`with: ${table.name}.aggregate.${name} 必须是对象`);
|
|
875
|
+
const result = [];
|
|
876
|
+
for (const [outKey, enabled] of Object.entries(value)) {
|
|
877
|
+
if (enabled !== true)
|
|
878
|
+
throw new Error(`with: ${table.name}.aggregate.${name}.${outKey} 必须是 true`);
|
|
879
|
+
const plan = table.__plan.find(p => p.outKey === outKey);
|
|
880
|
+
if (!plan)
|
|
881
|
+
throw new Error(`with: ${table.name}.aggregate.${name} 未知列 ${outKey}`);
|
|
882
|
+
if (name === 'sum' && !AGGREGATE_NUMERIC.test(plan.sqlType)) {
|
|
883
|
+
throw new Error(`with: ${table.name}.aggregate.sum.${outKey} 必须是数值列(${plan.sqlType})`);
|
|
884
|
+
}
|
|
885
|
+
result.push({ outKey, dbKey: plan.dbKey, parse: value => plan.parse.parse(value) });
|
|
886
|
+
}
|
|
887
|
+
return result;
|
|
888
|
+
};
|
|
889
|
+
const sum = parseColumns('sum');
|
|
890
|
+
const max = parseColumns('max');
|
|
891
|
+
if (!count && sum.length === 0 && max.length === 0) {
|
|
892
|
+
throw new Error(`with: ${table.name}.aggregate 至少需要 count/sum/max 之一`);
|
|
893
|
+
}
|
|
894
|
+
return { count, sum, max };
|
|
895
|
+
}
|
|
896
|
+
const MAX_WITH_DEPTH = 3;
|
|
897
|
+
function resolveWith(ctx, table, withMap, depth = 1) {
|
|
898
|
+
if (depth > MAX_WITH_DEPTH)
|
|
899
|
+
throw new Error(`with: 关系加载超过最大深度 ${MAX_WITH_DEPTH}`);
|
|
649
900
|
const entries = Object.entries(withMap).filter(([, v]) => v !== undefined && v !== false);
|
|
650
901
|
if (entries.length === 0)
|
|
651
902
|
return [];
|
|
@@ -662,13 +913,13 @@ function resolveWith(ctx, table, withMap) {
|
|
|
662
913
|
throw new Error(`with: ${table.name}.${key} 只支持 true/false 或关系选项对象`);
|
|
663
914
|
}
|
|
664
915
|
const opts = enabled;
|
|
665
|
-
const allowed = new Set(['where', 'orderBy', 'limit', 'offset', 'with']);
|
|
916
|
+
const allowed = new Set(['where', 'orderBy', 'limit', 'offset', 'with', 'aggregate']);
|
|
666
917
|
for (const k of Object.keys(opts)) {
|
|
667
918
|
if (!allowed.has(k))
|
|
668
919
|
throw new Error(`with: ${table.name}.${key} 不支持选项 ${k}`);
|
|
669
920
|
}
|
|
670
921
|
if (Object.keys(opts).length === 0)
|
|
671
|
-
throw new Error(`with: ${table.name}.${key} 需要 where/orderBy/limit/offset/with 之一`);
|
|
922
|
+
throw new Error(`with: ${table.name}.${key} 需要 where/orderBy/limit/offset/with/aggregate 之一`);
|
|
672
923
|
if (def.through && (opts.where !== undefined || opts.orderBy !== undefined || opts.limit !== undefined || opts.offset !== undefined)) {
|
|
673
924
|
throw new Error(`with: M2M through 关系 ${table.name}.${key} 暂不支持 where/orderBy/limit/offset`);
|
|
674
925
|
}
|
|
@@ -678,6 +929,10 @@ function resolveWith(ctx, table, withMap) {
|
|
|
678
929
|
if (opts.with !== undefined && (typeof opts.with !== 'object' || opts.with === null || Array.isArray(opts.with))) {
|
|
679
930
|
throw new Error(`with: ${table.name}.${key}.with 必须是对象`);
|
|
680
931
|
}
|
|
932
|
+
const aggregate = opts.aggregate === undefined ? undefined : parseAggregate(def.table, opts.aggregate);
|
|
933
|
+
if (aggregate && table.__plan.some(p => p.outKey === `${key}Aggregate`)) {
|
|
934
|
+
throw new Error(`with: 聚合别名 ${key}Aggregate 与 ${table.name} 的真实列冲突`);
|
|
935
|
+
}
|
|
681
936
|
return {
|
|
682
937
|
key,
|
|
683
938
|
def,
|
|
@@ -686,10 +941,11 @@ function resolveWith(ctx, table, withMap) {
|
|
|
686
941
|
limit: opts.limit,
|
|
687
942
|
offset: opts.offset,
|
|
688
943
|
with: opts.with,
|
|
944
|
+
aggregate,
|
|
689
945
|
};
|
|
690
946
|
});
|
|
691
947
|
}
|
|
692
|
-
async function attachThroughRelation(ctx, rows, key, def, through, nested) {
|
|
948
|
+
async function attachThroughRelation(ctx, rows, key, def, through, nested, depth = 1) {
|
|
693
949
|
const from = resolveRelationColumn(def.from);
|
|
694
950
|
const to = resolveRelationColumn(def.to);
|
|
695
951
|
const throughFrom = resolveRelationColumn(through.from);
|
|
@@ -727,7 +983,7 @@ async function attachThroughRelation(ctx, rows, key, def, through, nested) {
|
|
|
727
983
|
row[key] = bucket;
|
|
728
984
|
}
|
|
729
985
|
if (nested.length > 0 && children.length > 0)
|
|
730
|
-
await attachRelations(ctx, children, nested);
|
|
986
|
+
await attachRelations(ctx, children, nested, depth);
|
|
731
987
|
}
|
|
732
988
|
function validateWithNumber(name, value) {
|
|
733
989
|
if (value === undefined)
|
|
@@ -735,11 +991,76 @@ function validateWithNumber(name, value) {
|
|
|
735
991
|
if (!Number.isInteger(value) || value < 0)
|
|
736
992
|
throw new Error(`with: ${name} 必须是非负整数`);
|
|
737
993
|
}
|
|
738
|
-
async function
|
|
739
|
-
|
|
740
|
-
|
|
994
|
+
async function attachAggregate(ctx, rows, key, def, aggregate, where) {
|
|
995
|
+
const from = resolveRelationColumn(def.from);
|
|
996
|
+
const to = resolveRelationColumn(def.to);
|
|
997
|
+
const through = def.through;
|
|
998
|
+
const values = [...new Set(rows.map(r => r[from.outKey]).filter(v => v !== null && v !== undefined))];
|
|
999
|
+
const targetAliasName = '__zd_agg_target';
|
|
1000
|
+
const targetAlias = quoteIdent(targetAliasName);
|
|
1001
|
+
let parentRef;
|
|
1002
|
+
let fromClause;
|
|
1003
|
+
if (through) {
|
|
1004
|
+
const throughFrom = resolveRelationColumn(through.from);
|
|
1005
|
+
const throughTo = resolveRelationColumn(through.to);
|
|
1006
|
+
const throughAlias = quoteIdent('__zd_agg_through');
|
|
1007
|
+
parentRef = `${throughAlias}.${quoteIdent(throughFrom.dbKey)}`;
|
|
1008
|
+
fromClause = `from ${quoteIdent(through.table.name)} as ${throughAlias}`
|
|
1009
|
+
+ ` join ${quoteIdent(def.table.name)} as ${targetAlias}`
|
|
1010
|
+
+ ` on ${targetAlias}.${quoteIdent(to.dbKey)} = ${throughAlias}.${quoteIdent(throughTo.dbKey)}`;
|
|
1011
|
+
}
|
|
1012
|
+
else {
|
|
1013
|
+
parentRef = `${targetAlias}.${quoteIdent(to.dbKey)}`;
|
|
1014
|
+
fromClause = `from ${quoteIdent(def.table.name)} as ${targetAlias}`;
|
|
1015
|
+
}
|
|
1016
|
+
const sumFields = aggregate.sum.map((p, i) => ({ ...p, alias: `__zd_sum_${i}` }));
|
|
1017
|
+
const maxFields = aggregate.max.map((p, i) => ({ ...p, alias: `__zd_max_${i}` }));
|
|
1018
|
+
const select = [`${parentRef} as "__zd_parent"`];
|
|
1019
|
+
if (aggregate.count)
|
|
1020
|
+
select.push('count(*)::int as "__zd_count"');
|
|
1021
|
+
for (const p of sumFields)
|
|
1022
|
+
select.push(`sum(${targetAlias}.${quoteIdent(p.dbKey)})::float8 as ${quoteIdent(p.alias)}`);
|
|
1023
|
+
for (const p of maxFields)
|
|
1024
|
+
select.push(`max(${targetAlias}.${quoteIdent(p.dbKey)}) as ${quoteIdent(p.alias)}`);
|
|
1025
|
+
const wherePart = where === undefined
|
|
1026
|
+
? { text: '', params: [] }
|
|
1027
|
+
: compileWhere(def.table, where, 1, [], targetAliasName);
|
|
1028
|
+
const params = [values, ...wherePart.params];
|
|
1029
|
+
const predicate = wherePart.text ? ` and (${wherePart.text})` : '';
|
|
1030
|
+
const text = `select ${select.join(', ')} ${fromClause}`
|
|
1031
|
+
+ ` where ${parentRef} = any($1)${predicate}`
|
|
1032
|
+
+ ` group by ${parentRef}`;
|
|
1033
|
+
const aggregateRows = values.length === 0
|
|
1034
|
+
? []
|
|
1035
|
+
: await exec(ctx, text, params, { operation: 'with', table: def.table.name });
|
|
1036
|
+
const byParent = new Map();
|
|
1037
|
+
for (const row of aggregateRows) {
|
|
1038
|
+
const parent = row.__zd_parent;
|
|
1039
|
+
if (parent !== null && parent !== undefined)
|
|
1040
|
+
byParent.set(parent, row);
|
|
1041
|
+
}
|
|
1042
|
+
for (const row of rows) {
|
|
1043
|
+
const parent = row[from.outKey];
|
|
1044
|
+
const raw = parent === null || parent === undefined ? undefined : byParent.get(parent);
|
|
1045
|
+
const result = {};
|
|
1046
|
+
if (aggregate.count)
|
|
1047
|
+
result.count = Number(raw?.__zd_count ?? 0);
|
|
1048
|
+
if (sumFields.length > 0) {
|
|
1049
|
+
result.sum = Object.fromEntries(sumFields.map(p => [p.outKey, raw?.[p.alias] == null ? null : Number(raw[p.alias])]));
|
|
1050
|
+
}
|
|
1051
|
+
if (maxFields.length > 0) {
|
|
1052
|
+
result.max = Object.fromEntries(maxFields.map(p => [p.outKey, raw?.[p.alias] == null ? null : p.parse(raw[p.alias])]));
|
|
1053
|
+
}
|
|
1054
|
+
row[`${key}Aggregate`] = result;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
async function attachRelations(ctx, rows, resolved, depth = 1) {
|
|
1058
|
+
for (const { key, def, where, orderBy, limit, offset, with: nestedInput, aggregate } of resolved) {
|
|
1059
|
+
const nested = nestedInput !== undefined ? resolveWith(ctx, def.table, nestedInput, depth + 1) : [];
|
|
741
1060
|
if (def.through) {
|
|
742
|
-
await attachThroughRelation(ctx, rows, key, def, def.through, nested);
|
|
1061
|
+
await attachThroughRelation(ctx, rows, key, def, def.through, nested, depth + 1);
|
|
1062
|
+
if (aggregate)
|
|
1063
|
+
await attachAggregate(ctx, rows, key, def, aggregate, where);
|
|
743
1064
|
continue;
|
|
744
1065
|
}
|
|
745
1066
|
const from = resolveRelationColumn(def.from);
|
|
@@ -807,11 +1128,13 @@ async function attachRelations(ctx, rows, resolved) {
|
|
|
807
1128
|
row[key] = def._kind === 'many' ? (bucket ?? []) : (bucket?.[0] ?? null);
|
|
808
1129
|
}
|
|
809
1130
|
if (nested.length > 0 && children.length > 0)
|
|
810
|
-
await attachRelations(ctx, children, nested);
|
|
1131
|
+
await attachRelations(ctx, children, nested, depth + 1);
|
|
1132
|
+
if (aggregate)
|
|
1133
|
+
await attachAggregate(ctx, rows, key, def, aggregate, where);
|
|
811
1134
|
}
|
|
812
1135
|
}
|
|
813
1136
|
async function findManyImpl(ctx, table, opts = {}) {
|
|
814
|
-
const resolved = opts.with ? resolveWith(ctx, table, opts.with) : [];
|
|
1137
|
+
const resolved = opts.with ? resolveWith(ctx, table, opts.with, 1) : [];
|
|
815
1138
|
const selectEntries = opts.select ? Object.entries(opts.select).filter(([, v]) => v) : null;
|
|
816
1139
|
if (selectEntries && selectEntries.length === 0)
|
|
817
1140
|
throw new Error('select 不能为空');
|
|
@@ -890,6 +1213,7 @@ export function createDb(conn, validate = 'always', relations = [], opts = {}) {
|
|
|
890
1213
|
};
|
|
891
1214
|
const db = {
|
|
892
1215
|
raw: conn,
|
|
1216
|
+
rawQuery: (text, params) => rawQueryImpl(ctx, text, params),
|
|
893
1217
|
sql: ((strings, ...params) => {
|
|
894
1218
|
const args = params.map(p => isSelectFragment(p) ? p.__fragment : p);
|
|
895
1219
|
assertParams(args);
|