@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/runtime.js CHANGED
@@ -134,17 +134,22 @@ __export(runtime_exports, {
134
134
  NoopHookRegistry: () => NoopHookRegistry,
135
135
  ObjectSchemaService: () => ObjectSchemaService,
136
136
  PermissionService: () => PermissionService,
137
+ QueryBuilder: () => QueryBuilder,
138
+ QueryMultipleResultsError: () => QueryMultipleResultsError,
139
+ QueryNoResultError: () => QueryNoResultError,
137
140
  RecordService: () => RecordService,
138
141
  RelationResolverService: () => RelationResolverService,
139
142
  RelationService: () => RelationService,
140
143
  RollupScheduler: () => RollupScheduler,
141
144
  RollupService: () => RollupService,
145
+ SHORTCUT_TO_FILTER_OPERATOR: () => SHORTCUT_TO_FILTER_OPERATOR,
142
146
  UserProfileService: () => UserProfileService,
143
147
  UserService: () => UserService,
144
148
  ViewService: () => ViewService,
145
149
  buildAuditChanges: () => buildAuditChanges,
150
+ createDefaultState: () => createDefaultState,
146
151
  createMockAdapter: () => createMockAdapter,
147
- createRollupHooks: () => createRollupHooks,
152
+ createQueryBuilder: () => createQueryBuilder,
148
153
  enrichValuesWithSelectLabels: () => enrichValuesWithSelectLabels,
149
154
  evaluateFormula: () => evaluateFormula,
150
155
  evaluateFormulaAttribute: () => evaluateFormulaAttribute,
@@ -157,6 +162,8 @@ __export(runtime_exports, {
157
162
  extractRelationReferences: () => extractRelationReferences,
158
163
  flattenRelationsForEval: () => flattenRelationsForEval,
159
164
  formatFormulaResult: () => formatFormulaResult,
165
+ formatRecord: () => formatRecord,
166
+ formatRecords: () => formatRecords,
160
167
  getPathDepth: () => getPathDepth,
161
168
  getRelationPath: () => getRelationPath,
162
169
  getSyncPreview: () => getSyncPreview,
@@ -166,7 +173,6 @@ __export(runtime_exports, {
166
173
  isLabelExpression: () => isLabelExpression,
167
174
  parsePath: () => parsePath,
168
175
  pathHasManyCardinality: () => pathHasManyCardinality,
169
- registerAllRollupHooks: () => registerAllRollupHooks,
170
176
  renderLabelExpression: () => renderLabelExpression,
171
177
  resolveMultiplePaths: () => resolveMultiplePaths,
172
178
  resolveSingleValue: () => resolveSingleValue,
@@ -181,6 +187,477 @@ __export(runtime_exports, {
181
187
  });
182
188
  module.exports = __toCommonJS(runtime_exports);
183
189
 
190
+ // src/runtime/client/types.ts
191
+ function formatRecord(record) {
192
+ return {
193
+ id: record.id,
194
+ createdAt: record.createdAt,
195
+ updatedAt: record.updatedAt,
196
+ metadata: record.metadata ?? {},
197
+ ...record.values
198
+ };
199
+ }
200
+ function formatRecords(records) {
201
+ return records.map((r) => formatRecord(r));
202
+ }
203
+ function createDefaultState(objectName) {
204
+ return {
205
+ objectName,
206
+ filters: [],
207
+ combinator: "and",
208
+ sorts: [],
209
+ raw: false,
210
+ includeDeleted: false
211
+ };
212
+ }
213
+ var SHORTCUT_TO_FILTER_OPERATOR = {
214
+ eq: "is",
215
+ neq: "is_not",
216
+ gt: "gt",
217
+ gte: "gte",
218
+ lt: "lt",
219
+ lte: "lte",
220
+ contains: "contains",
221
+ notContains: "not_contains",
222
+ startsWith: "starts_with",
223
+ endsWith: "ends_with",
224
+ isEmpty: "is_empty",
225
+ isNotEmpty: "is_not_empty"
226
+ };
227
+ var QueryNoResultError = class extends Error {
228
+ constructor(objectName, filters) {
229
+ const filterInfo = filters?.length ? ` with filters: ${JSON.stringify(filters)}` : "";
230
+ super(`No record found in "${objectName}"${filterInfo}`);
231
+ this.name = "QueryNoResultError";
232
+ this.objectName = objectName;
233
+ this.filters = filters;
234
+ }
235
+ };
236
+ var QueryMultipleResultsError = class extends Error {
237
+ constructor(objectName, count) {
238
+ super(
239
+ `Expected single record in "${objectName}", but found ${count}. Use first() or add more filters.`
240
+ );
241
+ this.name = "QueryMultipleResultsError";
242
+ this.objectName = objectName;
243
+ this.count = count;
244
+ }
245
+ };
246
+
247
+ // src/runtime/client/query-builder.ts
248
+ var QueryBuilder = class _QueryBuilder {
249
+ constructor(recordService, adapter, objectName, state) {
250
+ this.objectId = null;
251
+ this.recordService = recordService;
252
+ this.adapter = adapter;
253
+ this.state = {
254
+ ...createDefaultState(objectName),
255
+ ...state
256
+ };
257
+ }
258
+ /**
259
+ * Clone the builder with new state (immutability pattern)
260
+ */
261
+ clone(updates) {
262
+ return new _QueryBuilder(this.recordService, this.adapter, this.state.objectName, {
263
+ ...this.state,
264
+ ...updates
265
+ });
266
+ }
267
+ /**
268
+ * Resolve object name to object ID (cached)
269
+ */
270
+ async resolveObjectId() {
271
+ if (this.objectId) return this.objectId;
272
+ const tenantId = this.recordService.tenantId;
273
+ const dbObject = await this.adapter.objects.findByName(tenantId, this.state.objectName);
274
+ if (!dbObject) {
275
+ throw new Error(`Object "${this.state.objectName}" not found`);
276
+ }
277
+ this.objectId = dbObject.id;
278
+ return dbObject.id;
279
+ }
280
+ // ============================================================================
281
+ // FILTER METHODS
282
+ // ============================================================================
283
+ /**
284
+ * Add a filter rule with explicit operator
285
+ *
286
+ * @example
287
+ * ```typescript
288
+ * qb.where('status', 'is', 'active')
289
+ * .where('price', 'gte', 100)
290
+ * ```
291
+ */
292
+ where(attribute, operator, value) {
293
+ const rule = { attribute, operator, value };
294
+ return this.clone({
295
+ filters: [...this.state.filters, rule]
296
+ });
297
+ }
298
+ /**
299
+ * Set filter combinator (default: 'and')
300
+ */
301
+ or() {
302
+ return this.clone({ combinator: "or" });
303
+ }
304
+ // ============================================================================
305
+ // SHORTCUT OPERATORS
306
+ // ============================================================================
307
+ /**
308
+ * Equal (is)
309
+ */
310
+ eq(attribute, value) {
311
+ return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.eq, value);
312
+ }
313
+ /**
314
+ * Not equal (is_not)
315
+ */
316
+ neq(attribute, value) {
317
+ return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.neq, value);
318
+ }
319
+ /**
320
+ * Greater than
321
+ */
322
+ gt(attribute, value) {
323
+ return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.gt, value);
324
+ }
325
+ /**
326
+ * Greater than or equal
327
+ */
328
+ gte(attribute, value) {
329
+ return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.gte, value);
330
+ }
331
+ /**
332
+ * Less than
333
+ */
334
+ lt(attribute, value) {
335
+ return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.lt, value);
336
+ }
337
+ /**
338
+ * Less than or equal
339
+ */
340
+ lte(attribute, value) {
341
+ return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.lte, value);
342
+ }
343
+ /**
344
+ * Contains (text/relation)
345
+ */
346
+ contains(attribute, value) {
347
+ return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.contains, value);
348
+ }
349
+ /**
350
+ * Not contains
351
+ */
352
+ notContains(attribute, value) {
353
+ return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.notContains, value);
354
+ }
355
+ /**
356
+ * Starts with (text)
357
+ */
358
+ startsWith(attribute, value) {
359
+ return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.startsWith, value);
360
+ }
361
+ /**
362
+ * Ends with (text)
363
+ */
364
+ endsWith(attribute, value) {
365
+ return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.endsWith, value);
366
+ }
367
+ /**
368
+ * Is empty
369
+ */
370
+ isEmpty(attribute) {
371
+ return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.isEmpty, null);
372
+ }
373
+ /**
374
+ * Is not empty
375
+ */
376
+ isNotEmpty(attribute) {
377
+ return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.isNotEmpty, null);
378
+ }
379
+ /**
380
+ * In list (any_of)
381
+ */
382
+ in(attribute, values) {
383
+ return this.where(attribute, "any_of", values);
384
+ }
385
+ /**
386
+ * Not in list (none_of)
387
+ */
388
+ notIn(attribute, values) {
389
+ return this.where(attribute, "none_of", values);
390
+ }
391
+ // ============================================================================
392
+ // SORT & PAGINATION
393
+ // ============================================================================
394
+ /**
395
+ * Add sort rule
396
+ */
397
+ orderBy(attribute, direction = "asc") {
398
+ return this.clone({
399
+ sorts: [...this.state.sorts, { attribute, direction }]
400
+ });
401
+ }
402
+ /**
403
+ * Set limit
404
+ */
405
+ limit(count) {
406
+ return this.clone({ limit: count });
407
+ }
408
+ /**
409
+ * Set offset
410
+ */
411
+ offset(count) {
412
+ return this.clone({ offset: count });
413
+ }
414
+ /**
415
+ * Include soft-deleted records
416
+ */
417
+ withDeleted() {
418
+ return this.clone({ includeDeleted: true });
419
+ }
420
+ // ============================================================================
421
+ // GROUP BY (KANBAN)
422
+ // ============================================================================
423
+ /**
424
+ * Group results by an attribute (for Kanban views).
425
+ * Only status and select attributes are supported.
426
+ *
427
+ * @example
428
+ * ```typescript
429
+ * const grouped = await qb
430
+ * .groupBy('status')
431
+ * .fetchGrouped();
432
+ * // → { groups: { 'pending': [...], 'active': [...] }, total: 50 }
433
+ * ```
434
+ */
435
+ groupBy(attribute) {
436
+ return this.clone({ groupBy: attribute });
437
+ }
438
+ // ============================================================================
439
+ // RAW MODE
440
+ // ============================================================================
441
+ /**
442
+ * Return raw ObjectRecord instead of formatted record.
443
+ * Includes all metadata (objectId, label, completionStatus, etc.)
444
+ */
445
+ raw() {
446
+ return this.clone({ raw: true });
447
+ }
448
+ // ============================================================================
449
+ // TERMINATION METHODS (READ)
450
+ // ============================================================================
451
+ /**
452
+ * Fetch records with current filters
453
+ * Returns formatted records by default, raw ObjectRecords if raw() was called
454
+ */
455
+ async fetch() {
456
+ const objectId = await this.resolveObjectId();
457
+ const filters = this.state.filters.length > 0 ? { combinator: this.state.combinator, rules: this.state.filters } : void 0;
458
+ const result = await this.recordService.listRecords(objectId, {
459
+ filters,
460
+ sorts: this.state.sorts.length > 0 ? this.state.sorts : void 0,
461
+ limit: this.state.limit,
462
+ offset: this.state.offset,
463
+ includeDeleted: this.state.includeDeleted
464
+ });
465
+ if (this.state.raw) {
466
+ return result;
467
+ }
468
+ return {
469
+ records: formatRecords(result.records),
470
+ total: result.total
471
+ };
472
+ }
473
+ /**
474
+ * Get a single record (throws if 0 or >1 results)
475
+ */
476
+ async single() {
477
+ const result = await this.limit(2).fetch();
478
+ if (result.records.length === 0) {
479
+ throw new QueryNoResultError(this.state.objectName, this.state.filters);
480
+ }
481
+ if (result.records.length > 1) {
482
+ throw new QueryMultipleResultsError(this.state.objectName, result.total);
483
+ }
484
+ return result.records[0];
485
+ }
486
+ /**
487
+ * Get first record or null
488
+ */
489
+ async first() {
490
+ const result = await this.limit(1).fetch();
491
+ return result.records[0] ?? null;
492
+ }
493
+ /**
494
+ * Get record by ID
495
+ */
496
+ async findById(id) {
497
+ const record = await this.recordService.getRecord(id);
498
+ if (!record) return null;
499
+ if (this.state.raw) {
500
+ return record;
501
+ }
502
+ return formatRecord(record);
503
+ }
504
+ /**
505
+ * Count records matching filters
506
+ */
507
+ async count() {
508
+ const result = await this.limit(0).fetch();
509
+ return result.total;
510
+ }
511
+ /**
512
+ * Fetch records grouped by the groupBy attribute (for Kanban views).
513
+ * Requires groupBy() to be called first.
514
+ *
515
+ * @example
516
+ * ```typescript
517
+ * const result = await schema.from('tasks')
518
+ * .groupBy('status')
519
+ * .fetchGrouped();
520
+ * // → { groups: { 'todo': [...], 'in_progress': [...], 'done': [...] }, total: 50 }
521
+ * ```
522
+ */
523
+ async fetchGrouped() {
524
+ if (!this.state.groupBy) {
525
+ throw new Error("groupBy() must be called before fetchGrouped()");
526
+ }
527
+ const result = await this.fetch();
528
+ const groupByAttr = this.state.groupBy;
529
+ const groups = {};
530
+ for (const record of result.records) {
531
+ const value = record[groupByAttr];
532
+ const key = value != null ? String(value) : "__null__";
533
+ if (!groups[key]) {
534
+ groups[key] = [];
535
+ }
536
+ groups[key].push(record);
537
+ }
538
+ return {
539
+ groups,
540
+ total: result.total
541
+ };
542
+ }
543
+ // ============================================================================
544
+ // TERMINATION METHODS (WRITE)
545
+ // ============================================================================
546
+ /**
547
+ * Create a new record
548
+ * System fields (id, createdAt, updatedAt) are managed automatically.
549
+ *
550
+ * @example
551
+ * ```typescript
552
+ * const product = await schema.from('products')
553
+ * .insert({ name: 'iPhone', price: 999 });
554
+ *
555
+ * // Allow draft (missing required fields)
556
+ * const draft = await schema.from('products')
557
+ * .insert({ name: 'Draft' }, { allowDraft: true });
558
+ *
559
+ * // With custom metadata
560
+ * const withMeta = await schema.from('products')
561
+ * .insert({ name: 'iPhone' }, { metadata: { externalId: 'ext-123' } });
562
+ * ```
563
+ */
564
+ async insert(data, options) {
565
+ const objectId = await this.resolveObjectId();
566
+ const record = await this.recordService.createRecord(
567
+ objectId,
568
+ data,
569
+ {
570
+ allowDraft: options?.allowDraft,
571
+ validate: options?.validate,
572
+ metadata: options?.metadata
573
+ }
574
+ );
575
+ if (this.state.raw) {
576
+ return record;
577
+ }
578
+ return formatRecord(record);
579
+ }
580
+ /**
581
+ * Update records matching filters (expects exactly 1 record by default)
582
+ * Use with eq('id', ...) for single record update.
583
+ * System fields (id, createdAt, updatedAt) are managed automatically.
584
+ *
585
+ * @example
586
+ * ```typescript
587
+ * const updated = await schema.from('products')
588
+ * .eq('id', 'rec-123')
589
+ * .update({ price: 899 });
590
+ *
591
+ * // Update with metadata (replaces existing metadata)
592
+ * const withMeta = await schema.from('products')
593
+ * .eq('id', 'rec-123')
594
+ * .update({ price: 899 }, { metadata: { synced: true } });
595
+ * ```
596
+ */
597
+ async update(data, options) {
598
+ const existing = await this.single();
599
+ const recordId = this.state.raw ? existing.id : existing.id;
600
+ const record = await this.recordService.updateRecord(
601
+ recordId,
602
+ data,
603
+ {
604
+ partial: true,
605
+ metadata: options?.metadata
606
+ }
607
+ );
608
+ if (this.state.raw) {
609
+ return record;
610
+ }
611
+ return formatRecord(record);
612
+ }
613
+ /**
614
+ * Delete record matching filters (expects exactly 1 record)
615
+ *
616
+ * @example
617
+ * ```typescript
618
+ * await schema.from('products')
619
+ * .eq('id', 'rec-123')
620
+ * .delete();
621
+ * ```
622
+ */
623
+ async delete() {
624
+ const existing = await this.single();
625
+ const recordId = this.state.raw ? existing.id : existing.id;
626
+ await this.recordService.deleteRecord(recordId);
627
+ }
628
+ /**
629
+ * Upsert (update if exists by ID, insert otherwise)
630
+ * Provide 'id' for lookup. System fields (createdAt, updatedAt) are managed automatically.
631
+ *
632
+ * @example
633
+ * ```typescript
634
+ * // Insert if not exists, update if exists
635
+ * const product = await schema.from('products')
636
+ * .upsert({ id: 'rec-123', name: 'iPhone', price: 999 });
637
+ *
638
+ * // With metadata
639
+ * const withMeta = await schema.from('products')
640
+ * .upsert({ name: 'iPhone' }, { metadata: { source: 'import' } });
641
+ * ```
642
+ */
643
+ async upsert(data, options) {
644
+ const { id, ...rest } = data;
645
+ const writeData = rest;
646
+ if (id) {
647
+ const existing = await this.findById(id);
648
+ if (existing) {
649
+ return this.eq("id", id).update(writeData, {
650
+ metadata: options?.metadata
651
+ });
652
+ }
653
+ }
654
+ return this.insert(writeData, options);
655
+ }
656
+ };
657
+ function createQueryBuilder(recordService, adapter, objectName) {
658
+ return new QueryBuilder(recordService, adapter, objectName);
659
+ }
660
+
184
661
  // src/runtime/formula/evaluator.ts
