@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/runtime.js
CHANGED
|
@@ -144,7 +144,6 @@ __export(runtime_exports, {
|
|
|
144
144
|
ViewService: () => ViewService,
|
|
145
145
|
buildAuditChanges: () => buildAuditChanges,
|
|
146
146
|
createMockAdapter: () => createMockAdapter,
|
|
147
|
-
createRollupHooks: () => createRollupHooks,
|
|
148
147
|
enrichValuesWithSelectLabels: () => enrichValuesWithSelectLabels,
|
|
149
148
|
evaluateFormula: () => evaluateFormula,
|
|
150
149
|
evaluateFormulaAttribute: () => evaluateFormulaAttribute,
|
|
@@ -166,7 +165,6 @@ __export(runtime_exports, {
|
|
|
166
165
|
isLabelExpression: () => isLabelExpression,
|
|
167
166
|
parsePath: () => parsePath,
|
|
168
167
|
pathHasManyCardinality: () => pathHasManyCardinality,
|
|
169
|
-
registerAllRollupHooks: () => registerAllRollupHooks,
|
|
170
168
|
renderLabelExpression: () => renderLabelExpression,
|
|
171
169
|
resolveMultiplePaths: () => resolveMultiplePaths,
|
|
172
170
|
resolveSingleValue: () => resolveSingleValue,
|
|
@@ -565,260 +563,6 @@ var NoopHookRegistry = class {
|
|
|
565
563
|
}
|
|
566
564
|
};
|
|
567
565
|
|
|
568
|
-
// src/runtime/services/rollup.service.ts
|
|
569
|
-
var RollupService = class {
|
|
570
|
-
constructor(adapter) {
|
|
571
|
-
this.adapter = adapter;
|
|
572
|
-
}
|
|
573
|
-
/**
|
|
574
|
-
* Calculate a rollup value for a record
|
|
575
|
-
*
|
|
576
|
-
* @param recordId - ID of the parent record
|
|
577
|
-
* @param rollupAttr - Rollup attribute definition
|
|
578
|
-
* @param schema - Schema of the parent object
|
|
579
|
-
* @returns Computed rollup value
|
|
580
|
-
*
|
|
581
|
-
* @example
|
|
582
|
-
* ```typescript
|
|
583
|
-
* // Sum all order amounts for a company
|
|
584
|
-
* const totalOrders = await rollupService.calculate(
|
|
585
|
-
* "company-123",
|
|
586
|
-
* {
|
|
587
|
-
* type: "rollup",
|
|
588
|
-
* name: "totalOrders",
|
|
589
|
-
* relationAttribute: "orders",
|
|
590
|
-
* targetAttribute: "amount",
|
|
591
|
-
* function: "sum",
|
|
592
|
-
* ...
|
|
593
|
-
* },
|
|
594
|
-
* companySchema
|
|
595
|
-
* );
|
|
596
|
-
* ```
|
|
597
|
-
*/
|
|
598
|
-
async calculate(recordId, rollupAttr, schema, tenantId) {
|
|
599
|
-
const relationAttr = schema.attributes.find(
|
|
600
|
-
(a) => a.type === "relation" && a.name === rollupAttr.relationAttribute
|
|
601
|
-
);
|
|
602
|
-
if (!relationAttr) {
|
|
603
|
-
return { value: null, recordCount: 0 };
|
|
604
|
-
}
|
|
605
|
-
const targetObjectName = relationAttr.targets[0]?.object;
|
|
606
|
-
if (!targetObjectName) {
|
|
607
|
-
return { value: null, recordCount: 0 };
|
|
608
|
-
}
|
|
609
|
-
const targetObject = await this.adapter.objects.findByName(tenantId, targetObjectName);
|
|
610
|
-
if (!targetObject) {
|
|
611
|
-
return { value: null, recordCount: 0 };
|
|
612
|
-
}
|
|
613
|
-
const relatedRecords = await this.adapter.objectRecords.findByRelation(
|
|
614
|
-
tenantId,
|
|
615
|
-
targetObject.id,
|
|
616
|
-
rollupAttr.relationAttribute,
|
|
617
|
-
recordId
|
|
618
|
-
);
|
|
619
|
-
if (relatedRecords.length === 0) {
|
|
620
|
-
return { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 };
|
|
621
|
-
}
|
|
622
|
-
const values = relatedRecords.map((r) => r.values[rollupAttr.targetAttribute]).filter((v) => v !== void 0);
|
|
623
|
-
const aggregated = this.aggregate(values, rollupAttr.function);
|
|
624
|
-
const result = typeof aggregated === "number" && rollupAttr.decimals !== void 0 ? Number(aggregated.toFixed(rollupAttr.decimals)) : aggregated;
|
|
625
|
-
return { value: result, recordCount: relatedRecords.length };
|
|
626
|
-
}
|
|
627
|
-
/**
|
|
628
|
-
* Calculate rollup values for multiple records (batched)
|
|
629
|
-
*
|
|
630
|
-
* More efficient than calling calculate() for each record individually.
|
|
631
|
-
*/
|
|
632
|
-
async calculateForMany(recordIds, rollupAttr, schema, tenantId) {
|
|
633
|
-
const results = /* @__PURE__ */ new Map();
|
|
634
|
-
for (const id of recordIds) {
|
|
635
|
-
results.set(id, { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 });
|
|
636
|
-
}
|
|
637
|
-
await Promise.all(
|
|
638
|
-
recordIds.map(async (id) => {
|
|
639
|
-
const result = await this.calculate(id, rollupAttr, schema, tenantId);
|
|
640
|
-
results.set(id, result);
|
|
641
|
-
})
|
|
642
|
-
);
|
|
643
|
-
return results;
|
|
644
|
-
}
|
|
645
|
-
/**
|
|
646
|
-
* Apply aggregation function to a set of values
|
|
647
|
-
*/
|
|
648
|
-
aggregate(values, fn) {
|
|
649
|
-
switch (fn) {
|
|
650
|
-
case "sum":
|
|
651
|
-
return this.sumNumbers(values);
|
|
652
|
-
case "avg":
|
|
653
|
-
return this.averageNumbers(values);
|
|
654
|
-
case "min":
|
|
655
|
-
return this.minNumbers(values);
|
|
656
|
-
case "max":
|
|
657
|
-
return this.maxNumbers(values);
|
|
658
|
-
case "count":
|
|
659
|
-
return values.length;
|
|
660
|
-
case "countValues":
|
|
661
|
-
return values.filter((v) => v != null && v !== "").length;
|
|
662
|
-
case "countEmpty":
|
|
663
|
-
return values.filter((v) => v == null || v === "").length;
|
|
664
|
-
case "percentFilled": {
|
|
665
|
-
if (values.length === 0) return 0;
|
|
666
|
-
const filled = values.filter((v) => v != null && v !== "").length;
|
|
667
|
-
return filled / values.length * 100;
|
|
668
|
-
}
|
|
669
|
-
case "percentEmpty": {
|
|
670
|
-
if (values.length === 0) return 0;
|
|
671
|
-
const empty = values.filter((v) => v == null || v === "").length;
|
|
672
|
-
return empty / values.length * 100;
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
/**
|
|
677
|
-
* Get the default empty value for a rollup function
|
|
678
|
-
*/
|
|
679
|
-
getEmptyValue(fn) {
|
|
680
|
-
switch (fn) {
|
|
681
|
-
case "sum":
|
|
682
|
-
case "count":
|
|
683
|
-
case "countValues":
|
|
684
|
-
case "countEmpty":
|
|
685
|
-
case "percentEmpty":
|
|
686
|
-
case "percentFilled":
|
|
687
|
-
return 0;
|
|
688
|
-
case "avg":
|
|
689
|
-
case "min":
|
|
690
|
-
case "max":
|
|
691
|
-
return null;
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
/**
|
|
695
|
-
* Sum numeric values
|
|
696
|
-
*/
|
|
697
|
-
sumNumbers(values) {
|
|
698
|
-
return values.filter((v) => typeof v === "number" && !Number.isNaN(v)).reduce((sum, n) => sum + n, 0);
|
|
699
|
-
}
|
|
700
|
-
/**
|
|
701
|
-
* Average numeric values
|
|
702
|
-
*/
|
|
703
|
-
averageNumbers(values) {
|
|
704
|
-
const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
|
|
705
|
-
if (nums.length === 0) return null;
|
|
706
|
-
return nums.reduce((sum, n) => sum + n, 0) / nums.length;
|
|
707
|
-
}
|
|
708
|
-
/**
|
|
709
|
-
* Minimum numeric value
|
|
710
|
-
*/
|
|
711
|
-
minNumbers(values) {
|
|
712
|
-
const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
|
|
713
|
-
if (nums.length === 0) return null;
|
|
714
|
-
return Math.min(...nums);
|
|
715
|
-
}
|
|
716
|
-
/**
|
|
717
|
-
* Maximum numeric value
|
|
718
|
-
*/
|
|
719
|
-
maxNumbers(values) {
|
|
720
|
-
const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
|
|
721
|
-
if (nums.length === 0) return null;
|
|
722
|
-
return Math.max(...nums);
|
|
723
|
-
}
|
|
724
|
-
/**
|
|
725
|
-
* Recalculate all rollup attributes for a record and update it
|
|
726
|
-
*
|
|
727
|
-
* Called after related records change to keep rollups up-to-date.
|
|
728
|
-
*/
|
|
729
|
-
async recalculateAndUpdate(record, schema, tenantId) {
|
|
730
|
-
const rollupAttrs = schema.attributes.filter((a) => a.type === "rollup");
|
|
731
|
-
if (rollupAttrs.length === 0) {
|
|
732
|
-
return record;
|
|
733
|
-
}
|
|
734
|
-
const updates = {};
|
|
735
|
-
for (const attr of rollupAttrs) {
|
|
736
|
-
const result = await this.calculate(record.id, attr, schema, tenantId);
|
|
737
|
-
updates[attr.name] = result.value;
|
|
738
|
-
}
|
|
739
|
-
return await this.adapter.objectRecords.update(record.id, updates);
|
|
740
|
-
}
|
|
741
|
-
/**
|
|
742
|
-
* Find parent records that need rollup recalculation when a child record changes
|
|
743
|
-
*
|
|
744
|
-
* Used by hooks to determine which parent records to recalculate after
|
|
745
|
-
* a child record is created, updated, or deleted.
|
|
746
|
-
*
|
|
747
|
-
* @param changedRecord - The record that was modified
|
|
748
|
-
* @param changedSchema - Schema of the changed record's object
|
|
749
|
-
* @returns Array of parent record IDs that need recalculation
|
|
750
|
-
*/
|
|
751
|
-
async findAffectedParentRecords(changedRecord, changedSchema) {
|
|
752
|
-
const affectedIds = [];
|
|
753
|
-
const relationAttrs = changedSchema.attributes.filter(
|
|
754
|
-
(a) => a.type === "relation"
|
|
755
|
-
);
|
|
756
|
-
for (const attr of relationAttrs) {
|
|
757
|
-
const relatedId = changedRecord.values[attr.name];
|
|
758
|
-
if (typeof relatedId === "string" && relatedId.length > 0) {
|
|
759
|
-
affectedIds.push(relatedId);
|
|
760
|
-
} else if (Array.isArray(relatedId)) {
|
|
761
|
-
affectedIds.push(...relatedId.filter((id) => typeof id === "string"));
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
return [...new Set(affectedIds)];
|
|
765
|
-
}
|
|
766
|
-
};
|
|
767
|
-
|
|
768
|
-
// src/runtime/hooks/rollup.hooks.ts
|
|
769
|
-
function createRollupHooks(objectName, config) {
|
|
770
|
-
const rollupService = new RollupService(config.adapter);
|
|
771
|
-
async function recalculateParentRollups(ctx) {
|
|
772
|
-
const schema = await config.getSchemaById(ctx.objectId);
|
|
773
|
-
if (!schema) return;
|
|
774
|
-
const affectedParentIds = await rollupService.findAffectedParentRecords(ctx.record, schema);
|
|
775
|
-
if (affectedParentIds.length === 0) return;
|
|
776
|
-
const parentRecords = await config.adapter.objectRecords.findByIds(affectedParentIds);
|
|
777
|
-
for (const parentRecord of parentRecords) {
|
|
778
|
-
const parentSchema = await config.getSchemaById(parentRecord.objectId);
|
|
779
|
-
if (!parentSchema) continue;
|
|
780
|
-
const hasRollups = parentSchema.attributes.some(
|
|
781
|
-
(a) => a.type === "rollup"
|
|
782
|
-
);
|
|
783
|
-
if (hasRollups) {
|
|
784
|
-
await rollupService.recalculateAndUpdate(parentRecord, parentSchema, config.tenantId);
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
return [
|
|
789
|
-
{
|
|
790
|
-
type: "afterCreate",
|
|
791
|
-
objectName,
|
|
792
|
-
handler: recalculateParentRollups,
|
|
793
|
-
priority: 100
|
|
794
|
-
// Run after other hooks
|
|
795
|
-
},
|
|
796
|
-
{
|
|
797
|
-
type: "afterUpdate",
|
|
798
|
-
objectName,
|
|
799
|
-
handler: recalculateParentRollups,
|
|
800
|
-
priority: 100
|
|
801
|
-
},
|
|
802
|
-
{
|
|
803
|
-
type: "afterDelete",
|
|
804
|
-
objectName,
|
|
805
|
-
handler: recalculateParentRollups,
|
|
806
|
-
priority: 100
|
|
807
|
-
}
|
|
808
|
-
];
|
|
809
|
-
}
|
|
810
|
-
function registerAllRollupHooks(hookRegistry, schemas, config) {
|
|
811
|
-
for (const schema of schemas) {
|
|
812
|
-
const hasRelations = schema.attributes.some((a) => a.type === "relation");
|
|
813
|
-
if (hasRelations && hookRegistry.register) {
|
|
814
|
-
const hooks = createRollupHooks(schema.name, config);
|
|
815
|
-
for (const hook of hooks) {
|
|
816
|
-
hookRegistry.register(hook);
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
|
|
822
566
|
// src/utils.ts
|
|
823
567
|
function generateId() {
|
|
824
568
|
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
@@ -3601,10 +3345,12 @@ var rollupConfigSchema = baseConfigSchema.extend({
|
|
|
3601
3345
|
"countValues",
|
|
3602
3346
|
"countEmpty",
|
|
3603
3347
|
"percentEmpty",
|
|
3604
|
-
"percentFilled"
|
|
3348
|
+
"percentFilled",
|
|
3349
|
+
"collect"
|
|
3605
3350
|
]),
|
|
3606
3351
|
decimals: import_zod4.z.number().int().min(0).max(10).optional(),
|
|
3607
|
-
|
|
3352
|
+
showAsOriginal: import_zod4.z.boolean().optional(),
|
|
3353
|
+
targetAttributeType: import_zod4.z.string().optional()
|
|
3608
3354
|
});
|
|
3609
3355
|
var attributeConfigSchemas = {
|
|
3610
3356
|
text: textConfigSchema,
|
|
@@ -5450,6 +5196,263 @@ var RelationService = class {
|
|
|
5450
5196
|
}
|
|
5451
5197
|
};
|
|
5452
5198
|
|
|
5199
|
+
// src/runtime/services/rollup.service.ts
|
|
5200
|
+
var RollupService = class {
|
|
5201
|
+
constructor(adapter) {
|
|
5202
|
+
this.adapter = adapter;
|
|
5203
|
+
}
|
|
5204
|
+
/**
|
|
5205
|
+
* Calculate a rollup value for a record
|
|
5206
|
+
*
|
|
5207
|
+
* @param recordId - ID of the parent record
|
|
5208
|
+
* @param rollupAttr - Rollup attribute definition
|
|
5209
|
+
* @param schema - Schema of the parent object
|
|
5210
|
+
* @returns Computed rollup value
|
|
5211
|
+
*
|
|
5212
|
+
* @example
|
|
5213
|
+
* ```typescript
|
|
5214
|
+
* // Sum all order amounts for a company
|
|
5215
|
+
* const totalOrders = await rollupService.calculate(
|
|
5216
|
+
* "company-123",
|
|
5217
|
+
* {
|
|
5218
|
+
* type: "rollup",
|
|
5219
|
+
* name: "totalOrders",
|
|
5220
|
+
* relationAttribute: "orders",
|
|
5221
|
+
* targetAttribute: "amount",
|
|
5222
|
+
* function: "sum",
|
|
5223
|
+
* ...
|
|
5224
|
+
* },
|
|
5225
|
+
* companySchema
|
|
5226
|
+
* );
|
|
5227
|
+
* ```
|
|
5228
|
+
*/
|
|
5229
|
+
async calculate(recordId, rollupAttr, schema) {
|
|
5230
|
+
const relationAttr = schema.attributes.find(
|
|
5231
|
+
(a) => a.type === "relation" && a.name === rollupAttr.relationAttribute
|
|
5232
|
+
);
|
|
5233
|
+
if (relationAttr) {
|
|
5234
|
+
return this.calculateForward(recordId, rollupAttr);
|
|
5235
|
+
}
|
|
5236
|
+
return this.calculateReverse(recordId, rollupAttr, schema);
|
|
5237
|
+
}
|
|
5238
|
+
/**
|
|
5239
|
+
* Forward pattern: this record has a relation attribute pointing to other records
|
|
5240
|
+
* Example: entreprise222 has relation "entreprises" → companies, rollup collects from companies
|
|
5241
|
+
*/
|
|
5242
|
+
async calculateForward(recordId, rollupAttr) {
|
|
5243
|
+
const record = await this.adapter.objectRecords.findById(recordId);
|
|
5244
|
+
if (!record) {
|
|
5245
|
+
return { value: null, recordCount: 0 };
|
|
5246
|
+
}
|
|
5247
|
+
const relationValue = record.values[rollupAttr.relationAttribute];
|
|
5248
|
+
let relatedIds = [];
|
|
5249
|
+
if (typeof relationValue === "string" && relationValue.length > 0) {
|
|
5250
|
+
relatedIds = [relationValue];
|
|
5251
|
+
} else if (Array.isArray(relationValue)) {
|
|
5252
|
+
relatedIds = relationValue.filter((id) => typeof id === "string" && id.length > 0);
|
|
5253
|
+
}
|
|
5254
|
+
if (relatedIds.length === 0) {
|
|
5255
|
+
return { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 };
|
|
5256
|
+
}
|
|
5257
|
+
const relatedRecords = await this.adapter.objectRecords.findByIds(relatedIds);
|
|
5258
|
+
if (relatedRecords.length === 0) {
|
|
5259
|
+
return { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 };
|
|
5260
|
+
}
|
|
5261
|
+
return this.aggregateValues(relatedRecords, rollupAttr);
|
|
5262
|
+
}
|
|
5263
|
+
/**
|
|
5264
|
+
* Reverse pattern: other records have a relation pointing to this record
|
|
5265
|
+
* Example: Company has rollup on "orders", Order has relation "company" → companies
|
|
5266
|
+
*/
|
|
5267
|
+
async calculateReverse(recordId, rollupAttr, schema) {
|
|
5268
|
+
const record = await this.adapter.objectRecords.findById(recordId);
|
|
5269
|
+
if (!record) {
|
|
5270
|
+
return { value: null, recordCount: 0 };
|
|
5271
|
+
}
|
|
5272
|
+
const parentObject = await this.adapter.objects.findById(record.objectId);
|
|
5273
|
+
if (!parentObject) {
|
|
5274
|
+
return { value: null, recordCount: 0 };
|
|
5275
|
+
}
|
|
5276
|
+
const tenantId = parentObject.tenantId;
|
|
5277
|
+
const sourceObjectName = rollupAttr.relationAttribute;
|
|
5278
|
+
const sourceObject = await this.adapter.objects.findByName(tenantId, sourceObjectName);
|
|
5279
|
+
if (!sourceObject) {
|
|
5280
|
+
return { value: null, recordCount: 0 };
|
|
5281
|
+
}
|
|
5282
|
+
const sourceAttributes = await this.adapter.attributes.findByObjectId(sourceObject.id);
|
|
5283
|
+
const reverseRelationAttr = sourceAttributes.find((attr) => {
|
|
5284
|
+
if (attr.type !== "relation") return false;
|
|
5285
|
+
const relationConfig = attr.config;
|
|
5286
|
+
return relationConfig?.targets?.some((t) => t.object === schema.name);
|
|
5287
|
+
});
|
|
5288
|
+
if (!reverseRelationAttr) {
|
|
5289
|
+
return { value: null, recordCount: 0 };
|
|
5290
|
+
}
|
|
5291
|
+
const relatedRecords = await this.adapter.objectRecords.findByRelation(
|
|
5292
|
+
tenantId,
|
|
5293
|
+
sourceObject.id,
|
|
5294
|
+
reverseRelationAttr.name,
|
|
5295
|
+
recordId
|
|
5296
|
+
);
|
|
5297
|
+
if (relatedRecords.length === 0) {
|
|
5298
|
+
return { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 };
|
|
5299
|
+
}
|
|
5300
|
+
return this.aggregateValues(relatedRecords, rollupAttr);
|
|
5301
|
+
}
|
|
5302
|
+
/**
|
|
5303
|
+
* Extract and aggregate values from related records
|
|
5304
|
+
*/
|
|
5305
|
+
aggregateValues(relatedRecords, rollupAttr) {
|
|
5306
|
+
const values = relatedRecords.map((r) => r.values[rollupAttr.targetAttribute]).filter((v) => v !== void 0 && v !== null);
|
|
5307
|
+
const aggregated = this.aggregate(values, rollupAttr.function);
|
|
5308
|
+
const result = typeof aggregated === "number" && rollupAttr.decimals !== void 0 ? Number(aggregated.toFixed(rollupAttr.decimals)) : aggregated;
|
|
5309
|
+
return { value: result, recordCount: relatedRecords.length };
|
|
5310
|
+
}
|
|
5311
|
+
/**
|
|
5312
|
+
* Calculate rollup values for multiple records (batched)
|
|
5313
|
+
*
|
|
5314
|
+
* More efficient than calling calculate() for each record individually.
|
|
5315
|
+
*/
|
|
5316
|
+
async calculateForMany(recordIds, rollupAttr, schema) {
|
|
5317
|
+
const results = /* @__PURE__ */ new Map();
|
|
5318
|
+
for (const id of recordIds) {
|
|
5319
|
+
results.set(id, { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 });
|
|
5320
|
+
}
|
|
5321
|
+
await Promise.all(
|
|
5322
|
+
recordIds.map(async (id) => {
|
|
5323
|
+
const result = await this.calculate(id, rollupAttr, schema);
|
|
5324
|
+
results.set(id, result);
|
|
5325
|
+
})
|
|
5326
|
+
);
|
|
5327
|
+
return results;
|
|
5328
|
+
}
|
|
5329
|
+
/**
|
|
5330
|
+
* Apply aggregation function to a set of values
|
|
5331
|
+
*/
|
|
5332
|
+
aggregate(values, fn) {
|
|
5333
|
+
switch (fn) {
|
|
5334
|
+
case "sum":
|
|
5335
|
+
return this.sumNumbers(values);
|
|
5336
|
+
case "avg":
|
|
5337
|
+
return this.averageNumbers(values);
|
|
5338
|
+
case "min":
|
|
5339
|
+
return this.minNumbers(values);
|
|
5340
|
+
case "max":
|
|
5341
|
+
return this.maxNumbers(values);
|
|
5342
|
+
case "count":
|
|
5343
|
+
return values.length;
|
|
5344
|
+
case "countValues":
|
|
5345
|
+
return values.filter((v) => v != null && v !== "").length;
|
|
5346
|
+
case "countEmpty":
|
|
5347
|
+
return values.filter((v) => v == null || v === "").length;
|
|
5348
|
+
case "percentFilled": {
|
|
5349
|
+
if (values.length === 0) return 0;
|
|
5350
|
+
const filled = values.filter((v) => v != null && v !== "").length;
|
|
5351
|
+
return filled / values.length * 100;
|
|
5352
|
+
}
|
|
5353
|
+
case "percentEmpty": {
|
|
5354
|
+
if (values.length === 0) return 0;
|
|
5355
|
+
const empty = values.filter((v) => v == null || v === "").length;
|
|
5356
|
+
return empty / values.length * 100;
|
|
5357
|
+
}
|
|
5358
|
+
case "collect":
|
|
5359
|
+
return values;
|
|
5360
|
+
}
|
|
5361
|
+
}
|
|
5362
|
+
/**
|
|
5363
|
+
* Get the default empty value for a rollup function
|
|
5364
|
+
*/
|
|
5365
|
+
getEmptyValue(fn) {
|
|
5366
|
+
switch (fn) {
|
|
5367
|
+
case "sum":
|
|
5368
|
+
case "count":
|
|
5369
|
+
case "countValues":
|
|
5370
|
+
case "countEmpty":
|
|
5371
|
+
case "percentEmpty":
|
|
5372
|
+
case "percentFilled":
|
|
5373
|
+
return 0;
|
|
5374
|
+
case "avg":
|
|
5375
|
+
case "min":
|
|
5376
|
+
case "max":
|
|
5377
|
+
return null;
|
|
5378
|
+
case "collect":
|
|
5379
|
+
return [];
|
|
5380
|
+
}
|
|
5381
|
+
}
|
|
5382
|
+
/**
|
|
5383
|
+
* Sum numeric values
|
|
5384
|
+
*/
|
|
5385
|
+
sumNumbers(values) {
|
|
5386
|
+
return values.filter((v) => typeof v === "number" && !Number.isNaN(v)).reduce((sum, n) => sum + n, 0);
|
|
5387
|
+
}
|
|
5388
|
+
/**
|
|
5389
|
+
* Average numeric values
|
|
5390
|
+
*/
|
|
5391
|
+
averageNumbers(values) {
|
|
5392
|
+
const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
|
|
5393
|
+
if (nums.length === 0) return null;
|
|
5394
|
+
return nums.reduce((sum, n) => sum + n, 0) / nums.length;
|
|
5395
|
+
}
|
|
5396
|
+
/**
|
|
5397
|
+
* Minimum numeric value
|
|
5398
|
+
*/
|
|
5399
|
+
minNumbers(values) {
|
|
5400
|
+
const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
|
|
5401
|
+
if (nums.length === 0) return null;
|
|
5402
|
+
return Math.min(...nums);
|
|
5403
|
+
}
|
|
5404
|
+
/**
|
|
5405
|
+
* Maximum numeric value
|
|
5406
|
+
*/
|
|
5407
|
+
maxNumbers(values) {
|
|
5408
|
+
const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
|
|
5409
|
+
if (nums.length === 0) return null;
|
|
5410
|
+
return Math.max(...nums);
|
|
5411
|
+
}
|
|
5412
|
+
/**
|
|
5413
|
+
* Recalculate all rollup attributes for a record and update it
|
|
5414
|
+
*
|
|
5415
|
+
* Called after related records change to keep rollups up-to-date.
|
|
5416
|
+
*/
|
|
5417
|
+
async recalculateAndUpdate(record, schema) {
|
|
5418
|
+
const rollupAttrs = schema.attributes.filter((a) => a.type === "rollup");
|
|
5419
|
+
if (rollupAttrs.length === 0) {
|
|
5420
|
+
return record;
|
|
5421
|
+
}
|
|
5422
|
+
const updates = {};
|
|
5423
|
+
for (const attr of rollupAttrs) {
|
|
5424
|
+
const result = await this.calculate(record.id, attr, schema);
|
|
5425
|
+
updates[attr.name] = result.value;
|
|
5426
|
+
}
|
|
5427
|
+
return await this.adapter.objectRecords.update(record.id, updates);
|
|
5428
|
+
}
|
|
5429
|
+
/**
|
|
5430
|
+
* Find parent records that need rollup recalculation when a child record changes
|
|
5431
|
+
*
|
|
5432
|
+
* Used by hooks to determine which parent records to recalculate after
|
|
5433
|
+
* a child record is created, updated, or deleted.
|
|
5434
|
+
*
|
|
5435
|
+
* @param changedRecord - The record that was modified
|
|
5436
|
+
* @param changedSchema - Schema of the changed record's object
|
|
5437
|
+
* @returns Array of parent record IDs that need recalculation
|
|
5438
|
+
*/
|
|
5439
|
+
async findAffectedParentRecords(changedRecord, changedSchema) {
|
|
5440
|
+
const affectedIds = [];
|
|
5441
|
+
const relationAttrs = changedSchema.attributes.filter(
|
|
5442
|
+
(a) => a.type === "relation"
|
|
5443
|
+
);
|
|
5444
|
+
for (const attr of relationAttrs) {
|
|
5445
|
+
const relatedId = changedRecord.values[attr.name];
|
|
5446
|
+
if (typeof relatedId === "string" && relatedId.length > 0) {
|
|
5447
|
+
affectedIds.push(relatedId);
|
|
5448
|
+
} else if (Array.isArray(relatedId)) {
|
|
5449
|
+
affectedIds.push(...relatedId.filter((id) => typeof id === "string"));
|
|
5450
|
+
}
|
|
5451
|
+
}
|
|
5452
|
+
return [...new Set(affectedIds)];
|
|
5453
|
+
}
|
|
5454
|
+
};
|
|
5455
|
+
|
|
5453
5456
|
// src/runtime/services/user.service.ts
|
|
5454
5457
|
var UserService = class {
|
|
5455
5458
|
constructor(adapter, tenantId) {
|
|
@@ -5575,6 +5578,7 @@ var RecordService = class {
|
|
|
5575
5578
|
this.schemaService = new ObjectSchemaService(adapter, registry);
|
|
5576
5579
|
this.relationService = new RelationService(adapter, registry);
|
|
5577
5580
|
this.userService = new UserService(adapter, tenantId);
|
|
5581
|
+
this.rollupService = new RollupService(adapter);
|
|
5578
5582
|
this.hookRegistry = options?.hookRegistry ?? new NoopHookRegistry();
|
|
5579
5583
|
this.permissionService = options?.permissionService;
|
|
5580
5584
|
this.auditService = options?.auditService ?? (adapter.audit ? new AuditService(adapter, tenantId) : void 0);
|
|
@@ -5736,6 +5740,7 @@ var RecordService = class {
|
|
|
5736
5740
|
};
|
|
5737
5741
|
await this.hookRegistry.execute("afterCreate", schema.name, afterCtx);
|
|
5738
5742
|
}
|
|
5743
|
+
await this.recalculateParentRollups(record, schema);
|
|
5739
5744
|
if (this.auditService && this.userId) {
|
|
5740
5745
|
await this.auditService.logRecordAction({
|
|
5741
5746
|
action: "record.created",
|
|
@@ -5851,6 +5856,7 @@ var RecordService = class {
|
|
|
5851
5856
|
};
|
|
5852
5857
|
await this.hookRegistry.execute("afterUpdate", schema.name, afterCtx);
|
|
5853
5858
|
}
|
|
5859
|
+
await this.recalculateParentRollups(updated, schema);
|
|
5854
5860
|
if (this.auditService && this.userId && changedAttributes.length > 0) {
|
|
5855
5861
|
const changes = changedAttributes.map((attr) => ({
|
|
5856
5862
|
field: attr,
|
|
@@ -5978,6 +5984,7 @@ var RecordService = class {
|
|
|
5978
5984
|
if (!options?.skipHooks) {
|
|
5979
5985
|
await this.hookRegistry.execute("afterDelete", schema.name, hookCtx);
|
|
5980
5986
|
}
|
|
5987
|
+
await this.recalculateParentRollups(record, schema);
|
|
5981
5988
|
if (this.auditService && this.userId) {
|
|
5982
5989
|
await this.auditService.logRecordAction({
|
|
5983
5990
|
action: "record.deleted",
|
|
@@ -6107,6 +6114,39 @@ var RecordService = class {
|
|
|
6107
6114
|
}
|
|
6108
6115
|
return records.map((record) => this.enrichWithFormulas(record, schema));
|
|
6109
6116
|
}
|
|
6117
|
+
/**
|
|
6118
|
+
* Recalculate rollups after a record changes
|
|
6119
|
+
*
|
|
6120
|
+
* This handles two cases:
|
|
6121
|
+
* 1. The record itself has rollups (e.g., aggregating from related records it points to)
|
|
6122
|
+
* 2. Parent records have rollups that aggregate from this record
|
|
6123
|
+
*
|
|
6124
|
+
* @param record - The record that was modified
|
|
6125
|
+
* @param schema - Schema of the record's object
|
|
6126
|
+
* @internal
|
|
6127
|
+
*/
|
|
6128
|
+
async recalculateParentRollups(record, schema) {
|
|
6129
|
+
const ownRollupAttrs = schema.attributes.filter(
|
|
6130
|
+
(a) => a.type === "rollup"
|
|
6131
|
+
);
|
|
6132
|
+
if (ownRollupAttrs.length > 0) {
|
|
6133
|
+
await this.rollupService.recalculateAndUpdate(record, schema);
|
|
6134
|
+
}
|
|
6135
|
+
const affectedParentIds = await this.rollupService.findAffectedParentRecords(record, schema);
|
|
6136
|
+
if (affectedParentIds.length === 0) {
|
|
6137
|
+
return;
|
|
6138
|
+
}
|
|
6139
|
+
const parentRecords = await this.adapter.objectRecords.findByIds(affectedParentIds);
|
|
6140
|
+
for (const parentRecord of parentRecords) {
|
|
6141
|
+
const parentSchema = await this.schemaService.getObjectSchema(parentRecord.objectId);
|
|
6142
|
+
const rollupAttrs = parentSchema.attributes.filter(
|
|
6143
|
+
(a) => a.type === "rollup"
|
|
6144
|
+
);
|
|
6145
|
+
if (rollupAttrs.length > 0) {
|
|
6146
|
+
await this.rollupService.recalculateAndUpdate(parentRecord, parentSchema);
|
|
6147
|
+
}
|
|
6148
|
+
}
|
|
6149
|
+
}
|
|
6110
6150
|
/**
|
|
6111
6151
|
* Permanently delete a record (hard delete)
|
|
6112
6152
|
*
|
|
@@ -6380,7 +6420,7 @@ var RollupScheduler = class {
|
|
|
6380
6420
|
clearTimeout(existing.timeout);
|
|
6381
6421
|
}
|
|
6382
6422
|
const timeout = setTimeout(async () => {
|
|
6383
|
-
await this.executeRecalculation(parentId, parentObjectId
|
|
6423
|
+
await this.executeRecalculation(parentId, parentObjectId);
|
|
6384
6424
|
this.pending.delete(key);
|
|
6385
6425
|
}, this.debounceMs);
|
|
6386
6426
|
this.pending.set(key, { parentId, parentObjectId, tenantId, timeout });
|
|
@@ -6407,19 +6447,19 @@ var RollupScheduler = class {
|
|
|
6407
6447
|
const schema = await this.getSchemaById(record.objectId);
|
|
6408
6448
|
const tenantId = tenantMap.get(record.id);
|
|
6409
6449
|
if (schema && tenantId) {
|
|
6410
|
-
await this.rollupService.recalculateAndUpdate(record, schema
|
|
6450
|
+
await this.rollupService.recalculateAndUpdate(record, schema);
|
|
6411
6451
|
}
|
|
6412
6452
|
}
|
|
6413
6453
|
}
|
|
6414
6454
|
/**
|
|
6415
6455
|
* Execute a single recalculation
|
|
6416
6456
|
*/
|
|
6417
|
-
async executeRecalculation(parentId, parentObjectId
|
|
6457
|
+
async executeRecalculation(parentId, parentObjectId) {
|
|
6418
6458
|
const record = await this.adapter.objectRecords.findById(parentId);
|
|
6419
6459
|
if (!record) return;
|
|
6420
6460
|
const schema = await this.getSchemaById(parentObjectId);
|
|
6421
6461
|
if (!schema) return;
|
|
6422
|
-
await this.rollupService.recalculateAndUpdate(record, schema
|
|
6462
|
+
await this.rollupService.recalculateAndUpdate(record, schema);
|
|
6423
6463
|
}
|
|
6424
6464
|
/**
|
|
6425
6465
|
* Get number of pending recalculations
|
|
@@ -7231,7 +7271,6 @@ var NoopGeocodingAdapter = class {
|
|
|
7231
7271
|
ViewService,
|
|
7232
7272
|
buildAuditChanges,
|
|
7233
7273
|
createMockAdapter,
|
|
7234
|
-
createRollupHooks,
|
|
7235
7274
|
enrichValuesWithSelectLabels,
|
|
7236
7275
|
evaluateFormula,
|
|
7237
7276
|
evaluateFormulaAttribute,
|
|
@@ -7253,7 +7292,6 @@ var NoopGeocodingAdapter = class {
|
|
|
7253
7292
|
isLabelExpression,
|
|
7254
7293
|
parsePath,
|
|
7255
7294
|
pathHasManyCardinality,
|
|
7256
|
-
registerAllRollupHooks,
|
|
7257
7295
|
renderLabelExpression,
|
|
7258
7296
|
resolveMultiplePaths,
|
|
7259
7297
|
resolveSingleValue,
|
package/dist/runtime.mjs
CHANGED
|
@@ -21,7 +21,6 @@ import {
|
|
|
21
21
|
ViewService,
|
|
22
22
|
buildAuditChanges,
|
|
23
23
|
createMockAdapter,
|
|
24
|
-
createRollupHooks,
|
|
25
24
|
enrichValuesWithSelectLabels,
|
|
26
25
|
evaluateFormula,
|
|
27
26
|
evaluateFormulaAttribute,
|
|
@@ -43,7 +42,6 @@ import {
|
|
|
43
42
|
isLabelExpression,
|
|
44
43
|
parsePath,
|
|
45
44
|
pathHasManyCardinality,
|
|
46
|
-
registerAllRollupHooks,
|
|
47
45
|
renderLabelExpression,
|
|
48
46
|
resolveMultiplePaths,
|
|
49
47
|
resolveSingleValue,
|
|
@@ -55,7 +53,7 @@ import {
|
|
|
55
53
|
validatePath,
|
|
56
54
|
verifyNativeObjectsSync,
|
|
57
55
|
verifyNativeViewsSync
|
|
58
|
-
} from "./chunk-
|
|
56
|
+
} from "./chunk-ZU5SR355.mjs";
|
|
59
57
|
export {
|
|
60
58
|
AuditService,
|
|
61
59
|
DEFAULT_LABEL_FALLBACK,
|
|
@@ -79,7 +77,6 @@ export {
|
|
|
79
77
|
ViewService,
|
|
80
78
|
buildAuditChanges,
|
|
81
79
|
createMockAdapter,
|
|
82
|
-
createRollupHooks,
|
|
83
80
|
enrichValuesWithSelectLabels,
|
|
84
81
|
evaluateFormula,
|
|
85
82
|
evaluateFormulaAttribute,
|
|
@@ -101,7 +98,6 @@ export {
|
|
|
101
98
|
isLabelExpression,
|
|
102
99
|
parsePath,
|
|
103
100
|
pathHasManyCardinality,
|
|
104
|
-
registerAllRollupHooks,
|
|
105
101
|
renderLabelExpression,
|
|
106
102
|
resolveMultiplePaths,
|
|
107
103
|
resolveSingleValue,
|