vona-module-a-orm 5.1.39 → 5.1.41

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.
Files changed (61) hide show
  1. package/dist/.metadata/index.d.ts +18 -18
  2. package/dist/.metadata/index.d.ts.map +1 -1
  3. package/dist/index.js +459 -58
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/aggregateResult.d.ts +7 -0
  6. package/dist/lib/aggregateResult.d.ts.map +1 -0
  7. package/dist/lib/analysis/controls.d.ts +27 -0
  8. package/dist/lib/analysis/controls.d.ts.map +1 -0
  9. package/dist/lib/analysis/execute.d.ts +6 -0
  10. package/dist/lib/analysis/execute.d.ts.map +1 -0
  11. package/dist/lib/analysis/index.d.ts +4 -0
  12. package/dist/lib/analysis/index.d.ts.map +1 -0
  13. package/dist/lib/analysis/policy.d.ts +6 -0
  14. package/dist/lib/analysis/policy.d.ts.map +1 -0
  15. package/dist/lib/bean.model/bean.model_cache.d.ts +4 -1
  16. package/dist/lib/bean.model/bean.model_cache.d.ts.map +1 -1
  17. package/dist/lib/bean.model/bean.model_crud_inner.d.ts +1 -1
  18. package/dist/lib/bean.model/bean.model_crud_inner.d.ts.map +1 -1
  19. package/dist/lib/dto/dto.d.ts +3 -0
  20. package/dist/lib/dto/dto.d.ts.map +1 -1
  21. package/dist/lib/dto/dtoAggregate.d.ts.map +1 -1
  22. package/dist/lib/dto/dtoAnalysis.d.ts +6 -0
  23. package/dist/lib/dto/dtoAnalysis.d.ts.map +1 -0
  24. package/dist/lib/dto/dtoCreate.d.ts +2 -2
  25. package/dist/lib/dto/dtoCreate.d.ts.map +1 -1
  26. package/dist/lib/dto/dtoGet.d.ts.map +1 -1
  27. package/dist/lib/dto/dtoMutate.d.ts +2 -2
  28. package/dist/lib/dto/dtoMutate.d.ts.map +1 -1
  29. package/dist/lib/dto/dtoUpdate.d.ts +2 -2
  30. package/dist/lib/dto/dtoUpdate.d.ts.map +1 -1
  31. package/dist/lib/index.d.ts +1 -0
  32. package/dist/lib/index.d.ts.map +1 -1
  33. package/dist/types/analysis.d.ts +105 -0
  34. package/dist/types/analysis.d.ts.map +1 -0
  35. package/dist/types/dto/dtoMutate.d.ts +28 -11
  36. package/dist/types/dto/dtoMutate.d.ts.map +1 -1
  37. package/dist/types/index.d.ts +1 -0
  38. package/dist/types/index.d.ts.map +1 -1
  39. package/dist/types/relationsAggr.d.ts +1 -1
  40. package/dist/types/relationsAggr.d.ts.map +1 -1
  41. package/package.json +2 -2
  42. package/src/.metadata/index.ts +26 -26
  43. package/src/lib/aggregateResult.ts +53 -0
  44. package/src/lib/analysis/controls.ts +163 -0
  45. package/src/lib/analysis/execute.ts +49 -0
  46. package/src/lib/analysis/index.ts +3 -0
  47. package/src/lib/analysis/policy.ts +140 -0
  48. package/src/lib/bean.model/bean.model_cache.ts +96 -12
  49. package/src/lib/bean.model/bean.model_crud_inner.ts +8 -3
  50. package/src/lib/dto/dto.ts +3 -0
  51. package/src/lib/dto/dtoAggregate.ts +4 -1
  52. package/src/lib/dto/dtoAnalysis.ts +34 -0
  53. package/src/lib/dto/dtoCreate.ts +7 -9
  54. package/src/lib/dto/dtoGet.ts +27 -3
  55. package/src/lib/dto/dtoMutate.ts +2 -2
  56. package/src/lib/dto/dtoUpdate.ts +7 -9
  57. package/src/lib/index.ts +1 -0
  58. package/src/types/analysis.ts +137 -0
  59. package/src/types/dto/dtoMutate.ts +124 -87
  60. package/src/types/index.ts +1 -0
  61. package/src/types/relationsAggr.ts +1 -1
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import BigNumber from 'bignumber.js';
2
- import { BeanInfo, BeanAopMethodBase, BeanBase, deepExtend, appResource, assertIsolatedTestDatabaseClient, assertTestClaimDatabaseClient, beanFullNameFromOnionName, cast, useApp, $makeLocaleMagic, Virtual, registerTestClaimDatabaseCreation, BeanSimple, combineConfigDefault, BeanScopeBase, createBeanDecorator, PickClassInner, $Class, disposeInstance } from 'vona';
2
+ import { BeanInfo, BeanBase, BeanAopMethodBase, deepExtend, appResource, assertIsolatedTestDatabaseClient, assertTestClaimDatabaseClient, beanFullNameFromOnionName, cast, useApp, $makeLocaleMagic, Virtual, registerTestClaimDatabaseCreation, BeanSimple, combineConfigDefault, BeanScopeBase, createBeanDecorator, PickClassInner, $Class, registerMappedClassMetadataKey, appMetadata, disposeInstance } from 'vona';
3
3
  import { __decorate, __metadata } from 'tslib';
4
+ import { Schedule } from 'vona-module-a-schedule';
4
5
  import { AopMethod } from 'vona-module-a-aspect';
5
6
  import { Service, Bean, Scope } from 'vona-module-a-bean';