185
662
  var import_expr_eval = require("expr-eval");
186
663
  function createFormulaParser() {
@@ -565,260 +1042,6 @@ var NoopHookRegistry = class {
565
1042
  }
566
1043
  };
567
1044
 
568
- // src/runtime/services/rollup.service.ts
569
- var RollupService = class {
570
- constructor(adapter) {
571
- this.adapter = adapter;
572
- }
573
- /**
574
- * Calculate a rollup value for a record
575
- *
576
- * @param recordId - ID of the parent record
577
- * @param rollupAttr - Rollup attribute definition
578
- * @param schema - Schema of the parent object
579
- * @returns Computed rollup value
580
- *
581
- * @example
582
- * ```typescript
583
- * // Sum all order amounts for a company
584
- * const totalOrders = await rollupService.calculate(
585
- * "company-123",
586
- * {
587
- * type: "rollup",
588
- * name: "totalOrders",
589
- * relationAttribute: "orders",
590
- * targetAttribute: "amount",
591
- * function: "sum",
592
- * ...
593
- * },
594
- * companySchema
595
- * );
596
- * ```
597
- */
598
- async calculate(recordId, rollupAttr, schema, tenantId) {
599
- const relationAttr = schema.attributes.find(
600
- (a) => a.type === "relation" && a.name === rollupAttr.relationAttribute
601
- );
602
- if (!relationAttr) {
603
- return { value: null, recordCount: 0 };
604
- }
605
- const targetObjectName = relationAttr.targets[0]?.object;
606
- if (!targetObjectName) {
607
- return { value: null, recordCount: 0 };
608
- }
609
- const targetObject = await this.adapter.objects.findByName(tenantId, targetObjectName);
610
- if (!targetObject) {
611
- return { value: null, recordCount: 0 };
612
- }
613
- const relatedRecords = await this.adapter.objectRecords.findByRelation(
614
- tenantId,
615
- targetObject.id,
616
- rollupAttr.relationAttribute,
617
- recordId
618
- );
619
- if (relatedRecords.length === 0) {
620
- return { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 };
621
- }
622
- const values = relatedRecords.map((r) => r.values[rollupAttr.targetAttribute]).filter((v) => v !== void 0);
623
- const aggregated = this.aggregate(values, rollupAttr.function);
624
- const result = typeof aggregated === "number" && rollupAttr.decimals !== void 0 ? Number(aggregated.toFixed(rollupAttr.decimals)) : aggregated;
625
- return { value: result, recordCount: relatedRecords.length };
626
- }
627
- /**
628
- * Calculate rollup values for multiple records (batched)
629
- *
630
- * More efficient than calling calculate() for each record individually.
631
- */
632
- async calculateForMany(recordIds, rollupAttr, schema, tenantId) {
633
- const results = /* @__PURE__ */ new Map();
634
- for (const id of recordIds) {
635
- results.set(id, { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 });
636
- }
637
- await Promise.all(
638
- recordIds.map(async (id) => {
639
- const result = await this.calculate(id, rollupAttr, schema, tenantId);
640
- results.set(id, result);
641
- })
642
- );
643
- return results;
644
- }
645
- /**
646
- * Apply aggregation function to a set of values
647
- */
648
- aggregate(values, fn) {
649
- switch (fn) {
650
- case "sum":
651
- return this.sumNumbers(values);
652
- case "avg":
653
- return this.averageNumbers(values);
654
- case "min":
655
- return this.minNumbers(values);
656
- case "max":
657
- return this.maxNumbers(values);
658
- case "count":
659
- return values.length;
660
- case "countValues":
661
- return values.filter((v) => v != null && v !== "").length;
662
- case "countEmpty":
663
- return values.filter((v) => v == null || v === "").length;
664
- case "percentFilled": {
665
- if (values.length === 0) return 0;
666
- const filled = values.filter((v) => v != null && v !== "").length;
667
- return filled / values.length * 100;
668
- }
669
- case "percentEmpty": {
670
- if (values.length === 0) return 0;
671
- const empty = values.filter((v) => v == null || v === "").length;
672
- return empty / values.length * 100;
673
- }
674
- }
675
- }
676
- /**
677
- * Get the default empty value for a rollup function
678
- */
679
- getEmptyValue(fn) {
680
- switch (fn) {
681
- case "sum":
682
- case "count":
683
- case "countValues":
684
- case "countEmpty":
685
- case "percentEmpty":
686
- case "percentFilled":
687
- return 0;
688
- case "avg":
689
- case "min":
690
- case "max":
691
- return null;
692
- }
693
- }
694
- /**
695
- * Sum numeric values
696
- */
697
- sumNumbers(values) {
698
- return values.filter((v) => typeof v === "number" && !Number.isNaN(v)).reduce((sum, n) => sum + n, 0);
699
- }
700
- /**
701
- * Average numeric values
702
- */
703
- averageNumbers(values) {
704
- const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
705
- if (nums.length === 0) return null;
706
- return nums.reduce((sum, n) => sum + n, 0) / nums.length;
707
- }
708
- /**
709
- * Minimum numeric value
710
- */
711
- minNumbers(values) {
712
- const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
713
- if (nums.length === 0) return null;
714
- return Math.min(...nums);
715
- }
716
- /**
717
- * Maximum numeric value
718
- */
719
- maxNumbers(values) {
720
- const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
721
- if (nums.length === 0) return null;
722
- return Math.max(...nums);
723
- }
724
- /**
725
- * Recalculate all rollup attributes for a record and update it
726
- *
727
- * Called after related records change to keep rollups up-to-date.
728
- */
729
- async recalculateAndUpdate(record, schema, tenantId) {
730
- const rollupAttrs = schema.attributes.filter((a) => a.type === "rollup");
731
- if (rollupAttrs.length === 0) {
732
- return record;
733
- }
734
- const updates = {};
735
- for (const attr of rollupAttrs) {
736
- const result = await this.calculate(record.id, attr, schema, tenantId);
737
- updates[attr.name] = result.value;
738
- }
739
- return await this.adapter.objectRecords.update(record.id, updates);
740
- }
741
- /**
742
- * Find parent records that need rollup recalculation when a child record changes
743
- *
744
- * Used by hooks to determine which parent records to recalculate after
745
- * a child record is created, updated, or deleted.
746
- *
747
- * @param changedRecord - The record that was modified
748
- * @param changedSchema - Schema of the changed record's object
749
- * @returns Array of parent record IDs that need recalculation
750
- */
751
- async findAffectedParentRecords(changedRecord, changedSchema) {
752
- const affectedIds = [];
753
- const relationAttrs = changedSchema.attributes.filter(
754
- (a) => a.type === "relation"
755
- );
756
- for (const attr of relationAttrs) {
757
- const relatedId = changedRecord.values[attr.name];
758
- if (typeof relatedId === "string" && relatedId.length > 0) {
759
- affectedIds.push(relatedId);
760
- } else if (Array.isArray(relatedId)) {
761
- affectedIds.push(...relatedId.filter((id) => typeof id === "string"));
762
- }
763
- }
764
- return [...new Set(affectedIds)];
765
- }
766
- };
767
-
768
- // src/runtime/hooks/rollup.hooks.ts
769
- function createRollupHooks(objectName, config) {
770
- const rollupService = new RollupService(config.adapter);
771
- async function recalculateParentRollups(ctx) {
772
- const schema = await config.getSchemaById(ctx.objectId);
773
- if (!schema) return;
774
- const affectedParentIds = await rollupService.findAffectedParentRecords(ctx.record, schema);
775
- if (affectedParentIds.length === 0) return;
776
- const parentRecords = await config.adapter.objectRecords.findByIds(affectedParentIds);
777
- for (const parentRecord of parentRecords) {
778
- const parentSchema = await config.getSchemaById(parentRecord.objectId);
779
- if (!parentSchema) continue;
780
- const hasRollups = parentSchema.attributes.some(
781
- (a) => a.type === "rollup"
782
- );
783
- if (hasRollups) {
784
- await rollupService.recalculateAndUpdate(parentRecord, parentSchema, config.tenantId);
785
- }
786
- }
787
- }
788
- return [
789
- {
790
- type: "afterCreate",
791
- objectName,
792
- handler: recalculateParentRollups,
793
- priority: 100
794
- // Run after other hooks
795
- },
796
- {
797
- type: "afterUpdate",
798
- objectName,
799
- handler: recalculateParentRollups,
800
- priority: 100
801
- },
802
- {
803
- type: "afterDelete",
804
- objectName,
805
- handler: recalculateParentRollups,
806
- priority: 100
807
- }
808
- ];
809
- }
810
- function registerAllRollupHooks(hookRegistry, schemas, config) {
811
- for (const schema of schemas) {
812
- const hasRelations = schema.attributes.some((a) => a.type === "relation");
813
- if (hasRelations && hookRegistry.register) {
814
- const hooks = createRollupHooks(schema.name, config);
815
- for (const hook of hooks) {
816
- hookRegistry.register(hook);
817
- }
818
- }
819
- }
820
- }
821
-
822
1045
  // src/utils.ts
