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

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
@@ -144,6 +144,8 @@ __export(index_exports, {
144
144
  GeocodingService: () => GeocodingService,
145
145
  GlobalSearchService: () => GlobalSearchService,
146
146
  GroupBuilder: () => GroupBuilder,
147
+ InvalidPathError: () => InvalidPathError,
148
+ MaxDepthExceededError: () => MaxDepthExceededError,
147
149
  NO_VALUE_OPERATORS: () => NO_VALUE_OPERATORS,
148
150
  NoopGeocodingAdapter: () => NoopGeocodingAdapter,
149
151
  NoopHookRegistry: () => NoopHookRegistry,
@@ -160,8 +162,11 @@ __export(index_exports, {
160
162
  RecordNotFoundError: () => RecordNotFoundError,
161
163
  RecordReferencedError: () => RecordReferencedError,
162
164
  RecordService: () => RecordService,
165
+ RelationResolverService: () => RelationResolverService,
163
166
  RelationService: () => RelationService,
164
167
  RoleNotFoundError: () => RoleNotFoundError,
168
+ RollupScheduler: () => RollupScheduler,
169
+ RollupService: () => RollupService,
165
170
  SYSTEM_RESOURCES: () => SYSTEM_RESOURCES,
166
171
  SYSTEM_RESOURCE_LABELS: () => SYSTEM_RESOURCE_LABELS,
167
172
  SchemaError: () => SchemaError,
@@ -186,6 +191,7 @@ __export(index_exports, {
186
191
  createDateValidator: () => createDateValidator,
187
192
  createDraftValidator: () => createDraftValidator,
188
193
  createFileValidator: () => createFileValidator,
194
+ createFormulaValidator: () => createFormulaValidator,
189
195
  createLocationValidator: () => createLocationValidator,
190
196
  createMockAdapter: () => createMockAdapter,
191
197
  createMultiRelationValidator: () => createMultiRelationValidator,
@@ -195,6 +201,8 @@ __export(index_exports, {
195
201
  createPhoneValidator: () => createPhoneValidator,
196
202
  createRatingValidator: () => createRatingValidator,
197
203
  createRelationValidator: () => createRelationValidator,
204
+ createRollupHooks: () => createRollupHooks,
205
+ createRollupValidator: () => createRollupValidator,
198
206
  createSelectValidator: () => createSelectValidator,
199
207
  createSingleRelationValidator: () => createSingleRelationValidator,
200
208
  createStatusValidator: () => createStatusValidator,
@@ -206,18 +214,34 @@ __export(index_exports, {
206
214
  date: () => date,
207
215
  dateConfigSchema: () => dateConfigSchema,
208
216
  enrichValuesWithSelectLabels: () => enrichValuesWithSelectLabels,
217
+ evaluateFormula: () => evaluateFormula,
218
+ evaluateFormulaAttribute: () => evaluateFormulaAttribute,
219
+ evaluateFormulaAttributeWithRelations: () => evaluateFormulaAttributeWithRelations,
220
+ evaluateFormulaWithRelations: () => evaluateFormulaWithRelations,
221
+ evaluateFormulaWithResult: () => evaluateFormulaWithResult,
209
222
  extractAttributeNames: () => extractAttributeNames,
223
+ extractFormulaVariables: () => extractFormulaVariables,
224
+ extractRelationNames: () => extractRelationNames,
225
+ extractRelationReferences: () => extractRelationReferences,
210
226
  file: () => file,
211
227
  fileConfigSchema: () => fileConfigSchema,
228
+ flattenRelationsForEval: () => flattenRelationsForEval,
212
229
  flow: () => flow,
213
230
  formatAttributeValue: () => formatAttributeValue,
231
+ formatFormulaResult: () => formatFormulaResult,
232
+ formula: () => formula,
233
+ formulaConfigSchema: () => formulaConfigSchema,
214
234
  generateId: () => generateId,
215
235
  generatePrefixedId: () => generatePrefixedId,
216
236
  getAttributeConfigSchema: () => getAttributeConfigSchema,
217
237
  getMissingRequiredAttributes: () => getMissingRequiredAttributes,
238
+ getPathDepth: () => getPathDepth,
239
+ getRelationPath: () => getRelationPath,
218
240
  getSyncPreview: () => getSyncPreview,
241
+ getTargetAttributeName: () => getTargetAttributeName,
219
242
  getViewSyncPreview: () => getViewSyncPreview,
220
243
  group: () => group,
244
+ hasRelationReferences: () => hasRelationReferences,
221
245
  isActivityTab: () => isActivityTab,
222
246
  isAdvancedFilterState: () => isAdvancedFilterState,
223
247
  isCustomTab: () => isCustomTab,
@@ -243,14 +267,21 @@ __export(index_exports, {
243
267
  numberConfigSchema: () => numberConfigSchema,
244
268
  object: () => object,
245
269
  parseAttributeConfig: () => parseAttributeConfig,
270
+ parsePath: () => parsePath,
271
+ pathHasManyCardinality: () => pathHasManyCardinality,
246
272
  phone: () => phone,
247
273
  phoneConfigSchema: () => phoneConfigSchema,
248
274
  rating: () => rating,
249
275
  ratingConfigSchema: () => ratingConfigSchema,
276
+ registerAllRollupHooks: () => registerAllRollupHooks,
250
277
  registry: () => registry,
251
278
  relation: () => relation,
252
279
  relationConfigSchema: () => relationConfigSchema,
253
280
  renderLabelExpression: () => renderLabelExpression,
281
+ resolveMultiplePaths: () => resolveMultiplePaths,
282
+ resolveSingleValue: () => resolveSingleValue,
283
+ rollup: () => rollup,
284
+ rollupConfigSchema: () => rollupConfigSchema,
254
285
  safeParseAttributeConfig: () => safeParseAttributeConfig,
255
286
  select: () => select,
256
287
  selectConfigSchema: () => selectConfigSchema,
@@ -267,14 +298,17 @@ __export(index_exports, {
267
298
  timestampConfigSchema: () => timestampConfigSchema,
268
299
  toAdvancedFilterState: () => toAdvancedFilterState,
269
300
  toSimpleFilterState: () => toSimpleFilterState,
301
+ traversePath: () => traversePath,
270
302
  user: () => user,
271
303
  userConfigSchema: () => userConfigSchema,
272
304
  validateAttribute: () => validateAttribute,
273
305
  validateAttributeConfig: () => validateAttributeConfig,
274
306
  validateDraft: () => validateDraft,
275
307
  validateDraftOrThrow: () => validateDraftOrThrow,
308
+ validateFormulaExpression: () => validateFormulaExpression,
276
309
  validateObject: () => validateObject,
277
310
  validateObjectOrThrow: () => validateObjectOrThrow,
311
+ validatePath: () => validatePath,
278
312
  verifyNativeObjectsSync: () => verifyNativeObjectsSync,
279
313
  verifyNativeViewsSync: () => verifyNativeViewsSync,
280
314
  view: () => view,
@@ -397,7 +431,24 @@ var OPERATORS_BY_TYPE = {
397
431
  file: ["is_empty", "is_not_empty"],
398
432
  user: ["contains", "not_contains", "is_empty", "is_not_empty"],
399
433
  relation: ["any_of", "none_of", "contains", "not_contains", "is_empty", "is_not_empty"],
400
- rating: ["eq", "neq", "lt", "gt", "lte", "gte", "is_empty", "is_not_empty"]
434
+ rating: ["eq", "neq", "lt", "gt", "lte", "gte", "is_empty", "is_not_empty"],
435
+ // Formula: operators depend on returnType, but we provide a general set
436
+ formula: [
437
+ "is",
438
+ "is_not",
439
+ "eq",
440
+ "neq",
441
+ "lt",
442
+ "gt",
443
+ "lte",
444
+ "gte",
445
+ "contains",
446
+ "not_contains",
447
+ "is_empty",
448
+ "is_not_empty"
449
+ ],
450
+ // Rollup: typically numeric aggregations
451
+ rollup: ["eq", "neq", "lt", "gt", "lte", "gte", "is_empty", "is_not_empty"]
401
452
  };
402
453
  var NO_VALUE_OPERATORS = [
403
454
  "is_empty",
@@ -956,7 +1007,9 @@ var BaseAttributeBuilder = class {
956
1007
  return this;
957
1008
  }
958
1009
  defaultValue(value) {
959
- this.attr.defaultValue = value;
1010
+ if ("defaultValue" in this.attr || !["formula", "rollup"].includes(this.attr.type)) {
1011
+ this.attr.defaultValue = value;
1012
+ }
960
1013
  return this;
961
1014
  }
962
1015
  metadata(value) {
@@ -1498,6 +1551,105 @@ var RatingAttributeBuilder = class extends BaseAttributeBuilder {
1498
1551
  function rating(config) {
1499
1552
  return new RatingAttributeBuilder(config.name, config.label);
1500
1553
  }
1554
+ var FormulaAttributeBuilder = class extends BaseAttributeBuilder {
1555
+ constructor(name, label) {
1556
+ super("formula", name, label);
1557
+ this.attr.required = false;
1558
+ }
1559
+ /**
1560
+ * Set the formula expression
1561
+ * @param expr - Expression to evaluate (e.g., "price * quantity")
1562
+ */
1563
+ expression(expr) {
1564
+ this.attr.expression = expr;
1565
+ return this;
1566
+ }
1567
+ /**
1568
+ * Set the expected return type for formatting
1569
+ * @param type - Return type: "text", "number", "boolean", or "date"
1570
+ */
1571
+ returns(type) {
1572
+ this.attr.returnType = type;
1573
+ return this;
1574
+ }
1575
+ /**
1576
+ * Set decimal places for number results
1577
+ */
1578
+ decimals(value) {
1579
+ this.attr.decimals = value;
1580
+ return this;
1581
+ }
1582
+ // Override to prevent making formulas required
1583
+ // biome-ignore lint/suspicious/noExplicitAny: intentional override
1584
+ required() {
1585
+ console.warn("Formula attributes cannot be required (they are read-only)");
1586
+ return this;
1587
+ }
1588
+ // biome-ignore lint/suspicious/noExplicitAny: intentional override
1589
+ optional() {
1590
+ return this;
1591
+ }
1592
+ };
1593
+ function formula(config) {
1594
+ return new FormulaAttributeBuilder(config.name, config.label);
1595
+ }
1596
+ var RollupAttributeBuilder = class extends BaseAttributeBuilder {
1597
+ constructor(name, label) {
1598
+ super("rollup", name, label);
1599
+ this.attr.required = false;
1600
+ }
1601
+ /**
1602
+ * Set the relation attribute to aggregate from
1603
+ * @param relationName - Name of the relation attribute on this object
1604
+ */
1605
+ from(relationName) {
1606
+ this.attr.relationAttribute = relationName;
1607
+ return this;
1608
+ }
1609
+ /**
1610
+ * Set the target attribute to aggregate
1611
+ * @param attrName - Name of the attribute on the related object
1612
+ */
1613
+ aggregate(attrName) {
1614
+ this.attr.targetAttribute = attrName;
1615
+ return this;
1616
+ }
1617
+ /**
1618
+ * Set the aggregation function
1619
+ */
1620
+ using(fn) {
1621
+ this.attr.function = fn;
1622
+ return this;
1623
+ }
1624
+ /**
1625
+ * Set decimal places for numeric results
1626
+ */
1627
+ decimals(value) {
1628
+ this.attr.decimals = value;
1629
+ return this;
1630
+ }
1631
+ /**
1632
+ * Set multi-level path for traversing nested relations (Phase 4+)
1633
+ * @param path - Dot notation path (e.g., "orders.items")
1634
+ */
1635
+ path(relationPath) {
1636
+ this.attr.relationPath = relationPath;
1637
+ return this;
1638
+ }
1639
+ // Override to prevent making rollups required
1640
+ // biome-ignore lint/suspicious/noExplicitAny: intentional override
1641
+ required() {
1642
+ console.warn("Rollup attributes cannot be required (they are read-only)");
1643
+ return this;
1644
+ }
1645
+ // biome-ignore lint/suspicious/noExplicitAny: intentional override
1646
+ optional() {
1647
+ return this;
1648
+ }
1649
+ };
1650
+ function rollup(config) {
1651
+ return new RollupAttributeBuilder(config.name, config.label);
1652
+ }
1501
1653
 
1502
1654
  // src/builders/flow-builder.ts
1503
1655
  var import_zod = require("zod");
@@ -2794,6 +2946,30 @@ var ratingConfigSchema = baseConfigSchema.extend({
2794
2946
  max: import_zod4.z.number().int().min(1).optional(),
2795
2947
  iconType: import_zod4.z.enum(["star", "heart", "thumbs", "number"]).optional()
2796
2948
  });
2949
+ var formulaConfigSchema = baseConfigSchema.extend({
2950
+ expression: import_zod4.z.string().min(1),
2951
+ returnType: import_zod4.z.enum(["text", "number", "boolean", "date"]),
2952
+ decimals: import_zod4.z.number().int().min(0).max(10).optional(),
2953
+ allowRelations: import_zod4.z.boolean().optional()
2954
+ });
2955
+ var rollupConfigSchema = baseConfigSchema.extend({
2956
+ relationAttribute: import_zod4.z.string().min(1).optional(),
2957
+ relationPath: import_zod4.z.string().optional(),
2958
+ targetAttribute: import_zod4.z.string().min(1),
2959
+ function: import_zod4.z.enum([
2960
+ "sum",
2961
+ "count",
2962
+ "avg",
2963
+ "min",
2964
+ "max",
2965
+ "countValues",
2966
+ "countEmpty",
2967
+ "percentEmpty",
2968
+ "percentFilled"
2969
+ ]),
2970
+ decimals: import_zod4.z.number().int().min(0).max(10).optional(),
2971
+ materialize: import_zod4.z.boolean().optional()
2972
+ });
2797
2973
  var attributeConfigSchemas = {
2798
2974
  text: textConfigSchema,
2799
2975
  textarea: textareaConfigSchema,
@@ -2810,7 +2986,9 @@ var attributeConfigSchemas = {
2810
2986
  file: fileConfigSchema,
2811
2987
  user: userConfigSchema,
2812
2988
  relation: relationConfigSchema,
2813
- rating: ratingConfigSchema
2989
+ rating: ratingConfigSchema,
2990
+ formula: formulaConfigSchema,
2991
+ rollup: rollupConfigSchema
2814
2992
  };
2815
2993
  function getAttributeConfigSchema(type) {
2816
2994
  return attributeConfigSchemas[type];
@@ -2983,6 +3161,12 @@ function createRatingValidator(attr) {
2983
3161
  }
2984
3162
  return schema;
2985
3163
  }
3164
+ function createFormulaValidator(_attr) {
3165
+ return import_zod4.z.unknown();
3166
+ }
3167
+ function createRollupValidator(_attr) {
3168
+ return import_zod4.z.unknown();
3169
+ }
2986
3170
  function createAttributeValidator(attr) {
2987
3171
  switch (attr.type) {
2988
3172
  case "text":
@@ -3015,6 +3199,10 @@ function createAttributeValidator(attr) {
3015
3199
  return createRelationValidator(attr);
3016
3200
  case "rating":
3017
3201
  return createRatingValidator(attr);
3202
+ case "formula":
3203
+ return createFormulaValidator(attr);
3204
+ case "rollup":
3205
+ return createRollupValidator(attr);
3018
3206
  default:
3019
3207
  return import_zod4.z.unknown();
3020
3208
  }
@@ -3140,12 +3328,644 @@ function computeRecordStatus(objectDef, data) {
3140
3328
  return isRecordComplete(objectDef, data) ? "complete" : "draft";
3141
3329
  }
3142
3330
 
3331
+ // src/runtime/formula/evaluator.ts
3332
+ var import_expr_eval = require("expr-eval");
3333
+ function createFormulaParser() {
3334
+ const parser = new import_expr_eval.Parser();
3335
+ parser.functions.IF = (condition, thenValue, elseValue) => condition ? thenValue : elseValue;
3336
+ parser.functions.AND = (...args) => args.every(Boolean);
3337
+ parser.functions.OR = (...args) => args.some(Boolean);
3338
+ parser.functions.NOT = (value) => !value;
3339
+ parser.functions.EMPTY = (value) => value === null || value === void 0 || value === "";
3340
+ parser.functions.COALESCE = (...args) => args.find((a) => a != null) ?? null;
3341
+ parser.functions.DEFAULT = (value, defaultValue) => value == null || value === "" ? defaultValue : value;
3342
+ parser.functions.CONCAT = (...args) => args.filter((a) => a != null).map(String).join("");
3343
+ parser.functions.UPPER = (value) => String(value ?? "").toUpperCase();
3344
+ parser.functions.LOWER = (value) => String(value ?? "").toLowerCase();
3345
+ parser.functions.TRIM = (value) => String(value ?? "").trim();
3346
+ parser.functions.LENGTH = (value) => String(value ?? "").length;
3347
+ parser.functions.LEFT = (value, count) => String(value ?? "").slice(0, count);
3348
+ parser.functions.RIGHT = (value, count) => String(value ?? "").slice(-count);
3349
+ parser.functions.REPLACE = (value, search, replacement) => String(value ?? "").replace(new RegExp(search, "g"), replacement);
3350
+ parser.functions.CONTAINS = (value, search) => String(value ?? "").toLowerCase().includes(String(search).toLowerCase());
3351
+ parser.functions.ROUND = (value, decimals = 0) => {
3352
+ if (typeof value !== "number" || Number.isNaN(value)) return null;
3353
+ const factor = 10 ** decimals;
3354
+ return Math.round(value * factor) / factor;
3355
+ };
3356
+ parser.functions.FLOOR = (value) => {
3357
+ if (typeof value !== "number" || Number.isNaN(value)) return null;
3358
+ return Math.floor(value);
3359
+ };
3360
+ parser.functions.CEIL = (value) => {
3361
+ if (typeof value !== "number" || Number.isNaN(value)) return null;
3362
+ return Math.ceil(value);
3363
+ };
3364
+ parser.functions.ABS = (value) => {
3365
+ if (typeof value !== "number" || Number.isNaN(value)) return null;
3366
+ return Math.abs(value);
3367
+ };
3368
+ parser.functions.MIN = (...args) => {
3369
+ const nums = args.filter((n) => typeof n === "number" && !Number.isNaN(n));
3370
+ return nums.length > 0 ? Math.min(...nums) : null;
3371
+ };
3372
+ parser.functions.MAX = (...args) => {
3373
+ const nums = args.filter((n) => typeof n === "number" && !Number.isNaN(n));
3374
+ return nums.length > 0 ? Math.max(...nums) : null;
3375
+ };
3376
+ parser.functions.POW = (base, exponent) => {
3377
+ if (typeof base !== "number" || typeof exponent !== "number") return null;
3378
+ return base ** exponent;
3379
+ };
3380
+ parser.functions.MOD = (a, b) => {
3381
+ if (typeof a !== "number" || typeof b !== "number" || b === 0) return null;
3382
+ return a % b;
3383
+ };
3384
+ parser.functions.NOW = () => (/* @__PURE__ */ new Date()).toISOString();
3385
+ parser.functions.TODAY = () => (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
3386
+ parser.functions.YEAR = (value) => {
3387
+ const date2 = parseDate(value);
3388
+ return date2 ? date2.getFullYear() : null;
3389
+ };
3390
+ parser.functions.MONTH = (value) => {
3391
+ const date2 = parseDate(value);
3392
+ return date2 ? date2.getMonth() + 1 : null;
3393
+ };
3394
+ parser.functions.DAY = (value) => {
3395
+ const date2 = parseDate(value);
3396
+ return date2 ? date2.getDate() : null;
3397
+ };
3398
+ parser.functions.DATE_DIFF = (date1, date2, unit = "days") => {
3399
+ const d1 = parseDate(date1);
3400
+ const d2 = parseDate(date2);
3401
+ if (d1 === null || d2 === null) return null;
3402
+ const diffMs = d1.getTime() - d2.getTime();
3403
+ const MS_PER_DAY = 1e3 * 60 * 60 * 24;
3404
+ const conversions = {
3405
+ years: MS_PER_DAY * 365,
3406
+ months: MS_PER_DAY * 30,
3407
+ weeks: MS_PER_DAY * 7,
3408
+ days: MS_PER_DAY,
3409
+ hours: 1e3 * 60 * 60,
3410
+ minutes: 1e3 * 60
3411
+ };
3412
+ const divisor = conversions[unit] ?? MS_PER_DAY;
3413
+ return diffMs / divisor;
3414
+ };
3415
+ return parser;
3416
+ }
3417
+ function parseDate(value) {
3418
+ if (!value) return null;
3419
+ if (value instanceof Date) return value;
3420
+ if (typeof value === "string" || typeof value === "number") {
3421
+ const date2 = new Date(value);
3422
+ return Number.isNaN(date2.getTime()) ? null : date2;
3423
+ }
3424
+ return null;
3425
+ }
3426
+ var formulaParser = createFormulaParser();
3427
+ function evaluateFormula(expression, values) {
3428
+ try {
3429
+ const parsed = formulaParser.parse(expression);
3430
+ return parsed.evaluate(values);
3431
+ } catch {
3432
+ return null;
3433
+ }
3434
+ }
3435
+ function evaluateFormulaWithResult(expression, values) {
3436
+ try {
3437
+ const parsed = formulaParser.parse(expression);
3438
+ const value = parsed.evaluate(values);
3439
+ return { value };
3440
+ } catch (error) {
3441
+ return {
3442
+ value: null,
3443
+ error: error instanceof Error ? error.message : "Unknown error"
3444
+ };
3445
+ }
3446
+ }
3447
+ function formatFormulaResult(value, returnType, decimals) {
3448
+ if (value === null || value === void 0) {
3449
+ return null;
3450
+ }
3451
+ switch (returnType) {
3452
+ case "number":
3453
+ return formatNumberResult(value, decimals);
3454
+ case "boolean":
3455
+ return Boolean(value);
3456
+ case "date":
3457
+ return formatDateResult(value);
3458
+ case "text":
3459
+ return String(value);
3460
+ }
3461
+ }
3462
+ function formatNumberResult(value, decimals) {
3463
+ const num = typeof value === "number" ? value : Number(value);
3464
+ if (Number.isNaN(num)) return null;
3465
+ return decimals !== void 0 ? Number(num.toFixed(decimals)) : num;
3466
+ }
3467
+ function formatDateResult(value) {
3468
+ if (value instanceof Date) return value.toISOString();
3469
+ if (typeof value === "string") return value;
3470
+ return null;
3471
+ }
3472
+ function evaluateFormulaAttribute(attr, values) {
3473
+ const raw = evaluateFormula(attr.expression, values);
3474
+ return formatFormulaResult(raw, attr.returnType, attr.decimals);
3475
+ }
3476
+ function validateFormulaExpression(expression) {
3477
+ try {
3478
+ formulaParser.parse(expression);
3479
+ return { valid: true };
3480
+ } catch (error) {
3481
+ return {
3482
+ valid: false,
3483
+ error: error instanceof Error ? error.message : "Invalid expression"
3484
+ };
3485
+ }
3486
+ }
3487
+ function extractFormulaVariables(expression) {
3488
+ try {
3489
+ const parsed = formulaParser.parse(expression);
3490
+ return parsed.variables();
3491
+ } catch {
3492
+ return [];
3493
+ }
3494
+ }
3495
+ var RELATION_REF_PATTERN = /\b([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)\b/g;
3496
+ function extractRelationReferences(expression) {
3497
+ const regex = new RegExp(RELATION_REF_PATTERN.source, "g");
3498
+ const matches = expression.matchAll(regex);
3499
+ return [...matches].map((m) => m[0]);
3500
+ }
3501
+ function extractRelationNames(expression) {
3502
+ const refs = extractRelationReferences(expression);
3503
+ const names = refs.map((ref) => ref.split(".")[0]);
3504
+ return [...new Set(names)];
3505
+ }
3506
+ function hasRelationReferences(expression) {
3507
+ const regex = new RegExp(RELATION_REF_PATTERN.source);
3508
+ return regex.test(expression);
3509
+ }
3510
+ function flattenRelationsForEval(resolvedRelations) {
3511
+ const flat = {};
3512
+ for (const [relationName, values] of Object.entries(resolvedRelations)) {
3513
+ for (const [key, value] of Object.entries(values)) {
3514
+ flat[`${relationName}.${key}`] = value;
3515
+ }
3516
+ }
3517
+ return flat;
3518
+ }
3519
+ async function evaluateFormulaWithRelations(expression, record, schema, resolver) {
3520
+ const relationNames = extractRelationNames(expression);
3521
+ if (relationNames.length === 0) {
3522
+ return evaluateFormula(expression, record.values);
3523
+ }
3524
+ const resolvedRelations = await resolver.resolveRelationValues(record, schema, relationNames);
3525
+ const flattenedRelations = flattenRelationsForEval(resolvedRelations);
3526
+ const allValues = {
3527
+ ...record.values,
3528
+ ...flattenedRelations
3529
+ };
3530
+ return evaluateFormula(expression, allValues);
3531
+ }
3532
+ async function evaluateFormulaAttributeWithRelations(attr, record, schema, resolver) {
3533
+ const raw = await evaluateFormulaWithRelations(attr.expression, record, schema, resolver);
3534
+ return formatFormulaResult(raw, attr.returnType, attr.decimals);
3535
+ }
3536
+
3537
+ // src/runtime/formula/path-parser.ts
3538
+ var InvalidPathError = class extends Error {
3539
+ constructor(path, segment, reason) {
3540
+ super(`Invalid path "${path}" at "${segment}": ${reason}`);
3541
+ this.path = path;
3542
+ this.segment = segment;
3543
+ this.reason = reason;
3544
+ this.name = "InvalidPathError";
3545
+ }
3546
+ };
3547
+ var MaxDepthExceededError = class extends Error {
3548
+ constructor(path, maxDepth) {
3549
+ super(`Path "${path}" exceeds maximum depth of ${maxDepth}`);
3550
+ this.path = path;
3551
+ this.maxDepth = maxDepth;
3552
+ this.name = "MaxDepthExceededError";
3553
+ }
3554
+ };
3555
+ async function parsePath(path, startSchema, getSchema, maxDepth = 5) {
3556
+ const segments = path.split(".");
3557
+ if (segments.length === 0 || segments.length === 1 && segments[0] === "") {
3558
+ throw new InvalidPathError(path, path, "Path cannot be empty");
3559
+ }
3560
+ if (segments.length > maxDepth) {
3561
+ throw new MaxDepthExceededError(path, maxDepth);
3562
+ }
3563
+ const result = [];
3564
+ let currentSchema = startSchema;
3565
+ for (let i = 0; i < segments.length; i++) {
3566
+ const segmentName = segments[i];
3567
+ const isLastSegment = i === segments.length - 1;
3568
+ const attr = currentSchema.attributes.find((a) => a.name === segmentName);
3569
+ if (!attr) {
3570
+ throw new InvalidPathError(
3571
+ path,
3572
+ segmentName,
3573
+ `Attribute "${segmentName}" not found in object "${currentSchema.name}"`
3574
+ );
3575
+ }
3576
+ if (attr.type === "relation") {
3577
+ const relationAttr = attr;
3578
+ const targetObject = relationAttr.targets[0]?.object;
3579
+ if (!targetObject) {
3580
+ throw new InvalidPathError(path, segmentName, "Relation has no target object");
3581
+ }
3582
+ result.push({
3583
+ name: segmentName,
3584
+ type: "relation",
3585
+ cardinality: relationAttr.cardinality,
3586
+ targetObject
3587
+ });
3588
+ if (!isLastSegment) {
3589
+ const nextSchema = await getSchema(targetObject);
3590
+ if (!nextSchema) {
3591
+ throw new InvalidPathError(
3592
+ path,
3593
+ segmentName,
3594
+ `Target object "${targetObject}" schema not found`
3595
+ );
3596
+ }
3597
+ currentSchema = nextSchema;
3598
+ }
3599
+ } else {
3600
+ if (!isLastSegment) {
3601
+ throw new InvalidPathError(
3602
+ path,
3603
+ segmentName,
3604
+ `"${segmentName}" is not a relation but has segments after it`
3605
+ );
3606
+ }
3607
+ result.push({
3608
+ name: segmentName,
3609
+ type: "attribute"
3610
+ });
3611
+ }
3612
+ }
3613
+ return result;
3614
+ }
3615
+ async function validatePath(path, startSchema, getSchema, maxDepth = 5) {
3616
+ try {
3617
+ await parsePath(path, startSchema, getSchema, maxDepth);
3618
+ return true;
3619
+ } catch {
3620
+ return false;
3621
+ }
3622
+ }
3623
+ function pathHasManyCardinality(segments) {
3624
+ return segments.some((s) => s.type === "relation" && s.cardinality === "many");
3625
+ }
3626
+ function getPathDepth(segments) {
3627
+ return segments.filter((s) => s.type === "relation").length;
3628
+ }
3629
+ function getTargetAttributeName(path) {
3630
+ const parts = path.split(".");
3631
+ return parts[parts.length - 1];
3632
+ }
3633
+ function getRelationPath(path) {
3634
+ const parts = path.split(".");
3635
+ if (parts.length <= 1) return null;
3636
+ return parts.slice(0, -1).join(".");
3637
+ }
3638
+
3639
+ // src/runtime/formula/path-traversal.ts
3640
+ async function traversePath(record, path, startSchemaName, adapter, getSchema, options) {
3641
+ const maxDepth = options?.maxDepth ?? 5;
3642
+ const startSchema = await getSchema(startSchemaName);
3643
+ if (!startSchema) {
3644
+ return { values: [], recordCounts: [0] };
3645
+ }
3646
+ const segments = await parsePath(path, startSchema, getSchema, maxDepth);
3647
+ if (segments.length > maxDepth) {
3648
+ throw new MaxDepthExceededError(path, maxDepth);
3649
+ }
3650
+ return await executeTraversal([record], segments, adapter, 0);
3651
+ }
3652
+ async function executeTraversal(records, segments, adapter, depth) {
3653
+ const recordCounts = [records.length];
3654
+ if (depth >= segments.length || records.length === 0) {
3655
+ return { values: [], recordCounts };
3656
+ }
3657
+ const segment = segments[depth];
3658
+ const isLastSegment = depth === segments.length - 1;
3659
+ if (segment.type === "attribute") {
3660
+ const values = records.map((r) => r.values[segment.name]).filter((v) => v !== void 0);
3661
+ return { values, recordCounts };
3662
+ }
3663
+ const relatedIds = collectRelatedIds(records, segment.name);
3664
+ if (relatedIds.length === 0) {
3665
+ return { values: [], recordCounts };
3666
+ }
3667
+ const relatedRecords = await adapter.objectRecords.findByIds(relatedIds);
3668
+ recordCounts.push(relatedRecords.length);
3669
+ if (isLastSegment) {
3670
+ return { values: relatedIds, recordCounts };
3671
+ }
3672
+ const nestedResult = await executeTraversal(relatedRecords, segments, adapter, depth + 1);
3673
+ return {
3674
+ values: nestedResult.values,
3675
+ recordCounts: [...recordCounts, ...nestedResult.recordCounts.slice(1)]
3676
+ };
3677
+ }
3678
+ function collectRelatedIds(records, relationName) {
3679
+ const ids = [];
3680
+ for (const record of records) {
3681
+ const value = record.values[relationName];
3682
+ if (typeof value === "string" && value.length > 0) {
3683
+ ids.push(value);
3684
+ } else if (Array.isArray(value)) {
3685
+ for (const id of value) {
3686
+ if (typeof id === "string" && id.length > 0) {
3687
+ ids.push(id);
3688
+ }
3689
+ }
3690
+ }
3691
+ }
3692
+ return [...new Set(ids)];
3693
+ }
3694
+ async function resolveSingleValue(record, path, startSchemaName, adapter, getSchema) {
3695
+ const result = await traversePath(record, path, startSchemaName, adapter, getSchema);
3696
+ return result.values[0] ?? null;
3697
+ }
3698
+ async function resolveMultiplePaths(record, paths, startSchemaName, adapter, getSchema) {
3699
+ const results = /* @__PURE__ */ new Map();
3700
+ await Promise.all(
3701
+ paths.map(async (path) => {
3702
+ const result = await traversePath(record, path, startSchemaName, adapter, getSchema);
3703
+ results.set(path, result.values);
3704
+ })
3705
+ );
3706
+ return results;
3707
+ }
3708
+
3143
3709
  // src/runtime/hooks/hook.registry.ts
3144
3710
  var NoopHookRegistry = class {
3145
3711
  async execute() {
3146
3712
  }
3147
3713
  };
3148
3714
 
3715
+ // src/runtime/services/rollup.service.ts
3716
+ var RollupService = class {
3717
+ constructor(adapter) {
3718
+ this.adapter = adapter;
3719
+ }
3720
+ /**
3721
+ * Calculate a rollup value for a record
3722
+ *
3723
+ * @param recordId - ID of the parent record
3724
+ * @param rollupAttr - Rollup attribute definition
3725
+ * @param schema - Schema of the parent object
3726
+ * @returns Computed rollup value
3727
+ *
3728
+ * @example
3729
+ * ```typescript
3730
+ * // Sum all order amounts for a company
3731
+ * const totalOrders = await rollupService.calculate(
3732
+ * "company-123",
3733
+ * {
3734
+ * type: "rollup",
3735
+ * name: "totalOrders",
3736
+ * relationAttribute: "orders",
3737
+ * targetAttribute: "amount",
3738
+ * function: "sum",
3739
+ * ...
3740
+ * },
3741
+ * companySchema
3742
+ * );
3743
+ * ```
3744
+ */
3745
+ async calculate(recordId, rollupAttr, schema, tenantId) {
3746
+ const relationAttr = schema.attributes.find(
3747
+ (a) => a.type === "relation" && a.name === rollupAttr.relationAttribute
3748
+ );
3749
+ if (!relationAttr) {
3750
+ return { value: null, recordCount: 0 };
3751
+ }
3752
+ const targetObjectName = relationAttr.targets[0]?.object;
3753
+ if (!targetObjectName) {
3754
+ return { value: null, recordCount: 0 };
3755
+ }
3756
+ const targetObject = await this.adapter.objects.findByName(tenantId, targetObjectName);
3757
+ if (!targetObject) {
3758
+ return { value: null, recordCount: 0 };
3759
+ }
3760
+ const relatedRecords = await this.adapter.objectRecords.findByRelation(
3761
+ tenantId,
3762
+ targetObject.id,
3763
+ rollupAttr.relationAttribute,
3764
+ recordId
3765
+ );
3766
+ if (relatedRecords.length === 0) {
3767
+ return { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 };
3768
+ }
3769
+ const values = relatedRecords.map((r) => r.values[rollupAttr.targetAttribute]).filter((v) => v !== void 0);
3770
+ const aggregated = this.aggregate(values, rollupAttr.function);
3771
+ const result = typeof aggregated === "number" && rollupAttr.decimals !== void 0 ? Number(aggregated.toFixed(rollupAttr.decimals)) : aggregated;
3772
+ return { value: result, recordCount: relatedRecords.length };
3773
+ }
3774
+ /**
3775
+ * Calculate rollup values for multiple records (batched)
3776
+ *
3777
+ * More efficient than calling calculate() for each record individually.
3778
+ */
3779
+ async calculateForMany(recordIds, rollupAttr, schema, tenantId) {
3780
+ const results = /* @__PURE__ */ new Map();
3781
+ for (const id of recordIds) {
3782
+ results.set(id, { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 });
3783
+ }
3784
+ await Promise.all(
3785
+ recordIds.map(async (id) => {
3786
+ const result = await this.calculate(id, rollupAttr, schema, tenantId);
3787
+ results.set(id, result);
3788
+ })
3789
+ );
3790
+ return results;
3791
+ }
3792
+ /**
3793
+ * Apply aggregation function to a set of values
3794
+ */
3795
+ aggregate(values, fn) {
3796
+ switch (fn) {
3797
+ case "sum":
3798
+ return this.sumNumbers(values);
3799
+ case "avg":
3800
+ return this.averageNumbers(values);
3801
+ case "min":
3802
+ return this.minNumbers(values);
3803
+ case "max":
3804
+ return this.maxNumbers(values);
3805
+ case "count":
3806
+ return values.length;
3807
+ case "countValues":
3808
+ return values.filter((v) => v != null && v !== "").length;
3809
+ case "countEmpty":
3810
+ return values.filter((v) => v == null || v === "").length;
3811
+ case "percentFilled": {
3812
+ if (values.length === 0) return 0;
3813
+ const filled = values.filter((v) => v != null && v !== "").length;
3814
+ return filled / values.length * 100;
3815
+ }
3816
+ case "percentEmpty": {
3817
+ if (values.length === 0) return 0;
3818
+ const empty = values.filter((v) => v == null || v === "").length;
3819
+ return empty / values.length * 100;
3820
+ }
3821
+ }
3822
+ }
3823
+ /**
3824
+ * Get the default empty value for a rollup function
3825
+ */
3826
+ getEmptyValue(fn) {
3827
+ switch (fn) {
3828
+ case "sum":
3829
+ case "count":
3830
+ case "countValues":
3831
+ case "countEmpty":
3832
+ case "percentEmpty":
3833
+ case "percentFilled":
3834
+ return 0;
3835
+ case "avg":
3836
+ case "min":
3837
+ case "max":
3838
+ return null;
3839
+ }
3840
+ }
3841
+ /**
3842
+ * Sum numeric values
3843
+ */
3844
+ sumNumbers(values) {
3845
+ return values.filter((v) => typeof v === "number" && !Number.isNaN(v)).reduce((sum, n) => sum + n, 0);
3846
+ }
3847
+ /**
3848
+ * Average numeric values
3849
+ */
3850
+ averageNumbers(values) {
3851
+ const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
3852
+ if (nums.length === 0) return null;
3853
+ return nums.reduce((sum, n) => sum + n, 0) / nums.length;
3854
+ }
3855
+ /**
3856
+ * Minimum numeric value
3857
+ */
3858
+ minNumbers(values) {
3859
+ const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
3860
+ if (nums.length === 0) return null;
3861
+ return Math.min(...nums);
3862
+ }
3863
+ /**
3864
+ * Maximum numeric value
3865
+ */
3866
+ maxNumbers(values) {
3867
+ const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
3868
+ if (nums.length === 0) return null;
3869
+ return Math.max(...nums);
3870
+ }
3871
+ /**
3872
+ * Recalculate all rollup attributes for a record and update it
3873
+ *
3874
+ * Called after related records change to keep rollups up-to-date.
3875
+ */
3876
+ async recalculateAndUpdate(record, schema, tenantId) {
3877
+ const rollupAttrs = schema.attributes.filter((a) => a.type === "rollup");
3878
+ if (rollupAttrs.length === 0) {
3879
+ return record;
3880
+ }
3881
+ const updates = {};
3882
+ for (const attr of rollupAttrs) {
3883
+ const result = await this.calculate(record.id, attr, schema, tenantId);
3884
+ updates[attr.name] = result.value;
3885
+ }
3886
+ return await this.adapter.objectRecords.update(record.id, updates);
3887
+ }
3888
+ /**
3889
+ * Find parent records that need rollup recalculation when a child record changes
3890
+ *
3891
+ * Used by hooks to determine which parent records to recalculate after
3892
+ * a child record is created, updated, or deleted.
3893
+ *
3894
+ * @param changedRecord - The record that was modified
3895
+ * @param changedSchema - Schema of the changed record's object
3896
+ * @returns Array of parent record IDs that need recalculation
3897
+ */
3898
+ async findAffectedParentRecords(changedRecord, changedSchema) {
3899
+ const affectedIds = [];
3900
+ const relationAttrs = changedSchema.attributes.filter(
3901
+ (a) => a.type === "relation"
3902
+ );
3903
+ for (const attr of relationAttrs) {
3904
+ const relatedId = changedRecord.values[attr.name];
3905
+ if (typeof relatedId === "string" && relatedId.length > 0) {
3906
+ affectedIds.push(relatedId);
3907
+ } else if (Array.isArray(relatedId)) {
3908
+ affectedIds.push(...relatedId.filter((id) => typeof id === "string"));
3909
+ }
3910
+ }
3911
+ return [...new Set(affectedIds)];
3912
+ }
3913
+ };
3914
+
3915
+ // src/runtime/hooks/rollup.hooks.ts
3916
+ function createRollupHooks(objectName, config) {
3917
+ const rollupService = new RollupService(config.adapter);
3918
+ async function recalculateParentRollups(ctx) {
3919
+ const schema = await config.getSchemaById(ctx.objectId);
3920
+ if (!schema) return;
3921
+ const affectedParentIds = await rollupService.findAffectedParentRecords(ctx.record, schema);
3922
+ if (affectedParentIds.length === 0) return;
3923
+ const parentRecords = await config.adapter.objectRecords.findByIds(affectedParentIds);
3924
+ for (const parentRecord of parentRecords) {
3925
+ const parentSchema = await config.getSchemaById(parentRecord.objectId);
3926
+ if (!parentSchema) continue;
3927
+ const hasRollups = parentSchema.attributes.some(
3928
+ (a) => a.type === "rollup"
3929
+ );
3930
+ if (hasRollups) {
3931
+ await rollupService.recalculateAndUpdate(parentRecord, parentSchema, config.tenantId);
3932
+ }
3933
+ }
3934
+ }
3935
+ return [
3936
+ {
3937
+ type: "afterCreate",
3938
+ objectName,
3939
+ handler: recalculateParentRollups,
3940
+ priority: 100
3941
+ // Run after other hooks
3942
+ },
3943
+ {
3944
+ type: "afterUpdate",
3945
+ objectName,
3946
+ handler: recalculateParentRollups,
3947
+ priority: 100
3948
+ },
3949
+ {
3950
+ type: "afterDelete",
3951
+ objectName,
3952
+ handler: recalculateParentRollups,
3953
+ priority: 100
3954
+ }
3955
+ ];
3956
+ }
3957
+ function registerAllRollupHooks(hookRegistry, schemas, config) {
3958
+ for (const schema of schemas) {
3959
+ const hasRelations = schema.attributes.some((a) => a.type === "relation");
3960
+ if (hasRelations && hookRegistry.register) {
3961
+ const hooks = createRollupHooks(schema.name, config);
3962
+ for (const hook of hooks) {
3963
+ hookRegistry.register(hook);
3964
+ }
3965
+ }
3966
+ }
3967
+ }
3968
+
3149
3969
  // src/runtime/template.ts
3150
3970
  var pipes = {
3151
3971
  /** Convert to uppercase */
@@ -3803,6 +4623,26 @@ function createMockObjectRecordsRepository(stores) {
3803
4623
  }
3804
4624
  }
3805
4625
  return Promise.resolve(updated);
4626
+ },
4627
+ findByRelation(objectName, relationAttributeName, targetRecordId) {
4628
+ const obj = Array.from(stores.objects.values()).find((o) => o.name === objectName);
4629
+ if (!obj) return Promise.resolve([]);
4630
+ const results = [];
4631
+ for (const record of stores.objectRecords.values()) {
4632
+ if (record.objectId !== obj.id) continue;
4633
+ const value = record.values[relationAttributeName];
4634
+ let matches = false;
4635
+ if (typeof value === "string" && value === targetRecordId) {
4636
+ matches = true;
4637
+ } else if (Array.isArray(value) && value.includes(targetRecordId)) {
4638
+ matches = true;
4639
+ }
4640
+ if (matches) {
4641
+ const { tenantId: _, ...publicRecord } = record;
4642
+ results.push(publicRecord);
4643
+ }
4644
+ }
4645
+ return Promise.resolve(results);
3806
4646
  }
3807
4647
  };
3808
4648
  }
@@ -6964,13 +7804,17 @@ var RecordService = class {
6964
7804
  if (!record) {
6965
7805
  return null;
6966
7806
  }
7807
+ const schema = await this.schemaService.getObjectSchema(record.objectId);
7808
+ let enrichedRecord = record;
7809
+ if (!options?.skipFormulas) {
7810
+ enrichedRecord = this.enrichWithFormulas(record, schema);
7811
+ }
6967
7812
  if (options?.includeSchema) {
6968
- const schema = await this.schemaService.getObjectSchema(record.objectId);
6969
- const recordWithSchema = record;
7813
+ const recordWithSchema = enrichedRecord;
6970
7814
  recordWithSchema.schema = schema;
6971
7815
  return recordWithSchema;
6972
7816
  }
6973
- return record;
7817
+ return enrichedRecord;
6974
7818
  }
6975
7819
  /**
6976
7820
  * Get a record by ID or throw if not found
@@ -7265,6 +8109,46 @@ var RecordService = class {
7265
8109
  timestamp: /* @__PURE__ */ new Date()
7266
8110
  };
7267
8111
  }
8112
+ /**
8113
+ * Enrich a record with computed formula values
8114
+ *
8115
+ * Formula attributes are calculated at read-time from the record's values.
8116
+ * This method adds the computed values to the record's values object.
8117
+ *
8118
+ * @param record - The record to enrich
8119
+ * @param schema - The object schema containing attribute definitions
8120
+ * @returns Record with formula values computed
8121
+ * @internal
8122
+ */
8123
+ enrichWithFormulas(record, schema) {
8124
+ const formulaAttrs = schema.attributes.filter(
8125
+ (a) => a.type === "formula"
8126
+ );
8127
+ if (formulaAttrs.length === 0) {
8128
+ return record;
8129
+ }
8130
+ const enrichedValues = { ...record.values };
8131
+ for (const attr of formulaAttrs) {
8132
+ enrichedValues[attr.name] = evaluateFormulaAttribute(attr, record.values);
8133
+ }
8134
+ return {
8135
+ ...record,
8136
+ values: enrichedValues
8137
+ };
8138
+ }
8139
+ /**
8140
+ * Enrich multiple records with computed formula values
8141
+ * @internal
8142
+ */
8143
+ enrichRecordsWithFormulas(records, schema) {
8144
+ const formulaAttrs = schema.attributes.filter(
8145
+ (a) => a.type === "formula"
8146
+ );
8147
+ if (formulaAttrs.length === 0) {
8148
+ return records;
8149
+ }
8150
+ return records.map((record) => this.enrichWithFormulas(record, schema));
8151
+ }
7268
8152
  /**
7269
8153
  * Permanently delete a record (hard delete)
7270
8154
  *
@@ -7293,11 +8177,18 @@ var RecordService = class {
7293
8177
  * @returns Records and total count
7294
8178
  */
7295
8179
  async listRecords(objectId, options) {
8180
+ const schema = await this.schemaService.getObjectSchema(objectId);
7296
8181
  if (this.permissionService && this.userId) {
7297
- const schema = await this.schemaService.getObjectSchema(objectId);
7298
8182
  await this.checkPermission(schema.name, "read");
7299
8183
  }
7300
- return await this.adapter.objectRecords.list(this.tenantId, objectId, options);
8184
+ const result = await this.adapter.objectRecords.list(this.tenantId, objectId, options);
8185
+ if (!options?.skipFormulas) {
8186
+ return {
8187
+ records: this.enrichRecordsWithFormulas(result.records, schema),
8188
+ total: result.total
8189
+ };
8190
+ }
8191
+ return result;
7301
8192
  }
7302
8193
  /**
7303
8194
  * Search records using full-text search
@@ -7308,11 +8199,18 @@ var RecordService = class {
7308
8199
  * @returns Matching records and total count
7309
8200
  */
7310
8201
  async searchRecords(objectId, query, options) {
8202
+ const schema = await this.schemaService.getObjectSchema(objectId);
7311
8203
  if (this.permissionService && this.userId) {
7312
- const schema = await this.schemaService.getObjectSchema(objectId);
7313
8204
  await this.checkPermission(schema.name, "read");
7314
8205
  }
7315
- return await this.adapter.objectRecords.search(this.tenantId, objectId, query, options);
8206
+ const result = await this.adapter.objectRecords.search(this.tenantId, objectId, query, options);
8207
+ if (!options?.skipFormulas) {
8208
+ return {
8209
+ records: this.enrichRecordsWithFormulas(result.records, schema),
8210
+ total: result.total
8211
+ };
8212
+ }
8213
+ return result;
7316
8214
  }
7317
8215
  /**
7318
8216
  * Validate data against object schema without saving
@@ -7357,6 +8255,231 @@ var RecordService = class {
7357
8255
  }
7358
8256
  };
7359
8257
 
8258
+ // src/runtime/services/relation-resolver.service.ts
8259
+ var RelationResolverService = class {
8260
+ constructor(adapter) {
8261
+ this.adapter = adapter;
8262
+ }
8263
+ /**
8264
+ * Resolve values from related records for formula evaluation
8265
+ *
8266
+ * Phase 2: Supports 1 level of relation traversal only
8267
+ *
8268
+ * @param record - The source record
8269
+ * @param schema - Schema of the source object
8270
+ * @param relationNames - Names of relation attributes to resolve
8271
+ * @returns Map of relation name to related record's values
8272
+ *
8273
+ * @example
8274
+ * ```typescript
8275
+ * // For an order with company relation
8276
+ * const resolved = await resolver.resolveRelationValues(
8277
+ * orderRecord,
8278
+ * orderSchema,
8279
+ * ["company"]
8280
+ * );
8281
+ * // → { company: { id: "...", name: "Acme Corp", ... } }
8282
+ * ```
8283
+ */
8284
+ async resolveRelationValues(record, schema, relationNames) {
8285
+ const result = {};
8286
+ const relationAttrs = schema.attributes.filter(
8287
+ (a) => a.type === "relation" && relationNames.includes(a.name)
8288
+ );
8289
+ if (relationAttrs.length === 0) {
8290
+ return result;
8291
+ }
8292
+ const idsToFetch = [];
8293
+ const attrIdMap = /* @__PURE__ */ new Map();
8294
+ for (const attr of relationAttrs) {
8295
+ const relatedId = this.extractSingleId(record.values[attr.name]);
8296
+ if (relatedId) {
8297
+ idsToFetch.push(relatedId);
8298
+ attrIdMap.set(relatedId, attr.name);
8299
+ }
8300
+ }
8301
+ if (idsToFetch.length === 0) {
8302
+ return result;
8303
+ }
8304
+ const relatedRecords = await this.adapter.objectRecords.findByIds(idsToFetch);
8305
+ for (const relatedRecord of relatedRecords) {
8306
+ const attrName = attrIdMap.get(relatedRecord.id);
8307
+ if (attrName) {
8308
+ result[attrName] = relatedRecord.values;
8309
+ }
8310
+ }
8311
+ return result;
8312
+ }
8313
+ /**
8314
+ * Resolve relation values for multiple records (batched)
8315
+ *
8316
+ * Optimized for list operations - fetches all related records in one batch
8317
+ *
8318
+ * @param records - Source records
8319
+ * @param schema - Schema of the source object
8320
+ * @param relationNames - Names of relation attributes to resolve
8321
+ * @returns Map of record ID to resolved relations
8322
+ */
8323
+ async resolveRelationValuesForMany(records, schema, relationNames) {
8324
+ const resultMap = /* @__PURE__ */ new Map();
8325
+ for (const record of records) {
8326
+ resultMap.set(record.id, {});
8327
+ }
8328
+ const relationAttrs = schema.attributes.filter(
8329
+ (a) => a.type === "relation" && relationNames.includes(a.name)
8330
+ );
8331
+ if (relationAttrs.length === 0) {
8332
+ return resultMap;
8333
+ }
8334
+ const allIdsToFetch = /* @__PURE__ */ new Set();
8335
+ const recordRelationMap = /* @__PURE__ */ new Map();
8336
+ for (const record of records) {
8337
+ const idToAttr = /* @__PURE__ */ new Map();
8338
+ for (const attr of relationAttrs) {
8339
+ const relatedId = this.extractSingleId(record.values[attr.name]);
8340
+ if (relatedId) {
8341
+ allIdsToFetch.add(relatedId);
8342
+ idToAttr.set(relatedId, attr.name);
8343
+ }
8344
+ }
8345
+ recordRelationMap.set(record.id, idToAttr);
8346
+ }
8347
+ if (allIdsToFetch.size === 0) {
8348
+ return resultMap;
8349
+ }
8350
+ const relatedRecords = await this.adapter.objectRecords.findByIds([...allIdsToFetch]);
8351
+ const relatedRecordMap = new Map(relatedRecords.map((r) => [r.id, r]));
8352
+ for (const record of records) {
8353
+ const result = resultMap.get(record.id);
8354
+ const idToAttr = recordRelationMap.get(record.id);
8355
+ for (const [relatedId, attrName] of idToAttr) {
8356
+ const relatedRecord = relatedRecordMap.get(relatedId);
8357
+ if (relatedRecord) {
8358
+ result[attrName] = relatedRecord.values;
8359
+ }
8360
+ }
8361
+ }
8362
+ return resultMap;
8363
+ }
8364
+ /**
8365
+ * Flatten resolved relations for formula evaluation
8366
+ *
8367
+ * Converts nested structure to dot-notation keys:
8368
+ * { company: { name: "Acme" } } → { "company.name": "Acme" }
8369
+ *
8370
+ * @param resolved - Resolved relations from resolveRelationValues
8371
+ * @returns Flattened values suitable for formula evaluation
8372
+ */
8373
+ flattenResolvedRelations(resolved) {
8374
+ const flat = {};
8375
+ for (const [relationName, values] of Object.entries(resolved)) {
8376
+ for (const [key, value] of Object.entries(values)) {
8377
+ flat[`${relationName}.${key}`] = value;
8378
+ }
8379
+ }
8380
+ return flat;
8381
+ }
8382
+ /**
8383
+ * Extract a single relation ID from a value
8384
+ * Handles both single (string) and multi (array) relations
8385
+ * @internal
8386
+ */
8387
+ extractSingleId(value) {
8388
+ if (typeof value === "string" && value.length > 0) {
8389
+ return value;
8390
+ }
8391
+ if (Array.isArray(value) && value.length > 0 && typeof value[0] === "string") {
8392
+ return value[0];
8393
+ }
8394
+ return null;
8395
+ }
8396
+ };
8397
+
8398
+ // src/runtime/services/rollup-scheduler.ts
8399
+ var RollupScheduler = class {
8400
+ constructor(adapter, getSchemaById, options) {
8401
+ this.adapter = adapter;
8402
+ this.getSchemaById = getSchemaById;
8403
+ this.pending = /* @__PURE__ */ new Map();
8404
+ this.rollupService = new RollupService(adapter);
8405
+ this.debounceMs = options?.debounceMs ?? 100;
8406
+ this.maxPending = options?.maxPending ?? 100;
8407
+ }
8408
+ /**
8409
+ * Schedule a rollup recalculation for a parent record
8410
+ *
8411
+ * If the same record is already pending, the timer is reset.
8412
+ * If too many recalculations are pending, triggers an immediate flush.
8413
+ *
8414
+ * @param parentId - ID of the parent record to recalculate
8415
+ * @param parentObjectId - Object ID of the parent
8416
+ * @param tenantId - Tenant ID
8417
+ */
8418
+ scheduleRecalculation(parentId, parentObjectId, tenantId) {
8419
+ const key = `${parentId}:${parentObjectId}`;
8420
+ const existing = this.pending.get(key);
8421
+ if (existing) {
8422
+ clearTimeout(existing.timeout);
8423
+ }
8424
+ const timeout = setTimeout(async () => {
8425
+ await this.executeRecalculation(parentId, parentObjectId, tenantId);
8426
+ this.pending.delete(key);
8427
+ }, this.debounceMs);
8428
+ this.pending.set(key, { parentId, parentObjectId, tenantId, timeout });
8429
+ if (this.pending.size >= this.maxPending) {
8430
+ this.flush();
8431
+ }
8432
+ }
8433
+ /**
8434
+ * Execute all pending recalculations immediately
8435
+ */
8436
+ async flush() {
8437
+ const entries = Array.from(this.pending.entries());
8438
+ for (const [, entry] of entries) {
8439
+ clearTimeout(entry.timeout);
8440
+ }
8441
+ this.pending.clear();
8442
+ const tenantMap = /* @__PURE__ */ new Map();
8443
+ for (const [, entry] of entries) {
8444
+ tenantMap.set(entry.parentId, entry.tenantId);
8445
+ }
8446
+ const allIds = entries.map(([, entry]) => entry.parentId);
8447
+ const records = await this.adapter.objectRecords.findByIds(allIds);
8448
+ for (const record of records) {
8449
+ const schema = await this.getSchemaById(record.objectId);
8450
+ const tenantId = tenantMap.get(record.id);
8451
+ if (schema && tenantId) {
8452
+ await this.rollupService.recalculateAndUpdate(record, schema, tenantId);
8453
+ }
8454
+ }
8455
+ }
8456
+ /**
8457
+ * Execute a single recalculation
8458
+ */
8459
+ async executeRecalculation(parentId, parentObjectId, tenantId) {
8460
+ const record = await this.adapter.objectRecords.findById(parentId);
8461
+ if (!record) return;
8462
+ const schema = await this.getSchemaById(parentObjectId);
8463
+ if (!schema) return;
8464
+ await this.rollupService.recalculateAndUpdate(record, schema, tenantId);
8465
+ }
8466
+ /**
8467
+ * Get number of pending recalculations
8468
+ */
8469
+ get pendingCount() {
8470
+ return this.pending.size;
8471
+ }
8472
+ /**
8473
+ * Clear all pending recalculations without executing them
8474
+ */
8475
+ clear() {
8476
+ for (const [, entry] of this.pending) {
8477
+ clearTimeout(entry.timeout);
8478
+ }
8479
+ this.pending.clear();
8480
+ }
8481
+ };
8482
+
7360
8483
  // src/runtime/services/user-profile.service.ts
7361
8484
  var UserProfileService = class {
7362
8485
  constructor(adapter, tenantId, options) {
@@ -8054,7 +9177,8 @@ async function syncAttributes(adapter, nativeObject, dbObject, existingObject, r
8054
9177
  hidden: attr.hidden,
8055
9178
  archived: attr.archived,
8056
9179
  deprecated: attr.deprecated,
8057
- defaultValue: attr.defaultValue,
9180
+ // Formula and rollup attributes don't have defaultValue
9181
+ defaultValue: "defaultValue" in attr ? attr.defaultValue : void 0,
8058
9182
  config: extractAttributeConfig(attr)
8059
9183
  });
8060
9184
  if (existingAttr) {
@@ -8136,6 +9260,8 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
8136
9260
  GeocodingService,
8137
9261
  GlobalSearchService,
8138
9262
  GroupBuilder,
9263
+ InvalidPathError,
9264
+ MaxDepthExceededError,
8139
9265
  NO_VALUE_OPERATORS,
8140
9266
  NoopGeocodingAdapter,
8141
9267
  NoopHookRegistry,
@@ -8152,8 +9278,11 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
8152
9278
  RecordNotFoundError,
8153
9279
  RecordReferencedError,
8154
9280
  RecordService,
9281
+ RelationResolverService,
8155
9282
  RelationService,
8156
9283
  RoleNotFoundError,
9284
+ RollupScheduler,
9285
+ RollupService,
8157
9286
  SYSTEM_RESOURCES,
8158
9287
  SYSTEM_RESOURCE_LABELS,
8159
9288
  SchemaError,
@@ -8178,6 +9307,7 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
8178
9307
  createDateValidator,
8179
9308
  createDraftValidator,
8180
9309
  createFileValidator,
9310
+ createFormulaValidator,
8181
9311
  createLocationValidator,
8182
9312
  createMockAdapter,
8183
9313
  createMultiRelationValidator,
@@ -8187,6 +9317,8 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
8187
9317
  createPhoneValidator,
8188
9318
  createRatingValidator,
8189
9319
  createRelationValidator,
9320
+ createRollupHooks,
9321
+ createRollupValidator,
8190
9322
  createSelectValidator,
8191
9323
  createSingleRelationValidator,
8192
9324
  createStatusValidator,
@@ -8198,18 +9330,34 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
8198
9330
  date,
8199
9331
  dateConfigSchema,
8200
9332
  enrichValuesWithSelectLabels,
9333
+ evaluateFormula,
9334
+ evaluateFormulaAttribute,
9335
+ evaluateFormulaAttributeWithRelations,
9336
+ evaluateFormulaWithRelations,
9337
+ evaluateFormulaWithResult,
8201
9338
  extractAttributeNames,
9339
+ extractFormulaVariables,
9340
+ extractRelationNames,
9341
+ extractRelationReferences,
8202
9342
  file,
8203
9343
  fileConfigSchema,
9344
+ flattenRelationsForEval,
8204
9345
  flow,
8205
9346
  formatAttributeValue,
9347
+ formatFormulaResult,
9348
+ formula,
9349
+ formulaConfigSchema,
8206
9350
  generateId,
8207
9351
  generatePrefixedId,
8208
9352
  getAttributeConfigSchema,
8209
9353
  getMissingRequiredAttributes,
9354
+ getPathDepth,
9355
+ getRelationPath,
8210
9356
  getSyncPreview,
9357
+ getTargetAttributeName,
8211
9358
  getViewSyncPreview,
8212
9359
  group,
9360
+ hasRelationReferences,
8213
9361
  isActivityTab,
8214
9362
  isAdvancedFilterState,
8215
9363
  isCustomTab,
@@ -8235,14 +9383,21 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
8235
9383
  numberConfigSchema,
8236
9384
  object,
8237
9385
  parseAttributeConfig,
9386
+ parsePath,
9387
+ pathHasManyCardinality,
8238
9388
  phone,
8239
9389
  phoneConfigSchema,
8240
9390
  rating,
8241
9391
  ratingConfigSchema,
9392
+ registerAllRollupHooks,
8242
9393
  registry,
8243
9394
  relation,
8244
9395
  relationConfigSchema,
8245
9396
  renderLabelExpression,
9397
+ resolveMultiplePaths,
9398
+ resolveSingleValue,
9399
+ rollup,
9400
+ rollupConfigSchema,
8246
9401
  safeParseAttributeConfig,
8247
9402
  select,
8248
9403
  selectConfigSchema,
@@ -8259,14 +9414,17 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
8259
9414
  timestampConfigSchema,
8260
9415
  toAdvancedFilterState,
8261
9416
  toSimpleFilterState,
9417
+ traversePath,
8262
9418
  user,
8263
9419
  userConfigSchema,
8264
9420
  validateAttribute,
8265
9421
  validateAttributeConfig,
8266
9422
  validateDraft,
8267
9423
  validateDraftOrThrow,
9424
+ validateFormulaExpression,
8268
9425
  validateObject,
8269
9426
  validateObjectOrThrow,
9427
+ validatePath,
8270
9428
  verifyNativeObjectsSync,
8271
9429
  verifyNativeViewsSync,
8272
9430
  view,