@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/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
- materialize: import_zod4.z.boolean().optional()
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, tenantId);
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, tenantId);
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, tenantId) {
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, tenantId);
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,