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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/chunk-4SW4V3YU.mjs +8854 -0
  2. package/dist/chunk-7GSIXU67.mjs +8820 -0
  3. package/dist/chunk-BKOV4XVX.mjs +8816 -0
  4. package/dist/chunk-BQUYTOB3.mjs +8815 -0
  5. package/dist/chunk-BSAR4UKK.mjs +8851 -0
  6. package/dist/chunk-DPOXIK3M.mjs +8816 -0
  7. package/dist/chunk-DU2HLOQH.mjs +8813 -0
  8. package/dist/chunk-I4TTNDN7.mjs +8910 -0
  9. package/dist/chunk-JD2MVLTW.mjs +8814 -0
  10. package/dist/chunk-KDHA2N7D.mjs +8927 -0
  11. package/dist/chunk-MX4X4UEN.mjs +8854 -0
  12. package/dist/chunk-OJWVSONX.mjs +8900 -0
  13. package/dist/chunk-PMYSW54L.mjs +8855 -0
  14. package/dist/chunk-SD2UXOSF.mjs +8814 -0
  15. package/dist/chunk-Y5KIRAND.mjs +8916 -0
  16. package/dist/chunk-YLVZT3H4.mjs +8793 -0
  17. package/dist/chunk-ZU5SR355.mjs +8862 -0
  18. package/dist/index.d.mts +119 -6
  19. package/dist/index.d.ts +119 -6
  20. package/dist/index.js +1215 -11
  21. package/dist/index.mjs +83 -2
  22. package/dist/runtime-09fOm3VO.d.mts +6383 -0
  23. package/dist/runtime-09fOm3VO.d.ts +6383 -0
  24. package/dist/runtime-BMHqETm7.d.mts +6450 -0
  25. package/dist/runtime-BMHqETm7.d.ts +6450 -0
  26. package/dist/runtime-BpsaNQPs.d.mts +6397 -0
  27. package/dist/runtime-BpsaNQPs.d.ts +6397 -0
  28. package/dist/runtime-CHAkwiwa.d.mts +6450 -0
  29. package/dist/runtime-CHAkwiwa.d.ts +6450 -0
  30. package/dist/runtime-CPggQqoW.d.mts +6428 -0
  31. package/dist/runtime-CPggQqoW.d.ts +6428 -0
  32. package/dist/runtime-CfVfZtoq.d.mts +6437 -0
  33. package/dist/runtime-CfVfZtoq.d.ts +6437 -0
  34. package/dist/runtime-DSMMZRLE.d.mts +6430 -0
  35. package/dist/runtime-DSMMZRLE.d.ts +6430 -0
  36. package/dist/runtime-xtAfMn_f.d.mts +6436 -0
  37. package/dist/runtime-xtAfMn_f.d.ts +6436 -0
  38. package/dist/runtime.d.mts +1 -1
  39. package/dist/runtime.d.ts +1 -1
  40. package/dist/runtime.js +1075 -9
  41. package/dist/runtime.mjs +53 -1
  42. package/package.json +3 -2
package/dist/runtime.js CHANGED
@@ -128,31 +128,435 @@ __export(runtime_exports, {
128
128
  FlowService: () => FlowService,
129
129
  GeocodingService: () => GeocodingService,
130
130
  GlobalSearchService: () => GlobalSearchService,
131
+ InvalidPathError: () => InvalidPathError,
132
+ MaxDepthExceededError: () => MaxDepthExceededError,
131
133
  NoopGeocodingAdapter: () => NoopGeocodingAdapter,
132
134
  NoopHookRegistry: () => NoopHookRegistry,
133
135
  ObjectSchemaService: () => ObjectSchemaService,
134
136
  PermissionService: () => PermissionService,
135
137
  RecordService: () => RecordService,
138
+ RelationResolverService: () => RelationResolverService,
136
139
  RelationService: () => RelationService,
140
+ RollupScheduler: () => RollupScheduler,
141
+ RollupService: () => RollupService,
137
142
  UserProfileService: () => UserProfileService,
138
143
  UserService: () => UserService,
139
144
  ViewService: () => ViewService,
140
145
  buildAuditChanges: () => buildAuditChanges,
141
146
  createMockAdapter: () => createMockAdapter,
142
147
  enrichValuesWithSelectLabels: () => enrichValuesWithSelectLabels,
148
+ evaluateFormula: () => evaluateFormula,
149
+ evaluateFormulaAttribute: () => evaluateFormulaAttribute,
150
+ evaluateFormulaAttributeWithRelations: () => evaluateFormulaAttributeWithRelations,
151
+ evaluateFormulaWithRelations: () => evaluateFormulaWithRelations,
152
+ evaluateFormulaWithResult: () => evaluateFormulaWithResult,
143
153
  extractAttributeNames: () => extractAttributeNames,
154
+ extractFormulaVariables: () => extractFormulaVariables,
155
+ extractRelationNames: () => extractRelationNames,
156
+ extractRelationReferences: () => extractRelationReferences,
157
+ flattenRelationsForEval: () => flattenRelationsForEval,
158
+ formatFormulaResult: () => formatFormulaResult,
159
+ getPathDepth: () => getPathDepth,
160
+ getRelationPath: () => getRelationPath,
144
161
  getSyncPreview: () => getSyncPreview,
162
+ getTargetAttributeName: () => getTargetAttributeName,
145
163
  getViewSyncPreview: () => getViewSyncPreview,
164
+ hasRelationReferences: () => hasRelationReferences,
146
165
  isLabelExpression: () => isLabelExpression,
166
+ parsePath: () => parsePath,
167
+ pathHasManyCardinality: () => pathHasManyCardinality,
147
168
  renderLabelExpression: () => renderLabelExpression,
169
+ resolveMultiplePaths: () => resolveMultiplePaths,
170
+ resolveSingleValue: () => resolveSingleValue,
148
171
  syncAll: () => syncAll,
149
172
  syncNativeObjects: () => syncNativeObjects,
150
173
  syncNativeViews: () => syncNativeViews,
174
+ traversePath: () => traversePath,
175
+ validateFormulaExpression: () => validateFormulaExpression,
176
+ validatePath: () => validatePath,
151
177
  verifyNativeObjectsSync: () => verifyNativeObjectsSync,
152
178
  verifyNativeViewsSync: () => verifyNativeViewsSync
153
179
  });
154
180
  module.exports = __toCommonJS(runtime_exports);
155
181
 
