@stndrds/schema 0.1.0-alpha.19 → 0.1.0-alpha.20
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/dist/chunk-4SW4V3YU.mjs +8854 -0
- package/dist/chunk-7GSIXU67.mjs +8820 -0
- package/dist/chunk-BQUYTOB3.mjs +8815 -0
- package/dist/chunk-BSAR4UKK.mjs +8851 -0
- package/dist/chunk-I4TTNDN7.mjs +8910 -0
- package/dist/chunk-KDHA2N7D.mjs +8927 -0
- package/dist/chunk-MX4X4UEN.mjs +8854 -0
- package/dist/chunk-OJWVSONX.mjs +8900 -0
- package/dist/chunk-PMYSW54L.mjs +8855 -0
- package/dist/chunk-Y5KIRAND.mjs +8916 -0
- package/dist/chunk-ZU5SR355.mjs +8862 -0
- package/dist/index.d.mts +7 -2
- package/dist/index.d.ts +7 -2
- package/dist/index.js +310 -264
- package/dist/index.mjs +1 -5
- package/dist/runtime-09fOm3VO.d.mts +6383 -0
- package/dist/runtime-09fOm3VO.d.ts +6383 -0
- package/dist/runtime-BMHqETm7.d.mts +6450 -0
- package/dist/runtime-BMHqETm7.d.ts +6450 -0
- package/dist/runtime-BpsaNQPs.d.mts +6397 -0
- package/dist/runtime-BpsaNQPs.d.ts +6397 -0
- package/dist/runtime-CHAkwiwa.d.mts +6450 -0
- package/dist/runtime-CHAkwiwa.d.ts +6450 -0
- package/dist/runtime-CfVfZtoq.d.mts +6437 -0
- package/dist/runtime-CfVfZtoq.d.ts +6437 -0
- package/dist/runtime-xtAfMn_f.d.mts +6436 -0
- package/dist/runtime-xtAfMn_f.d.ts +6436 -0
- package/dist/runtime.d.mts +1 -1
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +302 -264
- package/dist/runtime.mjs +1 -5
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -201,7 +201,6 @@ __export(index_exports, {
|
|
|
201
201
|
createPhoneValidator: () => createPhoneValidator,
|
|
202
202
|
createRatingValidator: () => createRatingValidator,
|
|
203
203
|
createRelationValidator: () => createRelationValidator,
|
|
204
|
-
createRollupHooks: () => createRollupHooks,
|
|
205
204
|
createRollupValidator: () => createRollupValidator,
|
|
206
205
|
createSelectValidator: () => createSelectValidator,
|
|
207
206
|
createSingleRelationValidator: () => createSingleRelationValidator,
|
|
@@ -273,7 +272,6 @@ __export(index_exports, {
|
|
|
273
272
|
phoneConfigSchema: () => phoneConfigSchema,
|
|
274
273
|
rating: () => rating,
|
|
275
274
|
ratingConfigSchema: () => ratingConfigSchema,
|
|
276
|
-
registerAllRollupHooks: () => registerAllRollupHooks,
|
|
277
275
|
registry: () => registry,
|
|
278
276
|
relation: () => relation,
|
|
279
277
|
relationConfigSchema: () => relationConfigSchema,
|
|
@@ -1628,6 +1626,14 @@ var RollupAttributeBuilder = class extends BaseAttributeBuilder {
|
|
|
1628
1626
|
this.attr.decimals = value;
|
|
1629
1627
|
return this;
|
|
1630
1628
|
}
|
|
1629
|
+
/**
|
|
1630
|
+
* Display the rollup value using the target attribute's type formatter
|
|
1631
|
+
* instead of the default rollup-specific formatting
|
|
1632
|
+
*/
|
|
1633
|
+
showAsOriginal() {
|
|
1634
|
+
this.attr.showAsOriginal = true;
|
|
1635
|
+
return this;
|
|
1636
|
+
}
|
|
1631
1637
|
/**
|
|
1632
1638
|
* Set multi-level path for traversing nested relations (Phase 4+)
|
|
1633
1639
|
* @param path - Dot notation path (e.g., "orders.items")
|
|
@@ -2965,10 +2971,12 @@ var rollupConfigSchema = baseConfigSchema.extend({
|
|
|
2965
2971
|
"countValues",
|
|
2966
2972
|
"countEmpty",
|
|
2967
2973
|
"percentEmpty",
|
|
2968
|
-
"percentFilled"
|
|
2974
|
+
"percentFilled",
|
|
2975
|
+
"collect"
|
|
2969
2976
|
]),
|
|
2970
2977
|
decimals: import_zod4.z.number().int().min(0).max(10).optional(),
|
|
2971
|
-
|
|
2978
|
+
showAsOriginal: import_zod4.z.boolean().optional(),
|
|
2979
|
+
targetAttributeType: import_zod4.z.string().optional()
|
|
2972
2980
|
});
|
|
2973
2981
|
var attributeConfigSchemas = {
|
|
2974
2982
|
text: textConfigSchema,
|
|
@@ -3712,260 +3720,6 @@ var NoopHookRegistry = class {
|
|
|
3712
3720
|
}
|
|
3713
3721
|
};
|
|
3714
3722
|
|
|
3715
|
-
// src/runtime/services/rollup.service.ts
|
|
3716
|
-
var RollupService = class {
|
|
3717
|
-
constructor(adapter) {
|
|
3718
|
-
this.adapter = adapter;
|
|
3719
|
-
}
|
|
3720
|
-
/**
|
|
3721
|
-
* Calculate a rollup value for a record
|
|
3722
|
-
*
|
|
3723
|
-
* @param recordId - ID of the parent record
|
|
3724
|
-
* @param rollupAttr - Rollup attribute definition
|
|
3725
|
-
* @param schema - Schema of the parent object
|
|
3726
|
-
* @returns Computed rollup value
|
|
3727
|
-
*
|
|
3728
|
-
* @example
|
|
3729
|
-
* ```typescript
|
|
3730
|
-
* // Sum all order amounts for a company
|
|
3731
|
-
* const totalOrders = await rollupService.calculate(
|
|
3732
|
-
* "company-123",
|
|
3733
|
-
* {
|
|
3734
|
-
* type: "rollup",
|
|
3735
|
-
* name: "totalOrders",
|
|
3736
|
-
* relationAttribute: "orders",
|
|
3737
|
-
* targetAttribute: "amount",
|
|
3738
|
-
* function: "sum",
|
|
3739
|
-
* ...
|
|
3740
|
-
* },
|
|
3741
|
-
* companySchema
|
|
3742
|
-
* );
|
|
3743
|
-
* ```
|
|
3744
|
-
*/
|
|
3745
|
-
async calculate(recordId, rollupAttr, schema, tenantId) {
|
|
3746
|
-
const relationAttr = schema.attributes.find(
|
|
3747
|
-
(a) => a.type === "relation" && a.name === rollupAttr.relationAttribute
|
|
3748
|
-
);
|
|
3749
|
-
if (!relationAttr) {
|
|
3750
|
-
return { value: null, recordCount: 0 };
|
|
3751
|
-
}
|
|
3752
|
-
const targetObjectName = relationAttr.targets[0]?.object;
|
|
3753
|
-
if (!targetObjectName) {
|
|
3754
|
-
return { value: null, recordCount: 0 };
|
|
3755
|
-
}
|
|
3756
|
-
const targetObject = await this.adapter.objects.findByName(tenantId, targetObjectName);
|
|
3757
|
-
if (!targetObject) {
|
|
3758
|
-
return { value: null, recordCount: 0 };
|
|
3759
|
-
}
|
|
3760
|
-
const relatedRecords = await this.adapter.objectRecords.findByRelation(
|
|
3761
|
-
tenantId,
|
|
3762
|
-
targetObject.id,
|
|
3763
|
-
rollupAttr.relationAttribute,
|
|
3764
|
-
recordId
|
|
3765
|
-
);
|
|
3766
|
-
if (relatedRecords.length === 0) {
|
|
3767
|
-
return { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 };
|
|
3768
|
-
}
|
|
3769
|
-
const values = relatedRecords.map((r) => r.values[rollupAttr.targetAttribute]).filter((v) => v !== void 0);
|
|
3770
|
-
const aggregated = this.aggregate(values, rollupAttr.function);
|
|
3771
|
-
const result = typeof aggregated === "number" && rollupAttr.decimals !== void 0 ? Number(aggregated.toFixed(rollupAttr.decimals)) : aggregated;
|
|
3772
|
-
return { value: result, recordCount: relatedRecords.length };
|
|
3773
|
-
}
|
|
3774
|
-
/**
|
|
3775
|
-
* Calculate rollup values for multiple records (batched)
|
|
3776
|
-
*
|
|
3777
|
-
* More efficient than calling calculate() for each record individually.
|
|
3778
|
-
*/
|
|
3779
|
-
async calculateForMany(recordIds, rollupAttr, schema, tenantId) {
|
|
3780
|
-
const results = /* @__PURE__ */ new Map();
|
|
3781
|
-
for (const id of recordIds) {
|
|
3782
|
-
results.set(id, { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 });
|
|
3783
|
-
}
|
|
3784
|
-
await Promise.all(
|
|
3785
|
-
recordIds.map(async (id) => {
|
|
3786
|
-
const result = await this.calculate(id, rollupAttr, schema, tenantId);
|
|
3787
|
-
results.set(id, result);
|
|
3788
|
-
})
|
|
3789
|
-
);
|
|
3790
|
-
return results;
|
|
3791
|
-
}
|
|
3792
|
-
/**
|
|
3793
|
-
* Apply aggregation function to a set of values
|
|
3794
|
-
*/
|
|
3795
|
-
aggregate(values, fn) {
|
|
3796
|
-
switch (fn) {
|
|
3797
|
-
case "sum":
|
|
3798
|
-
return this.sumNumbers(values);
|
|
3799
|
-
case "avg":
|
|
3800
|
-
return this.averageNumbers(values);
|
|
3801
|
-
case "min":
|
|
3802
|
-
return this.minNumbers(values);
|
|
3803
|
-
case "max":
|
|
3804
|
-
return this.maxNumbers(values);
|
|
3805
|
-
case "count":
|
|
3806
|
-
return values.length;
|
|
3807
|
-
case "countValues":
|
|
3808
|
-
return values.filter((v) => v != null && v !== "").length;
|
|
3809
|
-
case "countEmpty":
|
|
3810
|
-
return values.filter((v) => v == null || v === "").length;
|
|
3811
|
-
case "percentFilled": {
|
|
3812
|
-
if (values.length === 0) return 0;
|
|
3813
|
-
const filled = values.filter((v) => v != null && v !== "").length;
|
|
3814
|
-
return filled / values.length * 100;
|
|
3815
|
-
}
|
|
3816
|
-
case "percentEmpty": {
|
|
3817
|
-
if (values.length === 0) return 0;
|
|
3818
|
-
const empty = values.filter((v) => v == null || v === "").length;
|
|
3819
|
-
return empty / values.length * 100;
|
|
3820
|
-
}
|
|
3821
|
-
}
|
|
3822
|
-
}
|
|
3823
|
-
/**
|
|
3824
|
-
* Get the default empty value for a rollup function
|
|
3825
|
-
*/
|
|
3826
|
-
getEmptyValue(fn) {
|
|
3827
|
-
switch (fn) {
|
|
3828
|
-
case "sum":
|
|
3829
|
-
case "count":
|
|
3830
|
-
case "countValues":
|
|
3831
|
-
case "countEmpty":
|
|
3832
|
-
case "percentEmpty":
|
|
3833
|
-
case "percentFilled":
|
|
3834
|
-
return 0;
|
|
3835
|
-
case "avg":
|
|
3836
|
-
case "min":
|
|
3837
|
-
case "max":
|
|
3838
|
-
return null;
|
|
3839
|
-
}
|
|
3840
|
-
}
|
|
3841
|
-
/**
|
|
3842
|
-
* Sum numeric values
|
|
3843
|
-
*/
|
|
3844
|
-
sumNumbers(values) {
|
|
3845
|
-
return values.filter((v) => typeof v === "number" && !Number.isNaN(v)).reduce((sum, n) => sum + n, 0);
|
|
3846
|
-
}
|
|
3847
|
-
/**
|
|
3848
|
-
* Average numeric values
|
|
3849
|
-
*/
|
|
3850
|
-
averageNumbers(values) {
|
|
3851
|
-
const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
|
|
3852
|
-
if (nums.length === 0) return null;
|
|
3853
|
-
return nums.reduce((sum, n) => sum + n, 0) / nums.length;
|
|
3854
|
-
}
|
|
3855
|
-
/**
|
|
3856
|
-
* Minimum numeric value
|
|
3857
|
-
*/
|
|
3858
|
-
minNumbers(values) {
|
|
3859
|
-
const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
|
|
3860
|
-
if (nums.length === 0) return null;
|
|
3861
|
-
return Math.min(...nums);
|
|
3862
|
-
}
|
|
3863
|
-
/**
|
|
3864
|
-
* Maximum numeric value
|
|
3865
|
-
*/
|
|
3866
|
-
maxNumbers(values) {
|
|
3867
|
-
const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
|
|
3868
|
-
if (nums.length === 0) return null;
|
|
3869
|
-
return Math.max(...nums);
|
|
3870
|
-
}
|
|
3871
|
-
/**
|
|
3872
|
-
* Recalculate all rollup attributes for a record and update it
|
|
3873
|
-
*
|
|
3874
|
-
* Called after related records change to keep rollups up-to-date.
|
|
3875
|
-
*/
|
|
3876
|
-
async recalculateAndUpdate(record, schema, tenantId) {
|
|
3877
|
-
const rollupAttrs = schema.attributes.filter((a) => a.type === "rollup");
|
|
3878
|
-
if (rollupAttrs.length === 0) {
|
|
3879
|
-
return record;
|
|
3880
|
-
}
|
|
3881
|
-
const updates = {};
|
|
3882
|
-
for (const attr of rollupAttrs) {
|
|
3883
|
-
const result = await this.calculate(record.id, attr, schema, tenantId);
|
|
3884
|
-
updates[attr.name] = result.value;
|
|
3885
|
-
}
|
|
3886
|
-
return await this.adapter.objectRecords.update(record.id, updates);
|
|
3887
|
-
}
|
|
3888
|
-
/**
|
|
3889
|
-
* Find parent records that need rollup recalculation when a child record changes
|
|
3890
|
-
*
|
|
3891
|
-
* Used by hooks to determine which parent records to recalculate after
|
|
3892
|
-
* a child record is created, updated, or deleted.
|
|
3893
|
-
*
|
|
3894
|
-
* @param changedRecord - The record that was modified
|
|
3895
|
-
* @param changedSchema - Schema of the changed record's object
|
|
3896
|
-
* @returns Array of parent record IDs that need recalculation
|
|
3897
|
-
*/
|
|
3898
|
-
async findAffectedParentRecords(changedRecord, changedSchema) {
|
|
3899
|
-
const affectedIds = [];
|
|
3900
|
-
const relationAttrs = changedSchema.attributes.filter(
|
|
3901
|
-
(a) => a.type === "relation"
|
|
3902
|
-
);
|
|
3903
|
-
for (const attr of relationAttrs) {
|
|
3904
|
-
const relatedId = changedRecord.values[attr.name];
|
|
3905
|
-
if (typeof relatedId === "string" && relatedId.length > 0) {
|
|
3906
|
-
affectedIds.push(relatedId);
|
|
3907
|
-
} else if (Array.isArray(relatedId)) {
|
|
3908
|
-
affectedIds.push(...relatedId.filter((id) => typeof id === "string"));
|
|
3909
|
-
}
|
|
3910
|
-
}
|
|
3911
|
-
return [...new Set(affectedIds)];
|
|
3912
|
-
}
|
|
3913
|
-
};
|
|
3914
|
-
|
|
3915
|
-
// src/runtime/hooks/rollup.hooks.ts
|
|
3916
|
-
function createRollupHooks(objectName, config) {
|
|
3917
|
-
const rollupService = new RollupService(config.adapter);
|
|
3918
|
-
async function recalculateParentRollups(ctx) {
|
|
3919
|
-
const schema = await config.getSchemaById(ctx.objectId);
|
|
3920
|
-
if (!schema) return;
|
|
3921
|
-
const affectedParentIds = await rollupService.findAffectedParentRecords(ctx.record, schema);
|
|
3922
|
-
if (affectedParentIds.length === 0) return;
|
|
3923
|
-
const parentRecords = await config.adapter.objectRecords.findByIds(affectedParentIds);
|
|
3924
|
-
for (const parentRecord of parentRecords) {
|
|
3925
|
-
const parentSchema = await config.getSchemaById(parentRecord.objectId);
|
|
3926
|
-
if (!parentSchema) continue;
|
|
3927
|
-
const hasRollups = parentSchema.attributes.some(
|
|
3928
|
-
(a) => a.type === "rollup"
|
|
3929
|
-
);
|
|
3930
|
-
if (hasRollups) {
|
|
3931
|
-
await rollupService.recalculateAndUpdate(parentRecord, parentSchema, config.tenantId);
|
|
3932
|
-
}
|
|
3933
|
-
}
|
|
3934
|
-
}
|
|
3935
|
-
return [
|
|
3936
|
-
{
|
|
3937
|
-
type: "afterCreate",
|
|
3938
|
-
objectName,
|
|
3939
|
-
handler: recalculateParentRollups,
|
|
3940
|
-
priority: 100
|
|
3941
|
-
// Run after other hooks
|
|
3942
|
-
},
|
|
3943
|
-
{
|
|
3944
|
-
type: "afterUpdate",
|
|
3945
|
-
objectName,
|
|
3946
|
-
handler: recalculateParentRollups,
|
|
3947
|
-
priority: 100
|
|
3948
|
-
},
|
|
3949
|
-
{
|
|
3950
|
-
type: "afterDelete",
|
|
3951
|
-
objectName,
|
|
3952
|
-
handler: recalculateParentRollups,
|
|
3953
|
-
priority: 100
|
|
3954
|
-
}
|
|
3955
|
-
];
|
|
3956
|
-
}
|
|
3957
|
-
function registerAllRollupHooks(hookRegistry, schemas, config) {
|
|
3958
|
-
for (const schema of schemas) {
|
|
3959
|
-
const hasRelations = schema.attributes.some((a) => a.type === "relation");
|
|
3960
|
-
if (hasRelations && hookRegistry.register) {
|
|
3961
|
-
const hooks = createRollupHooks(schema.name, config);
|
|
3962
|
-
for (const hook of hooks) {
|
|
3963
|
-
hookRegistry.register(hook);
|
|
3964
|
-
}
|
|
3965
|
-
}
|
|
3966
|
-
}
|
|
3967
|
-
}
|
|
3968
|
-
|
|
3969
3723
|
// src/runtime/template.ts
|
|
3970
3724
|
var pipes = {
|
|
3971
3725
|
/** Convert to uppercase */
|
|
@@ -7492,6 +7246,263 @@ var RelationService = class {
|
|
|
7492
7246
|
}
|
|
7493
7247
|
};
|
|
7494
7248
|
|
|
7249
|
+
// src/runtime/services/rollup.service.ts
|
|
7250
|
+
var RollupService = class {
|
|
7251
|
+
constructor(adapter) {
|
|
7252
|
+
this.adapter = adapter;
|
|
7253
|
+
}
|
|
7254
|
+
/**
|
|
7255
|
+
* Calculate a rollup value for a record
|
|
7256
|
+
*
|
|
7257
|
+
* @param recordId - ID of the parent record
|
|
7258
|
+
* @param rollupAttr - Rollup attribute definition
|
|
7259
|
+
* @param schema - Schema of the parent object
|
|
7260
|
+
* @returns Computed rollup value
|
|
7261
|
+
*
|
|
7262
|
+
* @example
|
|
7263
|
+
* ```typescript
|
|
7264
|
+
* // Sum all order amounts for a company
|
|
7265
|
+
* const totalOrders = await rollupService.calculate(
|
|
7266
|
+
* "company-123",
|
|
7267
|
+
* {
|
|
7268
|
+
* type: "rollup",
|
|
7269
|
+
* name: "totalOrders",
|
|
7270
|
+
* relationAttribute: "orders",
|
|
7271
|
+
* targetAttribute: "amount",
|
|
7272
|
+
* function: "sum",
|
|
7273
|
+
* ...
|
|
7274
|
+
* },
|
|
7275
|
+
* companySchema
|
|
7276
|
+
* );
|
|
7277
|
+
* ```
|
|
7278
|
+
*/
|
|
7279
|
+
async calculate(recordId, rollupAttr, schema) {
|
|
7280
|
+
const relationAttr = schema.attributes.find(
|
|
7281
|
+
(a) => a.type === "relation" && a.name === rollupAttr.relationAttribute
|
|
7282
|
+
);
|
|
7283
|
+
if (relationAttr) {
|
|
7284
|
+
return this.calculateForward(recordId, rollupAttr);
|
|
7285
|
+
}
|
|
7286
|
+
return this.calculateReverse(recordId, rollupAttr, schema);
|
|
7287
|
+
}
|
|
7288
|
+
/**
|
|
7289
|
+
* Forward pattern: this record has a relation attribute pointing to other records
|
|
7290
|
+
* Example: entreprise222 has relation "entreprises" → companies, rollup collects from companies
|
|
7291
|
+
*/
|
|
7292
|
+
async calculateForward(recordId, rollupAttr) {
|
|
7293
|
+
const record = await this.adapter.objectRecords.findById(recordId);
|
|
7294
|
+
if (!record) {
|
|
7295
|
+
return { value: null, recordCount: 0 };
|
|
7296
|
+
}
|
|
7297
|
+
const relationValue = record.values[rollupAttr.relationAttribute];
|
|
7298
|
+
let relatedIds = [];
|
|
7299
|
+
if (typeof relationValue === "string" && relationValue.length > 0) {
|
|
7300
|
+
relatedIds = [relationValue];
|
|
7301
|
+
} else if (Array.isArray(relationValue)) {
|
|
7302
|
+
relatedIds = relationValue.filter((id) => typeof id === "string" && id.length > 0);
|
|
7303
|
+
}
|
|
7304
|
+
if (relatedIds.length === 0) {
|
|
7305
|
+
return { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 };
|
|
7306
|
+
}
|
|
7307
|
+
const relatedRecords = await this.adapter.objectRecords.findByIds(relatedIds);
|
|
7308
|
+
if (relatedRecords.length === 0) {
|
|
7309
|
+
return { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 };
|
|
7310
|
+
}
|
|
7311
|
+
return this.aggregateValues(relatedRecords, rollupAttr);
|
|
7312
|
+
}
|
|
7313
|
+
/**
|
|
7314
|
+
* Reverse pattern: other records have a relation pointing to this record
|
|
7315
|
+
* Example: Company has rollup on "orders", Order has relation "company" → companies
|
|
7316
|
+
*/
|
|
7317
|
+
async calculateReverse(recordId, rollupAttr, schema) {
|
|
7318
|
+
const record = await this.adapter.objectRecords.findById(recordId);
|
|
7319
|
+
if (!record) {
|
|
7320
|
+
return { value: null, recordCount: 0 };
|
|
7321
|
+
}
|
|
7322
|
+
const parentObject = await this.adapter.objects.findById(record.objectId);
|
|
7323
|
+
if (!parentObject) {
|
|
7324
|
+
return { value: null, recordCount: 0 };
|
|
7325
|
+
}
|
|
7326
|
+
const tenantId = parentObject.tenantId;
|
|
7327
|
+
const sourceObjectName = rollupAttr.relationAttribute;
|
|
7328
|
+
const sourceObject = await this.adapter.objects.findByName(tenantId, sourceObjectName);
|
|
7329
|
+
if (!sourceObject) {
|
|
7330
|
+
return { value: null, recordCount: 0 };
|
|
7331
|
+
}
|
|
7332
|
+
const sourceAttributes = await this.adapter.attributes.findByObjectId(sourceObject.id);
|
|
7333
|
+
const reverseRelationAttr = sourceAttributes.find((attr) => {
|
|
7334
|
+
if (attr.type !== "relation") return false;
|
|
7335
|
+
const relationConfig = attr.config;
|
|
7336
|
+
return relationConfig?.targets?.some((t) => t.object === schema.name);
|
|
7337
|
+
});
|
|
7338
|
+
if (!reverseRelationAttr) {
|
|
7339
|
+
return { value: null, recordCount: 0 };
|
|
7340
|
+
}
|
|
7341
|
+
const relatedRecords = await this.adapter.objectRecords.findByRelation(
|
|
7342
|
+
tenantId,
|
|
7343
|
+
sourceObject.id,
|
|
7344
|
+
reverseRelationAttr.name,
|
|
7345
|
+
recordId
|
|
7346
|
+
);
|
|
7347
|
+
if (relatedRecords.length === 0) {
|
|
7348
|
+
return { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 };
|
|
7349
|
+
}
|
|
7350
|
+
return this.aggregateValues(relatedRecords, rollupAttr);
|
|
7351
|
+
}
|
|
7352
|
+
/**
|
|
7353
|
+
* Extract and aggregate values from related records
|
|
7354
|
+
*/
|
|
7355
|
+
aggregateValues(relatedRecords, rollupAttr) {
|
|
7356
|
+
const values = relatedRecords.map((r) => r.values[rollupAttr.targetAttribute]).filter((v) => v !== void 0 && v !== null);
|
|
7357
|
+
const aggregated = this.aggregate(values, rollupAttr.function);
|
|
7358
|
+
const result = typeof aggregated === "number" && rollupAttr.decimals !== void 0 ? Number(aggregated.toFixed(rollupAttr.decimals)) : aggregated;
|
|
7359
|
+
return { value: result, recordCount: relatedRecords.length };
|
|
7360
|
+
}
|
|
7361
|
+
/**
|
|
7362
|
+
* Calculate rollup values for multiple records (batched)
|
|
7363
|
+
*
|
|
7364
|
+
* More efficient than calling calculate() for each record individually.
|
|
7365
|
+
*/
|
|
7366
|
+
async calculateForMany(recordIds, rollupAttr, schema) {
|
|
7367
|
+
const results = /* @__PURE__ */ new Map();
|
|
7368
|
+
for (const id of recordIds) {
|
|
7369
|
+
results.set(id, { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 });
|
|
7370
|
+
}
|
|
7371
|
+
await Promise.all(
|
|
7372
|
+
recordIds.map(async (id) => {
|
|
7373
|
+
const result = await this.calculate(id, rollupAttr, schema);
|
|
7374
|
+
results.set(id, result);
|
|
7375
|
+
})
|
|
7376
|
+
);
|
|
7377
|
+
return results;
|
|
7378
|
+
}
|
|
7379
|
+
/**
|
|
7380
|
+
* Apply aggregation function to a set of values
|
|
7381
|
+
*/
|
|
7382
|
+
aggregate(values, fn) {
|
|
7383
|
+
switch (fn) {
|
|
7384
|
+
case "sum":
|
|
7385
|
+
return this.sumNumbers(values);
|
|
7386
|
+
case "avg":
|
|
7387
|
+
return this.averageNumbers(values);
|
|
7388
|
+
case "min":
|
|
7389
|
+
return this.minNumbers(values);
|
|
7390
|
+
case "max":
|
|
7391
|
+
return this.maxNumbers(values);
|
|
7392
|
+
case "count":
|
|
7393
|
+
return values.length;
|
|
7394
|
+
case "countValues":
|
|
7395
|
+
return values.filter((v) => v != null && v !== "").length;
|
|
7396
|
+
case "countEmpty":
|
|
7397
|
+
return values.filter((v) => v == null || v === "").length;
|
|
7398
|
+
case "percentFilled": {
|
|
7399
|
+
if (values.length === 0) return 0;
|
|
7400
|
+
const filled = values.filter((v) => v != null && v !== "").length;
|
|
7401
|
+
return filled / values.length * 100;
|
|
7402
|
+
}
|
|
7403
|
+
case "percentEmpty": {
|
|
7404
|
+
if (values.length === 0) return 0;
|
|
7405
|
+
const empty = values.filter((v) => v == null || v === "").length;
|
|
7406
|
+
return empty / values.length * 100;
|
|
7407
|
+
}
|
|
7408
|
+
case "collect":
|
|
7409
|
+
return values;
|
|
7410
|
+
}
|
|
7411
|
+
}
|
|
7412
|
+
/**
|
|
7413
|
+
* Get the default empty value for a rollup function
|
|
7414
|
+
*/
|
|
7415
|
+
getEmptyValue(fn) {
|
|
7416
|
+
switch (fn) {
|
|
7417
|
+
case "sum":
|
|
7418
|
+
case "count":
|
|
7419
|
+
case "countValues":
|
|
7420
|
+
case "countEmpty":
|
|
7421
|
+
case "percentEmpty":
|
|
7422
|
+
case "percentFilled":
|
|
7423
|
+
return 0;
|
|
7424
|
+
case "avg":
|
|
7425
|
+
case "min":
|
|
7426
|
+
case "max":
|
|
7427
|
+
return null;
|
|
7428
|
+
case "collect":
|
|
7429
|
+
return [];
|
|
7430
|
+
}
|
|
7431
|
+
}
|
|
7432
|
+
/**
|
|
7433
|
+
* Sum numeric values
|
|
7434
|
+
*/
|
|
7435
|
+
sumNumbers(values) {
|
|
7436
|
+
return values.filter((v) => typeof v === "number" && !Number.isNaN(v)).reduce((sum, n) => sum + n, 0);
|
|
7437
|
+
}
|
|
7438
|
+
/**
|
|
7439
|
+
* Average numeric values
|
|
7440
|
+
*/
|
|
7441
|
+
averageNumbers(values) {
|
|
7442
|
+
const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
|
|
7443
|
+
if (nums.length === 0) return null;
|
|
7444
|
+
return nums.reduce((sum, n) => sum + n, 0) / nums.length;
|
|
7445
|
+
}
|
|
7446
|
+
/**
|
|
7447
|
+
* Minimum numeric value
|
|
7448
|
+
*/
|
|
7449
|
+
minNumbers(values) {
|
|
7450
|
+
const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
|
|
7451
|
+
if (nums.length === 0) return null;
|
|
7452
|
+
return Math.min(...nums);
|
|
7453
|
+
}
|
|
7454
|
+
/**
|
|
7455
|
+
* Maximum numeric value
|
|
7456
|
+
*/
|
|
7457
|
+
maxNumbers(values) {
|
|
7458
|
+
const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
|
|
7459
|
+
if (nums.length === 0) return null;
|
|
7460
|
+
return Math.max(...nums);
|
|
7461
|
+
}
|
|
7462
|
+
/**
|
|
7463
|
+
* Recalculate all rollup attributes for a record and update it
|
|
7464
|
+
*
|
|
7465
|
+
* Called after related records change to keep rollups up-to-date.
|
|
7466
|
+
*/
|
|
7467
|
+
async recalculateAndUpdate(record, schema) {
|
|
7468
|
+
const rollupAttrs = schema.attributes.filter((a) => a.type === "rollup");
|
|
7469
|
+
if (rollupAttrs.length === 0) {
|
|
7470
|
+
return record;
|
|
7471
|
+
}
|
|
7472
|
+
const updates = {};
|
|
7473
|
+
for (const attr of rollupAttrs) {
|
|
7474
|
+
const result = await this.calculate(record.id, attr, schema);
|
|
7475
|
+
updates[attr.name] = result.value;
|
|
7476
|
+
}
|
|
7477
|
+
return await this.adapter.objectRecords.update(record.id, updates);
|
|
7478
|
+
}
|
|
7479
|
+
/**
|
|
7480
|
+
* Find parent records that need rollup recalculation when a child record changes
|
|
7481
|
+
*
|
|
7482
|
+
* Used by hooks to determine which parent records to recalculate after
|
|
7483
|
+
* a child record is created, updated, or deleted.
|
|
7484
|
+
*
|
|
7485
|
+
* @param changedRecord - The record that was modified
|
|
7486
|
+
* @param changedSchema - Schema of the changed record's object
|
|
7487
|
+
* @returns Array of parent record IDs that need recalculation
|
|
7488
|
+
*/
|
|
7489
|
+
async findAffectedParentRecords(changedRecord, changedSchema) {
|
|
7490
|
+
const affectedIds = [];
|
|
7491
|
+
const relationAttrs = changedSchema.attributes.filter(
|
|
7492
|
+
(a) => a.type === "relation"
|
|
7493
|
+
);
|
|
7494
|
+
for (const attr of relationAttrs) {
|
|
7495
|
+
const relatedId = changedRecord.values[attr.name];
|
|
7496
|
+
if (typeof relatedId === "string" && relatedId.length > 0) {
|
|
7497
|
+
affectedIds.push(relatedId);
|
|
7498
|
+
} else if (Array.isArray(relatedId)) {
|
|
7499
|
+
affectedIds.push(...relatedId.filter((id) => typeof id === "string"));
|
|
7500
|
+
}
|
|
7501
|
+
}
|
|
7502
|
+
return [...new Set(affectedIds)];
|
|
7503
|
+
}
|
|
7504
|
+
};
|
|
7505
|
+
|
|
7495
7506
|
// src/runtime/services/user.service.ts
|
|
7496
7507
|
var UserService = class {
|
|
7497
7508
|
constructor(adapter, tenantId) {
|
|
@@ -7617,6 +7628,7 @@ var RecordService = class {
|
|
|
7617
7628
|
this.schemaService = new ObjectSchemaService(adapter, registry);
|
|
7618
7629
|
this.relationService = new RelationService(adapter, registry);
|
|
7619
7630
|
this.userService = new UserService(adapter, tenantId);
|
|
7631
|
+
this.rollupService = new RollupService(adapter);
|
|
7620
7632
|
this.hookRegistry = options?.hookRegistry ?? new NoopHookRegistry();
|
|
7621
7633
|
this.permissionService = options?.permissionService;
|
|
7622
7634
|
this.auditService = options?.auditService ?? (adapter.audit ? new AuditService(adapter, tenantId) : void 0);
|
|
@@ -7778,6 +7790,7 @@ var RecordService = class {
|
|
|
7778
7790
|
};
|
|
7779
7791
|
await this.hookRegistry.execute("afterCreate", schema.name, afterCtx);
|
|
7780
7792
|
}
|
|
7793
|
+
await this.recalculateParentRollups(record, schema);
|
|
7781
7794
|
if (this.auditService && this.userId) {
|
|
7782
7795
|
await this.auditService.logRecordAction({
|
|
7783
7796
|
action: "record.created",
|
|
@@ -7893,6 +7906,7 @@ var RecordService = class {
|
|
|
7893
7906
|
};
|
|
7894
7907
|
await this.hookRegistry.execute("afterUpdate", schema.name, afterCtx);
|
|
7895
7908
|
}
|
|
7909
|
+
await this.recalculateParentRollups(updated, schema);
|
|
7896
7910
|
if (this.auditService && this.userId && changedAttributes.length > 0) {
|
|
7897
7911
|
const changes = changedAttributes.map((attr) => ({
|
|
7898
7912
|
field: attr,
|
|
@@ -8020,6 +8034,7 @@ var RecordService = class {
|
|
|
8020
8034
|
if (!options?.skipHooks) {
|
|
8021
8035
|
await this.hookRegistry.execute("afterDelete", schema.name, hookCtx);
|
|
8022
8036
|
}
|
|
8037
|
+
await this.recalculateParentRollups(record, schema);
|
|
8023
8038
|
if (this.auditService && this.userId) {
|
|
8024
8039
|
await this.auditService.logRecordAction({
|
|
8025
8040
|
action: "record.deleted",
|
|
@@ -8149,6 +8164,39 @@ var RecordService = class {
|
|
|
8149
8164
|
}
|
|
8150
8165
|
return records.map((record) => this.enrichWithFormulas(record, schema));
|
|
8151
8166
|
}
|
|
8167
|
+
/**
|
|
8168
|
+
* Recalculate rollups after a record changes
|
|
8169
|
+
*
|
|
8170
|
+
* This handles two cases:
|
|
8171
|
+
* 1. The record itself has rollups (e.g., aggregating from related records it points to)
|
|
8172
|
+
* 2. Parent records have rollups that aggregate from this record
|
|
8173
|
+
*
|
|
8174
|
+
* @param record - The record that was modified
|
|
8175
|
+
* @param schema - Schema of the record's object
|
|
8176
|
+
* @internal
|
|
8177
|
+
*/
|
|
8178
|
+
async recalculateParentRollups(record, schema) {
|
|
8179
|
+
const ownRollupAttrs = schema.attributes.filter(
|
|
8180
|
+
(a) => a.type === "rollup"
|
|
8181
|
+
);
|
|
8182
|
+
if (ownRollupAttrs.length > 0) {
|
|
8183
|
+
await this.rollupService.recalculateAndUpdate(record, schema);
|
|
8184
|
+
}
|
|
8185
|
+
const affectedParentIds = await this.rollupService.findAffectedParentRecords(record, schema);
|
|
8186
|
+
if (affectedParentIds.length === 0) {
|
|
8187
|
+
return;
|
|
8188
|
+
}
|
|
8189
|
+
const parentRecords = await this.adapter.objectRecords.findByIds(affectedParentIds);
|
|
8190
|
+
for (const parentRecord of parentRecords) {
|
|
8191
|
+
const parentSchema = await this.schemaService.getObjectSchema(parentRecord.objectId);
|
|
8192
|
+
const rollupAttrs = parentSchema.attributes.filter(
|
|
8193
|
+
(a) => a.type === "rollup"
|
|
8194
|
+
);
|
|
8195
|
+
if (rollupAttrs.length > 0) {
|
|
8196
|
+
await this.rollupService.recalculateAndUpdate(parentRecord, parentSchema);
|
|
8197
|
+
}
|
|
8198
|
+
}
|
|
8199
|
+
}
|
|
8152
8200
|
/**
|
|
8153
8201
|
* Permanently delete a record (hard delete)
|
|
8154
8202
|
*
|
|
@@ -8422,7 +8470,7 @@ var RollupScheduler = class {
|
|
|
8422
8470
|
clearTimeout(existing.timeout);
|
|
8423
8471
|
}
|
|
8424
8472
|
const timeout = setTimeout(async () => {
|
|
8425
|
-
await this.executeRecalculation(parentId, parentObjectId
|
|
8473
|
+
await this.executeRecalculation(parentId, parentObjectId);
|
|
8426
8474
|
this.pending.delete(key);
|
|
8427
8475
|
}, this.debounceMs);
|
|
8428
8476
|
this.pending.set(key, { parentId, parentObjectId, tenantId, timeout });
|
|
@@ -8449,19 +8497,19 @@ var RollupScheduler = class {
|
|
|
8449
8497
|
const schema = await this.getSchemaById(record.objectId);
|
|
8450
8498
|
const tenantId = tenantMap.get(record.id);
|
|
8451
8499
|
if (schema && tenantId) {
|
|
8452
|
-
await this.rollupService.recalculateAndUpdate(record, schema
|
|
8500
|
+
await this.rollupService.recalculateAndUpdate(record, schema);
|
|
8453
8501
|
}
|
|
8454
8502
|
}
|
|
8455
8503
|
}
|
|
8456
8504
|
/**
|
|
8457
8505
|
* Execute a single recalculation
|
|
8458
8506
|
*/
|
|
8459
|
-
async executeRecalculation(parentId, parentObjectId
|
|
8507
|
+
async executeRecalculation(parentId, parentObjectId) {
|
|
8460
8508
|
const record = await this.adapter.objectRecords.findById(parentId);
|
|
8461
8509
|
if (!record) return;
|
|
8462
8510
|
const schema = await this.getSchemaById(parentObjectId);
|
|
8463
8511
|
if (!schema) return;
|
|
8464
|
-
await this.rollupService.recalculateAndUpdate(record, schema
|
|
8512
|
+
await this.rollupService.recalculateAndUpdate(record, schema);
|
|
8465
8513
|
}
|
|
8466
8514
|
/**
|
|
8467
8515
|
* Get number of pending recalculations
|
|
@@ -9317,7 +9365,6 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
9317
9365
|
createPhoneValidator,
|
|
9318
9366
|
createRatingValidator,
|
|
9319
9367
|
createRelationValidator,
|
|
9320
|
-
createRollupHooks,
|
|
9321
9368
|
createRollupValidator,
|
|
9322
9369
|
createSelectValidator,
|
|
9323
9370
|
createSingleRelationValidator,
|
|
@@ -9389,7 +9436,6 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
9389
9436
|
phoneConfigSchema,
|
|
9390
9437
|
rating,
|
|
9391
9438
|
ratingConfigSchema,
|
|
9392
|
-
registerAllRollupHooks,
|
|
9393
9439
|
registry,
|
|
9394
9440
|
relation,
|
|
9395
9441
|
relationConfigSchema,
|