@stndrds/schema 0.1.0-alpha.20 → 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.
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,6 +205,7 @@ __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
211
  createRollupValidator: () => createRollupValidator,
@@ -228,6 +235,8 @@ __export(index_exports, {
228
235
  flow: () => flow,
229
236
  formatAttributeValue: () => formatAttributeValue,
230
237
  formatFormulaResult: () => formatFormulaResult,
238
+ formatRecord: () => formatRecord,
239
+ formatRecords: () => formatRecords,
231
240
  formula: () => formula,
232
241
  formulaConfigSchema: () => formulaConfigSchema,
233
242
  generateId: () => generateId,
@@ -482,6 +491,9 @@ var NoopGeocodingAdapter = class {
482
491
  }
483
492
  };
484
493
 
494
+ // src/types/inference.ts
495
+ var SYSTEM_FIELD_NAMES = ["id", "createdAt", "updatedAt"];
496
+
485
497
  // src/types/views.ts
486
498
  function isFormTab(tab) {
487
499
  return tab.type === "form";
@@ -889,6 +901,7 @@ var VALID_ICONS = new Set(import_constants2.ICONS);
889
901
  var ATTRIBUTE_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
890
902
  var MAX_NAME_LENGTH = 63;
891
903
  var MAX_LABEL_LENGTH = 128;
904
+ var RESERVED_ATTRIBUTE_NAMES = ["id", "createdAt", "updatedAt", "metadata"];
892
905
  function validateAttributeName(name) {
893
906
  if (!name || name.length === 0) {
894
907
  throw new Error("[AttributeBuilder] Attribute name cannot be empty");
@@ -903,6 +916,12 @@ function validateAttributeName(name) {
903
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'"
904
917
  );
905
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
+ }
906
925
  }
907
926
  function validateLabel(label) {
908
927
  if (!label || label.length === 0) {
@@ -3057,7 +3076,7 @@ function createCheckboxValidator(_attr) {
3057
3076
  return import_zod4.z.boolean();
3058
3077
  }
3059
3078
  function createDateValidator(attr) {
3060
- 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` });
3061
3080
  }
3062
3081
  function createPhoneValidator(_attr) {
3063
3082
  return import_zod4.z.object({
@@ -3163,7 +3182,7 @@ function createRelationValidator(attr) {
3163
3182
  return createSingleRelationValidator(attr);
3164
3183
  }
3165
3184
  function createRatingValidator(attr) {
3166
- let schema = import_zod4.z.number().int().min(0);
3185
+ let schema = import_zod4.z.number().min(0);
3167
3186
  if (attr.max !== void 0) {
3168
3187
  schema = schema.max(attr.max, `${attr.label} must be at most ${attr.max}`);
3169
3188
  }
@@ -3336,6 +3355,477 @@ function computeRecordStatus(objectDef, data) {
3336
3355
  return isRecordComplete(objectDef, data) ? "complete" : "draft";
3337
3356
  }
3338
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
+
3339
3829
  // src/runtime/formula/evaluator.ts
3340
3830
  var import_expr_eval = require("expr-eval");
3341
3831
  function createFormulaParser() {
@@ -4177,6 +4667,7 @@ function createMockObjectRecordsRepository(stores) {
4177
4667
  label: data.label,
4178
4668
  completionStatus: data.completionStatus,
4179
4669
  values: data.data,
4670
+ metadata: data.metadata ?? {},
4180
4671
  createdAt: /* @__PURE__ */ new Date(),
4181
4672
  updatedAt: /* @__PURE__ */ new Date()
4182
4673
  };
@@ -4189,11 +4680,13 @@ function createMockObjectRecordsRepository(stores) {
4189
4680
  if (!existing) {
4190
4681
  return Promise.reject(new Error(`ObjectRecord ${id} not found`));
4191
4682
  }
4192
- const { __completionStatus, __label, ...valueData } = data;
4683
+ const { __completionStatus, __label, __metadata, ...valueData } = data;
4193
4684
  const updated = {
4194
4685
  ...existing,
4195
4686
  label: __label ?? existing.label,
4196
4687
  completionStatus: __completionStatus ?? existing.completionStatus,
4688
+ // Metadata uses replace behavior (not merge)
4689
+ metadata: __metadata !== void 0 ? __metadata : existing.metadata,
4197
4690
  values: { ...existing.values, ...valueData },
4198
4691
  updatedAt: /* @__PURE__ */ new Date()
4199
4692
  };
@@ -5871,7 +6364,7 @@ var GlobalSearchService = class {
5871
6364
  };
5872
6365
 
5873
6366
  // src/runtime/services/object-schema.service.ts
5874
- var ObjectSchemaService = class {
6367
+ var _ObjectSchemaService = class _ObjectSchemaService {
5875
6368
  constructor(adapter, nativeRegistry, options) {
5876
6369
  this.adapter = adapter;
5877
6370
  this.nativeRegistry = nativeRegistry;
@@ -6294,6 +6787,14 @@ var ObjectSchemaService = class {
6294
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'"
6295
6788
  );
6296
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
+ }
6297
6798
  }
6298
6799
  /**
6299
6800
  * Get complete object schema (system + custom attributes)
@@ -6540,6 +7041,17 @@ var ObjectSchemaService = class {
6540
7041
  return baseAttr;
6541
7042
  }
6542
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;
6543
7055
 
6544
7056
  // src/runtime/services/permission.service.ts
6545
7057
  var PermissionService = class {
@@ -7501,6 +8013,71 @@ var RollupService = class {
7501
8013
  }
7502
8014
  return [...new Set(affectedIds)];
7503
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
+ }
7504
8081
  };
7505
8082
 
7506
8083
  // src/runtime/services/user.service.ts
@@ -7625,7 +8202,11 @@ var RecordService = class {
7625
8202
  constructor(adapter, tenantId, options) {
7626
8203
  this.adapter = adapter;
7627
8204
  this.tenantId = tenantId;
7628
- 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
+ });
7629
8210
  this.relationService = new RelationService(adapter, registry);
7630
8211
  this.userService = new UserService(adapter, tenantId);
7631
8212
  this.rollupService = new RollupService(adapter);
@@ -7780,7 +8361,8 @@ var RecordService = class {
7780
8361
  objectId,
7781
8362
  data,
7782
8363
  label,
7783
- completionStatus
8364
+ completionStatus,
8365
+ metadata: options?.metadata
7784
8366
  });
7785
8367
  if (!options?.skipHooks) {
7786
8368
  const afterCtx = {
@@ -7894,11 +8476,15 @@ var RecordService = class {
7894
8476
  }
7895
8477
  const completionStatus = computeRecordStatus(schema, mergedData);
7896
8478
  const label = await this.computeLabel(schema, mergedData);
7897
- const updated = await this.adapter.objectRecords.update(recordId, {
8479
+ const updatePayload = {
7898
8480
  ...data,
7899
8481
  __completionStatus: completionStatus,
7900
8482
  __label: label
7901
- });
8483
+ };
8484
+ if (options?.metadata !== void 0) {
8485
+ updatePayload.__metadata = options.metadata;
8486
+ }
8487
+ const updated = await this.adapter.objectRecords.update(recordId, updatePayload);
7902
8488
  if (!options?.skipHooks) {
7903
8489
  const afterCtx = {
7904
8490
  ...hookCtx,
@@ -8167,9 +8753,10 @@ var RecordService = class {
8167
8753
  /**
8168
8754
  * Recalculate rollups after a record changes
8169
8755
  *
8170
- * This handles two cases:
8756
+ * This handles three cases:
8171
8757
  * 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
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)
8173
8760
  *
8174
8761
  * @param record - The record that was modified
8175
8762
  * @param schema - Schema of the record's object
@@ -8183,19 +8770,26 @@ var RecordService = class {
8183
8770
  await this.rollupService.recalculateAndUpdate(record, schema);
8184
8771
  }
8185
8772
  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);
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
+ }
8197
8783
  }
8198
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
+ }
8199
8793
  }
8200
8794
  /**
8201
8795
  * Permanently delete a record (hard delete)
@@ -9323,6 +9917,9 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
9323
9917
  PermissionService,
9324
9918
  ProtectedResourceError,
9325
9919
  ProtectedRoleError,
9920
+ QueryBuilder,
9921
+ QueryMultipleResultsError,
9922
+ QueryNoResultError,
9326
9923
  RecordNotFoundError,
9327
9924
  RecordReferencedError,
9328
9925
  RecordService,
@@ -9331,6 +9928,8 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
9331
9928
  RoleNotFoundError,
9332
9929
  RollupScheduler,
9333
9930
  RollupService,
9931
+ SHORTCUT_TO_FILTER_OPERATOR,
9932
+ SYSTEM_FIELD_NAMES,
9334
9933
  SYSTEM_RESOURCES,
9335
9934
  SYSTEM_RESOURCE_LABELS,
9336
9935
  SchemaError,
@@ -9353,6 +9952,7 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
9353
9952
  createCheckboxValidator,
9354
9953
  createCurrencyValidator,
9355
9954
  createDateValidator,
9955
+ createDefaultState,
9356
9956
  createDraftValidator,
9357
9957
  createFileValidator,
9358
9958
  createFormulaValidator,
@@ -9363,6 +9963,7 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
9363
9963
  createNumberValidator,
9364
9964
  createObjectValidator,
9365
9965
  createPhoneValidator,
9966
+ createQueryBuilder,
9366
9967
  createRatingValidator,
9367
9968
  createRelationValidator,
9368
9969
  createRollupValidator,
@@ -9392,6 +9993,8 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
9392
9993
  flow,
9393
9994
  formatAttributeValue,
9394
9995
  formatFormulaResult,
9996
+ formatRecord,
9997
+ formatRecords,
9395
9998
  formula,
9396
9999
  formulaConfigSchema,
9397
10000
  generateId,