182
+ // src/runtime/formula/evaluator.ts
183
+ var import_expr_eval = require("expr-eval");
184
+ function createFormulaParser() {
185
+ const parser = new import_expr_eval.Parser();
186
+ parser.functions.IF = (condition, thenValue, elseValue) => condition ? thenValue : elseValue;
187
+ parser.functions.AND = (...args) => args.every(Boolean);
188
+ parser.functions.OR = (...args) => args.some(Boolean);
189
+ parser.functions.NOT = (value) => !value;
190
+ parser.functions.EMPTY = (value) => value === null || value === void 0 || value === "";
191
+ parser.functions.COALESCE = (...args) => args.find((a) => a != null) ?? null;
192
+ parser.functions.DEFAULT = (value, defaultValue) => value == null || value === "" ? defaultValue : value;
193
+ parser.functions.CONCAT = (...args) => args.filter((a) => a != null).map(String).join("");
194
+ parser.functions.UPPER = (value) => String(value ?? "").toUpperCase();
195
+ parser.functions.LOWER = (value) => String(value ?? "").toLowerCase();
196
+ parser.functions.TRIM = (value) => String(value ?? "").trim();
197
+ parser.functions.LENGTH = (value) => String(value ?? "").length;
198
+ parser.functions.LEFT = (value, count) => String(value ?? "").slice(0, count);
199
+ parser.functions.RIGHT = (value, count) => String(value ?? "").slice(-count);
200
+ parser.functions.REPLACE = (value, search, replacement) => String(value ?? "").replace(new RegExp(search, "g"), replacement);
201
+ parser.functions.CONTAINS = (value, search) => String(value ?? "").toLowerCase().includes(String(search).toLowerCase());
202
+ parser.functions.ROUND = (value, decimals = 0) => {
203
+ if (typeof value !== "number" || Number.isNaN(value)) return null;
204
+ const factor = 10 ** decimals;
205
+ return Math.round(value * factor) / factor;
206
+ };
207
+ parser.functions.FLOOR = (value) => {
208
+ if (typeof value !== "number" || Number.isNaN(value)) return null;
209
+ return Math.floor(value);
210
+ };
211
+ parser.functions.CEIL = (value) => {
212
+ if (typeof value !== "number" || Number.isNaN(value)) return null;
213
+ return Math.ceil(value);
214
+ };
215
+ parser.functions.ABS = (value) => {
216
+ if (typeof value !== "number" || Number.isNaN(value)) return null;
217
+ return Math.abs(value);
218
+ };
219
+ parser.functions.MIN = (...args) => {
220
+ const nums = args.filter((n) => typeof n === "number" && !Number.isNaN(n));
221
+ return nums.length > 0 ? Math.min(...nums) : null;
222
+ };
223
+ parser.functions.MAX = (...args) => {
224
+ const nums = args.filter((n) => typeof n === "number" && !Number.isNaN(n));
225
+ return nums.length > 0 ? Math.max(...nums) : null;
226
+ };
227
+ parser.functions.POW = (base, exponent) => {
228
+ if (typeof base !== "number" || typeof exponent !== "number") return null;
229
+ return base ** exponent;
230
+ };
231
+ parser.functions.MOD = (a, b) => {
232
+ if (typeof a !== "number" || typeof b !== "number" || b === 0) return null;
233
+ return a % b;
234
+ };
235
+ parser.functions.NOW = () => (/* @__PURE__ */ new Date()).toISOString();
236
+ parser.functions.TODAY = () => (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
237
+ parser.functions.YEAR = (value) => {
238
+ const date = parseDate(value);
239
+ return date ? date.getFullYear() : null;
240
+ };
241
+ parser.functions.MONTH = (value) => {
242
+ const date = parseDate(value);
243
+ return date ? date.getMonth() + 1 : null;
244
+ };
245
+ parser.functions.DAY = (value) => {
246
+ const date = parseDate(value);
247
+ return date ? date.getDate() : null;
248
+ };
249
+ parser.functions.DATE_DIFF = (date1, date2, unit = "days") => {
250
+ const d1 = parseDate(date1);
251
+ const d2 = parseDate(date2);
252
+ if (d1 === null || d2 === null) return null;
253
+ const diffMs = d1.getTime() - d2.getTime();
254
+ const MS_PER_DAY = 1e3 * 60 * 60 * 24;
255
+ const conversions = {
256
+ years: MS_PER_DAY * 365,
257
+ months: MS_PER_DAY * 30,
258
+ weeks: MS_PER_DAY * 7,
259
+ days: MS_PER_DAY,
260
+ hours: 1e3 * 60 * 60,
261
+ minutes: 1e3 * 60
262
+ };
263
+ const divisor = conversions[unit] ?? MS_PER_DAY;
264
+ return diffMs / divisor;
265
+ };
266
+ return parser;
267
+ }
268
+ function parseDate(value) {
269
+ if (!value) return null;
270
+ if (value instanceof Date) return value;
271
+ if (typeof value === "string" || typeof value === "number") {
272
+ const date = new Date(value);
273
+ return Number.isNaN(date.getTime()) ? null : date;
274
+ }
275
+ return null;
276
+ }
277
+ var formulaParser = createFormulaParser();
278
+ function evaluateFormula(expression, values) {
279
+ try {
280
+ const parsed = formulaParser.parse(expression);
281
+ return parsed.evaluate(values);
282
+ } catch {
283
+ return null;
284
+ }
285
+ }
286
+ function evaluateFormulaWithResult(expression, values) {
287
+ try {
288
+ const parsed = formulaParser.parse(expression);
289
+ const value = parsed.evaluate(values);
290
+ return { value };
291
+ } catch (error) {
292
+ return {
293
+ value: null,
294
+ error: error instanceof Error ? error.message : "Unknown error"
295
+ };
296
+ }
297
+ }
298
+ function formatFormulaResult(value, returnType, decimals) {
299
+ if (value === null || value === void 0) {
300
+ return null;
301
+ }
302
+ switch (returnType) {
303
+ case "number":
304
+ return formatNumberResult(value, decimals);
305
+ case "boolean":
306
+ return Boolean(value);
307
+ case "date":
308
+ return formatDateResult(value);
309
+ case "text":
310
+ return String(value);
311
+ }
312
+ }
313
+ function formatNumberResult(value, decimals) {
314
+ const num = typeof value === "number" ? value : Number(value);
315
+ if (Number.isNaN(num)) return null;
316
+ return decimals !== void 0 ? Number(num.toFixed(decimals)) : num;
317
+ }
318
+ function formatDateResult(value) {
319
+ if (value instanceof Date) return value.toISOString();
320
+ if (typeof value === "string") return value;
321
+ return null;
322
+ }
323
+ function evaluateFormulaAttribute(attr, values) {
324
+ const raw = evaluateFormula(attr.expression, values);
325
+ return formatFormulaResult(raw, attr.returnType, attr.decimals);
326
+ }
327
+ function validateFormulaExpression(expression) {
328
+ try {
329
+ formulaParser.parse(expression);
330
+ return { valid: true };
331
+ } catch (error) {
332
+ return {
333
+ valid: false,
334
+ error: error instanceof Error ? error.message : "Invalid expression"
335
+ };
336
+ }
337
+ }
338
+ function extractFormulaVariables(expression) {
339
+ try {
340
+ const parsed = formulaParser.parse(expression);
341
+ return parsed.variables();
342
+ } catch {
343
+ return [];
344
+ }
345
+ }
346
+ var RELATION_REF_PATTERN = /\b([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)\b/g;
347
+ function extractRelationReferences(expression) {
348
+ const regex = new RegExp(RELATION_REF_PATTERN.source, "g");
349
+ const matches = expression.matchAll(regex);
350
+ return [...matches].map((m) => m[0]);
351
+ }
352
+ function extractRelationNames(expression) {
353
+ const refs = extractRelationReferences(expression);
354
+ const names = refs.map((ref) => ref.split(".")[0]);
355
+ return [...new Set(names)];
356
+ }
357
+ function hasRelationReferences(expression) {
358
+ const regex = new RegExp(RELATION_REF_PATTERN.source);
359
+ return regex.test(expression);
360
+ }
361
+ function flattenRelationsForEval(resolvedRelations) {
362
+ const flat = {};
363
+ for (const [relationName, values] of Object.entries(resolvedRelations)) {
364
+ for (const [key, value] of Object.entries(values)) {
365
+ flat[`${relationName}.${key}`] = value;
366
+ }
367
+ }
368
+ return flat;
369
+ }
370
+ async function evaluateFormulaWithRelations(expression, record, schema, resolver) {
371
+ const relationNames = extractRelationNames(expression);
372
+ if (relationNames.length === 0) {
373
+ return evaluateFormula(expression, record.values);
374
+ }
375
+ const resolvedRelations = await resolver.resolveRelationValues(record, schema, relationNames);
376
+ const flattenedRelations = flattenRelationsForEval(resolvedRelations);
377
+ const allValues = {
378
+ ...record.values,
379
+ ...flattenedRelations
380
+ };
381
+ return evaluateFormula(expression, allValues);
382
+ }
383
+ async function evaluateFormulaAttributeWithRelations(attr, record, schema, resolver) {
384
+ const raw = await evaluateFormulaWithRelations(attr.expression, record, schema, resolver);
385
+ return formatFormulaResult(raw, attr.returnType, attr.decimals);
386
+ }
387
+
388
+ // src/runtime/formula/path-parser.ts
389
+ var InvalidPathError = class extends Error {
390
+ constructor(path, segment, reason) {
391
+ super(`Invalid path "${path}" at "${segment}": ${reason}`);
392
+ this.path = path;
393
+ this.segment = segment;
394
+ this.reason = reason;
395
+ this.name = "InvalidPathError";
396
+ }
397
+ };
398
+ var MaxDepthExceededError = class extends Error {
399
+ constructor(path, maxDepth) {
400
+ super(`Path "${path}" exceeds maximum depth of ${maxDepth}`);
401
+ this.path = path;
402
+ this.maxDepth = maxDepth;
403
+ this.name = "MaxDepthExceededError";
404
+ }
405
+ };
406
+ async function parsePath(path, startSchema, getSchema, maxDepth = 5) {
407
+ const segments = path.split(".");
408
+ if (segments.length === 0 || segments.length === 1 && segments[0] === "") {
409
+ throw new InvalidPathError(path, path, "Path cannot be empty");
410
+ }
411
+ if (segments.length > maxDepth) {
412
+ throw new MaxDepthExceededError(path, maxDepth);
413
+ }
414
+ const result = [];
415
+ let currentSchema = startSchema;
416
+ for (let i = 0; i < segments.length; i++) {
417
+ const segmentName = segments[i];
418
+ const isLastSegment = i === segments.length - 1;
419
+ const attr = currentSchema.attributes.find((a) => a.name === segmentName);
420
+ if (!attr) {
421
+ throw new InvalidPathError(
422
+ path,
423
+ segmentName,
424
+ `Attribute "${segmentName}" not found in object "${currentSchema.name}"`
425
+ );
426
+ }
427
+ if (attr.type === "relation") {
428
+ const relationAttr = attr;
429
+ const targetObject = relationAttr.targets[0]?.object;
430
+ if (!targetObject) {
431
+ throw new InvalidPathError(path, segmentName, "Relation has no target object");
432
+ }
433
+ result.push({
434
+ name: segmentName,
435
+ type: "relation",
436
+ cardinality: relationAttr.cardinality,
437
+ targetObject
438
+ });
439
+ if (!isLastSegment) {
440
+ const nextSchema = await getSchema(targetObject);
441
+ if (!nextSchema) {
442
+ throw new InvalidPathError(
443
+ path,
444
+ segmentName,
445
+ `Target object "${targetObject}" schema not found`
446
+ );
447
+ }
448
+ currentSchema = nextSchema;
449
+ }
450
+ } else {
451
+ if (!isLastSegment) {
452
+ throw new InvalidPathError(
453
+ path,
454
+ segmentName,
455
+ `"${segmentName}" is not a relation but has segments after it`
456
+ );
457
+ }
458
+ result.push({
459
+ name: segmentName,
460
+ type: "attribute"
461
+ });
462
+ }
463
+ }
464
+ return result;
465
+ }
466
+ async function validatePath(path, startSchema, getSchema, maxDepth = 5) {
467
+ try {
468
+ await parsePath(path, startSchema, getSchema, maxDepth);
469
+ return true;
470
+ } catch {
471
+ return false;
472
+ }
473
+ }
474
+ function pathHasManyCardinality(segments) {
475
+ return segments.some((s) => s.type === "relation" && s.cardinality === "many");
476
+ }
477
+ function getPathDepth(segments) {
478
+ return segments.filter((s) => s.type === "relation").length;
479
+ }
480
+ function getTargetAttributeName(path) {
481
+ const parts = path.split(".");
482
+ return parts[parts.length - 1];
483
+ }
484
+ function getRelationPath(path) {
485
+ const parts = path.split(".");
486
+ if (parts.length <= 1) return null;
487
+ return parts.slice(0, -1).join(".");
488
+ }
489
+
490
+ // src/runtime/formula/path-traversal.ts
491
+ async function traversePath(record, path, startSchemaName, adapter, getSchema, options) {
492
+ const maxDepth = options?.maxDepth ?? 5;
493
+ const startSchema = await getSchema(startSchemaName);
494
+ if (!startSchema) {
495
+ return { values: [], recordCounts: [0] };
496
+ }
497
+ const segments = await parsePath(path, startSchema, getSchema, maxDepth);
498
+ if (segments.length > maxDepth) {
499
+ throw new MaxDepthExceededError(path, maxDepth);
500
+ }
501
+ return await executeTraversal([record], segments, adapter, 0);
502
+ }
503
+ async function executeTraversal(records, segments, adapter, depth) {
504
+ const recordCounts = [records.length];
505
+ if (depth >= segments.length || records.length === 0) {
506
+ return { values: [], recordCounts };
507
+ }
508
+ const segment = segments[depth];
509
+ const isLastSegment = depth === segments.length - 1;
510
+ if (segment.type === "attribute") {
511
+ const values = records.map((r) => r.values[segment.name]).filter((v) => v !== void 0);
512
+ return { values, recordCounts };
513
+ }
514
+ const relatedIds = collectRelatedIds(records, segment.name);
515
+ if (relatedIds.length === 0) {
516
+ return { values: [], recordCounts };
517
+ }
518
+ const relatedRecords = await adapter.objectRecords.findByIds(relatedIds);
519
+ recordCounts.push(relatedRecords.length);
520
+ if (isLastSegment) {
521
+ return { values: relatedIds, recordCounts };
522
+ }
523
+ const nestedResult = await executeTraversal(relatedRecords, segments, adapter, depth + 1);
524
+ return {
525
+ values: nestedResult.values,
526
+ recordCounts: [...recordCounts, ...nestedResult.recordCounts.slice(1)]
527
+ };
528
+ }
529
+ function collectRelatedIds(records, relationName) {
530
+ const ids = [];
531
+ for (const record of records) {
532
+ const value = record.values[relationName];
533
+ if (typeof value === "string" && value.length > 0) {
534
+ ids.push(value);
535
+ } else if (Array.isArray(value)) {
536
+ for (const id of value) {
537
+ if (typeof id === "string" && id.length > 0) {
538
+ ids.push(id);
539
+ }
540
+ }
541
+ }
542
+ }
543
+ return [...new Set(ids)];
544
+ }
545
+ async function resolveSingleValue(record, path, startSchemaName, adapter, getSchema) {
546
+ const result = await traversePath(record, path, startSchemaName, adapter, getSchema);
547
+ return result.values[0] ?? null;
548
+ }
549
+ async function resolveMultiplePaths(record, paths, startSchemaName, adapter, getSchema) {
550
+ const results = /* @__PURE__ */ new Map();
551
+ await Promise.all(
552
+ paths.map(async (path) => {
553
+ const result = await traversePath(record, path, startSchemaName, adapter, getSchema);
554
+ results.set(path, result.values);
555
+ })
556
+ );
557
+ return results;
558
+ }
559
+
156
560
  // src/runtime/hooks/hook.registry.ts
157
561
  var NoopHookRegistry = class {
158
562
  async execute() {
@@ -997,6 +1401,26 @@ function createMockObjectRecordsRepository(stores) {
997
1401
  }
998
1402
  }
999
1403
  return Promise.resolve(updated);
1404
+ },
1405
+ findByRelation(objectName, relationAttributeName, targetRecordId) {
1406
+ const obj = Array.from(stores.objects.values()).find((o) => o.name === objectName);
1407
+ if (!obj) return Promise.resolve([]);
1408
+ const results = [];
1409
+ for (const record of stores.objectRecords.values()) {
1410
+ if (record.objectId !== obj.id) continue;
1411
+ const value = record.values[relationAttributeName];
1412
+ let matches = false;
1413
+ if (typeof value === "string" && value === targetRecordId) {
1414
+ matches = true;
1415
+ } else if (Array.isArray(value) && value.includes(targetRecordId)) {
1416
+ matches = true;
1417
+ }
1418
+ if (matches) {
1419
+ const { tenantId: _, ...publicRecord } = record;
1420
+ results.push(publicRecord);
1421
+ }
1422
+ }
1423
+ return Promise.resolve(results);
1000
1424
  }
1001
1425
  };
1002
1426
  }