6
7
  import { AsyncLocalStorage, AsyncResource } from 'node:async_hooks';
@@ -9,18 +10,58 @@ import knex from 'knex';
9
10
  import { BeanMutateBase } from 'vona-module-a-beanmutate';
10
11
  import { swapDeps } from '@cabloy/deps';
11
12
  import { $tableNameFromEntity, $tableDefaults, $columnsAll } from 'vona-module-a-ormutils';
12
- import { prepareClassType, Api, v, schemaRenderVisible, getSchemaDynamic, SymbolSchemaDynamicRefId, addSchemaDynamic, getTargetDecoratorRuleColumns, mergeTargetDecoratorDtoOpenapi, mergeDtoFieldsOpenapiMetadata } from 'vona-module-a-openapiutils';
13
+ import { prepareClassType, Api, v, getTargetDecoratorRules, schemaRenderVisible, getSchemaDynamic, SymbolSchemaDynamicRefId, addSchemaDynamic, getTargetDecoratorRuleColumns, mergeTargetDecoratorDtoOpenapi, mergeDtoFieldsOpenapiMetadata } from 'vona-module-a-openapiutils';
13
14
  import z from 'zod';
14
15
  import { Broadcast, BeanBroadcastBase } from 'vona-module-a-broadcast';
15
16
  import { Event, BeanEventBase } from 'vona-module-a-event';
16
17
  import { Hmr } from 'vona-module-a-hmrbase';
17
18
  import { Queue, BeanQueueBase } from 'vona-module-a-queue';
18
- import { Schedule } from 'vona-module-a-schedule';
19
19
  import { configAll, configRedis } from 'vona-module-a-summer';
20
20
  import { ServiceDatabaseAsyncLocalStorage as ServiceDatabaseAsyncLocalStorage$1, ServiceTransactionConsistency‌ as ServiceTransactionConsistency_ } from 'vona-module-a-orm';
21
+ import { ZodMetadata } from '@cabloy/zod-openapi';
21
22
  import { mutate } from 'mutate-on-copy';
22
23
  import { parseFirstWord, toLowerCaseFirstChar } from '@cabloy/word-utils';
23
24
 