823
1046
  function generateId() {
824
1047
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
@@ -1457,6 +1680,7 @@ function createMockObjectRecordsRepository(stores) {
1457
1680
  label: data.label,
1458
1681
  completionStatus: data.completionStatus,
1459
1682
  values: data.data,
1683
+ metadata: data.metadata ?? {},
1460
1684
  createdAt: /* @__PURE__ */ new Date(),
1461
1685
  updatedAt: /* @__PURE__ */ new Date()
1462
1686
  };
@@ -1469,11 +1693,13 @@ function createMockObjectRecordsRepository(stores) {
1469
1693
  if (!existing) {
1470
1694
  return Promise.reject(new Error(`ObjectRecord ${id} not found`));
1471
1695
  }
1472
- const { __completionStatus, __label, ...valueData } = data;
1696
+ const { __completionStatus, __label, __metadata, ...valueData } = data;
1473
1697
  const updated = {
1474
1698
  ...existing,
1475
1699
  label: __label ?? existing.label,
1476
1700
  completionStatus: __completionStatus ?? existing.completionStatus,
1701
+ // Metadata uses replace behavior (not merge)
1702
+ metadata: __metadata !== void 0 ? __metadata : existing.metadata,
1477
1703
  values: { ...existing.values, ...valueData },
1478
1704
  updatedAt: /* @__PURE__ */ new Date()
1479
1705
  };
@@ -3601,10 +3827,12 @@ var rollupConfigSchema = baseConfigSchema.extend({
3601
3827
  "countValues",
3602
3828
  "countEmpty",
3603
3829
  "percentEmpty",
3604
- "percentFilled"
3830
+ "percentFilled",
3831
+ "collect"
3605
3832
  ]),
3606
3833
  decimals: import_zod4.z.number().int().min(0).max(10).optional(),
3607
- materialize: import_zod4.z.boolean().optional()
3834
+ showAsOriginal: import_zod4.z.boolean().optional(),
3835
+ targetAttributeType: import_zod4.z.string().optional()
3608
3836
  });