@@ -2902,6 +3326,32 @@ var ratingConfigSchema = baseConfigSchema.extend({
2902
3326
  max: import_zod4.z.number().int().min(1).optional(),
2903
3327
  iconType: import_zod4.z.enum(["star", "heart", "thumbs", "number"]).optional()
2904
3328
  });
3329
+ var formulaConfigSchema = baseConfigSchema.extend({
3330
+ expression: import_zod4.z.string().min(1),
3331
+ returnType: import_zod4.z.enum(["text", "number", "boolean", "date"]),
3332
+ decimals: import_zod4.z.number().int().min(0).max(10).optional(),
3333
+ allowRelations: import_zod4.z.boolean().optional()
3334
+ });
3335
+ var rollupConfigSchema = baseConfigSchema.extend({
3336
+ relationAttribute: import_zod4.z.string().min(1).optional(),
3337
+ relationPath: import_zod4.z.string().optional(),
3338
+ targetAttribute: import_zod4.z.string().min(1),
3339
+ function: import_zod4.z.enum([
3340
+ "sum",
3341
+ "count",
3342
+ "avg",
3343
+ "min",
3344
+ "max",
3345
+ "countValues",
3346
+ "countEmpty",
3347
+ "percentEmpty",
3348
+ "percentFilled",
3349
+ "collect"
3350
+ ]),
3351
+ decimals: import_zod4.z.number().int().min(0).max(10).optional(),
3352
+ showAsOriginal: import_zod4.z.boolean().optional(),
3353
+ targetAttributeType: import_zod4.z.string().optional()
3354
+ });
2905
3355
  var attributeConfigSchemas = {
2906
3356
  text: textConfigSchema,
2907
3357
  textarea: textareaConfigSchema,
@@ -2918,7 +3368,9 @@ var attributeConfigSchemas = {
2918
3368
  file: fileConfigSchema,
2919
3369
  user: userConfigSchema,
2920
3370
  relation: relationConfigSchema,
2921
- rating: ratingConfigSchema
3371
+ rating: ratingConfigSchema,
3372
+ formula: formulaConfigSchema,
3373
+ rollup: rollupConfigSchema
2922
3374
  };
2923
3375
  function getAttributeConfigSchema(type) {
2924
3376
  return attributeConfigSchemas[type];
@@ -3075,6 +3527,12 @@ function createRatingValidator(attr) {
3075
3527
  }
3076
3528
  return schema;
3077
3529
  }
3530
+ function createFormulaValidator(_attr) {
3531
+ return import_zod4.z.unknown();
3532
+ }
3533
+ function createRollupValidator(_attr) {
3534
+ return import_zod4.z.unknown();
3535
+ }
3078
3536
  function createAttributeValidator(attr) {
3079
3537
  switch (attr.type) {
3080
3538
  case "text":
@@ -3107,6 +3565,10 @@ function createAttributeValidator(attr) {
3107
3565
  return createRelationValidator(attr);
3108
3566
  case "rating":
3109
3567
  return createRatingValidator(attr);
3568
+ case "formula":
3569
+ return createFormulaValidator(attr);
3570
+ case "rollup":
3571
+ return createRollupValidator(attr);
3110
3572
  default:
3111
3573
  return import_zod4.z.unknown();
3112
3574
  }
@@ -4734,6 +5196,263 @@ var RelationService = class {
4734
5196
  }
4735
5197
  };
4736
5198
 
5199
+ // src/runtime/services/rollup.service.ts
5200
+ var RollupService = class {
5201
+ constructor(adapter) {
5202
+ this.adapter = adapter;
5203
+ }
5204
+ /**
5205
+ * Calculate a rollup value for a record
5206
+ *
5207
+ * @param recordId - ID of the parent record
5208
+ * @param rollupAttr - Rollup attribute definition
5209
+ * @param schema - Schema of the parent object
5210
+ * @returns Computed rollup value
5211
+ *
5212
+ * @example
5213
+ * ```typescript
5214
+ * // Sum all order amounts for a company
5215
+ * const totalOrders = await rollupService.calculate(
5216
+ * "company-123",
5217
+ * {
5218
+ * type: "rollup",
5219
+ * name: "totalOrders",
5220
+ * relationAttribute: "orders",
5221
+ * targetAttribute: "amount",
5222
+ * function: "sum",
5223
+ * ...
5224
+ * },
5225
+ * companySchema
5226
+ * );
5227
+ * ```
5228
+ */
5229
+ async calculate(recordId, rollupAttr, schema) {
5230
+ const relationAttr = schema.attributes.find(
5231
+ (a) => a.type === "relation" && a.name === rollupAttr.relationAttribute
5232
+ );
5233
+ if (relationAttr) {
5234
+ return this.calculateForward(recordId, rollupAttr);
5235
+ }
5236
+ return this.calculateReverse(recordId, rollupAttr, schema);
5237
+ }
5238
+ /**
5239
+ * Forward pattern: this record has a relation attribute pointing to other records
5240
+ * Example: entreprise222 has relation "entreprises" → companies, rollup collects from companies
5241
+ */
5242
+ async calculateForward(recordId, rollupAttr) {
5243
+ const record = await this.adapter.objectRecords.findById(recordId);
5244
+ if (!record) {
5245
+ return { value: null, recordCount: 0 };
5246
+ }
5247
+ const relationValue = record.values[rollupAttr.relationAttribute];
5248
+ let relatedIds = [];
5249
+ if (typeof relationValue === "string" && relationValue.length > 0) {
5250
+ relatedIds = [relationValue];
5251
+ } else if (Array.isArray(relationValue)) {
5252
+ relatedIds = relationValue.filter((id) => typeof id === "string" && id.length > 0);
5253
+ }
5254
+ if (relatedIds.length === 0) {
5255
+ return { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 };
5256
+ }
5257
+ const relatedRecords = await this.adapter.objectRecords.findByIds(relatedIds);
5258
+ if (relatedRecords.length === 0) {
5259
+ return { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 };
5260
+ }
5261
+ return this.aggregateValues(relatedRecords, rollupAttr);
5262
+ }
5263
+ /**
5264
+ * Reverse pattern: other records have a relation pointing to this record
5265
+ * Example: Company has rollup on "orders", Order has relation "company" → companies
5266
+ */
5267
+ async calculateReverse(recordId, rollupAttr, schema) {
5268
+ const record = await this.adapter.objectRecords.findById(recordId);
5269
+ if (!record) {
5270
+ return { value: null, recordCount: 0 };
5271
+ }
5272
+ const parentObject = await this.adapter.objects.findById(record.objectId);
5273
+ if (!parentObject) {
5274
+ return { value: null, recordCount: 0 };
5275
+ }
5276
+ const tenantId = parentObject.tenantId;
5277
+ const sourceObjectName = rollupAttr.relationAttribute;
5278
+ const sourceObject = await this.adapter.objects.findByName(tenantId, sourceObjectName);
5279
+ if (!sourceObject) {
5280
+ return { value: null, recordCount: 0 };
5281
+ }
5282
+ const sourceAttributes = await this.adapter.attributes.findByObjectId(sourceObject.id);
5283
+ const reverseRelationAttr = sourceAttributes.find((attr) => {
5284
+ if (attr.type !== "relation") return false;
5285
+ const relationConfig = attr.config;
5286
+ return relationConfig?.targets?.some((t) => t.object === schema.name);
5287
+ });
5288
+ if (!reverseRelationAttr) {
5289
+ return { value: null, recordCount: 0 };
5290
+ }
5291
+ const relatedRecords = await this.adapter.objectRecords.findByRelation(
5292
+ tenantId,
5293
+ sourceObject.id,
5294
+ reverseRelationAttr.name,
5295
+ recordId
5296
+ );
5297
+ if (relatedRecords.length === 0) {
5298
+ return { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 };
5299
+ }
5300
+ return this.aggregateValues(relatedRecords, rollupAttr);
5301
+ }
5302
+ /**
5303
+ * Extract and aggregate values from related records
5304
+ */
5305
+ aggregateValues(relatedRecords, rollupAttr) {
5306
+ const values = relatedRecords.map((r) => r.values[rollupAttr.targetAttribute]).filter((v) => v !== void 0 && v !== null);
5307
+ const aggregated = this.aggregate(values, rollupAttr.function);
5308
+ const result = typeof aggregated === "number" && rollupAttr.decimals !== void 0 ? Number(aggregated.toFixed(rollupAttr.decimals)) : aggregated;
5309
+ return { value: result, recordCount: relatedRecords.length };
5310
+ }
5311
+ /**
5312
+ * Calculate rollup values for multiple records (batched)
5313
+ *
5314
+ * More efficient than calling calculate() for each record individually.
5315
+ */
5316
+ async calculateForMany(recordIds, rollupAttr, schema) {
5317
+ const results = /* @__PURE__ */ new Map();
5318
+ for (const id of recordIds) {
5319
+ results.set(id, { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 });
5320
+ }
5321
+ await Promise.all(
5322
+ recordIds.map(async (id) => {
5323
+ const result = await this.calculate(id, rollupAttr, schema);
5324
+ results.set(id, result);
5325
+ })
5326
+ );
5327
+ return results;
5328
+ }
5329
+ /**
5330
+ * Apply aggregation function to a set of values
5331
+ */
5332
+ aggregate(values, fn) {
5333
+ switch (fn) {
5334
+ case "sum":
5335
+ return this.sumNumbers(values);
5336
+ case "avg":
5337
+ return this.averageNumbers(values);
5338
+ case "min":
5339
+ return this.minNumbers(values);
5340
+ case "max":
5341
+ return this.maxNumbers(values);
5342
+ case "count":
5343
+ return values.length;
5344
+ case "countValues":
5345
+ return values.filter((v) => v != null && v !== "").length;
5346
+ case "countEmpty":
5347
+ return values.filter((v) => v == null || v === "").length;
5348
+ case "percentFilled": {
5349
+ if (values.length === 0) return 0;
5350
+ const filled = values.filter((v) => v != null && v !== "").length;
5351
+ return filled / values.length * 100;
5352
+ }
5353
+ case "percentEmpty": {
5354
+ if (values.length === 0) return 0;
5355
+ const empty = values.filter((v) => v == null || v === "").length;
5356
+ return empty / values.length * 100;
5357
+ }
5358
+ case "collect":
5359
+ return values;
5360
+ }
5361
+ }
5362
+ /**
5363
+ * Get the default empty value for a rollup function
5364
+ */
5365
+ getEmptyValue(fn) {
5366
+ switch (fn) {
5367
+ case "sum":
5368
+ case "count":
5369
+ case "countValues":
5370
+ case "countEmpty":
5371
+ case "percentEmpty":
5372
+ case "percentFilled":
5373
+ return 0;
5374
+ case "avg":
5375
+ case "min":
5376
+ case "max":
5377
+ return null;
5378
+ case "collect":
5379
+ return [];
5380
+ }
5381
+ }
5382
+ /**
5383
+ * Sum numeric values
5384
+ */
5385
+ sumNumbers(values) {
5386
+ return values.filter((v) => typeof v === "number" && !Number.isNaN(v)).reduce((sum, n) => sum + n, 0);
5387
+ }
5388
+ /**
5389
+ * Average numeric values
5390
+ */
5391
+ averageNumbers(values) {
5392
+ const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
5393
+ if (nums.length === 0) return null;
5394
+ return nums.reduce((sum, n) => sum + n, 0) / nums.length;
5395
+ }
5396
+ /**
5397
+ * Minimum numeric value
5398
+ */
5399
+ minNumbers(values) {
5400
+ const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
5401
+ if (nums.length === 0) return null;
5402
+ return Math.min(...nums);
5403
+ }
5404
+ /**
5405
+ * Maximum numeric value
5406
+ */
5407
+ maxNumbers(values) {
5408
+ const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
5409
+ if (nums.length === 0) return null;
5410
+ return Math.max(...nums);
5411
+ }
5412
+ /**
5413
+ * Recalculate all rollup attributes for a record and update it
5414
+ *
5415
+ * Called after related records change to keep rollups up-to-date.
5416
+ */
5417
+ async recalculateAndUpdate(record, schema) {
5418
+ const rollupAttrs = schema.attributes.filter((a) => a.type === "rollup");
5419
+ if (rollupAttrs.length === 0) {
5420
+ return record;
5421
+ }
5422
+ const updates = {};
5423
+ for (const attr of rollupAttrs) {
5424
+ const result = await this.calculate(record.id, attr, schema);
5425
+ updates[attr.name] = result.value;
5426
+ }
5427
+ return await this.adapter.objectRecords.update(record.id, updates);
5428
+ }
5429
+ /**
5430
+ * Find parent records that need rollup recalculation when a child record changes
5431
+ *
5432
+ * Used by hooks to determine which parent records to recalculate after
5433
+ * a child record is created, updated, or deleted.
5434
+ *
5435
+ * @param changedRecord - The record that was modified
5436
+ * @param changedSchema - Schema of the changed record's object
5437
+ * @returns Array of parent record IDs that need recalculation
5438
+ */
5439
+ async findAffectedParentRecords(changedRecord, changedSchema) {
5440
+ const affectedIds = [];
5441
+ const relationAttrs = changedSchema.attributes.filter(
5442
+ (a) => a.type === "relation"
5443
+ );
5444
+ for (const attr of relationAttrs) {
5445
+ const relatedId = changedRecord.values[attr.name];
5446
+ if (typeof relatedId === "string" && relatedId.length > 0) {
5447
+ affectedIds.push(relatedId);
5448
+ } else if (Array.isArray(relatedId)) {
5449
+ affectedIds.push(...relatedId.filter((id) => typeof id === "string"));
5450
+ }
5451
+ }
5452
+ return [...new Set(affectedIds)];
5453
+ }
5454
+ };
5455
+
4737
5456
  // src/runtime/services/user.service.ts
4738
5457
  var UserService = class {
4739
5458
  constructor(adapter, tenantId) {
@@ -4859,6 +5578,7 @@ var RecordService = class {
4859
5578
  this.schemaService = new ObjectSchemaService(adapter, registry);
4860
5579
  this.relationService = new RelationService(adapter, registry);
4861
5580
  this.userService = new UserService(adapter, tenantId);
5581
+ this.rollupService = new RollupService(adapter);
4862
5582
  this.hookRegistry = options?.hookRegistry ?? new NoopHookRegistry();
4863
5583
  this.permissionService = options?.permissionService;
4864
5584
  this.auditService = options?.auditService ?? (adapter.audit ? new AuditService(adapter, tenantId) : void 0);
@@ -5020,6 +5740,7 @@ var RecordService = class {
5020
5740
  };
5021
5741
  await this.hookRegistry.execute("afterCreate", schema.name, afterCtx);
5022
5742
  }
5743
+ await this.recalculateParentRollups(record, schema);
5023
5744
  if (this.auditService && this.userId) {
5024
5745
  await this.auditService.logRecordAction({
5025
5746
  action: "record.created",
@@ -5046,13 +5767,17 @@ var RecordService = class {
5046
5767
  if (!record) {
5047
5768
  return null;
5048
5769
  }
5770
+ const schema = await this.schemaService.getObjectSchema(record.objectId);
5771
+ let enrichedRecord = record;
5772
+ if (!options?.skipFormulas) {
5773
+ enrichedRecord = this.enrichWithFormulas(record, schema);
5774
+ }
5049
5775
  if (options?.includeSchema) {
5050
- const schema = await this.schemaService.getObjectSchema(record.objectId);
5051
- const recordWithSchema = record;
5776
+ const recordWithSchema = enrichedRecord;
5052
5777
  recordWithSchema.schema = schema;
5053
5778
  return recordWithSchema;
5054
5779
  }
5055
- return record;
5780
+ return enrichedRecord;
5056
5781
  }
5057
5782
  /**
5058
5783
  * Get a record by ID or throw if not found
@@ -5131,6 +5856,7 @@ var RecordService = class {
5131
5856
  };
5132
5857
  await this.hookRegistry.execute("afterUpdate", schema.name, afterCtx);
5133
5858
  }
5859
+ await this.recalculateParentRollups(updated, schema);
5134
5860
  if (this.auditService && this.userId && changedAttributes.length > 0) {
5135
5861
  const changes = changedAttributes.map((attr) => ({
5136
5862
  field: attr,
@@ -5258,6 +5984,7 @@ var RecordService = class {
5258
5984
  if (!options?.skipHooks) {
5259
5985
  await this.hookRegistry.execute("afterDelete", schema.name, hookCtx);
5260
5986
  }
5987
+ await this.recalculateParentRollups(record, schema);
5261
5988
  if (this.auditService && this.userId) {
5262
5989
  await this.auditService.logRecordAction({
5263
5990
  action: "record.deleted",
@@ -5347,6 +6074,79 @@ var RecordService = class {
5347
6074
  timestamp: /* @__PURE__ */ new Date()
5348
6075
  };
5349
6076
  }
6077
+ /**
6078
+ * Enrich a record with computed formula values
6079
+ *
6080
+ * Formula attributes are calculated at read-time from the record's values.
6081
+ * This method adds the computed values to the record's values object.
6082
+ *
6083
+ * @param record - The record to enrich
6084
+ * @param schema - The object schema containing attribute definitions
6085
+ * @returns Record with formula values computed
6086
+ * @internal
6087
+ */
6088
+ enrichWithFormulas(record, schema) {
6089
+ const formulaAttrs = schema.attributes.filter(
6090
+ (a) => a.type === "formula"
6091
+ );
6092
+ if (formulaAttrs.length === 0) {
6093
+ return record;
6094
+ }
6095
+ const enrichedValues = { ...record.values };
6096
+ for (const attr of formulaAttrs) {
6097
+ enrichedValues[attr.name] = evaluateFormulaAttribute(attr, record.values);
6098
+ }
6099
+ return {
6100
+ ...record,
6101
+ values: enrichedValues
6102
+ };
6103
+ }
6104
+ /**
6105
+ * Enrich multiple records with computed formula values
6106
+ * @internal
6107
+ */
6108
+ enrichRecordsWithFormulas(records, schema) {
6109
+ const formulaAttrs = schema.attributes.filter(
6110
+ (a) => a.type === "formula"
6111
+ );
6112
+ if (formulaAttrs.length === 0) {
6113
+ return records;
6114
+ }
6115
+ return records.map((record) => this.enrichWithFormulas(record, schema));
6116
+ }
6117
+ /**
6118
+ * Recalculate rollups after a record changes
6119
+ *
6120
+ * This handles two cases:
6121
+ * 1. The record itself has rollups (e.g., aggregating from related records it points to)
6122
+ * 2. Parent records have rollups that aggregate from this record
6123
+ *
6124
+ * @param record - The record that was modified
6125
+ * @param schema - Schema of the record's object
6126
+ * @internal
6127
+ */
6128
+ async recalculateParentRollups(record, schema) {
6129
+ const ownRollupAttrs = schema.attributes.filter(
6130
+ (a) => a.type === "rollup"
6131
+ );
6132
+ if (ownRollupAttrs.length > 0) {
6133
+ await this.rollupService.recalculateAndUpdate(record, schema);
6134
+ }
6135
+ const affectedParentIds = await this.rollupService.findAffectedParentRecords(record, schema);
6136
+ if (affectedParentIds.length === 0) {
6137
+ return;
6138
+ }
6139
+ const parentRecords = await this.adapter.objectRecords.findByIds(affectedParentIds);
6140
+ for (const parentRecord of parentRecords) {
6141
+ const parentSchema = await this.schemaService.getObjectSchema(parentRecord.objectId);
6142
+ const rollupAttrs = parentSchema.attributes.filter(
6143
+ (a) => a.type === "rollup"
6144
+ );
6145
+ if (rollupAttrs.length > 0) {
6146
+ await this.rollupService.recalculateAndUpdate(parentRecord, parentSchema);
6147
+ }
6148
+ }
6149
+ }
5350
6150
  /**
5351
6151
  * Permanently delete a record (hard delete)
5352
6152
  *
@@ -5375,11 +6175,18 @@ var RecordService = class {
5375
6175
  * @returns Records and total count
5376
6176
  */
5377
6177
  async listRecords(objectId, options) {
6178
+ const schema = await this.schemaService.getObjectSchema(objectId);
5378
6179
  if (this.permissionService && this.userId) {
5379
- const schema = await this.schemaService.getObjectSchema(objectId);
5380
6180
  await this.checkPermission(schema.name, "read");
5381
6181
  }
5382
- return await this.adapter.objectRecords.list(this.tenantId, objectId, options);
6182
+ const result = await this.adapter.objectRecords.list(this.tenantId, objectId, options);
6183
+ if (!options?.skipFormulas) {
6184
+ return {
6185
+ records: this.enrichRecordsWithFormulas(result.records, schema),
6186
+ total: result.total
6187
+ };
6188
+ }
6189
+ return result;
5383
6190
  }
5384
6191
  /**
5385
6192
  * Search records using full-text search
@@ -5390,11 +6197,18 @@ var RecordService = class {
5390
6197
  * @returns Matching records and total count
5391
6198
  */
5392
6199
  async searchRecords(objectId, query, options) {
6200
+ const schema = await this.schemaService.getObjectSchema(objectId);
5393
6201
  if (this.permissionService && this.userId) {
5394
- const schema = await this.schemaService.getObjectSchema(objectId);
5395
6202
  await this.checkPermission(schema.name, "read");
5396
6203
  }
5397
- return await this.adapter.objectRecords.search(this.tenantId, objectId, query, options);
6204
+ const result = await this.adapter.objectRecords.search(this.tenantId, objectId, query, options);
6205
+ if (!options?.skipFormulas) {
6206
+ return {
6207
+ records: this.enrichRecordsWithFormulas(result.records, schema),
6208
+ total: result.total
6209
+ };
6210
+ }
6211
+ return result;
5398
6212
  }
5399
6213
  /**
5400
6214
  * Validate data against object schema without saving
@@ -5439,6 +6253,231 @@ var RecordService = class {
5439
6253
  }
5440
6254
  };
5441
6255
 
6256
+ // src/runtime/services/relation-resolver.service.ts
6257
+ var RelationResolverService = class {
6258
+ constructor(adapter) {
6259
+ this.adapter = adapter;
6260
+ }
6261
+ /**
6262
+ * Resolve values from related records for formula evaluation
6263
+ *
6264
+ * Phase 2: Supports 1 level of relation traversal only
6265
+ *
6266
+ * @param record - The source record
6267
+ * @param schema - Schema of the source object
6268
+ * @param relationNames - Names of relation attributes to resolve
6269
+ * @returns Map of relation name to related record's values
6270
+ *
6271
+ * @example
6272
+ * ```typescript
6273
+ * // For an order with company relation
6274
+ * const resolved = await resolver.resolveRelationValues(
6275
+ * orderRecord,
6276
+ * orderSchema,
6277
+ * ["company"]
6278
+ * );
6279
+ * // → { company: { id: "...", name: "Acme Corp", ... } }
6280
+ * ```
6281
+ */
6282
+ async resolveRelationValues(record, schema, relationNames) {
6283
+ const result = {};
6284
+ const relationAttrs = schema.attributes.filter(
6285
+ (a) => a.type === "relation" && relationNames.includes(a.name)
6286
+ );
6287
+ if (relationAttrs.length === 0) {
6288
+ return result;
6289
+ }
6290
+ const idsToFetch = [];
6291
+ const attrIdMap = /* @__PURE__ */ new Map();
6292
+ for (const attr of relationAttrs) {
6293
+ const relatedId = this.extractSingleId(record.values[attr.name]);
6294
+ if (relatedId) {
6295
+ idsToFetch.push(relatedId);
6296
+ attrIdMap.set(relatedId, attr.name);
6297
+ }
6298
+ }
6299
+ if (idsToFetch.length === 0) {
6300
+ return result;
6301
+ }
6302
+ const relatedRecords = await this.adapter.objectRecords.findByIds(idsToFetch);
6303
+ for (const relatedRecord of relatedRecords) {
6304
+ const attrName = attrIdMap.get(relatedRecord.id);
6305
+ if (attrName) {
6306
+ result[attrName] = relatedRecord.values;
6307
+ }
6308
+ }
6309
+ return result;
6310
+ }
6311
+ /**
6312
+ * Resolve relation values for multiple records (batched)
6313
+ *
6314
+ * Optimized for list operations - fetches all related records in one batch
6315
+ *
6316
+ * @param records - Source records
6317
+ * @param schema - Schema of the source object
6318
+ * @param relationNames - Names of relation attributes to resolve
6319
+ * @returns Map of record ID to resolved relations
6320
+ */
6321
+ async resolveRelationValuesForMany(records, schema, relationNames) {
6322
+ const resultMap = /* @__PURE__ */ new Map();
6323
+ for (const record of records) {
6324
+ resultMap.set(record.id, {});
6325
+ }
6326
+ const relationAttrs = schema.attributes.filter(
6327
+ (a) => a.type === "relation" && relationNames.includes(a.name)
6328
+ );
6329
+ if (relationAttrs.length === 0) {
6330
+ return resultMap;
6331
+ }
6332
+ const allIdsToFetch = /* @__PURE__ */ new Set();
6333
+ const recordRelationMap = /* @__PURE__ */ new Map();
6334
+ for (const record of records) {
6335
+ const idToAttr = /* @__PURE__ */ new Map();
6336
+ for (const attr of relationAttrs) {
6337
+ const relatedId = this.extractSingleId(record.values[attr.name]);
6338
+ if (relatedId) {
6339
+ allIdsToFetch.add(relatedId);
6340
+ idToAttr.set(relatedId, attr.name);
6341
+ }
6342
+ }
6343
+ recordRelationMap.set(record.id, idToAttr);
6344
+ }
6345
+ if (allIdsToFetch.size === 0) {
6346
+ return resultMap;
6347
+ }
6348
+ const relatedRecords = await this.adapter.objectRecords.findByIds([...allIdsToFetch]);
6349
+ const relatedRecordMap = new Map(relatedRecords.map((r) => [r.id, r]));
6350
+ for (const record of records) {
6351
+ const result = resultMap.get(record.id);
6352
+ const idToAttr = recordRelationMap.get(record.id);
6353
+ for (const [relatedId, attrName] of idToAttr) {
6354
+ const relatedRecord = relatedRecordMap.get(relatedId);
6355
+ if (relatedRecord) {
6356
+ result[attrName] = relatedRecord.values;
6357
+ }
6358
+ }
6359
+ }
6360
+ return resultMap;
6361
+ }
6362
+ /**
6363
+ * Flatten resolved relations for formula evaluation
6364
+ *
6365
+ * Converts nested structure to dot-notation keys:
6366
+ * { company: { name: "Acme" } } → { "company.name": "Acme" }
6367
+ *
6368
+ * @param resolved - Resolved relations from resolveRelationValues
6369
+ * @returns Flattened values suitable for formula evaluation
6370
+ */
6371
+ flattenResolvedRelations(resolved) {
6372
+ const flat = {};
6373
+ for (const [relationName, values] of Object.entries(resolved)) {
6374
+ for (const [key, value] of Object.entries(values)) {
6375
+ flat[`${relationName}.${key}`] = value;
6376
+ }
6377
+ }
6378
+ return flat;
6379
+ }
6380
+ /**
6381
+ * Extract a single relation ID from a value
6382
+ * Handles both single (string) and multi (array) relations
6383
+ * @internal
6384
+ */
6385
+ extractSingleId(value) {
6386
+ if (typeof value === "string" && value.length > 0) {
6387
+ return value;
6388
+ }
6389
+ if (Array.isArray(value) && value.length > 0 && typeof value[0] === "string") {
6390
+ return value[0];
6391
+ }
6392
+ return null;
6393
+ }
6394
+ };
6395
+
6396
+ // src/runtime/services/rollup-scheduler.ts
6397
+ var RollupScheduler = class {
6398
+ constructor(adapter, getSchemaById, options) {
6399
+ this.adapter = adapter;
6400
+ this.getSchemaById = getSchemaById;
6401
+ this.pending = /* @__PURE__ */ new Map();
6402
+ this.rollupService = new RollupService(adapter);
6403
+ this.debounceMs = options?.debounceMs ?? 100;
6404
+ this.maxPending = options?.maxPending ?? 100;
6405
+ }
6406
+ /**
6407
+ * Schedule a rollup recalculation for a parent record
6408
+ *
6409
+ * If the same record is already pending, the timer is reset.
6410
+ * If too many recalculations are pending, triggers an immediate flush.
6411
+ *
6412
+ * @param parentId - ID of the parent record to recalculate
6413
+ * @param parentObjectId - Object ID of the parent
6414
+ * @param tenantId - Tenant ID
6415
+ */
6416
+ scheduleRecalculation(parentId, parentObjectId, tenantId) {
6417
+ const key = `${parentId}:${parentObjectId}`;
6418
+ const existing = this.pending.get(key);
6419
+ if (existing) {
6420
+ clearTimeout(existing.timeout);
6421
+ }
6422
+ const timeout = setTimeout(async () => {
6423
+ await this.executeRecalculation(parentId, parentObjectId);
6424
+ this.pending.delete(key);
6425
+ }, this.debounceMs);
6426
+ this.pending.set(key, { parentId, parentObjectId, tenantId, timeout });
6427
+ if (this.pending.size >= this.maxPending) {
6428
+ this.flush();
6429
+ }
6430
+ }
6431
+ /**
6432
+ * Execute all pending recalculations immediately
6433
+ */
6434
+ async flush() {
6435
+ const entries = Array.from(this.pending.entries());
6436
+ for (const [, entry] of entries) {
6437
+ clearTimeout(entry.timeout);
6438
+ }
6439
+ this.pending.clear();
6440
+ const tenantMap = /* @__PURE__ */ new Map();
6441
+ for (const [, entry] of entries) {
6442
+ tenantMap.set(entry.parentId, entry.tenantId);
6443
+ }
6444
+ const allIds = entries.map(([, entry]) => entry.parentId);
6445
+ const records = await this.adapter.objectRecords.findByIds(allIds);
6446
+ for (const record of records) {
6447
+ const schema = await this.getSchemaById(record.objectId);
6448
+ const tenantId = tenantMap.get(record.id);
6449
+ if (schema && tenantId) {
6450
+ await this.rollupService.recalculateAndUpdate(record, schema);
6451
+ }
6452
+ }
6453
+ }
6454
+ /**
6455
+ * Execute a single recalculation
6456
+ */
6457
+ async executeRecalculation(parentId, parentObjectId) {
6458
+ const record = await this.adapter.objectRecords.findById(parentId);
6459
+ if (!record) return;
6460
+ const schema = await this.getSchemaById(parentObjectId);
6461
+ if (!schema) return;
6462
+ await this.rollupService.recalculateAndUpdate(record, schema);
6463
+ }
6464
+ /**
6465
+ * Get number of pending recalculations
6466
+ */
6467
+ get pendingCount() {
6468
+ return this.pending.size;
6469
+ }
6470
+ /**
6471
+ * Clear all pending recalculations without executing them
6472
+ */
6473
+ clear() {
6474
+ for (const [, entry] of this.pending) {
6475
+ clearTimeout(entry.timeout);
6476
+ }
6477
+ this.pending.clear();
6478
+ }
6479
+ };
6480
+
5442
6481
  // src/runtime/services/user-profile.service.ts
5443
6482
  var UserProfileService = class {
5444
6483
  constructor(adapter, tenantId, options) {
@@ -6136,7 +7175,8 @@ async function syncAttributes(adapter, nativeObject, dbObject, existingObject, r
6136
7175
  hidden: attr.hidden,
6137
7176
  archived: attr.archived,
6138
7177
  deprecated: attr.deprecated,
6139
- defaultValue: attr.defaultValue,
7178
+ // Formula and rollup attributes don't have defaultValue
7179
+ defaultValue: "defaultValue" in attr ? attr.defaultValue : void 0,
6140
7180
  config: extractAttributeConfig(attr)
6141
7181
  });
6142
7182
  if (existingAttr) {
@@ -6215,26 +7255,52 @@ var NoopGeocodingAdapter = class {
6215
7255
  FlowService,
6216
7256
  GeocodingService,
6217
7257
  GlobalSearchService,
7258
+ InvalidPathError,
7259
+ MaxDepthExceededError,
6218
7260
  NoopGeocodingAdapter,
6219
7261
  NoopHookRegistry,
6220
7262
  ObjectSchemaService,
6221
7263
  PermissionService,
6222
7264
  RecordService,
7265
+ RelationResolverService,
6223
7266
  RelationService,
7267
+ RollupScheduler,
7268
+ RollupService,
6224
7269
  UserProfileService,
6225
7270
  UserService,
6226
7271
  ViewService,
6227
7272
  buildAuditChanges,
6228
7273
  createMockAdapter,
6229
7274
  enrichValuesWithSelectLabels,
7275
+ evaluateFormula,
7276
+ evaluateFormulaAttribute,
7277
+ evaluateFormulaAttributeWithRelations,
7278
+ evaluateFormulaWithRelations,
7279
+ evaluateFormulaWithResult,
6230
7280
  extractAttributeNames,
7281
+ extractFormulaVariables,
7282
+ extractRelationNames,
7283
+ extractRelationReferences,
7284
+ flattenRelationsForEval,
7285
+ formatFormulaResult,
7286
+ getPathDepth,
7287
+ getRelationPath,
6231
7288
  getSyncPreview,
7289
+ getTargetAttributeName,
6232
7290
  getViewSyncPreview,
7291
+ hasRelationReferences,
6233
7292
  isLabelExpression,
7293
+ parsePath,
7294
+ pathHasManyCardinality,
6234
7295
  renderLabelExpression,
7296
+ resolveMultiplePaths,
7297
+ resolveSingleValue,
6235
7298
  syncAll,
6236
7299
  syncNativeObjects,
6237
7300
  syncNativeViews,
7301
+ traversePath,
7302
+ validateFormulaExpression,
7303
+ validatePath,
6238
7304
  verifyNativeObjectsSync,
6239
7305
  verifyNativeViewsSync
6240
7306
  });