@stndrds/schema 0.1.0-alpha.20 → 0.1.0-alpha.22

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 (42) hide show
  1. package/dist/chunk-3RVLIIUK.mjs +9257 -0
  2. package/dist/chunk-6FNFXEQY.mjs +9316 -0
  3. package/dist/chunk-6TJR4WDK.mjs +9262 -0
  4. package/dist/chunk-7GLSGSUP.mjs +9456 -0
  5. package/dist/chunk-B74KNXDP.mjs +9457 -0
  6. package/dist/chunk-F27AKBTL.mjs +9452 -0
  7. package/dist/chunk-HSNBVKQU.mjs +9316 -0
  8. package/dist/chunk-IKUIWNHZ.mjs +9460 -0
  9. package/dist/chunk-JJLODIBK.mjs +9389 -0
  10. package/dist/chunk-MAXEN3GU.mjs +9266 -0
  11. package/dist/chunk-ZD3BFM25.mjs +9316 -0
  12. package/dist/index.d.mts +3 -197
  13. package/dist/index.d.ts +3 -197
  14. package/dist/index.js +632 -21
  15. package/dist/index.mjs +21 -1
  16. package/dist/runtime-0gaIqXql.d.mts +7074 -0
  17. package/dist/runtime-0gaIqXql.d.ts +7074 -0
  18. package/dist/runtime-3u5doYAK.d.mts +6934 -0
  19. package/dist/runtime-3u5doYAK.d.ts +6934 -0
  20. package/dist/runtime-B6mOrMV4.d.mts +6985 -0
  21. package/dist/runtime-B6mOrMV4.d.ts +6985 -0
  22. package/dist/runtime-BNML-fPx.d.mts +6969 -0
  23. package/dist/runtime-BNML-fPx.d.ts +6969 -0
  24. package/dist/runtime-BPdsKDJo.d.mts +6969 -0
  25. package/dist/runtime-BPdsKDJo.d.ts +6969 -0
  26. package/dist/runtime-BZZRu7A1.d.mts +7083 -0
  27. package/dist/runtime-BZZRu7A1.d.ts +7083 -0
  28. package/dist/runtime-CmYcicVr.d.mts +7083 -0
  29. package/dist/runtime-CmYcicVr.d.ts +7083 -0
  30. package/dist/runtime-D1jyS1lS.d.mts +7008 -0
  31. package/dist/runtime-D1jyS1lS.d.ts +7008 -0
  32. package/dist/runtime-DjEUAeL4.d.mts +6713 -0
  33. package/dist/runtime-DjEUAeL4.d.ts +6713 -0
  34. package/dist/runtime-rTM9ddEe.d.mts +6934 -0
  35. package/dist/runtime-rTM9ddEe.d.ts +6934 -0
  36. package/dist/runtime-xLzAkAnp.d.mts +6969 -0
  37. package/dist/runtime-xLzAkAnp.d.ts +6969 -0
  38. package/dist/runtime.d.mts +1 -1
  39. package/dist/runtime.d.ts +1 -1
  40. package/dist/runtime.js +620 -21
  41. package/dist/runtime.mjs +17 -1
  42. 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,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 (merged with existing, set undefined to remove)