3609
3837
  var attributeConfigSchemas = {
3610
3838
  text: textConfigSchema,
@@ -3669,7 +3897,7 @@ function createCheckboxValidator(_attr) {
3669
3897
  return import_zod4.z.boolean();
3670
3898
  }
3671
3899
  function createDateValidator(attr) {
3672
- return import_zod4.z.string().datetime({ message: `${attr.label} must be a valid ISO date` });
3900
+ return import_zod4.z.coerce.date({ message: `${attr.label} must be a valid ISO date` });
3673
3901
  }
3674
3902
  function createPhoneValidator(_attr) {
3675
3903
  return import_zod4.z.object({
@@ -3775,7 +4003,7 @@ function createRelationValidator(attr) {
3775
4003
  return createSingleRelationValidator(attr);
3776
4004
  }
3777
4005
  function createRatingValidator(attr) {
3778
- let schema = import_zod4.z.number().int().min(0);
4006
+ let schema = import_zod4.z.number().min(0);
3779
4007
  if (attr.max !== void 0) {
3780
4008
  schema = schema.max(attr.max, `${attr.label} must be at most ${attr.max}`);
3781
4009
  }
@@ -3929,7 +4157,7 @@ function computeRecordStatus(objectDef, data) {
3929
4157
  }
3930
4158
 
3931
4159
  // src/runtime/services/object-schema.service.ts
3932
- var ObjectSchemaService = class {
4160
+ var _ObjectSchemaService = class _ObjectSchemaService {
3933
4161
  constructor(adapter, nativeRegistry, options) {
3934
4162
  this.adapter = adapter;
3935
4163
  this.nativeRegistry = nativeRegistry;
@@ -4352,6 +4580,14 @@ var ObjectSchemaService = class {
4352
4580
  "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'"
4353
4581
  );
4354
4582
  }
4583
+ if (_ObjectSchemaService.RESERVED_ATTRIBUTE_NAMES.includes(
4584
+ name
4585
+ )) {
4586
+ throw new Error(
4587
+ `"${name}" is a reserved name and cannot be used as an attribute name.
4588
+ Reserved names: ${_ObjectSchemaService.RESERVED_ATTRIBUTE_NAMES.join(", ")}`
4589
+ );
4590
+ }
4355
4591
  }
4356
4592
  /**
4357
4593
  * Get complete object schema (system + custom attributes)
@@ -4598,6 +4834,17 @@ var ObjectSchemaService = class {
4598
4834
  return baseAttr;
4599
4835
  }
4600
4836
  };
4837
+ /**
4838
+ * Reserved attribute names that cannot be used.
4839
+ * These names conflict with system fields on ObjectRecord.
4840
+ */
4841
+ _ObjectSchemaService.RESERVED_ATTRIBUTE_NAMES = [
4842
+ "id",
4843
+ "createdAt",
4844
+ "updatedAt",
4845
+ "metadata"
4846
+ ];
4847
+ var ObjectSchemaService = _ObjectSchemaService;
4601
4848
 
4602
4849
  // src/runtime/services/permission.service.ts
4603
4850
  var PermissionService = class {
@@ -5450,6 +5697,328 @@ var RelationService = class {
5450
5697
  }
5451
5698
  };
5452
5699
 
5700
+ // src/runtime/services/rollup.service.ts
5701
+ var RollupService = class {
5702
+ constructor(adapter) {
5703
+ this.adapter = adapter;
5704
+ }
5705
+ /**
5706
+ * Calculate a rollup value for a record
5707
+ *
5708
+ * @param recordId - ID of the parent record
5709
+ * @param rollupAttr - Rollup attribute definition
5710
+ * @param schema - Schema of the parent object
5711
+ * @returns Computed rollup value
5712
+ *
5713
+ * @example
5714
+ * ```typescript
5715
+ * // Sum all order amounts for a company
5716
+ * const totalOrders = await rollupService.calculate(
5717
+ * "company-123",
5718
+ * {
5719
+ * type: "rollup",
5720
+ * name: "totalOrders",
5721
+ * relationAttribute: "orders",
5722
+ * targetAttribute: "amount",
5723
+ * function: "sum",
5724
+ * ...
5725
+ * },
5726
+ * companySchema
5727
+ * );
5728
+ * ```
5729
+ */
5730
+ async calculate(recordId, rollupAttr, schema) {
5731
+ const relationAttr = schema.attributes.find(
5732
+ (a) => a.type === "relation" && a.name === rollupAttr.relationAttribute
5733
+ );
5734
+ if (relationAttr) {
5735
+ return this.calculateForward(recordId, rollupAttr);
5736
+ }
5737
+ return this.calculateReverse(recordId, rollupAttr, schema);
5738
+ }
5739
+ /**
5740
+ * Forward pattern: this record has a relation attribute pointing to other records
5741
+ * Example: entreprise222 has relation "entreprises" → companies, rollup collects from companies
5742
+ */
5743
+ async calculateForward(recordId, rollupAttr) {
5744
+ const record = await this.adapter.objectRecords.findById(recordId);
5745
+ if (!record) {
5746
+ return { value: null, recordCount: 0 };
5747
+ }
5748
+ const relationValue = record.values[rollupAttr.relationAttribute];
5749
+ let relatedIds = [];
5750
+ if (typeof relationValue === "string" && relationValue.length > 0) {
5751
+ relatedIds = [relationValue];
5752
+ } else if (Array.isArray(relationValue)) {
5753
+ relatedIds = relationValue.filter((id) => typeof id === "string" && id.length > 0);
5754
+ }
5755
+ if (relatedIds.length === 0) {
5756
+ return { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 };
5757
+ }
5758
+ const relatedRecords = await this.adapter.objectRecords.findByIds(relatedIds);
5759
+ if (relatedRecords.length === 0) {
5760
+ return { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 };
5761
+ }
5762
+ return this.aggregateValues(relatedRecords, rollupAttr);
5763
+ }
5764
+ /**
5765
+ * Reverse pattern: other records have a relation pointing to this record
5766
+ * Example: Company has rollup on "orders", Order has relation "company" → companies
5767
+ */
5768
+ async calculateReverse(recordId, rollupAttr, schema) {
5769
+ const record = await this.adapter.objectRecords.findById(recordId);
5770
+ if (!record) {
5771
+ return { value: null, recordCount: 0 };
5772
+ }
5773
+ const parentObject = await this.adapter.objects.findById(record.objectId);
5774
+ if (!parentObject) {
5775
+ return { value: null, recordCount: 0 };
5776
+ }
5777
+ const tenantId = parentObject.tenantId;
5778
+ const sourceObjectName = rollupAttr.relationAttribute;
5779
+ const sourceObject = await this.adapter.objects.findByName(tenantId, sourceObjectName);
5780
+ if (!sourceObject) {
5781
+ return { value: null, recordCount: 0 };
5782
+ }
5783
+ const sourceAttributes = await this.adapter.attributes.findByObjectId(sourceObject.id);
5784
+ const reverseRelationAttr = sourceAttributes.find((attr) => {
5785
+ if (attr.type !== "relation") return false;
5786
+ const relationConfig = attr.config;
5787
+ return relationConfig?.targets?.some((t) => t.object === schema.name);
5788
+ });
5789
+ if (!reverseRelationAttr) {
5790
+ return { value: null, recordCount: 0 };
5791
+ }
5792
+ const relatedRecords = await this.adapter.objectRecords.findByRelation(
5793
+ tenantId,
5794
+ sourceObject.id,
5795
+ reverseRelationAttr.name,
5796
+ recordId
5797
+ );
5798
+ if (relatedRecords.length === 0) {
5799
+ return { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 };
5800
+ }
5801
+ return this.aggregateValues(relatedRecords, rollupAttr);
5802
+ }
5803
+ /**
5804
+ * Extract and aggregate values from related records
5805
+ */
5806
+ aggregateValues(relatedRecords, rollupAttr) {
5807
+ const values = relatedRecords.map((r) => r.values[rollupAttr.targetAttribute]).filter((v) => v !== void 0 && v !== null);
5808
+ const aggregated = this.aggregate(values, rollupAttr.function);
5809
+ const result = typeof aggregated === "number" && rollupAttr.decimals !== void 0 ? Number(aggregated.toFixed(rollupAttr.decimals)) : aggregated;
5810
+ return { value: result, recordCount: relatedRecords.length };
5811
+ }
5812
+ /**
5813
+ * Calculate rollup values for multiple records (batched)
5814
+ *
5815
+ * More efficient than calling calculate() for each record individually.
5816
+ */
5817
+ async calculateForMany(recordIds, rollupAttr, schema) {
5818
+ const results = /* @__PURE__ */ new Map();
5819
+ for (const id of recordIds) {
5820
+ results.set(id, { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 });
5821
+ }
5822
+ await Promise.all(
5823
+ recordIds.map(async (id) => {
5824
+ const result = await this.calculate(id, rollupAttr, schema);
5825
+ results.set(id, result);
5826
+ })
5827
+ );
5828
+ return results;
5829
+ }
5830
+ /**
5831
+ * Apply aggregation function to a set of values
5832
+ */
5833
+ aggregate(values, fn) {
5834
+ switch (fn) {
5835
+ case "sum":
5836
+ return this.sumNumbers(values);
5837
+ case "avg":
5838
+ return this.averageNumbers(values);
5839
+ case "min":
5840
+ return this.minNumbers(values);
5841
+ case "max":
5842
+ return this.maxNumbers(values);
5843
+ case "count":
5844
+ return values.length;
5845
+ case "countValues":
5846
+ return values.filter((v) => v != null && v !== "").length;
5847
+ case "countEmpty":
5848
+ return values.filter((v) => v == null || v === "").length;
5849
+ case "percentFilled": {
5850
+ if (values.length === 0) return 0;
5851
+ const filled = values.filter((v) => v != null && v !== "").length;
5852
+ return filled / values.length * 100;
5853
+ }
5854
+ case "percentEmpty": {
5855
+ if (values.length === 0) return 0;
5856
+ const empty = values.filter((v) => v == null || v === "").length;
5857
+ return empty / values.length * 100;
5858
+ }
5859
+ case "collect":
5860
+ return values;
5861
+ }
5862
+ }
5863
+ /**
5864
+ * Get the default empty value for a rollup function
5865
+ */
5866
+ getEmptyValue(fn) {
5867
+ switch (fn) {
5868
+ case "sum":
5869
+ case "count":
5870
+ case "countValues":
5871
+ case "countEmpty":
5872
+ case "percentEmpty":
5873
+ case "percentFilled":
5874
+ return 0;
5875
+ case "avg":
5876
+ case "min":
5877
+ case "max":
5878
+ return null;
5879
+ case "collect":
5880
+ return [];
5881
+ }
5882
+ }
5883
+ /**
5884
+ * Sum numeric values
5885
+ */
5886
+ sumNumbers(values) {
5887
+ return values.filter((v) => typeof v === "number" && !Number.isNaN(v)).reduce((sum, n) => sum + n, 0);
5888
+ }
5889
+ /**
5890
+ * Average numeric values
5891
+ */
5892
+ averageNumbers(values) {
5893
+ const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
5894
+ if (nums.length === 0) return null;
5895
+ return nums.reduce((sum, n) => sum + n, 0) / nums.length;
5896
+ }
5897
+ /**
5898
+ * Minimum numeric value
5899
+ */
5900
+ minNumbers(values) {
5901
+ const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
5902
+ if (nums.length === 0) return null;
5903
+ return Math.min(...nums);
5904
+ }
5905
+ /**
5906
+ * Maximum numeric value
5907
+ */
5908
+ maxNumbers(values) {
5909
+ const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
5910
+ if (nums.length === 0) return null;
5911
+ return Math.max(...nums);
5912
+ }
5913
+ /**
5914
+ * Recalculate all rollup attributes for a record and update it
5915
+ *
5916
+ * Called after related records change to keep rollups up-to-date.
5917
+ */
5918
+ async recalculateAndUpdate(record, schema) {
5919
+ const rollupAttrs = schema.attributes.filter((a) => a.type === "rollup");
5920
+ if (rollupAttrs.length === 0) {
5921
+ return record;
5922
+ }
5923
+ const updates = {};
5924
+ for (const attr of rollupAttrs) {
5925
+ const result = await this.calculate(record.id, attr, schema);
5926
+ updates[attr.name] = result.value;
5927
+ }
5928
+ return await this.adapter.objectRecords.update(record.id, updates);
5929
+ }
5930
+ /**
5931
+ * Find parent records that need rollup recalculation when a child record changes
5932
+ *
5933
+ * Used by hooks to determine which parent records to recalculate after
5934
+ * a child record is created, updated, or deleted.
5935
+ *
5936
+ * @param changedRecord - The record that was modified
5937
+ * @param changedSchema - Schema of the changed record's object
5938
+ * @returns Array of parent record IDs that need recalculation
5939
+ */
5940
+ async findAffectedParentRecords(changedRecord, changedSchema) {
5941
+ const affectedIds = [];
5942
+ const relationAttrs = changedSchema.attributes.filter(
5943
+ (a) => a.type === "relation"
5944
+ );
5945
+ for (const attr of relationAttrs) {
5946
+ const relatedId = changedRecord.values[attr.name];
5947
+ if (typeof relatedId === "string" && relatedId.length > 0) {
5948
+ affectedIds.push(relatedId);
5949
+ } else if (Array.isArray(relatedId)) {
5950
+ affectedIds.push(...relatedId.filter((id) => typeof id === "string"));
5951
+ }
5952
+ }
5953
+ return [...new Set(affectedIds)];
5954
+ }
5955
+ /**
5956
+ * Find records that have forward rollups pointing to the modified record.
5957
+ *
5958
+ * Forward rollups are rollups where the record has a relation attribute
5959
+ * pointing to another object, and the rollup aggregates values from that target.
5960
+ * When the target record changes, we need to recalculate these rollups.
5961
+ *
5962
+ * Example: Order has relation "company" → Company, and rollup "capitalSocial"
5963
+ * aggregating from the Company. When Company.capitalSocial changes,
5964
+ * all Orders pointing to that Company need their rollup recalculated.
5965
+ *
5966
+ * @param changedRecord - The record that was modified
5967
+ * @param changedSchema - Schema of the changed record's object
5968
+ * @returns Array of records that need their forward rollups recalculated
5969
+ */
5970
+ async findRecordsWithForwardRollup(changedRecord, changedSchema) {
5971
+ const affectedRecords = [];
5972
+ const recordObject = await this.adapter.objects.findById(changedRecord.objectId);
5973
+ if (!recordObject) {
5974
+ return affectedRecords;
5975
+ }
5976
+ const tenantId = recordObject.tenantId;
5977
+ const allObjects = await this.adapter.objects.list(tenantId);
5978
+ for (const obj of allObjects) {
5979
+ if (obj.id === changedRecord.objectId) {
5980
+ continue;
5981
+ }
5982
+ const attributes = await this.adapter.attributes.findByObjectId(obj.id);
5983
+ const rollupAttrs = attributes.filter((a) => a.type === "rollup");
5984
+ if (rollupAttrs.length === 0) {
5985
+ continue;
5986
+ }
5987
+ for (const rollupDbAttr of rollupAttrs) {
5988
+ const rollupConfig = rollupDbAttr.config;
5989
+ if (!rollupConfig?.relationAttribute) {
5990
+ continue;
5991
+ }
5992
+ const relationAttr = attributes.find(
5993
+ (a) => a.type === "relation" && a.name === rollupConfig.relationAttribute
5994
+ );
5995
+ if (!relationAttr) {
5996
+ continue;
5997
+ }
5998
+ const relationConfig = relationAttr.config;
5999
+ const targetsChangedObject = relationConfig?.targets?.some(
6000
+ (t) => t.object === changedSchema.name
6001
+ );
6002
+ if (!targetsChangedObject) {
6003
+ continue;
6004
+ }
6005
+ const recordsPointingToChanged = await this.adapter.objectRecords.findByRelation(
6006
+ tenantId,
6007
+ obj.id,
6008
+ relationAttr.name,
6009
+ changedRecord.id
6010
+ );
6011
+ affectedRecords.push(...recordsPointingToChanged);
6012
+ }
6013
+ }
6014
+ const uniqueRecords = /* @__PURE__ */ new Map();
6015
+ for (const record of affectedRecords) {
6016
+ uniqueRecords.set(record.id, record);
6017
+ }
6018
+ return Array.from(uniqueRecords.values());
6019
+ }
6020
+ };
6021
+
5453
6022
  // src/runtime/services/user.service.ts
5454
6023
  var UserService = class {
5455
6024
  constructor(adapter, tenantId) {
@@ -5572,9 +6141,14 @@ var RecordService = class {
5572
6141
  constructor(adapter, tenantId, options) {
5573
6142
  this.adapter = adapter;
5574
6143
  this.tenantId = tenantId;
5575
- this.schemaService = new ObjectSchemaService(adapter, registry);
6144
+ this.schemaService = new ObjectSchemaService(adapter, registry, {
6145
+ userId: options?.userId,
6146
+ userEmail: options?.userEmail,
6147
+ auditService: options?.auditService
6148
+ });
5576
6149
  this.relationService = new RelationService(adapter, registry);
5577
6150
  this.userService = new UserService(adapter, tenantId);
6151
+ this.rollupService = new RollupService(adapter);
5578
6152
  this.hookRegistry = options?.hookRegistry ?? new NoopHookRegistry();
5579
6153
  this.permissionService = options?.permissionService;
5580
6154
  this.auditService = options?.auditService ?? (adapter.audit ? new AuditService(adapter, tenantId) : void 0);
@@ -5726,7 +6300,8 @@ var RecordService = class {
5726
6300
  objectId,
5727
6301
  data,
5728
6302
  label,
5729
- completionStatus
6303
+ completionStatus,
6304
+ metadata: options?.metadata
5730
6305
  });
5731
6306
  if (!options?.skipHooks) {
5732
6307
  const afterCtx = {
@@ -5736,6 +6311,7 @@ var RecordService = class {
5736
6311
  };
5737
6312
  await this.hookRegistry.execute("afterCreate", schema.name, afterCtx);
5738
6313
  }
6314
+ await this.recalculateParentRollups(record, schema);
5739
6315
  if (this.auditService && this.userId) {
5740
6316
  await this.auditService.logRecordAction({
5741
6317
  action: "record.created",
@@ -5839,11 +6415,15 @@ var RecordService = class {
5839
6415
  }
5840
6416
  const completionStatus = computeRecordStatus(schema, mergedData);
5841
6417
  const label = await this.computeLabel(schema, mergedData);
5842
- const updated = await this.adapter.objectRecords.update(recordId, {
6418
+ const updatePayload = {
5843
6419
  ...data,
5844
6420
  __completionStatus: completionStatus,
5845
6421
  __label: label
5846
- });
6422
+ };
6423
+ if (options?.metadata !== void 0) {
6424
+ updatePayload.__metadata = options.metadata;
6425
+ }
6426
+ const updated = await this.adapter.objectRecords.update(recordId, updatePayload);
5847
6427
  if (!options?.skipHooks) {
5848
6428
  const afterCtx = {
5849
6429
  ...hookCtx,
@@ -5851,6 +6431,7 @@ var RecordService = class {
5851
6431
  };
5852
6432
  await this.hookRegistry.execute("afterUpdate", schema.name, afterCtx);
5853
6433
  }
6434
+ await this.recalculateParentRollups(updated, schema);
5854
6435
  if (this.auditService && this.userId && changedAttributes.length > 0) {
5855
6436
  const changes = changedAttributes.map((attr) => ({
5856
6437
  field: attr,
@@ -5978,6 +6559,7 @@ var RecordService = class {
5978
6559
  if (!options?.skipHooks) {
5979
6560
  await this.hookRegistry.execute("afterDelete", schema.name, hookCtx);
5980
6561
  }
6562
+ await this.recalculateParentRollups(record, schema);
5981
6563
  if (this.auditService && this.userId) {
5982
6564
  await this.auditService.logRecordAction({
5983
6565
  action: "record.deleted",
@@ -6107,6 +6689,47 @@ var RecordService = class {
6107
6689
  }
6108
6690
  return records.map((record) => this.enrichWithFormulas(record, schema));
6109
6691
  }
6692
+ /**
6693
+ * Recalculate rollups after a record changes
6694
+ *
6695
+ * This handles three cases:
6696
+ * 1. The record itself has rollups (e.g., aggregating from related records it points to)
6697
+ * 2. Parent records have rollups that aggregate from this record (reverse pattern)
6698
+ * 3. Records that have forward rollups pointing to this record (forward pattern)
6699
+ *
6700
+ * @param record - The record that was modified
6701
+ * @param schema - Schema of the record's object
6702
+ * @internal
6703
+ */
6704
+ async recalculateParentRollups(record, schema) {
6705
+ const ownRollupAttrs = schema.attributes.filter(
6706
+ (a) => a.type === "rollup"
6707
+ );
6708
+ if (ownRollupAttrs.length > 0) {
6709
+ await this.rollupService.recalculateAndUpdate(record, schema);
6710
+ }
6711
+ const affectedParentIds = await this.rollupService.findAffectedParentRecords(record, schema);
6712
+ if (affectedParentIds.length > 0) {
6713
+ const parentRecords = await this.adapter.objectRecords.findByIds(affectedParentIds);
6714
+ for (const parentRecord of parentRecords) {
6715
+ const parentSchema = await this.schemaService.getObjectSchema(parentRecord.objectId);
6716
+ const rollupAttrs = parentSchema.attributes.filter(
6717
+ (a) => a.type === "rollup"
6718
+ );
6719
+ if (rollupAttrs.length > 0) {
6720
+ await this.rollupService.recalculateAndUpdate(parentRecord, parentSchema);
6721
+ }
6722
+ }
6723
+ }
6724
+ const affectedForwardRecords = await this.rollupService.findRecordsWithForwardRollup(
6725
+ record,
6726
+ schema
6727
+ );
6728
+ for (const forwardRecord of affectedForwardRecords) {
6729
+ const forwardSchema = await this.schemaService.getObjectSchema(forwardRecord.objectId);
6730
+ await this.rollupService.recalculateAndUpdate(forwardRecord, forwardSchema);
6731
+ }
6732
+ }
6110
6733
  /**
6111
6734
  * Permanently delete a record (hard delete)
6112
6735
  *
@@ -6380,7 +7003,7 @@ var RollupScheduler = class {
6380
7003
  clearTimeout(existing.timeout);
6381
7004
  }
6382
7005
  const timeout = setTimeout(async () => {
6383
- await this.executeRecalculation(parentId, parentObjectId, tenantId);
7006
+ await this.executeRecalculation(parentId, parentObjectId);
6384
7007
  this.pending.delete(key);
6385
7008
  }, this.debounceMs);
6386
7009
  this.pending.set(key, { parentId, parentObjectId, tenantId, timeout });
@@ -6407,19 +7030,19 @@ var RollupScheduler = class {
6407
7030
  const schema = await this.getSchemaById(record.objectId);
6408
7031
  const tenantId = tenantMap.get(record.id);
6409
7032
  if (schema && tenantId) {
6410
- await this.rollupService.recalculateAndUpdate(record, schema, tenantId);
7033
+ await this.rollupService.recalculateAndUpdate(record, schema);
6411
7034
  }
6412
7035
  }
6413
7036
  }
6414
7037
  /**
6415
7038
  * Execute a single recalculation
6416
7039
  */
6417
- async executeRecalculation(parentId, parentObjectId, tenantId) {
7040
+ async executeRecalculation(parentId, parentObjectId) {
6418
7041
  const record = await this.adapter.objectRecords.findById(parentId);
6419
7042
  if (!record) return;
6420
7043
  const schema = await this.getSchemaById(parentObjectId);
6421
7044
  if (!schema) return;
6422
- await this.rollupService.recalculateAndUpdate(record, schema, tenantId);
7045
+ await this.rollupService.recalculateAndUpdate(record, schema);
6423
7046
  }
6424
7047
  /**
6425
7048
  * Get number of pending recalculations
@@ -7221,17 +7844,22 @@ var NoopGeocodingAdapter = class {
7221
7844
  NoopHookRegistry,
7222
7845
  ObjectSchemaService,
7223
7846
  PermissionService,
7847
+ QueryBuilder,
7848
+ QueryMultipleResultsError,
7849
+ QueryNoResultError,
7224
7850
  RecordService,
7225
7851
  RelationResolverService,
7226
7852
  RelationService,
7227
7853
  RollupScheduler,
7228
7854
  RollupService,
7855
+ SHORTCUT_TO_FILTER_OPERATOR,
7229
7856
  UserProfileService,
7230
7857
  UserService,
7231
7858
  ViewService,
7232
7859
  buildAuditChanges,
7860
+ createDefaultState,
7233
7861
  createMockAdapter,
7234
- createRollupHooks,
7862
+ createQueryBuilder,
7235
7863
  enrichValuesWithSelectLabels,
7236
7864
  evaluateFormula,
7237
7865
  evaluateFormulaAttribute,
@@ -7244,6 +7872,8 @@ var NoopGeocodingAdapter = class {
7244
7872
  extractRelationReferences,
7245
7873
  flattenRelationsForEval,
7246
7874
  formatFormulaResult,
7875
+ formatRecord,
7876
+ formatRecords,
7247
7877
  getPathDepth,
7248
7878
  getRelationPath,
7249
7879
  getSyncPreview,
@@ -7253,7 +7883,6 @@ var NoopGeocodingAdapter = class {
7253
7883
  isLabelExpression,
7254
7884
  parsePath,
7255
7885
  pathHasManyCardinality,
7256
- registerAllRollupHooks,
7257
7886
  renderLabelExpression,
7258
7887
  resolveMultiplePaths,
7259
7888
  resolveSingleValue,