@stndrds/schema 0.1.0-alpha.19 → 0.1.0-alpha.21

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 (58) hide show
  1. package/dist/chunk-3RVLIIUK.mjs +9257 -0
  2. package/dist/chunk-4SW4V3YU.mjs +8854 -0
  3. package/dist/chunk-6FNFXEQY.mjs +9316 -0
  4. package/dist/chunk-6TJR4WDK.mjs +9262 -0
  5. package/dist/chunk-7GSIXU67.mjs +8820 -0
  6. package/dist/chunk-BQUYTOB3.mjs +8815 -0
  7. package/dist/chunk-BSAR4UKK.mjs +8851 -0
  8. package/dist/chunk-F27AKBTL.mjs +9452 -0
  9. package/dist/chunk-HSNBVKQU.mjs +9316 -0
  10. package/dist/chunk-I4TTNDN7.mjs +8910 -0
  11. package/dist/chunk-JJLODIBK.mjs +9389 -0
  12. package/dist/chunk-KDHA2N7D.mjs +8927 -0
  13. package/dist/chunk-MAXEN3GU.mjs +9266 -0
  14. package/dist/chunk-MX4X4UEN.mjs +8854 -0
  15. package/dist/chunk-OJWVSONX.mjs +8900 -0
  16. package/dist/chunk-PMYSW54L.mjs +8855 -0
  17. package/dist/chunk-Y5KIRAND.mjs +8916 -0
  18. package/dist/chunk-ZD3BFM25.mjs +9316 -0
  19. package/dist/chunk-ZU5SR355.mjs +8862 -0
  20. package/dist/index.d.mts +8 -197
  21. package/dist/index.d.ts +8 -197
  22. package/dist/index.js +921 -272
  23. package/dist/index.mjs +21 -5
  24. package/dist/runtime-09fOm3VO.d.mts +6383 -0
  25. package/dist/runtime-09fOm3VO.d.ts +6383 -0
  26. package/dist/runtime-0gaIqXql.d.mts +7074 -0
  27. package/dist/runtime-0gaIqXql.d.ts +7074 -0
  28. package/dist/runtime-3u5doYAK.d.mts +6934 -0
  29. package/dist/runtime-3u5doYAK.d.ts +6934 -0
  30. package/dist/runtime-B6mOrMV4.d.mts +6985 -0
  31. package/dist/runtime-B6mOrMV4.d.ts +6985 -0
  32. package/dist/runtime-BMHqETm7.d.mts +6450 -0
  33. package/dist/runtime-BMHqETm7.d.ts +6450 -0
  34. package/dist/runtime-BNML-fPx.d.mts +6969 -0
  35. package/dist/runtime-BNML-fPx.d.ts +6969 -0
  36. package/dist/runtime-BPdsKDJo.d.mts +6969 -0
  37. package/dist/runtime-BPdsKDJo.d.ts +6969 -0
  38. package/dist/runtime-BpsaNQPs.d.mts +6397 -0
  39. package/dist/runtime-BpsaNQPs.d.ts +6397 -0
  40. package/dist/runtime-CHAkwiwa.d.mts +6450 -0
  41. package/dist/runtime-CHAkwiwa.d.ts +6450 -0
  42. package/dist/runtime-CfVfZtoq.d.mts +6437 -0
  43. package/dist/runtime-CfVfZtoq.d.ts +6437 -0
  44. package/dist/runtime-D1jyS1lS.d.mts +7008 -0
  45. package/dist/runtime-D1jyS1lS.d.ts +7008 -0
  46. package/dist/runtime-DjEUAeL4.d.mts +6713 -0
  47. package/dist/runtime-DjEUAeL4.d.ts +6713 -0
  48. package/dist/runtime-rTM9ddEe.d.mts +6934 -0
  49. package/dist/runtime-rTM9ddEe.d.ts +6934 -0
  50. package/dist/runtime-xLzAkAnp.d.mts +6969 -0
  51. package/dist/runtime-xLzAkAnp.d.ts +6969 -0
  52. package/dist/runtime-xtAfMn_f.d.mts +6436 -0
  53. package/dist/runtime-xtAfMn_f.d.ts +6436 -0
  54. package/dist/runtime.d.mts +1 -1
  55. package/dist/runtime.d.ts +1 -1
  56. package/dist/runtime.js +901 -272
  57. package/dist/runtime.mjs +17 -5
  58. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -159,6 +159,9 @@ __export(index_exports, {
159
159
  PermissionService: () => PermissionService,
160
160
  ProtectedResourceError: () => ProtectedResourceError,
161
161
  ProtectedRoleError: () => ProtectedRoleError,
162
+ QueryBuilder: () => QueryBuilder,
163
+ QueryMultipleResultsError: () => QueryMultipleResultsError,
164
+ QueryNoResultError: () => QueryNoResultError,
162
165
  RecordNotFoundError: () => RecordNotFoundError,
163
166
  RecordReferencedError: () => RecordReferencedError,
164
167
  RecordService: () => RecordService,
@@ -167,6 +170,8 @@ __export(index_exports, {
167
170
  RoleNotFoundError: () => RoleNotFoundError,
168
171
  RollupScheduler: () => RollupScheduler,
169
172
  RollupService: () => RollupService,
173
+ SHORTCUT_TO_FILTER_OPERATOR: () => SHORTCUT_TO_FILTER_OPERATOR,
174
+ SYSTEM_FIELD_NAMES: () => SYSTEM_FIELD_NAMES,
170
175
  SYSTEM_RESOURCES: () => SYSTEM_RESOURCES,
171
176
  SYSTEM_RESOURCE_LABELS: () => SYSTEM_RESOURCE_LABELS,
172
177
  SchemaError: () => SchemaError,
@@ -189,6 +194,7 @@ __export(index_exports, {
189
194
  createCheckboxValidator: () => createCheckboxValidator,
190
195
  createCurrencyValidator: () => createCurrencyValidator,
191
196
  createDateValidator: () => createDateValidator,
197
+ createDefaultState: () => createDefaultState,
192
198
  createDraftValidator: () => createDraftValidator,
193
199
  createFileValidator: () => createFileValidator,
194
200
  createFormulaValidator: () => createFormulaValidator,
@@ -199,9 +205,9 @@ __export(index_exports, {
199
205
  createNumberValidator: () => createNumberValidator,
200
206
  createObjectValidator: () => createObjectValidator,
201
207
  createPhoneValidator: () => createPhoneValidator,
208
+ createQueryBuilder: () => createQueryBuilder,
202
209
  createRatingValidator: () => createRatingValidator,
203
210
  createRelationValidator: () => createRelationValidator,
204
- createRollupHooks: () => createRollupHooks,
205
211
  createRollupValidator: () => createRollupValidator,
206
212
  createSelectValidator: () => createSelectValidator,
207
213
  createSingleRelationValidator: () => createSingleRelationValidator,
@@ -229,6 +235,8 @@ __export(index_exports, {
229
235
  flow: () => flow,
230
236
  formatAttributeValue: () => formatAttributeValue,
231
237
  formatFormulaResult: () => formatFormulaResult,
238
+ formatRecord: () => formatRecord,
239
+ formatRecords: () => formatRecords,
232
240
  formula: () => formula,
233
241
  formulaConfigSchema: () => formulaConfigSchema,
234
242
  generateId: () => generateId,
@@ -273,7 +281,6 @@ __export(index_exports, {
273
281
  phoneConfigSchema: () => phoneConfigSchema,
274
282
  rating: () => rating,
275
283
  ratingConfigSchema: () => ratingConfigSchema,
276
- registerAllRollupHooks: () => registerAllRollupHooks,
277
284
  registry: () => registry,
278
285
  relation: () => relation,
279
286
  relationConfigSchema: () => relationConfigSchema,
@@ -484,6 +491,9 @@ var NoopGeocodingAdapter = class {
484
491
  }
485
492
  };
486
493
 
494
+ // src/types/inference.ts
495
+ var SYSTEM_FIELD_NAMES = ["id", "createdAt", "updatedAt"];
496
+
487
497
  // src/types/views.ts
488
498
  function isFormTab(tab) {
489
499
  return tab.type === "form";
@@ -891,6 +901,7 @@ var VALID_ICONS = new Set(import_constants2.ICONS);
891
901
  var ATTRIBUTE_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
892
902
  var MAX_NAME_LENGTH = 63;
893
903
  var MAX_LABEL_LENGTH = 128;
904
+ var RESERVED_ATTRIBUTE_NAMES = ["id", "createdAt", "updatedAt", "metadata"];
894
905
  function validateAttributeName(name) {
895
906
  if (!name || name.length === 0) {
896
907
  throw new Error("[AttributeBuilder] Attribute name cannot be empty");
@@ -905,6 +916,12 @@ function validateAttributeName(name) {
905
916
  "[AttributeBuilder] Invalid attribute name format.\nName must be a valid variable identifier:\n \u2705 Valid: 'firstName', 'first_name', 'FirstName', 'FIRST_NAME'\n \u274C Invalid: 'first-name', 'first name', '123name', 'first.name'"
906
917
  );
907
918
  }
919
+ if (RESERVED_ATTRIBUTE_NAMES.includes(name)) {
920
+ throw new Error(
921
+ `[AttributeBuilder] "${name}" is a reserved name and cannot be used as an attribute name.
922
+ Reserved names: ${RESERVED_ATTRIBUTE_NAMES.join(", ")}`
923
+ );
924
+ }
908
925
  }
909
926
  function validateLabel(label) {
910
927
  if (!label || label.length === 0) {
@@ -1628,6 +1645,14 @@ var RollupAttributeBuilder = class extends BaseAttributeBuilder {
1628
1645
  this.attr.decimals = value;
1629
1646
  return this;
1630
1647
  }
1648
+ /**
1649
+ * Display the rollup value using the target attribute's type formatter
1650
+ * instead of the default rollup-specific formatting
1651
+ */
1652
+ showAsOriginal() {
1653
+ this.attr.showAsOriginal = true;
1654
+ return this;
1655
+ }
1631
1656
  /**
1632
1657
  * Set multi-level path for traversing nested relations (Phase 4+)
1633
1658
  * @param path - Dot notation path (e.g., "orders.items")
@@ -2965,10 +2990,12 @@ var rollupConfigSchema = baseConfigSchema.extend({
2965
2990
  "countValues",
2966
2991
  "countEmpty",
2967
2992
  "percentEmpty",
2968
- "percentFilled"
2993
+ "percentFilled",
2994
+ "collect"
2969
2995
  ]),
2970
2996
  decimals: import_zod4.z.number().int().min(0).max(10).optional(),
2971
- materialize: import_zod4.z.boolean().optional()
2997
+ showAsOriginal: import_zod4.z.boolean().optional(),
2998
+ targetAttributeType: import_zod4.z.string().optional()
2972
2999
  });
2973
3000
  var attributeConfigSchemas = {
2974
3001
  text: textConfigSchema,
@@ -3049,7 +3076,7 @@ function createCheckboxValidator(_attr) {
3049
3076
  return import_zod4.z.boolean();
3050
3077
  }
3051
3078
  function createDateValidator(attr) {
3052
- return import_zod4.z.string().datetime({ message: `${attr.label} must be a valid ISO date` });
3079
+ return import_zod4.z.coerce.date({ message: `${attr.label} must be a valid ISO date` });
3053
3080
  }
3054
3081
  function createPhoneValidator(_attr) {
3055
3082
  return import_zod4.z.object({
@@ -3155,7 +3182,7 @@ function createRelationValidator(attr) {
3155
3182
  return createSingleRelationValidator(attr);
3156
3183
  }
3157
3184
  function createRatingValidator(attr) {
3158
- let schema = import_zod4.z.number().int().min(0);
3185
+ let schema = import_zod4.z.number().min(0);
3159
3186
  if (attr.max !== void 0) {
3160
3187
  schema = schema.max(attr.max, `${attr.label} must be at most ${attr.max}`);
3161
3188
  }
@@ -3328,6 +3355,477 @@ function computeRecordStatus(objectDef, data) {
3328
3355
  return isRecordComplete(objectDef, data) ? "complete" : "draft";
3329
3356
  }
3330
3357
 
3358
+ // src/runtime/client/types.ts
3359
+ function formatRecord(record) {
3360
+ return {
3361
+ id: record.id,
3362
+ createdAt: record.createdAt,
3363
+ updatedAt: record.updatedAt,
3364
+ metadata: record.metadata ?? {},
3365
+ ...record.values
3366
+ };
3367
+ }
3368
+ function formatRecords(records) {
3369
+ return records.map((r) => formatRecord(r));
3370
+ }
3371
+ function createDefaultState(objectName) {
3372
+ return {
3373
+ objectName,
3374
+ filters: [],
3375
+ combinator: "and",
3376
+ sorts: [],
3377
+ raw: false,
3378
+ includeDeleted: false
3379
+ };
3380
+ }
3381
+ var SHORTCUT_TO_FILTER_OPERATOR = {
3382
+ eq: "is",
3383
+ neq: "is_not",
3384
+ gt: "gt",
3385
+ gte: "gte",
3386
+ lt: "lt",
3387
+ lte: "lte",
3388
+ contains: "contains",
3389
+ notContains: "not_contains",
3390
+ startsWith: "starts_with",
3391
+ endsWith: "ends_with",
3392
+ isEmpty: "is_empty",
3393
+ isNotEmpty: "is_not_empty"
3394
+ };
3395
+ var QueryNoResultError = class extends Error {
3396
+ constructor(objectName, filters) {
3397
+ const filterInfo = filters?.length ? ` with filters: ${JSON.stringify(filters)}` : "";
3398
+ super(`No record found in "${objectName}"${filterInfo}`);
3399
+ this.name = "QueryNoResultError";
3400
+ this.objectName = objectName;
3401
+ this.filters = filters;
3402
+ }
3403
+ };
3404
+ var QueryMultipleResultsError = class extends Error {
3405
+ constructor(objectName, count) {
3406
+ super(
3407
+ `Expected single record in "${objectName}", but found ${count}. Use first() or add more filters.`
3408
+ );
3409
+ this.name = "QueryMultipleResultsError";
3410
+ this.objectName = objectName;
3411
+ this.count = count;
3412
+ }
3413
+ };
3414
+
3415
+ // src/runtime/client/query-builder.ts
3416
+ var QueryBuilder = class _QueryBuilder {
3417
+ constructor(recordService, adapter, objectName, state) {
3418
+ this.objectId = null;
3419
+ this.recordService = recordService;
3420
+ this.adapter = adapter;
3421
+ this.state = {
3422
+ ...createDefaultState(objectName),
3423
+ ...state
3424
+ };
3425
+ }
3426
+ /**
3427
+ * Clone the builder with new state (immutability pattern)
3428
+ */
3429
+ clone(updates) {
3430
+ return new _QueryBuilder(this.recordService, this.adapter, this.state.objectName, {
3431
+ ...this.state,
3432
+ ...updates
3433
+ });
3434
+ }
3435
+ /**
3436
+ * Resolve object name to object ID (cached)
3437
+ */
3438
+ async resolveObjectId() {
3439
+ if (this.objectId) return this.objectId;
3440
+ const tenantId = this.recordService.tenantId;
3441
+ const dbObject = await this.adapter.objects.findByName(tenantId, this.state.objectName);
3442
+ if (!dbObject) {
3443
+ throw new Error(`Object "${this.state.objectName}" not found`);
3444
+ }
3445
+ this.objectId = dbObject.id;
3446
+ return dbObject.id;
3447
+ }
3448
+ // ============================================================================
3449
+ // FILTER METHODS
3450
+ // ============================================================================
3451
+ /**
3452
+ * Add a filter rule with explicit operator
3453
+ *
3454
+ * @example
3455
+ * ```typescript
3456
+ * qb.where('status', 'is', 'active')
3457
+ * .where('price', 'gte', 100)
3458
+ * ```
3459
+ */
3460
+ where(attribute, operator, value) {
3461
+ const rule = { attribute, operator, value };
3462
+ return this.clone({
3463
+ filters: [...this.state.filters, rule]
3464
+ });
3465
+ }
3466
+ /**
3467
+ * Set filter combinator (default: 'and')
3468
+ */
3469
+ or() {
3470
+ return this.clone({ combinator: "or" });
3471
+ }
3472
+ // ============================================================================
3473
+ // SHORTCUT OPERATORS
3474
+ // ============================================================================
3475
+ /**
3476
+ * Equal (is)
3477
+ */
3478
+ eq(attribute, value) {
3479
+ return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.eq, value);
3480
+ }
3481
+ /**
3482
+ * Not equal (is_not)
3483
+ */
3484
+ neq(attribute, value) {
3485
+ return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.neq, value);
3486
+ }
3487
+ /**
3488
+ * Greater than
3489
+ */
3490
+ gt(attribute, value) {
3491
+ return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.gt, value);
3492
+ }
3493
+ /**
3494
+ * Greater than or equal
3495
+ */
3496
+ gte(attribute, value) {
3497
+ return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.gte, value);
3498
+ }
3499
+ /**
3500
+ * Less than
3501
+ */
3502
+ lt(attribute, value) {
3503
+ return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.lt, value);
3504
+ }
3505
+ /**
3506
+ * Less than or equal
3507
+ */
3508
+ lte(attribute, value) {
3509
+ return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.lte, value);
3510
+ }
3511
+ /**
3512
+ * Contains (text/relation)
3513
+ */
3514
+ contains(attribute, value) {
3515
+ return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.contains, value);
3516
+ }
3517
+ /**
3518
+ * Not contains
3519
+ */
3520
+ notContains(attribute, value) {
3521
+ return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.notContains, value);
3522
+ }
3523
+ /**
3524
+ * Starts with (text)
3525
+ */
3526
+ startsWith(attribute, value) {
3527
+ return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.startsWith, value);
3528
+ }
3529
+ /**
3530
+ * Ends with (text)
3531
+ */
3532
+ endsWith(attribute, value) {
3533
+ return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.endsWith, value);
3534
+ }
3535
+ /**
3536
+ * Is empty
3537
+ */
3538
+ isEmpty(attribute) {
3539
+ return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.isEmpty, null);
3540
+ }
3541
+ /**
3542
+ * Is not empty
3543
+ */
3544
+ isNotEmpty(attribute) {
3545
+ return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.isNotEmpty, null);
3546
+ }
3547
+ /**
3548
+ * In list (any_of)
3549
+ */
3550
+ in(attribute, values) {
3551
+ return this.where(attribute, "any_of", values);
3552
+ }
3553
+ /**
3554
+ * Not in list (none_of)
3555
+ */
3556
+ notIn(attribute, values) {
3557
+ return this.where(attribute, "none_of", values);
3558
+ }
3559
+ // ============================================================================
3560
+ // SORT & PAGINATION
3561
+ // ============================================================================
3562
+ /**
3563
+ * Add sort rule
3564
+ */
3565
+ orderBy(attribute, direction = "asc") {
3566
+ return this.clone({
3567
+ sorts: [...this.state.sorts, { attribute, direction }]
3568
+ });
3569
+ }
3570
+ /**
3571
+ * Set limit
3572
+ */
3573
+ limit(count) {
3574
+ return this.clone({ limit: count });
3575
+ }
3576
+ /**
3577
+ * Set offset
3578
+ */
3579
+ offset(count) {
3580
+ return this.clone({ offset: count });
3581
+ }
3582
+ /**
3583
+ * Include soft-deleted records
3584
+ */
3585
+ withDeleted() {
3586
+ return this.clone({ includeDeleted: true });
3587
+ }
3588
+ // ============================================================================
3589
+ // GROUP BY (KANBAN)
3590
+ // ============================================================================
3591
+ /**
3592
+ * Group results by an attribute (for Kanban views).
3593
+ * Only status and select attributes are supported.
3594
+ *
3595
+ * @example
3596
+ * ```typescript
3597
+ * const grouped = await qb
3598
+ * .groupBy('status')
3599
+ * .fetchGrouped();
3600
+ * // → { groups: { 'pending': [...], 'active': [...] }, total: 50 }
3601
+ * ```
3602
+ */
3603
+ groupBy(attribute) {
3604
+ return this.clone({ groupBy: attribute });
3605
+ }
3606
+ // ============================================================================
3607
+ // RAW MODE
3608
+ // ============================================================================
3609
+ /**
3610
+ * Return raw ObjectRecord instead of formatted record.
3611
+ * Includes all metadata (objectId, label, completionStatus, etc.)
3612
+ */
3613
+ raw() {
3614
+ return this.clone({ raw: true });
3615
+ }
3616
+ // ============================================================================
3617
+ // TERMINATION METHODS (READ)
3618
+ // ============================================================================
3619
+ /**
3620
+ * Fetch records with current filters
3621
+ * Returns formatted records by default, raw ObjectRecords if raw() was called
3622
+ */
3623
+ async fetch() {
3624
+ const objectId = await this.resolveObjectId();
3625
+ const filters = this.state.filters.length > 0 ? { combinator: this.state.combinator, rules: this.state.filters } : void 0;
3626
+ const result = await this.recordService.listRecords(objectId, {
3627
+ filters,
3628
+ sorts: this.state.sorts.length > 0 ? this.state.sorts : void 0,
3629
+ limit: this.state.limit,
3630
+ offset: this.state.offset,
3631
+ includeDeleted: this.state.includeDeleted
3632
+ });
3633
+ if (this.state.raw) {
3634
+ return result;
3635
+ }
3636
+ return {
3637
+ records: formatRecords(result.records),
3638
+ total: result.total
3639
+ };
3640
+ }
3641
+ /**
3642
+ * Get a single record (throws if 0 or >1 results)
3643
+ */
3644
+ async single() {
3645
+ const result = await this.limit(2).fetch();
3646
+ if (result.records.length === 0) {
3647
+ throw new QueryNoResultError(this.state.objectName, this.state.filters);
3648
+ }
3649
+ if (result.records.length > 1) {
3650
+ throw new QueryMultipleResultsError(this.state.objectName, result.total);
3651
+ }
3652
+ return result.records[0];
3653
+ }
3654
+ /**
3655
+ * Get first record or null
3656
+ */
3657
+ async first() {
3658
+ const result = await this.limit(1).fetch();
3659
+ return result.records[0] ?? null;
3660
+ }
3661
+ /**
3662
+ * Get record by ID
3663
+ */
3664
+ async findById(id) {
3665
+ const record = await this.recordService.getRecord(id);
3666
+ if (!record) return null;
3667
+ if (this.state.raw) {
3668
+ return record;
3669
+ }
3670
+ return formatRecord(record);
3671
+ }
3672
+ /**
3673
+ * Count records matching filters
3674
+ */
3675
+ async count() {
3676
+ const result = await this.limit(0).fetch();
3677
+ return result.total;
3678
+ }
3679
+ /**
3680
+ * Fetch records grouped by the groupBy attribute (for Kanban views).
3681
+ * Requires groupBy() to be called first.
3682
+ *
3683
+ * @example
3684
+ * ```typescript
3685
+ * const result = await schema.from('tasks')
3686
+ * .groupBy('status')
3687
+ * .fetchGrouped();
3688
+ * // → { groups: { 'todo': [...], 'in_progress': [...], 'done': [...] }, total: 50 }
3689
+ * ```
3690
+ */
3691
+ async fetchGrouped() {
3692
+ if (!this.state.groupBy) {
3693
+ throw new Error("groupBy() must be called before fetchGrouped()");
3694
+ }
3695
+ const result = await this.fetch();
3696
+ const groupByAttr = this.state.groupBy;
3697
+ const groups = {};
3698
+ for (const record of result.records) {
3699
+ const value = record[groupByAttr];
3700
+ const key = value != null ? String(value) : "__null__";
3701
+ if (!groups[key]) {
3702
+ groups[key] = [];
3703
+ }
3704
+ groups[key].push(record);
3705
+ }
3706
+ return {
3707
+ groups,
3708
+ total: result.total
3709
+ };
3710
+ }
3711
+ // ============================================================================
3712
+ // TERMINATION METHODS (WRITE)
3713
+ // ============================================================================
3714
+ /**
3715
+ * Create a new record
3716
+ * System fields (id, createdAt, updatedAt) are managed automatically.
3717
+ *
3718
+ * @example
3719
+ * ```typescript
3720
+ * const product = await schema.from('products')
3721
+ * .insert({ name: 'iPhone', price: 999 });
3722
+ *
3723
+ * // Allow draft (missing required fields)
3724
+ * const draft = await schema.from('products')
3725
+ * .insert({ name: 'Draft' }, { allowDraft: true });
3726
+ *
3727
+ * // With custom metadata
3728
+ * const withMeta = await schema.from('products')
3729
+ * .insert({ name: 'iPhone' }, { metadata: { externalId: 'ext-123' } });
3730
+ * ```
3731
+ */
3732
+ async insert(data, options) {
3733
+ const objectId = await this.resolveObjectId();
3734
+ const record = await this.recordService.createRecord(
3735
+ objectId,
3736
+ data,
3737
+ {
3738
+ allowDraft: options?.allowDraft,
3739
+ validate: options?.validate,
3740
+ metadata: options?.metadata
3741
+ }
3742
+ );
3743
+ if (this.state.raw) {
3744
+ return record;
3745
+ }
3746
+ return formatRecord(record);
3747
+ }
3748
+ /**
3749
+ * Update records matching filters (expects exactly 1 record by default)
3750
+ * Use with eq('id', ...) for single record update.
3751
+ * System fields (id, createdAt, updatedAt) are managed automatically.
3752
+ *
3753
+ * @example
3754
+ * ```typescript
3755
+ * const updated = await schema.from('products')
3756
+ * .eq('id', 'rec-123')
3757
+ * .update({ price: 899 });
3758
+ *
3759
+ * // Update with metadata (replaces existing metadata)
3760
+ * const withMeta = await schema.from('products')
3761
+ * .eq('id', 'rec-123')
3762
+ * .update({ price: 899 }, { metadata: { synced: true } });
3763
+ * ```
3764
+ */
3765
+ async update(data, options) {
3766
+ const existing = await this.single();
3767
+ const recordId = this.state.raw ? existing.id : existing.id;
3768
+ const record = await this.recordService.updateRecord(
3769
+ recordId,
3770
+ data,
3771
+ {
3772
+ partial: true,
3773
+ metadata: options?.metadata
3774
+ }
3775
+ );
3776
+ if (this.state.raw) {
3777
+ return record;
3778
+ }
3779
+ return formatRecord(record);
3780
+ }
3781
+ /**
3782
+ * Delete record matching filters (expects exactly 1 record)
3783
+ *
3784
+ * @example
3785
+ * ```typescript
3786
+ * await schema.from('products')
3787
+ * .eq('id', 'rec-123')
3788
+ * .delete();
3789
+ * ```
3790
+ */
3791
+ async delete() {
3792
+ const existing = await this.single();
3793
+ const recordId = this.state.raw ? existing.id : existing.id;
3794
+ await this.recordService.deleteRecord(recordId);
3795
+ }
3796
+ /**
3797
+ * Upsert (update if exists by ID, insert otherwise)
3798
+ * Provide 'id' for lookup. System fields (createdAt, updatedAt) are managed automatically.
3799
+ *
3800
+ * @example
3801
+ * ```typescript
3802
+ * // Insert if not exists, update if exists
3803
+ * const product = await schema.from('products')
3804
+ * .upsert({ id: 'rec-123', name: 'iPhone', price: 999 });
3805
+ *
3806
+ * // With metadata
3807
+ * const withMeta = await schema.from('products')
3808
+ * .upsert({ name: 'iPhone' }, { metadata: { source: 'import' } });
3809
+ * ```
3810
+ */
3811
+ async upsert(data, options) {
3812
+ const { id, ...rest } = data;
3813
+ const writeData = rest;
3814
+ if (id) {
3815
+ const existing = await this.findById(id);
3816
+ if (existing) {
3817
+ return this.eq("id", id).update(writeData, {
3818
+ metadata: options?.metadata
3819
+ });
3820
+ }
3821
+ }
3822
+ return this.insert(writeData, options);
3823
+ }
3824
+ };
3825
+ function createQueryBuilder(recordService, adapter, objectName) {
3826
+ return new QueryBuilder(recordService, adapter, objectName);
3827
+ }
3828
+
3331
3829
  // src/runtime/formula/evaluator.ts
3332
3830
  var import_expr_eval = require("expr-eval");
3333
3831
  function createFormulaParser() {
@@ -3712,260 +4210,6 @@ var NoopHookRegistry = class {
3712
4210
  }
3713
4211
  };
3714
4212
 
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
4213
  // src/runtime/template.ts
3970
4214
  var pipes = {
3971
4215
  /** Convert to uppercase */
@@ -4423,6 +4667,7 @@ function createMockObjectRecordsRepository(stores) {
4423
4667
  label: data.label,
4424
4668
  completionStatus: data.completionStatus,
4425
4669
  values: data.data,
4670
+ metadata: data.metadata ?? {},
4426
4671
  createdAt: /* @__PURE__ */ new Date(),
4427
4672
  updatedAt: /* @__PURE__ */ new Date()
4428
4673
  };
@@ -4435,11 +4680,13 @@ function createMockObjectRecordsRepository(stores) {
4435
4680
  if (!existing) {
4436
4681
  return Promise.reject(new Error(`ObjectRecord ${id} not found`));
4437
4682
  }
4438
- const { __completionStatus, __label, ...valueData } = data;
4683
+ const { __completionStatus, __label, __metadata, ...valueData } = data;
4439
4684
  const updated = {
4440
4685
  ...existing,
4441
4686
  label: __label ?? existing.label,
4442
4687
  completionStatus: __completionStatus ?? existing.completionStatus,
4688
+ // Metadata uses replace behavior (not merge)
4689
+ metadata: __metadata !== void 0 ? __metadata : existing.metadata,
4443
4690
  values: { ...existing.values, ...valueData },
4444
4691
  updatedAt: /* @__PURE__ */ new Date()
4445
4692
  };
@@ -6117,7 +6364,7 @@ var GlobalSearchService = class {
6117
6364
  };
6118
6365
 
6119
6366
  // src/runtime/services/object-schema.service.ts
6120
- var ObjectSchemaService = class {
6367
+ var _ObjectSchemaService = class _ObjectSchemaService {
6121
6368
  constructor(adapter, nativeRegistry, options) {
6122
6369
  this.adapter = adapter;
6123
6370
  this.nativeRegistry = nativeRegistry;
@@ -6540,6 +6787,14 @@ var ObjectSchemaService = class {
6540
6787
  "Invalid attribute name format.\nName must be a valid variable identifier:\n \u2705 Valid: 'firstName', 'first_name', 'FirstName', 'FIRST_NAME'\n \u274C Invalid: 'first-name', 'first name', '123name', 'first.name'"
6541
6788
  );
6542
6789
  }
6790
+ if (_ObjectSchemaService.RESERVED_ATTRIBUTE_NAMES.includes(
6791
+ name
6792
+ )) {
6793
+ throw new Error(
6794
+ `"${name}" is a reserved name and cannot be used as an attribute name.
6795
+ Reserved names: ${_ObjectSchemaService.RESERVED_ATTRIBUTE_NAMES.join(", ")}`
6796
+ );
6797
+ }
6543
6798
  }
6544
6799
  /**
6545
6800
  * Get complete object schema (system + custom attributes)
@@ -6786,6 +7041,17 @@ var ObjectSchemaService = class {
6786
7041
  return baseAttr;
6787
7042
  }
6788
7043
  };
7044
+ /**
7045
+ * Reserved attribute names that cannot be used.
7046
+ * These names conflict with system fields on ObjectRecord.
7047
+ */
7048
+ _ObjectSchemaService.RESERVED_ATTRIBUTE_NAMES = [
7049
+ "id",
7050
+ "createdAt",
7051
+ "updatedAt",
7052
+ "metadata"
7053
+ ];
7054
+ var ObjectSchemaService = _ObjectSchemaService;
6789
7055
 
6790
7056
  // src/runtime/services/permission.service.ts
6791
7057
  var PermissionService = class {
@@ -7492,6 +7758,328 @@ var RelationService = class {
7492
7758
  }
7493
7759
  };
7494
7760
 
7761
+ // src/runtime/services/rollup.service.ts
7762
+ var RollupService = class {
7763
+ constructor(adapter) {
7764
+ this.adapter = adapter;
7765
+ }
7766
+ /**
7767
+ * Calculate a rollup value for a record
7768
+ *
7769
+ * @param recordId - ID of the parent record
7770
+ * @param rollupAttr - Rollup attribute definition
7771
+ * @param schema - Schema of the parent object
7772
+ * @returns Computed rollup value
7773
+ *
7774
+ * @example
7775
+ * ```typescript
7776
+ * // Sum all order amounts for a company
7777
+ * const totalOrders = await rollupService.calculate(
7778
+ * "company-123",
7779
+ * {
7780
+ * type: "rollup",
7781
+ * name: "totalOrders",
7782
+ * relationAttribute: "orders",
7783
+ * targetAttribute: "amount",
7784
+ * function: "sum",
7785
+ * ...
7786
+ * },
7787
+ * companySchema
7788
+ * );
7789
+ * ```
7790
+ */
7791
+ async calculate(recordId, rollupAttr, schema) {
7792
+ const relationAttr = schema.attributes.find(
7793
+ (a) => a.type === "relation" && a.name === rollupAttr.relationAttribute
7794
+ );
7795
+ if (relationAttr) {
7796
+ return this.calculateForward(recordId, rollupAttr);
7797
+ }
7798
+ return this.calculateReverse(recordId, rollupAttr, schema);
7799
+ }
7800
+ /**
7801
+ * Forward pattern: this record has a relation attribute pointing to other records
7802
+ * Example: entreprise222 has relation "entreprises" → companies, rollup collects from companies
7803
+ */
7804
+ async calculateForward(recordId, rollupAttr) {
7805
+ const record = await this.adapter.objectRecords.findById(recordId);
7806
+ if (!record) {
7807
+ return { value: null, recordCount: 0 };
7808
+ }
7809
+ const relationValue = record.values[rollupAttr.relationAttribute];
7810
+ let relatedIds = [];
7811
+ if (typeof relationValue === "string" && relationValue.length > 0) {
7812
+ relatedIds = [relationValue];
7813
+ } else if (Array.isArray(relationValue)) {
7814
+ relatedIds = relationValue.filter((id) => typeof id === "string" && id.length > 0);
7815
+ }
7816
+ if (relatedIds.length === 0) {
7817
+ return { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 };
7818
+ }
7819
+ const relatedRecords = await this.adapter.objectRecords.findByIds(relatedIds);
7820
+ if (relatedRecords.length === 0) {
7821
+ return { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 };
7822
+ }
7823
+ return this.aggregateValues(relatedRecords, rollupAttr);
7824
+ }
7825
+ /**
7826
+ * Reverse pattern: other records have a relation pointing to this record
7827
+ * Example: Company has rollup on "orders", Order has relation "company" → companies
7828
+ */
7829
+ async calculateReverse(recordId, rollupAttr, schema) {
7830
+ const record = await this.adapter.objectRecords.findById(recordId);
7831
+ if (!record) {
7832
+ return { value: null, recordCount: 0 };
7833
+ }
7834
+ const parentObject = await this.adapter.objects.findById(record.objectId);
7835
+ if (!parentObject) {
7836
+ return { value: null, recordCount: 0 };
7837
+ }
7838
+ const tenantId = parentObject.tenantId;
7839
+ const sourceObjectName = rollupAttr.relationAttribute;
7840
+ const sourceObject = await this.adapter.objects.findByName(tenantId, sourceObjectName);
7841
+ if (!sourceObject) {
7842
+ return { value: null, recordCount: 0 };
7843
+ }
7844
+ const sourceAttributes = await this.adapter.attributes.findByObjectId(sourceObject.id);
7845
+ const reverseRelationAttr = sourceAttributes.find((attr) => {
7846
+ if (attr.type !== "relation") return false;
7847
+ const relationConfig = attr.config;
7848
+ return relationConfig?.targets?.some((t) => t.object === schema.name);
7849
+ });
7850
+ if (!reverseRelationAttr) {
7851
+ return { value: null, recordCount: 0 };
7852
+ }
7853
+ const relatedRecords = await this.adapter.objectRecords.findByRelation(
7854
+ tenantId,
7855
+ sourceObject.id,
7856
+ reverseRelationAttr.name,
7857
+ recordId
7858
+ );
7859
+ if (relatedRecords.length === 0) {
7860
+ return { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 };
7861
+ }
7862
+ return this.aggregateValues(relatedRecords, rollupAttr);
7863
+ }
7864
+ /**
7865
+ * Extract and aggregate values from related records
7866
+ */
7867
+ aggregateValues(relatedRecords, rollupAttr) {
7868
+ const values = relatedRecords.map((r) => r.values[rollupAttr.targetAttribute]).filter((v) => v !== void 0 && v !== null);
7869
+ const aggregated = this.aggregate(values, rollupAttr.function);
7870
+ const result = typeof aggregated === "number" && rollupAttr.decimals !== void 0 ? Number(aggregated.toFixed(rollupAttr.decimals)) : aggregated;
7871
+ return { value: result, recordCount: relatedRecords.length };
7872
+ }
7873
+ /**
7874
+ * Calculate rollup values for multiple records (batched)
7875
+ *
7876
+ * More efficient than calling calculate() for each record individually.
7877
+ */
7878
+ async calculateForMany(recordIds, rollupAttr, schema) {
7879
+ const results = /* @__PURE__ */ new Map();
7880
+ for (const id of recordIds) {
7881
+ results.set(id, { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 });
7882
+ }
7883
+ await Promise.all(
7884
+ recordIds.map(async (id) => {
7885
+ const result = await this.calculate(id, rollupAttr, schema);
7886
+ results.set(id, result);
7887
+ })
7888
+ );
7889
+ return results;
7890
+ }
7891
+ /**
7892
+ * Apply aggregation function to a set of values
7893
+ */
7894
+ aggregate(values, fn) {
7895
+ switch (fn) {
7896
+ case "sum":
7897
+ return this.sumNumbers(values);
7898
+ case "avg":
7899
+ return this.averageNumbers(values);
7900
+ case "min":
7901
+ return this.minNumbers(values);
7902
+ case "max":
7903
+ return this.maxNumbers(values);
7904
+ case "count":
7905
+ return values.length;
7906
+ case "countValues":
7907
+ return values.filter((v) => v != null && v !== "").length;
7908
+ case "countEmpty":
7909
+ return values.filter((v) => v == null || v === "").length;
7910
+ case "percentFilled": {
7911
+ if (values.length === 0) return 0;
7912
+ const filled = values.filter((v) => v != null && v !== "").length;
7913
+ return filled / values.length * 100;
7914
+ }
7915
+ case "percentEmpty": {
7916
+ if (values.length === 0) return 0;
7917
+ const empty = values.filter((v) => v == null || v === "").length;
7918
+ return empty / values.length * 100;
7919
+ }
7920
+ case "collect":
7921
+ return values;
7922
+ }
7923
+ }
7924
+ /**
7925
+ * Get the default empty value for a rollup function
7926
+ */
7927
+ getEmptyValue(fn) {
7928
+ switch (fn) {
7929
+ case "sum":
7930
+ case "count":
7931
+ case "countValues":
7932
+ case "countEmpty":
7933
+ case "percentEmpty":
7934
+ case "percentFilled":
7935
+ return 0;
7936
+ case "avg":
7937
+ case "min":
7938
+ case "max":
7939
+ return null;
7940
+ case "collect":
7941
+ return [];
7942
+ }
7943
+ }
7944
+ /**
7945
+ * Sum numeric values
7946
+ */
7947
+ sumNumbers(values) {
7948
+ return values.filter((v) => typeof v === "number" && !Number.isNaN(v)).reduce((sum, n) => sum + n, 0);
7949
+ }
7950
+ /**
7951
+ * Average numeric values
7952
+ */
7953
+ averageNumbers(values) {
7954
+ const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
7955
+ if (nums.length === 0) return null;
7956
+ return nums.reduce((sum, n) => sum + n, 0) / nums.length;
7957
+ }
7958
+ /**
7959
+ * Minimum numeric value
7960
+ */
7961
+ minNumbers(values) {
7962
+ const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
7963
+ if (nums.length === 0) return null;
7964
+ return Math.min(...nums);
7965
+ }
7966
+ /**
7967
+ * Maximum numeric value
7968
+ */
7969
+ maxNumbers(values) {
7970
+ const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
7971
+ if (nums.length === 0) return null;
7972
+ return Math.max(...nums);
7973
+ }
7974
+ /**
7975
+ * Recalculate all rollup attributes for a record and update it
7976
+ *
7977
+ * Called after related records change to keep rollups up-to-date.
7978
+ */
7979
+ async recalculateAndUpdate(record, schema) {
7980
+ const rollupAttrs = schema.attributes.filter((a) => a.type === "rollup");
7981
+ if (rollupAttrs.length === 0) {
7982
+ return record;
7983
+ }
7984
+ const updates = {};
7985
+ for (const attr of rollupAttrs) {
7986
+ const result = await this.calculate(record.id, attr, schema);
7987
+ updates[attr.name] = result.value;
7988
+ }
7989
+ return await this.adapter.objectRecords.update(record.id, updates);
7990
+ }
7991
+ /**
7992
+ * Find parent records that need rollup recalculation when a child record changes
7993
+ *
7994
+ * Used by hooks to determine which parent records to recalculate after
7995
+ * a child record is created, updated, or deleted.
7996
+ *
7997
+ * @param changedRecord - The record that was modified
7998
+ * @param changedSchema - Schema of the changed record's object
7999
+ * @returns Array of parent record IDs that need recalculation
8000
+ */
8001
+ async findAffectedParentRecords(changedRecord, changedSchema) {
8002
+ const affectedIds = [];
8003
+ const relationAttrs = changedSchema.attributes.filter(
8004
+ (a) => a.type === "relation"
8005
+ );
8006
+ for (const attr of relationAttrs) {
8007
+ const relatedId = changedRecord.values[attr.name];
8008
+ if (typeof relatedId === "string" && relatedId.length > 0) {
8009
+ affectedIds.push(relatedId);
8010
+ } else if (Array.isArray(relatedId)) {
8011
+ affectedIds.push(...relatedId.filter((id) => typeof id === "string"));
8012
+ }
8013
+ }
8014
+ return [...new Set(affectedIds)];
8015
+ }
8016
+ /**
8017
+ * Find records that have forward rollups pointing to the modified record.
8018
+ *
8019
+ * Forward rollups are rollups where the record has a relation attribute
8020
+ * pointing to another object, and the rollup aggregates values from that target.
8021
+ * When the target record changes, we need to recalculate these rollups.
8022
+ *
8023
+ * Example: Order has relation "company" → Company, and rollup "capitalSocial"
8024
+ * aggregating from the Company. When Company.capitalSocial changes,
8025
+ * all Orders pointing to that Company need their rollup recalculated.
8026
+ *
8027
+ * @param changedRecord - The record that was modified
8028
+ * @param changedSchema - Schema of the changed record's object
8029
+ * @returns Array of records that need their forward rollups recalculated
8030
+ */
8031
+ async findRecordsWithForwardRollup(changedRecord, changedSchema) {
8032
+ const affectedRecords = [];
8033
+ const recordObject = await this.adapter.objects.findById(changedRecord.objectId);
8034
+ if (!recordObject) {
8035
+ return affectedRecords;
8036
+ }
8037
+ const tenantId = recordObject.tenantId;
8038
+ const allObjects = await this.adapter.objects.list(tenantId);
8039
+ for (const obj of allObjects) {
8040
+ if (obj.id === changedRecord.objectId) {
8041
+ continue;
8042
+ }
8043
+ const attributes = await this.adapter.attributes.findByObjectId(obj.id);
8044
+ const rollupAttrs = attributes.filter((a) => a.type === "rollup");
8045
+ if (rollupAttrs.length === 0) {
8046
+ continue;
8047
+ }
8048
+ for (const rollupDbAttr of rollupAttrs) {
8049
+ const rollupConfig = rollupDbAttr.config;
8050
+ if (!rollupConfig?.relationAttribute) {
8051
+ continue;
8052
+ }
8053
+ const relationAttr = attributes.find(
8054
+ (a) => a.type === "relation" && a.name === rollupConfig.relationAttribute
8055
+ );
8056
+ if (!relationAttr) {
8057
+ continue;
8058
+ }
8059
+ const relationConfig = relationAttr.config;
8060
+ const targetsChangedObject = relationConfig?.targets?.some(
8061
+ (t) => t.object === changedSchema.name
8062
+ );
8063
+ if (!targetsChangedObject) {
8064
+ continue;
8065
+ }
8066
+ const recordsPointingToChanged = await this.adapter.objectRecords.findByRelation(
8067
+ tenantId,
8068
+ obj.id,
8069
+ relationAttr.name,
8070
+ changedRecord.id
8071
+ );
8072
+ affectedRecords.push(...recordsPointingToChanged);
8073
+ }
8074
+ }
8075
+ const uniqueRecords = /* @__PURE__ */ new Map();
8076
+ for (const record of affectedRecords) {
8077
+ uniqueRecords.set(record.id, record);
8078
+ }
8079
+ return Array.from(uniqueRecords.values());
8080
+ }
8081
+ };
8082
+
7495
8083
  // src/runtime/services/user.service.ts
7496
8084
  var UserService = class {
7497
8085
  constructor(adapter, tenantId) {
@@ -7614,9 +8202,14 @@ var RecordService = class {
7614
8202
  constructor(adapter, tenantId, options) {
7615
8203
  this.adapter = adapter;
7616
8204
  this.tenantId = tenantId;
7617
- this.schemaService = new ObjectSchemaService(adapter, registry);
8205
+ this.schemaService = new ObjectSchemaService(adapter, registry, {
8206
+ userId: options?.userId,
8207
+ userEmail: options?.userEmail,
8208
+ auditService: options?.auditService
8209
+ });
7618
8210
  this.relationService = new RelationService(adapter, registry);
7619
8211
  this.userService = new UserService(adapter, tenantId);
8212
+ this.rollupService = new RollupService(adapter);
7620
8213
  this.hookRegistry = options?.hookRegistry ?? new NoopHookRegistry();
7621
8214
  this.permissionService = options?.permissionService;
7622
8215
  this.auditService = options?.auditService ?? (adapter.audit ? new AuditService(adapter, tenantId) : void 0);
@@ -7768,7 +8361,8 @@ var RecordService = class {
7768
8361
  objectId,
7769
8362
  data,
7770
8363
  label,
7771
- completionStatus
8364
+ completionStatus,
8365
+ metadata: options?.metadata
7772
8366
  });
7773
8367
  if (!options?.skipHooks) {
7774
8368
  const afterCtx = {
@@ -7778,6 +8372,7 @@ var RecordService = class {
7778
8372
  };
7779
8373
  await this.hookRegistry.execute("afterCreate", schema.name, afterCtx);
7780
8374
  }
8375
+ await this.recalculateParentRollups(record, schema);
7781
8376
  if (this.auditService && this.userId) {
7782
8377
  await this.auditService.logRecordAction({
7783
8378
  action: "record.created",
@@ -7881,11 +8476,15 @@ var RecordService = class {
7881
8476
  }
7882
8477
  const completionStatus = computeRecordStatus(schema, mergedData);
7883
8478
  const label = await this.computeLabel(schema, mergedData);
7884
- const updated = await this.adapter.objectRecords.update(recordId, {
8479
+ const updatePayload = {
7885
8480
  ...data,
7886
8481
  __completionStatus: completionStatus,
7887
8482
  __label: label
7888
- });
8483
+ };
8484
+ if (options?.metadata !== void 0) {
8485
+ updatePayload.__metadata = options.metadata;
8486
+ }
8487
+ const updated = await this.adapter.objectRecords.update(recordId, updatePayload);
7889
8488
  if (!options?.skipHooks) {
7890
8489
  const afterCtx = {
7891
8490
  ...hookCtx,
@@ -7893,6 +8492,7 @@ var RecordService = class {
7893
8492
  };
7894
8493
  await this.hookRegistry.execute("afterUpdate", schema.name, afterCtx);
7895
8494
  }
8495
+ await this.recalculateParentRollups(updated, schema);
7896
8496
  if (this.auditService && this.userId && changedAttributes.length > 0) {
7897
8497
  const changes = changedAttributes.map((attr) => ({
7898
8498
  field: attr,
@@ -8020,6 +8620,7 @@ var RecordService = class {
8020
8620
  if (!options?.skipHooks) {
8021
8621
  await this.hookRegistry.execute("afterDelete", schema.name, hookCtx);
8022
8622
  }
8623
+ await this.recalculateParentRollups(record, schema);
8023
8624
  if (this.auditService && this.userId) {
8024
8625
  await this.auditService.logRecordAction({
8025
8626
  action: "record.deleted",
@@ -8149,6 +8750,47 @@ var RecordService = class {
8149
8750
  }
8150
8751
  return records.map((record) => this.enrichWithFormulas(record, schema));
8151
8752
  }
8753
+ /**
8754
+ * Recalculate rollups after a record changes
8755
+ *
8756
+ * This handles three cases:
8757
+ * 1. The record itself has rollups (e.g., aggregating from related records it points to)
8758
+ * 2. Parent records have rollups that aggregate from this record (reverse pattern)
8759
+ * 3. Records that have forward rollups pointing to this record (forward pattern)
8760
+ *
8761
+ * @param record - The record that was modified
8762
+ * @param schema - Schema of the record's object
8763
+ * @internal
8764
+ */
8765
+ async recalculateParentRollups(record, schema) {
8766
+ const ownRollupAttrs = schema.attributes.filter(
8767
+ (a) => a.type === "rollup"
8768
+ );
8769
+ if (ownRollupAttrs.length > 0) {
8770
+ await this.rollupService.recalculateAndUpdate(record, schema);
8771
+ }
8772
+ const affectedParentIds = await this.rollupService.findAffectedParentRecords(record, schema);
8773
+ if (affectedParentIds.length > 0) {
8774
+ const parentRecords = await this.adapter.objectRecords.findByIds(affectedParentIds);
8775
+ for (const parentRecord of parentRecords) {
8776
+ const parentSchema = await this.schemaService.getObjectSchema(parentRecord.objectId);
8777
+ const rollupAttrs = parentSchema.attributes.filter(
8778
+ (a) => a.type === "rollup"
8779
+ );
8780
+ if (rollupAttrs.length > 0) {
8781
+ await this.rollupService.recalculateAndUpdate(parentRecord, parentSchema);
8782
+ }
8783
+ }
8784
+ }
8785
+ const affectedForwardRecords = await this.rollupService.findRecordsWithForwardRollup(
8786
+ record,
8787
+ schema
8788
+ );
8789
+ for (const forwardRecord of affectedForwardRecords) {
8790
+ const forwardSchema = await this.schemaService.getObjectSchema(forwardRecord.objectId);
8791
+ await this.rollupService.recalculateAndUpdate(forwardRecord, forwardSchema);
8792
+ }
8793
+ }
8152
8794
  /**
8153
8795
  * Permanently delete a record (hard delete)
8154
8796
  *
@@ -8422,7 +9064,7 @@ var RollupScheduler = class {
8422
9064
  clearTimeout(existing.timeout);
8423
9065
  }
8424
9066
  const timeout = setTimeout(async () => {
8425
- await this.executeRecalculation(parentId, parentObjectId, tenantId);
9067
+ await this.executeRecalculation(parentId, parentObjectId);
8426
9068
  this.pending.delete(key);
8427
9069
  }, this.debounceMs);
8428
9070
  this.pending.set(key, { parentId, parentObjectId, tenantId, timeout });
@@ -8449,19 +9091,19 @@ var RollupScheduler = class {
8449
9091
  const schema = await this.getSchemaById(record.objectId);
8450
9092
  const tenantId = tenantMap.get(record.id);
8451
9093
  if (schema && tenantId) {
8452
- await this.rollupService.recalculateAndUpdate(record, schema, tenantId);
9094
+ await this.rollupService.recalculateAndUpdate(record, schema);
8453
9095
  }
8454
9096
  }
8455
9097
  }
8456
9098
  /**
8457
9099
  * Execute a single recalculation
8458
9100
  */
8459
- async executeRecalculation(parentId, parentObjectId, tenantId) {
9101
+ async executeRecalculation(parentId, parentObjectId) {
8460
9102
  const record = await this.adapter.objectRecords.findById(parentId);
8461
9103
  if (!record) return;
8462
9104
  const schema = await this.getSchemaById(parentObjectId);
8463
9105
  if (!schema) return;
8464
- await this.rollupService.recalculateAndUpdate(record, schema, tenantId);
9106
+ await this.rollupService.recalculateAndUpdate(record, schema);
8465
9107
  }
8466
9108
  /**
8467
9109
  * Get number of pending recalculations
@@ -9275,6 +9917,9 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
9275
9917
  PermissionService,
9276
9918
  ProtectedResourceError,
9277
9919
  ProtectedRoleError,
9920
+ QueryBuilder,
9921
+ QueryMultipleResultsError,
9922
+ QueryNoResultError,
9278
9923
  RecordNotFoundError,
9279
9924
  RecordReferencedError,
9280
9925
  RecordService,
@@ -9283,6 +9928,8 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
9283
9928
  RoleNotFoundError,
9284
9929
  RollupScheduler,
9285
9930
  RollupService,
9931
+ SHORTCUT_TO_FILTER_OPERATOR,
9932
+ SYSTEM_FIELD_NAMES,
9286
9933
  SYSTEM_RESOURCES,
9287
9934
  SYSTEM_RESOURCE_LABELS,
9288
9935
  SchemaError,
@@ -9305,6 +9952,7 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
9305
9952
  createCheckboxValidator,
9306
9953
  createCurrencyValidator,
9307
9954
  createDateValidator,
9955
+ createDefaultState,
9308
9956
  createDraftValidator,
9309
9957
  createFileValidator,
9310
9958
  createFormulaValidator,
@@ -9315,9 +9963,9 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
9315
9963
  createNumberValidator,
9316
9964
  createObjectValidator,
9317
9965
  createPhoneValidator,
9966
+ createQueryBuilder,
9318
9967
  createRatingValidator,
9319
9968
  createRelationValidator,
9320
- createRollupHooks,
9321
9969
  createRollupValidator,
9322
9970
  createSelectValidator,
9323
9971
  createSingleRelationValidator,
@@ -9345,6 +9993,8 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
9345
9993
  flow,
9346
9994
  formatAttributeValue,
9347
9995
  formatFormulaResult,
9996
+ formatRecord,
9997
+ formatRecords,
9348
9998
  formula,
9349
9999
  formulaConfigSchema,
9350
10000
  generateId,
@@ -9389,7 +10039,6 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
9389
10039
  phoneConfigSchema,
9390
10040
  rating,
9391
10041
  ratingConfigSchema,
9392
- registerAllRollupHooks,
9393
10042
  registry,
9394
10043
  relation,
9395
10044
  relationConfigSchema,