3760
+ * const withMeta = await schema.from('products')
3761
+ * .eq('id', 'rec-123')
3762
+ * .update({ price: 899 }, { metadata: { synced: true, oldKey: undefined } });
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 {
@@ -7353,6 +7865,9 @@ var RollupService = class {
7353
7865
  * Extract and aggregate values from related records
7354
7866
  */
7355
7867
  aggregateValues(relatedRecords, rollupAttr) {
7868
+ if (rollupAttr.function === "count") {
7869
+ return { value: relatedRecords.length, recordCount: relatedRecords.length };
7870
+ }
7356
7871
  const values = relatedRecords.map((r) => r.values[rollupAttr.targetAttribute]).filter((v) => v !== void 0 && v !== null);
7357
7872
  const aggregated = this.aggregate(values, rollupAttr.function);
7358
7873
  const result = typeof aggregated === "number" && rollupAttr.decimals !== void 0 ? Number(aggregated.toFixed(rollupAttr.decimals)) : aggregated;
@@ -7501,6 +8016,71 @@ var RollupService = class {
7501
8016
  }
7502
8017
  return [...new Set(affectedIds)];
7503
8018
  }
8019
+ /**
8020
+ * Find records that have forward rollups pointing to the modified record.
8021
+ *
8022
+ * Forward rollups are rollups where the record has a relation attribute
8023
+ * pointing to another object, and the rollup aggregates values from that target.
8024
+ * When the target record changes, we need to recalculate these rollups.
8025
+ *
8026
+ * Example: Order has relation "company" → Company, and rollup "capitalSocial"
8027
+ * aggregating from the Company. When Company.capitalSocial changes,
8028
+ * all Orders pointing to that Company need their rollup recalculated.
8029
+ *
8030
+ * @param changedRecord - The record that was modified
8031
+ * @param changedSchema - Schema of the changed record's object
8032
+ * @returns Array of records that need their forward rollups recalculated
8033
+ */
8034
+ async findRecordsWithForwardRollup(changedRecord, changedSchema) {
8035
+ const affectedRecords = [];
8036
+ const recordObject = await this.adapter.objects.findById(changedRecord.objectId);
8037
+ if (!recordObject) {
8038
+ return affectedRecords;
8039
+ }
8040
+ const tenantId = recordObject.tenantId;
8041
+ const allObjects = await this.adapter.objects.list(tenantId);
8042
+ for (const obj of allObjects) {
8043
+ if (obj.id === changedRecord.objectId) {
8044
+ continue;
8045
+ }
8046
+ const attributes = await this.adapter.attributes.findByObjectId(obj.id);
8047
+ const rollupAttrs = attributes.filter((a) => a.type === "rollup");
8048
+ if (rollupAttrs.length === 0) {
8049
+ continue;
8050
+ }
8051
+ for (const rollupDbAttr of rollupAttrs) {
8052
+ const rollupConfig = rollupDbAttr.config;
8053
+ if (!rollupConfig?.relationAttribute) {
8054
+ continue;
8055
+ }
8056
+ const relationAttr = attributes.find(
8057
+ (a) => a.type === "relation" && a.name === rollupConfig.relationAttribute
8058
+ );
8059
+ if (!relationAttr) {
8060
+ continue;
8061
+ }
8062
+ const relationConfig = relationAttr.config;
8063
+ const targetsChangedObject = relationConfig?.targets?.some(
8064
+ (t) => t.object === changedSchema.name
8065
+ );
8066
+ if (!targetsChangedObject) {
8067
+ continue;
8068
+ }
8069
+ const recordsPointingToChanged = await this.adapter.objectRecords.findByRelation(
8070
+ tenantId,
8071
+ obj.id,
8072
+ relationAttr.name,
8073
+ changedRecord.id
8074
+ );
8075
+ affectedRecords.push(...recordsPointingToChanged);
8076
+ }
8077
+ }
8078
+ const uniqueRecords = /* @__PURE__ */ new Map();
8079
+ for (const record of affectedRecords) {
8080
+ uniqueRecords.set(record.id, record);
8081
+ }
8082
+ return Array.from(uniqueRecords.values());
8083
+ }
7504
8084
  };
7505
8085
 
7506
8086
  // src/runtime/services/user.service.ts
@@ -7625,7 +8205,11 @@ var RecordService = class {
7625
8205
  constructor(adapter, tenantId, options) {
7626
8206
  this.adapter = adapter;
7627
8207
  this.tenantId = tenantId;
7628
- this.schemaService = new ObjectSchemaService(adapter, registry);
8208
+ this.schemaService = new ObjectSchemaService(adapter, registry, {
8209
+ userId: options?.userId,
8210
+ userEmail: options?.userEmail,
8211
+ auditService: options?.auditService
8212
+ });
7629
8213
  this.relationService = new RelationService(adapter, registry);
7630
8214
  this.userService = new UserService(adapter, tenantId);
7631
8215
  this.rollupService = new RollupService(adapter);
@@ -7780,7 +8364,8 @@ var RecordService = class {
7780
8364
  objectId,
7781
8365
  data,
7782
8366
  label,
7783
- completionStatus
8367
+ completionStatus,
8368
+ metadata: options?.metadata
7784
8369
  });
7785
8370
  if (!options?.skipHooks) {
7786
8371
  const afterCtx = {
@@ -7894,11 +8479,20 @@ var RecordService = class {
7894
8479
  }
7895
8480
  const completionStatus = computeRecordStatus(schema, mergedData);
7896
8481
  const label = await this.computeLabel(schema, mergedData);
7897
- const updated = await this.adapter.objectRecords.update(recordId, {
8482
+ const updatePayload = {
7898
8483
  ...data,
7899
8484
  __completionStatus: completionStatus,
7900
8485
  __label: label
7901
- });
8486
+ };
8487
+ if (options?.metadata !== void 0) {
8488
+ const existingMetadata = existing.metadata ?? {};
8489
+ const mergedMetadata = { ...existingMetadata, ...options.metadata };
8490
+ const cleanedMetadata = Object.fromEntries(
8491
+ Object.entries(mergedMetadata).filter(([, value]) => value !== void 0)
8492
+ );
8493
+ updatePayload.__metadata = cleanedMetadata;
8494
+ }
8495
+ const updated = await this.adapter.objectRecords.update(recordId, updatePayload);
7902
8496
  if (!options?.skipHooks) {
7903
8497
  const afterCtx = {
7904
8498
  ...hookCtx,
@@ -8167,9 +8761,10 @@ var RecordService = class {
8167
8761
  /**
8168
8762
  * Recalculate rollups after a record changes
8169
8763
  *
8170
- * This handles two cases:
8764
+ * This handles three cases:
8171
8765
  * 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
8766
+ * 2. Parent records have rollups that aggregate from this record (reverse pattern)
8767
+ * 3. Records that have forward rollups pointing to this record (forward pattern)
8173
8768
  *
8174
8769
  * @param record - The record that was modified
8175
8770
  * @param schema - Schema of the record's object
@@ -8183,19 +8778,26 @@ var RecordService = class {
8183
8778
  await this.rollupService.recalculateAndUpdate(record, schema);
8184
8779
  }
8185
8780
  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);
8781
+ if (affectedParentIds.length > 0) {
8782
+ const parentRecords = await this.adapter.objectRecords.findByIds(affectedParentIds);
8783
+ for (const parentRecord of parentRecords) {
8784
+ const parentSchema = await this.schemaService.getObjectSchema(parentRecord.objectId);
8785
+ const rollupAttrs = parentSchema.attributes.filter(
8786
+ (a) => a.type === "rollup"
8787
+ );
8788
+ if (rollupAttrs.length > 0) {
8789
+ await this.rollupService.recalculateAndUpdate(parentRecord, parentSchema);
8790
+ }
8197
8791
  }
8198
8792
  }
8793
+ const affectedForwardRecords = await this.rollupService.findRecordsWithForwardRollup(
8794
+ record,
8795
+ schema
8796
+ );
8797
+ for (const forwardRecord of affectedForwardRecords) {
8798
+ const forwardSchema = await this.schemaService.getObjectSchema(forwardRecord.objectId);
8799
+ await this.rollupService.recalculateAndUpdate(forwardRecord, forwardSchema);
8800
+ }
8199
8801
  }
8200
8802
  /**
8201
8803
  * Permanently delete a record (hard delete)
@@ -9323,6 +9925,9 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
9323
9925
  PermissionService,
9324
9926
  ProtectedResourceError,
9325
9927
  ProtectedRoleError,
9928
+ QueryBuilder,
9929
+ QueryMultipleResultsError,
9930
+ QueryNoResultError,
9326
9931
  RecordNotFoundError,
9327
9932
  RecordReferencedError,
9328
9933
  RecordService,
@@ -9331,6 +9936,8 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
9331
9936
  RoleNotFoundError,
9332
9937
  RollupScheduler,
9333
9938
  RollupService,
9939
+ SHORTCUT_TO_FILTER_OPERATOR,
9940
+ SYSTEM_FIELD_NAMES,
9334
9941
  SYSTEM_RESOURCES,
9335
9942
  SYSTEM_RESOURCE_LABELS,
9336
9943
  SchemaError,
@@ -9353,6 +9960,7 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
9353
9960
  createCheckboxValidator,
9354
9961
  createCurrencyValidator,
9355
9962
  createDateValidator,
9963
+ createDefaultState,
9356
9964
  createDraftValidator,
9357
9965
  createFileValidator,
9358
9966
  createFormulaValidator,
@@ -9363,6 +9971,7 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
9363
9971
  createNumberValidator,
9364
9972
  createObjectValidator,
9365
9973
  createPhoneValidator,
9974
+ createQueryBuilder,
9366
9975
  createRatingValidator,
9367
9976
  createRelationValidator,
9368
9977
  createRollupValidator,
@@ -9392,6 +10001,8 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
9392
10001
  flow,
9393
10002
  formatAttributeValue,
9394
10003
  formatFormulaResult,
10004
+ formatRecord,
10005
+ formatRecords,
9395
10006
  formula,
9396
10007
  formulaConfigSchema,
9397
10008
  generateId,