25
+ let ScheduleSoftDeletionPrune = class ScheduleSoftDeletionPrune extends BeanBase {
26
+ async execute() {
27
+ const onionSlices = this.bean.onion.model.getOnionsEnabledCached();
28
+ for (const onionSlice of onionSlices) {
29
+ if (onionSlice.beanOptions.options?.disableDeleted) continue;
30
+ let softDeletionPrune = onionSlice.beanOptions.options?.softDeletionPrune ?? this.scope.config.softDeletionPrune.enable;
31
+ if (!softDeletionPrune) continue;
32
+ if (softDeletionPrune === true) softDeletionPrune = {};
33
+ await this._modulePrune(onionSlice, softDeletionPrune);
34
+ }
35
+ }
36
+ async _modulePrune(onionSlice, softDeletionPrune) {
37
+ const handler = softDeletionPrune.handler;
38
+ const expired = softDeletionPrune.expired ?? this.scope.config.softDeletionPrune.expired;
39
+ const modelTarget = this.bean._getBean(onionSlice.beanOptions.beanFullName);
40
+ if (handler) {
41
+ await handler(this.ctx, modelTarget, {
42
+ expired
43
+ });
44
+ } else {
45
+ const expiredTime = new Date(Date.now() - expired);
46
+ await modelTarget.delete({
47
+ deleted: true,
48
+ updatedAt: {
49
+ _lt_: expiredTime
50
+ }
51
+ }, {
52
+ disableDeleted: true
53
+ });
54
+ }
55
+ }
56
+ };
57
+ ScheduleSoftDeletionPrune = __decorate([Schedule({
58
+ repeat: {
59
+ every: 24 * 3600 * 1000
60
+ }
61
+ }), BeanInfo({
62
+ module: "a-orm"
63
+ })], ScheduleSoftDeletionPrune);
64
+
24
65
  let AopMethodTransaction = class AopMethodTransaction extends BeanAopMethodBase {
25
66
  execute(options, _args, next, _receiver, _prop) {
26
67
  return this.bean.database.current.transaction.begin(() => {
@@ -781,6 +822,50 @@ BeanDatabase = __decorate([Bean(), BeanInfo({
781
822
  module: "a-orm"
782
823
  })], BeanDatabase);
783
824
 
825
+ function normalizeAggregateValue(value, fn) {
826
+ if (value === undefined) return value;
827
+ if (value === null && fn !== 'count') return value;
828
+ if (typeof value === 'string') {
829
+ if (fn !== 'count' || /^(0|[1-9]\d*)$/.test(value)) return value;
830
+ } else if (typeof value === 'number') {
831
+ if (Number.isFinite(value) && (fn !== 'count' || Number.isSafeInteger(value) && value >= 0)) {
832
+ return String(value);
833
+ }
834
+ } else if (typeof value === 'bigint' && (fn !== 'count' || value >= 0n)) {
835
+ return String(value);
836
+ }
837
+ throw new TypeError(`Invalid ORM ${fn} aggregate result`);
838
+ }
839
+ function normalizeAggregateRows(rows, aggrs) {
840
+ if (!aggrs || rows.length === 0) return rows;
841
+ const aliases = [];
842
+ for (const fn of ['count', 'sum', 'avg', 'min', 'max']) {
843
+ const columns = aggrs[fn];
844
+ if (columns === undefined) continue;
845
+ for (const column of typeof columns === 'object' ? columns : [columns]) {
846
+ aliases.push([`${fn}_${column === '*' ? 'all' : String(column)}`, fn]);
847
+ }
848
+ }
849
+ let result = rows;
850
+ for (const [index, row] of rows.entries()) {
851
+ let target = row;
852
+ for (const [alias, fn] of aliases) {
853
+ if (!Object.hasOwn(row, alias)) continue;
854
+ const value = row[alias];
855
+ const normalized = normalizeAggregateValue(value, fn);
856
+ if (value === normalized) continue;
857
+ if (target === row) target = {
858
+ ...row
859
+ };
860
+ target[alias] = normalized;
861
+ }
862
+ if (target === row) continue;
863
+ if (result === rows) result = [...rows];
864
+ result[index] = target;
865
+ }
866
+ return result;
867
+ }
868
+
784
869
  const OpAggrs = ['count', 'sum', 'avg', 'max', 'min'];
785
870
  const OpJoint = {
786
871
  and: '_and_',
@@ -1068,6 +1153,8 @@ function _safeColumn(column) {
1068
1153
  return column.replace(/[\\.#%'"`;,() ]/g, '');
1069
1154
  }
1070
1155
 
1156
+ const SymbolDtoMutateRelations = Symbol('SymbolDtoMutateRelations');
1157
+
1071
1158
  const app$1 = useApp();
1072
1159
  const ormConfig$1 = app$1.util.getModuleConfigRaw('a-orm');
1073
1160
  const ordersDefault = ormConfig$1?.rest?.query?.orders?.default ?? 'createdAt,desc';
@@ -1764,7 +1851,8 @@ class BeanModelCrudInner extends BeanModelView {
1764
1851
  }
1765
1852
  return `model.select: ${tip}, ${builder.toQuery()}`;
1766
1853
  });
1767
- return await this.dialect.select(builder, datas, () => this.columns(table));
1854
+ const rows = await this.dialect.select(builder, datas, () => this.columns(table));
1855
+ return normalizeAggregateRows(rows, params?.aggrs);
1768
1856
  }
1769
1857
  _select_buildParams(table, params, options) {
1770
1858
  // builder
@@ -1827,7 +1915,7 @@ class BeanModelCrudInner extends BeanModelView {
1827
1915
  // ready
1828
1916
  this.$loggerChild('model').debug(() => `model.count: ${builder.toQuery()}`);
1829
1917
  const res = await builder;
1830
- return this.extractFirstNumber(res);
1918
+ return normalizeAggregateValue(this.extractFirstValue(res), 'count');
1831
1919
  }
1832
1920
  async _increment(table, params, options) {
1833
1921
  // table
@@ -3080,46 +3168,6 @@ QueueDoubleDelete = __decorate([Queue({
3080
3168
  module: "a-orm"
3081
3169
  })], QueueDoubleDelete);
3082
3170
 
3083
- let ScheduleSoftDeletionPrune = class ScheduleSoftDeletionPrune extends BeanBase {
3084
- async execute() {
3085
- const onionSlices = this.bean.onion.model.getOnionsEnabledCached();
3086
- for (const onionSlice of onionSlices) {
3087
- if (onionSlice.beanOptions.options?.disableDeleted) continue;
3088
- let softDeletionPrune = onionSlice.beanOptions.options?.softDeletionPrune ?? this.scope.config.softDeletionPrune.enable;
3089
- if (!softDeletionPrune) continue;
3090
- if (softDeletionPrune === true) softDeletionPrune = {};
3091
- await this._modulePrune(onionSlice, softDeletionPrune);
3092
- }
3093
- }
3094
- async _modulePrune(onionSlice, softDeletionPrune) {
3095
- const handler = softDeletionPrune.handler;
3096
- const expired = softDeletionPrune.expired ?? this.scope.config.softDeletionPrune.expired;
3097
- const modelTarget = this.bean._getBean(onionSlice.beanOptions.beanFullName);
3098
- if (handler) {
3099
- await handler(this.ctx, modelTarget, {
3100
- expired
3101
- });
3102
- } else {
3103
- const expiredTime = new Date(Date.now() - expired);
3104
- await modelTarget.delete({
3105
- deleted: true,
3106
- updatedAt: {
3107
- _lt_: expiredTime
3108
- }
3109
- }, {
3110
- disableDeleted: true
3111
- });
3112
- }
3113
- }
3114
- };
3115
- ScheduleSoftDeletionPrune = __decorate([Schedule({
3116
- repeat: {
3117
- every: 24 * 3600 * 1000
3118
- }
3119
- }), BeanInfo({
3120
- module: "a-orm"
3121
- })], ScheduleSoftDeletionPrune);
3122
-
3123
3171
  function config(_app) {
3124
3172
  return {
3125
3173
  sharding: {
@@ -3332,6 +3380,255 @@ ScopeModuleAOrm = __decorate([Scope(), BeanInfo({
3332
3380
  })], ScopeModuleAOrm);
3333
3381
  /** scope: end */
3334
3382
 
3383
+ const analysisFunctions = ['count', 'sum', 'avg', 'min', 'max'];
3384
+ const analysisReservedFields = new Set(['groups', 'aggrs', 'where', 'joins', 'having', 'columns', 'orders', 'limit', 'offset', 'pageNo', 'pageSize', 'distinct', 'include', 'with']);
3385
+ /** A deliberate validation/resource-bound error, also usable without an application context. */
3386
+ class AnalysisError extends Error {
3387
+ constructor(...args) {
3388
+ super(...args);
3389
+ this.code = 422;
3390
+ this.status = 422;
3391
+ }
3392
+ }
3393
+ function analysisFailure(message) {
3394
+ throw new AnalysisError(message);
3395
+ }
3396
+ function actionDefaults(policy, mode) {
3397
+ if (mode !== 'group' && mode !== 'aggregate') analysisFailure('Unknown analysis action');
3398
+ const defaults = policy.defaults[mode];
3399
+ if (!defaults) analysisFailure(`Analysis ${mode} action is not enabled`);
3400
+ return defaults;
3401
+ }
3402
+ function columnArray(columns, max) {
3403
+ return z.array(z.enum([...columns])).min(1).max(max).superRefine((value, ctx) => {
3404
+ if (new Set(value).size !== value.length) {
3405
+ ctx.addIssue({
3406
+ code: 'custom',
3407
+ message: 'Duplicate analysis columns are not allowed'
3408
+ });
3409
+ }
3410
+ });
3411
+ }
3412
+ /** Native array/object schemas preserve finite enums for query coercion and OpenAPI. */
3413
+ function analysisControlSchemas(policy, mode) {
3414
+ const defaults = actionDefaults(policy, mode);
3415
+ const shape = {};
3416
+ for (const fn of analysisFunctions) {
3417
+ const columns = policy.aggrs[fn];
3418
+ if (columns) shape[fn] = columnArray(columns, policy.limits.maxMetrics).optional();
3419
+ }
3420
+ const aggrs = z.strictObject(shape).superRefine((value, ctx) => {
3421
+ const count = Object.values(value).reduce((total, columns) => total + (Array.isArray(columns) ? columns.length : 0), 0);
3422
+ if (count === 0 || count > policy.limits.maxMetrics) {
3423
+ ctx.addIssue({
3424
+ code: 'custom',
3425
+ message: `Analysis requires 1 to ${policy.limits.maxMetrics} metrics`
3426
+ });
3427
+ }
3428
+ }).default(() => Object.fromEntries(Object.entries(defaults.aggrs).map(([fn, columns]) => [fn, [...columns]])));
3429
+ const groups = mode === 'group' ? columnArray(policy.groups, policy.limits.maxGroups).default(() => [...defaults.groups]) : undefined;
3430
+ const selection = z.strictObject({
3431
+ groups: groups ?? z.never().optional(),
3432
+ aggrs
3433
+ });
3434
+ return {
3435
+ groups,
3436
+ aggrs,
3437
+ selection
3438
+ };
3439
+ }
3440
+ /** Validate controls independently of row filters and ignored list controls. */
3441
+ function resolveAnalysis(policy, mode, input) {
3442
+ if (input !== undefined && (!input || typeof input !== 'object' || Array.isArray(input))) {
3443
+ analysisFailure('Analysis parameters must be an object');
3444
+ }
3445
+ const params = input ?? {};
3446
+ for (const key of ['joins', 'having', 'distinct', 'include', 'with']) {
3447
+ if (Object.hasOwn(params, key) && params[key] !== undefined) {
3448
+ analysisFailure(`Analysis does not support ${key}`);
3449
+ }
3450
+ }
3451
+ if (mode === 'aggregate' && Object.hasOwn(params, 'groups')) {
3452
+ analysisFailure('Aggregate analysis does not support groups');
3453
+ }
3454
+ // The request context can enable generic query coercion. Direct service calls must
3455
+ // still supply native arrays/objects, never JSON strings or coerced scalar items.
3456
+ const assertNativeColumns = columns => {
3457
+ if (!Array.isArray(columns) || columns.some(column => typeof column !== 'string')) {
3458
+ analysisFailure('Analysis columns must be arrays of strings');
3459
+ }
3460
+ };
3461
+ if (params.groups !== undefined) assertNativeColumns(params.groups);
3462
+ if (params.aggrs !== undefined) {
3463
+ if (!params.aggrs || typeof params.aggrs !== 'object' || Array.isArray(params.aggrs)) {
3464
+ analysisFailure('Analysis metrics must be an object of column arrays');
3465
+ }
3466
+ for (const columns of Object.values(params.aggrs)) assertNativeColumns(columns);
3467
+ }
3468
+ const parsed = analysisControlSchemas(policy, mode).selection.safeParse({
3469
+ ...(mode === 'group' ? {
3470
+ groups: params.groups
3471
+ } : {}),
3472
+ aggrs: params.aggrs
3473
+ });
3474
+ if (!parsed.success) analysisFailure(parsed.error.message);
3475
+ const result = {
3476
+ aggrs: {}
3477
+ };
3478
+ if (mode === 'group') {
3479
+ // Canonical policy order also stabilizes SQL/cache keys for equivalent selections.
3480
+ const selected = parsed.data.groups;
3481
+ result.groups = policy.groups.filter(column => selected.includes(column));
3482
+ }
3483
+ for (const fn of analysisFunctions) {
3484
+ const selected = parsed.data.aggrs[fn];
3485
+ if (selected) result.aggrs[fn] = policy.aggrs[fn].filter(column => selected.includes(column));
3486
+ }
3487
+ return result;
3488
+ }
3489
+ function analysisCapabilities(policy, mode) {
3490
+ const defaults = actionDefaults(policy, mode);
3491
+ return {
3492
+ mode,
3493
+ groups: mode === 'group' ? [...policy.groups] : [],
3494
+ aggrs: Object.fromEntries(Object.entries(policy.aggrs).map(([fn, columns]) => [fn, [...columns]])),
3495
+ defaults: {
3496
+ ...(mode === 'group' ? {
3497
+ groups: [...defaults.groups]
3498
+ } : {}),
3499
+ aggrs: Object.fromEntries(Object.entries(defaults.aggrs).map(([fn, columns]) => [fn, [...columns]]))
3500
+ },
3501
+ limits: {
3502
+ ...policy.limits
3503
+ }
3504
+ };
3505
+ }
3506
+
3507
+ function assertModel(model, policy) {
3508
+ if (!(model instanceof policy.modelClass)) analysisFailure('Analysis policy does not belong to this model');
3509
+ }
3510
+ /** Keep instance/soft-delete scope, caching and scalar normalization owned by the ORM. */
3511
+ async function $executeAnalysisGroup(model, policy, params) {
3512
+ assertModel(model, policy);
3513
+ const {
3514
+ groups,
3515
+ aggrs
3516
+ } = resolveAnalysis(policy, 'group', params);
3517
+ const rows = await model.group({
3518
+ where: params?.where,
3519
+ groups: groups,
3520
+ aggrs,
3521
+ orders: groups.map(column => [column, 'asc']),
3522
+ limit: policy.limits.maxGroupRows + 1
3523
+ });
3524
+ if (rows.length > policy.limits.maxGroupRows) analysisFailure(`Analysis exceeds ${policy.limits.maxGroupRows} group rows`);
3525
+ return rows;
3526
+ }
3527
+ async function $executeAnalysisAggregate(model, policy, params) {
3528
+ assertModel(model, policy);
3529
+ const {
3530
+ aggrs
3531
+ } = resolveAnalysis(policy, 'aggregate', params);
3532
+ return await model.aggregate({
3533
+ where: params?.where,
3534
+ aggrs
3535
+ });
3536
+ }
3537
+
3538
+ function invalid(message) {
3539
+ throw new TypeError(`Invalid analysis policy: ${message}`);
3540
+ }
3541
+ function assertColumns(columns, label, allowEmpty = false) {
3542
+ if (!Array.isArray(columns) || !allowEmpty && columns.length === 0) invalid(`${label} must be a nonempty column array`);
3543
+ if (columns.some(column => typeof column !== 'string')) invalid(`${label} columns must be strings`);
3544
+ if (new Set(columns).size !== columns.length) invalid(`${label} contains duplicate columns`);
3545
+ }
3546
+ function copyAggrs(aggrs) {
3547
+ return Object.freeze(Object.fromEntries(Object.entries(aggrs).map(([fn, columns]) => [fn, Object.freeze([...columns])])));
3548
+ }
3549
+ /** Static model/entity metadata only: safe while DTO classes are being evaluated. */
3550
+ function $defineAnalysis(modelLike, config) {
3551
+ const modelClass = prepareClassModel(modelLike);
3552
+ const entityClass = getClassEntityFromClassModel(modelClass);
3553
+ const rules = getTargetDecoratorRules(entityClass.prototype, true);
3554
+ assertColumns(config.groups, 'groups', true);
3555
+ const validateField = (column, numeric) => {
3556
+ if (!/^[A-Z_]\w*$/i.test(column) || analysisReservedFields.has(column)) invalid(`unsupported column ${column}`);
3557
+ if (!Object.hasOwn(rules, column)) invalid(`unknown entity column ${column}`);
3558
+ const schema = ZodMetadata.unwrapChained(ZodMetadata.resolveLazySchema(rules[column]));
3559
+ if (numeric && (column === 'id' || column === 'iid' || schema.def.type !== 'number')) invalid(`non-count metrics require numeric columns: ${column}`);
3560
+ };
3561
+ for (const column of config.groups) validateField(column, false);
3562
+ if (!config.aggrs || typeof config.aggrs !== 'object' || Array.isArray(config.aggrs)) invalid('aggrs must be an object');
3563
+ const aliases = new Set(config.groups);
3564
+ let metricCount = 0;
3565
+ for (const [fn, columns] of Object.entries(config.aggrs)) {
3566
+ if (!analysisFunctions.includes(fn)) invalid(`unsupported function ${fn}`);
3567
+ assertColumns(columns, fn);
3568
+ for (const column of columns) {
3569
+ if (column !== '*' || fn !== 'count') validateField(column, fn !== 'count');
3570
+ const alias = `${fn}_${column === '*' ? 'all' : column}`;
3571
+ if (aliases.has(alias) || analysisReservedFields.has(alias)) invalid(`alias collision: ${alias}`);
3572
+ aliases.add(alias);
3573
+ metricCount++;
3574
+ }
3575
+ }
3576
+ if (metricCount === 0) invalid('at least one allowed metric is required');
3577
+ const limits = {
3578
+ maxGroups: 3,
3579
+ maxMetrics: 10,
3580
+ maxGroupRows: 1000,
3581
+ ...config.limits
3582
+ };
3583
+ for (const [key, value] of Object.entries(limits)) {
3584
+ if (!['maxGroups', 'maxMetrics', 'maxGroupRows'].includes(key) || !Number.isSafeInteger(value) || value < 1 || value >= Number.MAX_SAFE_INTEGER) {
3585
+ invalid(`invalid limit ${key}`);
3586
+ }
3587
+ }
3588
+ if (!config.defaults || Object.keys(config.defaults).some(key => key !== 'group' && key !== 'aggregate')) {
3589
+ invalid('unknown or missing action defaults');
3590
+ }
3591
+ if (!config.defaults.group && !config.defaults.aggregate) invalid('at least one action default is required');
3592
+ const policy = {
3593
+ modelLike,
3594
+ modelClass,
3595
+ groups: Object.freeze([...config.groups]),
3596
+ aggrs: copyAggrs(config.aggrs),
3597
+ defaults: {},
3598
+ limits: Object.freeze(limits)
3599
+ };
3600
+ const defaults = {};
3601
+ for (const mode of ['group', 'aggregate']) {
3602
+ const input = config.defaults[mode];
3603
+ if (!input) continue;
3604
+ // Do not allow omitted defaults to expand authorization implicitly.
3605
+ if (!Object.hasOwn(input, 'aggrs') || mode === 'group' && !Object.hasOwn(input, 'groups')) invalid(`incomplete ${mode} defaults`);
3606
+ if (Object.keys(input).some(key => key !== 'aggrs' && !(mode === 'group' && key === 'groups'))) invalid(`unsupported ${mode} default control`);
3607
+ const forValidation = {
3608
+ ...policy,
3609
+ defaults: {
3610
+ [mode]: input
3611
+ }
3612
+ };
3613
+ try {
3614
+ const resolved = resolveAnalysis(forValidation, mode, input);
3615
+ const aggrs = copyAggrs(resolved.aggrs);
3616
+ if (mode === 'group') defaults.group = Object.freeze({
3617
+ groups: Object.freeze(resolved.groups),
3618
+ aggrs
3619
+ });else defaults.aggregate = Object.freeze({
3620
+ aggrs
3621
+ });
3622
+ } catch (error) {
3623
+ invalid(`${mode} defaults: ${error.message}`);
3624
+ }
3625
+ }
3626
+ return Object.freeze({
3627
+ ...policy,
3628
+ defaults: Object.freeze(defaults)
3629
+ });
3630
+ }
3631
+
3335
3632
  function DatabaseDialect(options) {
3336
3633
  return createBeanDecorator('databaseDialect', options);
3337
3634
  }
@@ -3349,7 +3646,7 @@ function _DtoAggregate_inner(classTarget, _modelLike, aggrs) {
3349
3646
  if (!columns) continue;
3350
3647
  for (const column of columns) {
3351
3648
  const column2 = `${key}_${column === '*' ? 'all' : column}`;
3352
- Api.field(v.optional(), z.string())(classTarget.prototype, column2);
3649
+ Api.field(v.optional(), key === 'count' ? z.string() : z.string().nullable())(classTarget.prototype, column2);
3353
3650
  }
3354
3651
  }
3355
3652
  return classTarget;
@@ -3377,6 +3674,19 @@ function _DtoGroup_raw(modelLike, groups, aggrs, columns) {
3377
3674
  return TargetClass;
3378
3675
  }
3379
3676
 
3677
+ /** A finite optional superset: absent fields are unrequested, null retains SQL meaning. */
3678
+ function DtoAnalysisGroup(policy) {
3679
+ if (!policy.defaults.group) throw new TypeError('Analysis group action is not enabled');
3680
+ const base = DtoGroup(policy.modelLike, [...policy.groups], policy.aggrs);
3681
+ return $Class.partial(base);
3682
+ }
3683
+ function DtoAnalysisAggregate(policy) {
3684
+ if (!policy.defaults.aggregate) throw new TypeError('Analysis aggregate action is not enabled');
3685
+ class TargetClass {}
3686
+ // Aggregate aliases deliberately do not copy field validators/renderers from source columns.
3687
+ return _DtoAggregate_inner(TargetClass, policy.modelLike, policy.aggrs);
3688
+ }
3689
+
3380
3690
  function DtoGet(modelLike, params) {
3381
3691
  return _DtoGet_raw(modelLike, params);
3382
3692
  }
@@ -3394,10 +3704,25 @@ function _DtoGet_raw(modelLike, params) {
3394
3704
  return entityClass;
3395
3705
  }
3396
3706
  function _DtoGet_relations(modelClass, entityClass, includeWrapper, mutateTypeTopLevel) {
3397
- // relations
3398
3707
  const relations = _DtoGet_relations_collection(modelClass, includeWrapper);
3399
- if (!relations) return;
3708
+ // Mapped-class metadata follows the dtoClass field projection, including pick/omit.
3709
+ registerMappedClassMetadataKey(entityClass.prototype, SymbolDtoMutateRelations, {});
3710
+ const retained = appMetadata.getOwnMetadataMap(true, SymbolDtoMutateRelations, entityClass.prototype);
3711
+ if (mutateTypeTopLevel) {
3712
+ const overridden = new Set(relations.map(relation => relation[0]));
3713
+ for (const name in retained) {
3714
+ if (!overridden.has(name)) {
3715
+ _DtoGet_relation_handle(entityClass, retained[name], mutateTypeTopLevel);
3716
+ }
3717
+ }
3718
+ }
3400
3719
  for (const relation of relations) {
3720
+ const [name, definition] = relation;
3721
+ if (mutateTypeTopLevel && definition !== false && (definition.type === 'hasOne' || definition.type === 'hasMany')) {
3722
+ retained[name] = relation;
3723
+ } else {
3724
+ delete retained[name];
3725
+ }
3401
3726
  _DtoGet_relation_handle(entityClass, relation, mutateTypeTopLevel);
3402
3727
  }
3403
3728
  }
@@ -3623,6 +3948,8 @@ const $Dto = {
3623
3948
  mutate: DtoMutate,
3624
3949
  get: DtoGet,
3625
3950
  aggregate: DtoAggregate,
3951
+ analysisGroup: DtoAnalysisGroup,
3952
+ analysisAggregate: DtoAnalysisAggregate,
3626
3953
  group: DtoGroup,
3627
3954
  query: DtoQuery,
3628
3955
  queryPage: DtoQueryPage,
@@ -3757,7 +4084,7 @@ class BeanModelCache extends BeanModelCrud {
3757
4084
  }
3758
4085
  // insert/update
3759
4086
  const itemsInsertNew = await this.__insertBulk_raw(table, itemsInsert, options);
3760
- await this.__updateBulk_raw(table, itemsUpdate, options);
4087
+ const idsUpdate = await this.__updateBulk_raw(table, itemsUpdate, options);
3761
4088
  const itemsMutate = itemsInsert.map((item, index) => {
3762
4089
  return Object.assign({}, item, {
3763
4090
  id: cast(itemsInsertNew[index]).id
@@ -3765,7 +4092,7 @@ class BeanModelCache extends BeanModelCrud {
3765
4092
  }).concat(itemsUpdate);
3766
4093
  let itemsMutateResult = itemsInsertNew.concat(itemsUpdate);
3767
4094
  const indexesMutate = indexesInsert.concat(indexesUpdate);
3768
- itemsMutateResult = await this.relations.handleRelationsMutate(itemsMutateResult, itemsMutate, options, options);
4095
+ itemsMutateResult = await this.__handleRelationsMutateAdmitted(itemsMutateResult, itemsMutate, [...itemsInsertNew.map(item => [cast(item).id]), ...idsUpdate], options);
3769
4096
  let result = [];
3770
4097
  for (let index = 0; index < indexesMutate.length; index++) {
3771
4098
  result[indexesMutate[index]] = itemsMutateResult[index];
@@ -3997,7 +4324,7 @@ class BeanModelCache extends BeanModelCrud {
3997
4324
  },
3998
4325
  db: this.db
3999
4326
  });
4000
- return items;
4327
+ return normalizeAggregateRows(items, params?.aggrs);
4001
4328
  }
4002
4329
  async get(where, options) {
4003
4330
  const relations = this.relations.handleRelationsCollection(options);
@@ -4106,19 +4433,80 @@ class BeanModelCache extends BeanModelCrud {
4106
4433
  return items[0];
4107
4434
  }
4108
4435
  async updateBulk(items, options) {
4109
- await this.__updateBulk_raw(undefined, items, options);
4110
- return await this.relations.handleRelationsMutate(items, items, options, options);
4436
+ const ids = await this.__updateBulk_raw(undefined, items, options);
4437
+ return await this.__handleRelationsMutateAdmitted(items, items, ids, options);
4111
4438
  }
4112
4439
  async __updateBulk_raw(table, items, options) {
4113
- if (items.length === 0) return;
4440
+ const ids = [];
4114
4441
  for (const item of items) {
4115
- await this.__update_raw(table, item, options);
4442
+ ids.push(await this.__update_raw(table, item, options));
4116
4443
  }
4444
+ return ids;
4445
+ }
4446
+ async __handleRelationsMutateAdmitted(results, items, ids, options) {
4447
+ const indexes = [];
4448
+ const itemsAdmitted = [];
4449
+ const resultsAdmitted = [];
4450
+ for (let index = 0; index < items.length; index++) {
4451
+ const idsItem = ids[index];
4452
+ if (!idsItem || idsItem.length !== 1) continue;
4453
+ indexes.push(index);
4454
+ itemsAdmitted.push(Object.assign({}, items[index], {
4455
+ id: idsItem[0]
4456
+ }));
4457
+ resultsAdmitted.push(results[index]);
4458
+ }
4459
+ const resultsMutated = await this.relations.handleRelationsMutate(resultsAdmitted, itemsAdmitted, options, options);
4460
+ const result = results.concat();
4461
+ for (let index = 0; index < indexes.length; index++) {
4462
+ result[indexes[index]] = resultsMutated[index];
4463
+ }
4464
+ return result;
4465
+ }
4466
+ __hasWritableRelations(options) {
4467
+ return this.relations.handleRelationsCollection(options).some(([, relation]) => {
4468
+ return relation !== false && relation.type !== 'belongsTo';
4469
+ });
4470
+ }
4471
+ async __selectMutationIds(table, where, options) {
4472
+ // Admission must use current scoped rows, not cached entities or changed-row counts.
4473
+ const items = await super._select(table, {
4474
+ where,
4475
+ columns: ['id']
4476
+ }, options);
4477
+ return items.map(item => cast(item).id);
4117
4478
  }
4118
4479
  async __update_raw(table, data, options) {
4119
4480
  // table
4120
4481
  table = table || this.getTable(data);
4121
4482
  if (!table) return this.scopeOrm.error.ShouldSpecifyTable.throw();
4483
+ if (this.__hasWritableRelations(options)) {
4484
+ const where = Object.assign({}, options?.where);
4485
+ const dataNew = Object.assign({}, data);
4486
+ let hasId = false;
4487
+ for (const key of ['id', `${table}.id`]) {
4488
+ if (!isNil(dataNew[key])) {
4489
+ cast(where).id = dataNew[key];
4490
+ delete dataNew[key];
4491
+ hasId = true;
4492
+ }
4493
+ }
4494
+ if (!hasId && !options?.where) {
4495
+ throw new Error('id should be specified for update method');
4496
+ }
4497
+ const ids = await this.__selectMutationIds(table, where, options);
4498
+ if (ids.length === 0) return [];
4499
+ await super._update(table, dataNew, {
4500
+ ...options,
4501
+ where: Object.assign({}, where, {
4502
+ id: ids
4503
+ })
4504
+ });
4505
+ if (!this._checkDisableCacheEntityByOptions(options)) {
4506
+ await this.cacheEntityDel(ids, table);
4507
+ }
4508
+ return ids;
4509
+ }
4122
4510
  // check if cache
4123
4511
  if (this._checkDisableCacheEntityByOptions(options)) {
4124
4512
  await super._update(table, data, options);
@@ -4185,15 +4573,28 @@ class BeanModelCache extends BeanModelCrud {
4185
4573
  }
4186
4574
  async __deleteBulk_raw_with_relations(table, ids, options) {
4187
4575
  if (ids.length === 0) return;
4188
- await this.__delete_raw(table, {
4576
+ const idsDeleted = await this.__delete_raw(table, {
4189
4577
  id: ids
4190
4578
  }, options);
4191
- await this.relations.handleRelationsDelete(ids, options, options);
4579
+ if (idsDeleted) {
4580
+ await this.relations.handleRelationsDelete(idsDeleted, options, options);
4581
+ }
4192
4582
  }
4193
4583
  async __delete_raw(table, where, options) {
4194
4584
  // table
4195
4585
  table = table || this.getTable(where);
4196
4586
  if (!table) return this.scopeOrm.error.ShouldSpecifyTable.throw();
4587
+ if (this.__hasWritableRelations(options)) {
4588
+ const ids = await this.__selectMutationIds(table, where, options);
4589
+ if (ids.length === 0) return [];
4590
+ await super._delete(table, Object.assign({}, where, {
4591
+ id: ids
4592
+ }), options);
4593
+ if (!this._checkDisableCacheEntityByOptions(options)) {
4594
+ await this.cacheEntityDel(ids, table);
4595
+ }
4596
+ return ids;
4597
+ }
4197
4598
  // check if cache
4198
4599
  if (this._checkDisableCacheEntityByOptions(options)) {
4199
4600
  return await super._delete(table, where, options);
@@ -4623,5 +5024,5 @@ const $relationMutate = {
4623
5024
  belongsToMany
4624
5025
  };
4625
5026
 
4626
- export { $Dto, $locale, $relation, $relationDynamic, $relationMutate, AopMethodTransaction, BeanDatabase, BeanModel, BeanModelBase, BeanModelMeta, BroadcastColumnsClear, DatabaseDialect, DtoQueryBase, DtoQueryPageBase, Entity, EntityBase, EntityBaseEmpty, EntityBaseInner, EntityBaseSimple, EventClientNameReal, EventColumnsClear, ExtendKnex, ExtendSchemaBuilder, ExtendTableBuilder, HmrEntity, HmrModel, Main, Model, Op, OpAggrs, OpJoint, OpJointValues, OpNormal, OpNormalValues, OpValues, QueueDoubleDelete, ScheduleSoftDeletionPrune, ScopeModuleAOrm, ServiceCacheEntity, ServiceCacheQuery, ServiceColumns, ServiceColumnsCache, ServiceDatabase, ServiceDatabaseAsyncLocalStorage, ServiceDatabaseClient, ServiceDatabaseDialectBase, ServiceDb, ServiceEntityResolver, ServiceModelResolver, ServiceRelations, ServiceTransaction, ServiceTransactionAsyncLocalStorage, ServiceTransactionConsistency‌, ServiceTransactionFiber, ServiceTransactionState, SymbolCacheModelCacheInstances, SymbolCacheModelsClear, SymbolKeyEntity, SymbolKeyEntityMeta, SymbolKeyFieldsMore, SymbolKeyModelOptions, TransactionIsolationLevelsMap, buildWhere, clearAllCacheModelsClear, clearCacheModelCacheInstance, config, configDefault, errors, getCacheModelCacheInstances, getCacheModelsClear, getClassEntityFromClassModel, getTableOrTableAlias, getTargetColumnName, isRaw, isRef, locales, prepareClassModel, prepareColumns };
5027
+ export { $Dto, $defineAnalysis, $executeAnalysisAggregate, $executeAnalysisGroup, $locale, $relation, $relationDynamic, $relationMutate, AnalysisError, AopMethodTransaction, BeanDatabase, BeanModel, BeanModelBase, BeanModelMeta, BroadcastColumnsClear, DatabaseDialect, DtoQueryBase, DtoQueryPageBase, Entity, EntityBase, EntityBaseEmpty, EntityBaseInner, EntityBaseSimple, EventClientNameReal, EventColumnsClear, ExtendKnex, ExtendSchemaBuilder, ExtendTableBuilder, HmrEntity, HmrModel, Main, Model, Op, OpAggrs, OpJoint, OpJointValues, OpNormal, OpNormalValues, OpValues, QueueDoubleDelete, ScheduleSoftDeletionPrune, ScopeModuleAOrm, ServiceCacheEntity, ServiceCacheQuery, ServiceColumns, ServiceColumnsCache, ServiceDatabase, ServiceDatabaseAsyncLocalStorage, ServiceDatabaseClient, ServiceDatabaseDialectBase, ServiceDb, ServiceEntityResolver, ServiceModelResolver, ServiceRelations, ServiceTransaction, ServiceTransactionAsyncLocalStorage, ServiceTransactionConsistency‌, ServiceTransactionFiber, ServiceTransactionState, SymbolCacheModelCacheInstances, SymbolCacheModelsClear, SymbolDtoMutateRelations, SymbolKeyEntity, SymbolKeyEntityMeta, SymbolKeyFieldsMore, SymbolKeyModelOptions, TransactionIsolationLevelsMap, analysisCapabilities, analysisControlSchemas, analysisFailure, analysisFunctions, analysisReservedFields, buildWhere, clearAllCacheModelsClear, clearCacheModelCacheInstance, config, configDefault, errors, getCacheModelCacheInstances, getCacheModelsClear, getClassEntityFromClassModel, getTableOrTableAlias, getTargetColumnName, isRaw, isRef, locales, prepareClassModel, prepareColumns, resolveAnalysis };
4627
5028
  //# sourceMappingURL=index.js.map