@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/runtime.js CHANGED
@@ -128,37 +128,697 @@ __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,
147
+ createRollupHooks: () => createRollupHooks,
142
148
  enrichValuesWithSelectLabels: () => enrichValuesWithSelectLabels,
149
+ evaluateFormula: () => evaluateFormula,
150
+ evaluateFormulaAttribute: () => evaluateFormulaAttribute,
151
+ evaluateFormulaAttributeWithRelations: () => evaluateFormulaAttributeWithRelations,
152
+ evaluateFormulaWithRelations: () => evaluateFormulaWithRelations,
153
+ evaluateFormulaWithResult: () => evaluateFormulaWithResult,
143
154
  extractAttributeNames: () => extractAttributeNames,
155
+ extractFormulaVariables: () => extractFormulaVariables,
156
+ extractRelationNames: () => extractRelationNames,
157
+ extractRelationReferences: () => extractRelationReferences,
158
+ flattenRelationsForEval: () => flattenRelationsForEval,
159
+ formatFormulaResult: () => formatFormulaResult,
160
+ getPathDepth: () => getPathDepth,
161
+ getRelationPath: () => getRelationPath,
144
162
  getSyncPreview: () => getSyncPreview,
163
+ getTargetAttributeName: () => getTargetAttributeName,
145
164
  getViewSyncPreview: () => getViewSyncPreview,
165
+ hasRelationReferences: () => hasRelationReferences,
146
166
  isLabelExpression: () => isLabelExpression,
167
+ parsePath: () => parsePath,
168
+ pathHasManyCardinality: () => pathHasManyCardinality,
169
+ registerAllRollupHooks: () => registerAllRollupHooks,
147
170
  renderLabelExpression: () => renderLabelExpression,
171
+ resolveMultiplePaths: () => resolveMultiplePaths,
172
+ resolveSingleValue: () => resolveSingleValue,
148
173
  syncAll: () => syncAll,
149
174
  syncNativeObjects: () => syncNativeObjects,
150
175
  syncNativeViews: () => syncNativeViews,
176
+ traversePath: () => traversePath,
177
+ validateFormulaExpression: () => validateFormulaExpression,
178
+ validatePath: () => validatePath,
151
179
  verifyNativeObjectsSync: () => verifyNativeObjectsSync,
152
180
  verifyNativeViewsSync: () => verifyNativeViewsSync
153
181
  });
154
182
  module.exports = __toCommonJS(runtime_exports);
155
183
 
184
+ // src/runtime/formula/evaluator.ts
185
+ var import_expr_eval = require("expr-eval");
186
+ function createFormulaParser() {
187
+ const parser = new import_expr_eval.Parser();
188
+ parser.functions.IF = (condition, thenValue, elseValue) => condition ? thenValue : elseValue;
189
+ parser.functions.AND = (...args) => args.every(Boolean);
190
+ parser.functions.OR = (...args) => args.some(Boolean);
191
+ parser.functions.NOT = (value) => !value;
192
+ parser.functions.EMPTY = (value) => value === null || value === void 0 || value === "";
193
+ parser.functions.COALESCE = (...args) => args.find((a) => a != null) ?? null;
194
+ parser.functions.DEFAULT = (value, defaultValue) => value == null || value === "" ? defaultValue : value;
195
+ parser.functions.CONCAT = (...args) => args.filter((a) => a != null).map(String).join("");
196
+ parser.functions.UPPER = (value) => String(value ?? "").toUpperCase();
197
+ parser.functions.LOWER = (value) => String(value ?? "").toLowerCase();
198
+ parser.functions.TRIM = (value) => String(value ?? "").trim();
199
+ parser.functions.LENGTH = (value) => String(value ?? "").length;
200
+ parser.functions.LEFT = (value, count) => String(value ?? "").slice(0, count);
201
+ parser.functions.RIGHT = (value, count) => String(value ?? "").slice(-count);
202
+ parser.functions.REPLACE = (value, search, replacement) => String(value ?? "").replace(new RegExp(search, "g"), replacement);
203
+ parser.functions.CONTAINS = (value, search) => String(value ?? "").toLowerCase().includes(String(search).toLowerCase());
204
+ parser.functions.ROUND = (value, decimals = 0) => {
205
+ if (typeof value !== "number" || Number.isNaN(value)) return null;
206
+ const factor = 10 ** decimals;
207
+ return Math.round(value * factor) / factor;
208
+ };
209
+ parser.functions.FLOOR = (value) => {
210
+ if (typeof value !== "number" || Number.isNaN(value)) return null;
211
+ return Math.floor(value);
212
+ };
213
+ parser.functions.CEIL = (value) => {
214
+ if (typeof value !== "number" || Number.isNaN(value)) return null;
215
+ return Math.ceil(value);
216
+ };
217
+ parser.functions.ABS = (value) => {
218
+ if (typeof value !== "number" || Number.isNaN(value)) return null;
219
+ return Math.abs(value);
220
+ };
221
+ parser.functions.MIN = (...args) => {
222
+ const nums = args.filter((n) => typeof n === "number" && !Number.isNaN(n));
223
+ return nums.length > 0 ? Math.min(...nums) : null;
224
+ };
225
+ parser.functions.MAX = (...args) => {
226
+ const nums = args.filter((n) => typeof n === "number" && !Number.isNaN(n));
227
+ return nums.length > 0 ? Math.max(...nums) : null;
228
+ };
229
+ parser.functions.POW = (base, exponent) => {
230
+ if (typeof base !== "number" || typeof exponent !== "number") return null;
231
+ return base ** exponent;
232
+ };
233
+ parser.functions.MOD = (a, b) => {
234
+ if (typeof a !== "number" || typeof b !== "number" || b === 0) return null;
235
+ return a % b;
236
+ };
237
+ parser.functions.NOW = () => (/* @__PURE__ */ new Date()).toISOString();
238
+ parser.functions.TODAY = () => (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
239
+ parser.functions.YEAR = (value) => {
240
+ const date = parseDate(value);
241
+ return date ? date.getFullYear() : null;
242
+ };
243
+ parser.functions.MONTH = (value) => {
244
+ const date = parseDate(value);
245
+ return date ? date.getMonth() + 1 : null;
246
+ };
247
+ parser.functions.DAY = (value) => {
248
+ const date = parseDate(value);
249
+ return date ? date.getDate() : null;
250
+ };
251
+ parser.functions.DATE_DIFF = (date1, date2, unit = "days") => {
252
+ const d1 = parseDate(date1);
253
+ const d2 = parseDate(date2);
254
+ if (d1 === null || d2 === null) return null;
255
+ const diffMs = d1.getTime() - d2.getTime();
256
+ const MS_PER_DAY = 1e3 * 60 * 60 * 24;
257
+ const conversions = {
258
+ years: MS_PER_DAY * 365,
259
+ months: MS_PER_DAY * 30,
260
+ weeks: MS_PER_DAY * 7,
261
+ days: MS_PER_DAY,
262
+ hours: 1e3 * 60 * 60,
263
+ minutes: 1e3 * 60
264
+ };
265
+ const divisor = conversions[unit] ?? MS_PER_DAY;
266
+ return diffMs / divisor;
267
+ };
268
+ return parser;
269
+ }
270
+ function parseDate(value) {
271
+ if (!value) return null;
272
+ if (value instanceof Date) return value;
273
+ if (typeof value === "string" || typeof value === "number") {
274
+ const date = new Date(value);
275
+ return Number.isNaN(date.getTime()) ? null : date;
276
+ }
277
+ return null;
278
+ }
279
+ var formulaParser = createFormulaParser();
280
+ function evaluateFormula(expression, values) {
281
+ try {
282
+ const parsed = formulaParser.parse(expression);
283
+ return parsed.evaluate(values);
284
+ } catch {
285
+ return null;
286
+ }
287
+ }
288
+ function evaluateFormulaWithResult(expression, values) {
289
+ try {
290
+ const parsed = formulaParser.parse(expression);
291
+ const value = parsed.evaluate(values);
292
+ return { value };
293
+ } catch (error) {
294
+ return {
295
+ value: null,
296
+ error: error instanceof Error ? error.message : "Unknown error"
297
+ };
298
+ }
299
+ }
300
+ function formatFormulaResult(value, returnType, decimals) {
301
+ if (value === null || value === void 0) {
302
+ return null;
303
+ }
304
+ switch (returnType) {
305
+ case "number":
306
+ return formatNumberResult(value, decimals);
307
+ case "boolean":
308
+ return Boolean(value);
309
+ case "date":
310
+ return formatDateResult(value);
311
+ case "text":
312
+ return String(value);
313
+ }
314
+ }
315
+ function formatNumberResult(value, decimals) {
316
+ const num = typeof value === "number" ? value : Number(value);
317
+ if (Number.isNaN(num)) return null;
318
+ return decimals !== void 0 ? Number(num.toFixed(decimals)) : num;
319
+ }
320
+ function formatDateResult(value) {
321
+ if (value instanceof Date) return value.toISOString();
322
+ if (typeof value === "string") return value;
323
+ return null;
324
+ }
325
+ function evaluateFormulaAttribute(attr, values) {
326
+ const raw = evaluateFormula(attr.expression, values);
327
+ return formatFormulaResult(raw, attr.returnType, attr.decimals);
328
+ }
329
+ function validateFormulaExpression(expression) {
330
+ try {
331
+ formulaParser.parse(expression);
332
+ return { valid: true };
333
+ } catch (error) {
334
+ return {
335
+ valid: false,
336
+ error: error instanceof Error ? error.message : "Invalid expression"
337
+ };
338
+ }
339
+ }
340
+ function extractFormulaVariables(expression) {
341
+ try {
342
+ const parsed = formulaParser.parse(expression);
343
+ return parsed.variables();
344
+ } catch {
345
+ return [];
346
+ }
347
+ }
348
+ var RELATION_REF_PATTERN = /\b([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)\b/g;
349
+ function extractRelationReferences(expression) {
350
+ const regex = new RegExp(RELATION_REF_PATTERN.source, "g");
351
+ const matches = expression.matchAll(regex);
352
+ return [...matches].map((m) => m[0]);
353
+ }
354
+ function extractRelationNames(expression) {
355
+ const refs = extractRelationReferences(expression);
356
+ const names = refs.map((ref) => ref.split(".")[0]);
357
+ return [...new Set(names)];
358
+ }
359
+ function hasRelationReferences(expression) {
360
+ const regex = new RegExp(RELATION_REF_PATTERN.source);
361
+ return regex.test(expression);
362
+ }
363
+ function flattenRelationsForEval(resolvedRelations) {
364
+ const flat = {};
365
+ for (const [relationName, values] of Object.entries(resolvedRelations)) {
366
+ for (const [key, value] of Object.entries(values)) {
367
+ flat[`${relationName}.${key}`] = value;
368
+ }
369
+ }
370
+ return flat;
371
+ }
372
+ async function evaluateFormulaWithRelations(expression, record, schema, resolver) {
373
+ const relationNames = extractRelationNames(expression);
374
+ if (relationNames.length === 0) {
375
+ return evaluateFormula(expression, record.values);
376
+ }
377
+ const resolvedRelations = await resolver.resolveRelationValues(record, schema, relationNames);
378
+ const flattenedRelations = flattenRelationsForEval(resolvedRelations);
379
+ const allValues = {
380
+ ...record.values,
381
+ ...flattenedRelations
382
+ };
383
+ return evaluateFormula(expression, allValues);
384
+ }
385
+ async function evaluateFormulaAttributeWithRelations(attr, record, schema, resolver) {
386
+ const raw = await evaluateFormulaWithRelations(attr.expression, record, schema, resolver);
387
+ return formatFormulaResult(raw, attr.returnType, attr.decimals);
388
+ }
389
+
390
+ // src/runtime/formula/path-parser.ts
391
+ var InvalidPathError = class extends Error {
392
+ constructor(path, segment, reason) {
393
+ super(`Invalid path "${path}" at "${segment}": ${reason}`);
394
+ this.path = path;
395
+ this.segment = segment;
396
+ this.reason = reason;
397
+ this.name = "InvalidPathError";
398
+ }
399
+ };
400
+ var MaxDepthExceededError = class extends Error {
401
+ constructor(path, maxDepth) {
402
+ super(`Path "${path}" exceeds maximum depth of ${maxDepth}`);
403
+ this.path = path;
404
+ this.maxDepth = maxDepth;
405
+ this.name = "MaxDepthExceededError";
406
+ }
407
+ };
408
+ async function parsePath(path, startSchema, getSchema, maxDepth = 5) {
409
+ const segments = path.split(".");
410
+ if (segments.length === 0 || segments.length === 1 && segments[0] === "") {
411
+ throw new InvalidPathError(path, path, "Path cannot be empty");
412
+ }
413
+ if (segments.length > maxDepth) {
414
+ throw new MaxDepthExceededError(path, maxDepth);
415
+ }
416
+ const result = [];
417
+ let currentSchema = startSchema;
418
+ for (let i = 0; i < segments.length; i++) {
419
+ const segmentName = segments[i];
420
+ const isLastSegment = i === segments.length - 1;
421
+ const attr = currentSchema.attributes.find((a) => a.name === segmentName);
422
+ if (!attr) {
423
+ throw new InvalidPathError(
424
+ path,
425
+ segmentName,
426
+ `Attribute "${segmentName}" not found in object "${currentSchema.name}"`
427
+ );
428
+ }
429
+ if (attr.type === "relation") {
430
+ const relationAttr = attr;
431
+ const targetObject = relationAttr.targets[0]?.object;
432
+ if (!targetObject) {
433
+ throw new InvalidPathError(path, segmentName, "Relation has no target object");
434
+ }
435
+ result.push({
436
+ name: segmentName,
437
+ type: "relation",
438
+ cardinality: relationAttr.cardinality,
439
+ targetObject
440
+ });
441
+ if (!isLastSegment) {
442
+ const nextSchema = await getSchema(targetObject);
443
+ if (!nextSchema) {
444
+ throw new InvalidPathError(
445
+ path,
446
+ segmentName,
447
+ `Target object "${targetObject}" schema not found`
448
+ );
449
+ }
450
+ currentSchema = nextSchema;
451
+ }
452
+ } else {
453
+ if (!isLastSegment) {
454
+ throw new InvalidPathError(
455
+ path,
456
+ segmentName,
457
+ `"${segmentName}" is not a relation but has segments after it`
458
+ );
459
+ }
460
+ result.push({
461
+ name: segmentName,
462
+ type: "attribute"
463
+ });
464
+ }
465
+ }
466
+ return result;
467
+ }
468
+ async function validatePath(path, startSchema, getSchema, maxDepth = 5) {
469
+ try {
470
+ await parsePath(path, startSchema, getSchema, maxDepth);
471
+ return true;
472
+ } catch {
473
+ return false;
474
+ }
475
+ }
476
+ function pathHasManyCardinality(segments) {
477
+ return segments.some((s) => s.type === "relation" && s.cardinality === "many");
478
+ }
479
+ function getPathDepth(segments) {
480
+ return segments.filter((s) => s.type === "relation").length;
481
+ }
482
+ function getTargetAttributeName(path) {
483
+ const parts = path.split(".");
484
+ return parts[parts.length - 1];
485
+ }
486
+ function getRelationPath(path) {
487
+ const parts = path.split(".");
488
+ if (parts.length <= 1) return null;
489
+ return parts.slice(0, -1).join(".");
490
+ }
491
+
492
+ // src/runtime/formula/path-traversal.ts
493
+ async function traversePath(record, path, startSchemaName, adapter, getSchema, options) {
494
+ const maxDepth = options?.maxDepth ?? 5;
495
+ const startSchema = await getSchema(startSchemaName);
496
+ if (!startSchema) {
497
+ return { values: [], recordCounts: [0] };
498
+ }
499
+ const segments = await parsePath(path, startSchema, getSchema, maxDepth);
500
+ if (segments.length > maxDepth) {
501
+ throw new MaxDepthExceededError(path, maxDepth);
502
+ }
503
+ return await executeTraversal([record], segments, adapter, 0);
504
+ }
505
+ async function executeTraversal(records, segments, adapter, depth) {
506
+ const recordCounts = [records.length];
507
+ if (depth >= segments.length || records.length === 0) {
508
+ return { values: [], recordCounts };
509
+ }
510
+ const segment = segments[depth];
511
+ const isLastSegment = depth === segments.length - 1;
512
+ if (segment.type === "attribute") {
513
+ const values = records.map((r) => r.values[segment.name]).filter((v) => v !== void 0);
514
+ return { values, recordCounts };
515
+ }
516
+ const relatedIds = collectRelatedIds(records, segment.name);
517
+ if (relatedIds.length === 0) {
518
+ return { values: [], recordCounts };
519
+ }
520
+ const relatedRecords = await adapter.objectRecords.findByIds(relatedIds);
521
+ recordCounts.push(relatedRecords.length);
522
+ if (isLastSegment) {
523
+ return { values: relatedIds, recordCounts };
524
+ }
525
+ const nestedResult = await executeTraversal(relatedRecords, segments, adapter, depth + 1);
526
+ return {
527
+ values: nestedResult.values,
528
+ recordCounts: [...recordCounts, ...nestedResult.recordCounts.slice(1)]
529
+ };
530
+ }
531
+ function collectRelatedIds(records, relationName) {
532
+ const ids = [];
533
+ for (const record of records) {
534
+ const value = record.values[relationName];
535
+ if (typeof value === "string" && value.length > 0) {
536
+ ids.push(value);
537
+ } else if (Array.isArray(value)) {
538
+ for (const id of value) {
539
+ if (typeof id === "string" && id.length > 0) {
540
+ ids.push(id);
541
+ }
542
+ }
543
+ }
544
+ }
545
+ return [...new Set(ids)];
546
+ }
547
+ async function resolveSingleValue(record, path, startSchemaName, adapter, getSchema) {
548
+ const result = await traversePath(record, path, startSchemaName, adapter, getSchema);
549
+ return result.values[0] ?? null;
550
+ }
551
+ async function resolveMultiplePaths(record, paths, startSchemaName, adapter, getSchema) {
552
+ const results = /* @__PURE__ */ new Map();
553
+ await Promise.all(
554
+ paths.map(async (path) => {
555
+ const result = await traversePath(record, path, startSchemaName, adapter, getSchema);
556
+ results.set(path, result.values);
557
+ })
558
+ );
559
+ return results;
560
+ }
561
+
156
562
  // src/runtime/hooks/hook.registry.ts
157
563
  var NoopHookRegistry = class {
158
564
  async execute() {
159
565
  }
160
566
  };
161
567
 
568
+ // src/runtime/services/rollup.service.ts
569
+ var RollupService = class {
570
+ constructor(adapter) {
571
+ this.adapter = adapter;
572
+ }
573
+ /**
574
+ * Calculate a rollup value for a record
575
+ *
576
+ * @param recordId - ID of the parent record
577
+ * @param rollupAttr - Rollup attribute definition
578
+ * @param schema - Schema of the parent object
579
+ * @returns Computed rollup value
580
+ *
581
+ * @example
582
+ * ```typescript
583
+ * // Sum all order amounts for a company
584
+ * const totalOrders = await rollupService.calculate(
585
+ * "company-123",
586
+ * {
587
+ * type: "rollup",
588
+ * name: "totalOrders",
589
+ * relationAttribute: "orders",
590
+ * targetAttribute: "amount",
591
+ * function: "sum",
592
+ * ...
593
+ * },
594
+ * companySchema
595
+ * );
596
+ * ```
597
+ */
598
+ async calculate(recordId, rollupAttr, schema, tenantId) {
599
+ const relationAttr = schema.attributes.find(
600
+ (a) => a.type === "relation" && a.name === rollupAttr.relationAttribute
601
+ );
602
+ if (!relationAttr) {
603
+ return { value: null, recordCount: 0 };
604
+ }
605
+ const targetObjectName = relationAttr.targets[0]?.object;
606
+ if (!targetObjectName) {
607
+ return { value: null, recordCount: 0 };
608
+ }
609
+ const targetObject = await this.adapter.objects.findByName(tenantId, targetObjectName);
610
+ if (!targetObject) {
611
+ return { value: null, recordCount: 0 };
612
+ }
613
+ const relatedRecords = await this.adapter.objectRecords.findByRelation(
614
+ tenantId,
615
+ targetObject.id,
616
+ rollupAttr.relationAttribute,
617
+ recordId
618
+ );
619
+ if (relatedRecords.length === 0) {
620
+ return { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 };
621
+ }
622
+ const values = relatedRecords.map((r) => r.values[rollupAttr.targetAttribute]).filter((v) => v !== void 0);
623
+ const aggregated = this.aggregate(values, rollupAttr.function);
624
+ const result = typeof aggregated === "number" && rollupAttr.decimals !== void 0 ? Number(aggregated.toFixed(rollupAttr.decimals)) : aggregated;
625
+ return { value: result, recordCount: relatedRecords.length };
626
+ }
627
+ /**
628
+ * Calculate rollup values for multiple records (batched)
629
+ *
630
+ * More efficient than calling calculate() for each record individually.
631
+ */
632
+ async calculateForMany(recordIds, rollupAttr, schema, tenantId) {
633
+ const results = /* @__PURE__ */ new Map();
634
+ for (const id of recordIds) {
635
+ results.set(id, { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 });
636
+ }
637
+ await Promise.all(
638
+ recordIds.map(async (id) => {
639
+ const result = await this.calculate(id, rollupAttr, schema, tenantId);
640
+ results.set(id, result);
641
+ })
642
+ );
643
+ return results;
644
+ }
645
+ /**
646
+ * Apply aggregation function to a set of values
647
+ */
648
+ aggregate(values, fn) {
649
+ switch (fn) {
650
+ case "sum":
651
+ return this.sumNumbers(values);
652
+ case "avg":
653
+ return this.averageNumbers(values);
654
+ case "min":
655
+ return this.minNumbers(values);
656
+ case "max":
657
+ return this.maxNumbers(values);
658
+ case "count":
659
+ return values.length;
660
+ case "countValues":
661
+ return values.filter((v) => v != null && v !== "").length;
662
+ case "countEmpty":
663
+ return values.filter((v) => v == null || v === "").length;
664
+ case "percentFilled": {
665
+ if (values.length === 0) return 0;
666
+ const filled = values.filter((v) => v != null && v !== "").length;
667
+ return filled / values.length * 100;
668
+ }
669
+ case "percentEmpty": {
670
+ if (values.length === 0) return 0;
671
+ const empty = values.filter((v) => v == null || v === "").length;
672
+ return empty / values.length * 100;
673
+ }
674
+ }
675
+ }
676
+ /**
677
+ * Get the default empty value for a rollup function
678
+ */
679
+ getEmptyValue(fn) {
680
+ switch (fn) {
681
+ case "sum":
682
+ case "count":
683
+ case "countValues":
684
+ case "countEmpty":
685
+ case "percentEmpty":
686
+ case "percentFilled":
687
+ return 0;
688
+ case "avg":
689
+ case "min":
690
+ case "max":
691
+ return null;
692
+ }
693
+ }
694
+ /**
695
+ * Sum numeric values
696
+ */
697
+ sumNumbers(values) {
698
+ return values.filter((v) => typeof v === "number" && !Number.isNaN(v)).reduce((sum, n) => sum + n, 0);
699
+ }
700
+ /**
701
+ * Average numeric values
702
+ */
703
+ averageNumbers(values) {
704
+ const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
705
+ if (nums.length === 0) return null;
706
+ return nums.reduce((sum, n) => sum + n, 0) / nums.length;
707
+ }
708
+ /**
709
+ * Minimum numeric value
710
+ */
711
+ minNumbers(values) {
712
+ const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
713
+ if (nums.length === 0) return null;
714
+ return Math.min(...nums);
715
+ }
716
+ /**
717
+ * Maximum numeric value
718
+ */
719
+ maxNumbers(values) {
720
+ const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
721
+ if (nums.length === 0) return null;
722
+ return Math.max(...nums);
723
+ }
724
+ /**
725
+ * Recalculate all rollup attributes for a record and update it
726
+ *
727
+ * Called after related records change to keep rollups up-to-date.
728
+ */
729
+ async recalculateAndUpdate(record, schema, tenantId) {
730
+ const rollupAttrs = schema.attributes.filter((a) => a.type === "rollup");
731
+ if (rollupAttrs.length === 0) {
732
+ return record;
733
+ }
734
+ const updates = {};
735
+ for (const attr of rollupAttrs) {
736
+ const result = await this.calculate(record.id, attr, schema, tenantId);
737
+ updates[attr.name] = result.value;
738
+ }
739
+ return await this.adapter.objectRecords.update(record.id, updates);
740
+ }
741
+ /**
742
+ * Find parent records that need rollup recalculation when a child record changes
743
+ *
744
+ * Used by hooks to determine which parent records to recalculate after
745
+ * a child record is created, updated, or deleted.
746
+ *
747
+ * @param changedRecord - The record that was modified
748
+ * @param changedSchema - Schema of the changed record's object
749
+ * @returns Array of parent record IDs that need recalculation
750
+ */
751
+ async findAffectedParentRecords(changedRecord, changedSchema) {
752
+ const affectedIds = [];
753
+ const relationAttrs = changedSchema.attributes.filter(
754
+ (a) => a.type === "relation"
755
+ );
756
+ for (const attr of relationAttrs) {
757
+ const relatedId = changedRecord.values[attr.name];
758
+ if (typeof relatedId === "string" && relatedId.length > 0) {
759
+ affectedIds.push(relatedId);
760
+ } else if (Array.isArray(relatedId)) {
761
+ affectedIds.push(...relatedId.filter((id) => typeof id === "string"));
762
+ }
763
+ }
764
+ return [...new Set(affectedIds)];
765
+ }
766
+ };
767
+
768
+ // src/runtime/hooks/rollup.hooks.ts
769
+ function createRollupHooks(objectName, config) {
770
+ const rollupService = new RollupService(config.adapter);
771
+ async function recalculateParentRollups(ctx) {
772
+ const schema = await config.getSchemaById(ctx.objectId);
773
+ if (!schema) return;
774
+ const affectedParentIds = await rollupService.findAffectedParentRecords(ctx.record, schema);
775
+ if (affectedParentIds.length === 0) return;
776
+ const parentRecords = await config.adapter.objectRecords.findByIds(affectedParentIds);
777
+ for (const parentRecord of parentRecords) {
778
+ const parentSchema = await config.getSchemaById(parentRecord.objectId);
779
+ if (!parentSchema) continue;
780
+ const hasRollups = parentSchema.attributes.some(
781
+ (a) => a.type === "rollup"
782
+ );
783
+ if (hasRollups) {
784
+ await rollupService.recalculateAndUpdate(parentRecord, parentSchema, config.tenantId);
785
+ }
786
+ }
787
+ }
788
+ return [
789
+ {
790
+ type: "afterCreate",
791
+ objectName,
792
+ handler: recalculateParentRollups,
793
+ priority: 100
794
+ // Run after other hooks
795
+ },
796
+ {
797
+ type: "afterUpdate",
798
+ objectName,
799
+ handler: recalculateParentRollups,
800
+ priority: 100
801
+ },
802
+ {
803
+ type: "afterDelete",
804
+ objectName,
805
+ handler: recalculateParentRollups,
806
+ priority: 100
807
+ }
808
+ ];
809
+ }
810
+ function registerAllRollupHooks(hookRegistry, schemas, config) {
811
+ for (const schema of schemas) {
812
+ const hasRelations = schema.attributes.some((a) => a.type === "relation");
813
+ if (hasRelations && hookRegistry.register) {
814
+ const hooks = createRollupHooks(schema.name, config);
815
+ for (const hook of hooks) {
816
+ hookRegistry.register(hook);
817
+ }
818
+ }
819
+ }
820
+ }
821
+
162
822
  // src/utils.ts
163
823
  function generateId() {
164
824
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
@@ -997,6 +1657,26 @@ function createMockObjectRecordsRepository(stores) {
997
1657
  }
998
1658
  }
999
1659
  return Promise.resolve(updated);
1660
+ },
1661
+ findByRelation(objectName, relationAttributeName, targetRecordId) {
1662
+ const obj = Array.from(stores.objects.values()).find((o) => o.name === objectName);
1663
+ if (!obj) return Promise.resolve([]);
1664
+ const results = [];
1665
+ for (const record of stores.objectRecords.values()) {
1666
+ if (record.objectId !== obj.id) continue;
1667
+ const value = record.values[relationAttributeName];
1668
+ let matches = false;
1669
+ if (typeof value === "string" && value === targetRecordId) {
1670
+ matches = true;
1671
+ } else if (Array.isArray(value) && value.includes(targetRecordId)) {
1672
+ matches = true;
1673
+ }
1674
+ if (matches) {
1675
+ const { tenantId: _, ...publicRecord } = record;
1676
+ results.push(publicRecord);
1677
+ }
1678
+ }
1679
+ return Promise.resolve(results);
1000
1680
  }
1001
1681
  };
1002
1682
  }
@@ -2902,6 +3582,30 @@ var ratingConfigSchema = baseConfigSchema.extend({
2902
3582
  max: import_zod4.z.number().int().min(1).optional(),
2903
3583
  iconType: import_zod4.z.enum(["star", "heart", "thumbs", "number"]).optional()
2904
3584
  });
3585
+ var formulaConfigSchema = baseConfigSchema.extend({
3586
+ expression: import_zod4.z.string().min(1),
3587
+ returnType: import_zod4.z.enum(["text", "number", "boolean", "date"]),
3588
+ decimals: import_zod4.z.number().int().min(0).max(10).optional(),
3589
+ allowRelations: import_zod4.z.boolean().optional()
3590
+ });
3591
+ var rollupConfigSchema = baseConfigSchema.extend({
3592
+ relationAttribute: import_zod4.z.string().min(1).optional(),
3593
+ relationPath: import_zod4.z.string().optional(),
3594
+ targetAttribute: import_zod4.z.string().min(1),
3595
+ function: import_zod4.z.enum([
3596
+ "sum",
3597
+ "count",
3598
+ "avg",
3599
+ "min",
3600
+ "max",
3601
+ "countValues",
3602
+ "countEmpty",
3603
+ "percentEmpty",
3604
+ "percentFilled"
3605
+ ]),
3606
+ decimals: import_zod4.z.number().int().min(0).max(10).optional(),
3607
+ materialize: import_zod4.z.boolean().optional()
3608
+ });
2905
3609
  var attributeConfigSchemas = {
2906
3610
  text: textConfigSchema,
2907
3611
  textarea: textareaConfigSchema,
@@ -2918,7 +3622,9 @@ var attributeConfigSchemas = {
2918
3622
  file: fileConfigSchema,
2919
3623
  user: userConfigSchema,
2920
3624
  relation: relationConfigSchema,
2921
- rating: ratingConfigSchema
3625
+ rating: ratingConfigSchema,
3626
+ formula: formulaConfigSchema,
3627
+ rollup: rollupConfigSchema
2922
3628
  };
2923
3629
  function getAttributeConfigSchema(type) {
2924
3630
  return attributeConfigSchemas[type];
@@ -3075,6 +3781,12 @@ function createRatingValidator(attr) {
3075
3781
  }
3076
3782
  return schema;
3077
3783
  }
3784
+ function createFormulaValidator(_attr) {
3785
+ return import_zod4.z.unknown();
3786
+ }
3787
+ function createRollupValidator(_attr) {
3788
+ return import_zod4.z.unknown();
3789
+ }
3078
3790
  function createAttributeValidator(attr) {
3079
3791
  switch (attr.type) {
3080
3792
  case "text":
@@ -3107,6 +3819,10 @@ function createAttributeValidator(attr) {
3107
3819
  return createRelationValidator(attr);
3108
3820
  case "rating":
3109
3821
  return createRatingValidator(attr);
3822
+ case "formula":
3823
+ return createFormulaValidator(attr);
3824
+ case "rollup":
3825
+ return createRollupValidator(attr);
3110
3826
  default:
3111
3827
  return import_zod4.z.unknown();
3112
3828
  }
@@ -5046,13 +5762,17 @@ var RecordService = class {
5046
5762
  if (!record) {
5047
5763
  return null;
5048
5764
  }
5765
+ const schema = await this.schemaService.getObjectSchema(record.objectId);
5766
+ let enrichedRecord = record;
5767
+ if (!options?.skipFormulas) {
5768
+ enrichedRecord = this.enrichWithFormulas(record, schema);
5769
+ }
5049
5770
  if (options?.includeSchema) {
5050
- const schema = await this.schemaService.getObjectSchema(record.objectId);
5051
- const recordWithSchema = record;
5771
+ const recordWithSchema = enrichedRecord;
5052
5772
  recordWithSchema.schema = schema;
5053
5773
  return recordWithSchema;
5054
5774
  }
5055
- return record;
5775
+ return enrichedRecord;
5056
5776
  }
5057
5777
  /**
5058
5778
  * Get a record by ID or throw if not found
@@ -5347,6 +6067,46 @@ var RecordService = class {
5347
6067
  timestamp: /* @__PURE__ */ new Date()
5348
6068
  };
5349
6069
  }
6070
+ /**
6071
+ * Enrich a record with computed formula values
6072
+ *
6073
+ * Formula attributes are calculated at read-time from the record's values.
6074
+ * This method adds the computed values to the record's values object.
6075
+ *
6076
+ * @param record - The record to enrich
6077
+ * @param schema - The object schema containing attribute definitions
6078
+ * @returns Record with formula values computed
6079
+ * @internal
6080
+ */
6081
+ enrichWithFormulas(record, schema) {
6082
+ const formulaAttrs = schema.attributes.filter(
6083
+ (a) => a.type === "formula"
6084
+ );
6085
+ if (formulaAttrs.length === 0) {
6086
+ return record;
6087
+ }
6088
+ const enrichedValues = { ...record.values };
6089
+ for (const attr of formulaAttrs) {
6090
+ enrichedValues[attr.name] = evaluateFormulaAttribute(attr, record.values);
6091
+ }
6092
+ return {
6093
+ ...record,
6094
+ values: enrichedValues
6095
+ };
6096
+ }
6097
+ /**
6098
+ * Enrich multiple records with computed formula values
6099
+ * @internal
6100
+ */
6101
+ enrichRecordsWithFormulas(records, schema) {
6102
+ const formulaAttrs = schema.attributes.filter(
6103
+ (a) => a.type === "formula"
6104
+ );
6105
+ if (formulaAttrs.length === 0) {
6106
+ return records;
6107
+ }
6108
+ return records.map((record) => this.enrichWithFormulas(record, schema));
6109
+ }
5350
6110
  /**
5351
6111
  * Permanently delete a record (hard delete)
5352
6112
  *
@@ -5375,11 +6135,18 @@ var RecordService = class {
5375
6135
  * @returns Records and total count
5376
6136
  */
5377
6137
  async listRecords(objectId, options) {
6138
+ const schema = await this.schemaService.getObjectSchema(objectId);
5378
6139
  if (this.permissionService && this.userId) {
5379
- const schema = await this.schemaService.getObjectSchema(objectId);
5380
6140
  await this.checkPermission(schema.name, "read");
5381
6141
  }
5382
- return await this.adapter.objectRecords.list(this.tenantId, objectId, options);
6142
+ const result = await this.adapter.objectRecords.list(this.tenantId, objectId, options);
6143
+ if (!options?.skipFormulas) {
6144
+ return {
6145
+ records: this.enrichRecordsWithFormulas(result.records, schema),
6146
+ total: result.total
6147
+ };
6148
+ }
6149
+ return result;
5383
6150
  }
5384
6151
  /**
5385
6152
  * Search records using full-text search
@@ -5390,11 +6157,18 @@ var RecordService = class {
5390
6157
  * @returns Matching records and total count
5391
6158
  */
5392
6159
  async searchRecords(objectId, query, options) {
6160
+ const schema = await this.schemaService.getObjectSchema(objectId);
5393
6161
  if (this.permissionService && this.userId) {
5394
- const schema = await this.schemaService.getObjectSchema(objectId);
5395
6162
  await this.checkPermission(schema.name, "read");
5396
6163
  }
5397
- return await this.adapter.objectRecords.search(this.tenantId, objectId, query, options);
6164
+ const result = await this.adapter.objectRecords.search(this.tenantId, objectId, query, options);
6165
+ if (!options?.skipFormulas) {
6166
+ return {
6167
+ records: this.enrichRecordsWithFormulas(result.records, schema),
6168
+ total: result.total
6169
+ };
6170
+ }
6171
+ return result;
5398
6172
  }
5399
6173
  /**
5400
6174
  * Validate data against object schema without saving
@@ -5439,6 +6213,231 @@ var RecordService = class {
5439
6213
  }
5440
6214
  };
5441
6215
 
6216
+ // src/runtime/services/relation-resolver.service.ts
6217
+ var RelationResolverService = class {
6218
+ constructor(adapter) {
6219
+ this.adapter = adapter;
6220
+ }
6221
+ /**
6222
+ * Resolve values from related records for formula evaluation
6223
+ *
6224
+ * Phase 2: Supports 1 level of relation traversal only
6225
+ *
6226
+ * @param record - The source record
6227
+ * @param schema - Schema of the source object
6228
+ * @param relationNames - Names of relation attributes to resolve
6229
+ * @returns Map of relation name to related record's values
6230
+ *
6231
+ * @example
6232
+ * ```typescript
6233
+ * // For an order with company relation
6234
+ * const resolved = await resolver.resolveRelationValues(
6235
+ * orderRecord,
6236
+ * orderSchema,
6237
+ * ["company"]
6238
+ * );
6239
+ * // → { company: { id: "...", name: "Acme Corp", ... } }
6240
+ * ```
6241
+ */
6242
+ async resolveRelationValues(record, schema, relationNames) {
6243
+ const result = {};
6244
+ const relationAttrs = schema.attributes.filter(
6245
+ (a) => a.type === "relation" && relationNames.includes(a.name)
6246
+ );
6247
+ if (relationAttrs.length === 0) {
6248
+ return result;
6249
+ }
6250
+ const idsToFetch = [];
6251
+ const attrIdMap = /* @__PURE__ */ new Map();
6252
+ for (const attr of relationAttrs) {
6253
+ const relatedId = this.extractSingleId(record.values[attr.name]);
6254
+ if (relatedId) {
6255
+ idsToFetch.push(relatedId);
6256
+ attrIdMap.set(relatedId, attr.name);
6257
+ }
6258
+ }
6259
+ if (idsToFetch.length === 0) {
6260
+ return result;
6261
+ }
6262
+ const relatedRecords = await this.adapter.objectRecords.findByIds(idsToFetch);
6263
+ for (const relatedRecord of relatedRecords) {
6264
+ const attrName = attrIdMap.get(relatedRecord.id);
6265
+ if (attrName) {
6266
+ result[attrName] = relatedRecord.values;
6267
+ }
6268
+ }
6269
+ return result;
6270
+ }
6271
+ /**
6272
+ * Resolve relation values for multiple records (batched)
6273
+ *
6274
+ * Optimized for list operations - fetches all related records in one batch
6275
+ *
6276
+ * @param records - Source records
6277
+ * @param schema - Schema of the source object
6278
+ * @param relationNames - Names of relation attributes to resolve
6279
+ * @returns Map of record ID to resolved relations
6280
+ */
6281
+ async resolveRelationValuesForMany(records, schema, relationNames) {
6282
+ const resultMap = /* @__PURE__ */ new Map();
6283
+ for (const record of records) {
6284
+ resultMap.set(record.id, {});
6285
+ }
6286
+ const relationAttrs = schema.attributes.filter(
6287
+ (a) => a.type === "relation" && relationNames.includes(a.name)
6288
+ );
6289
+ if (relationAttrs.length === 0) {
6290
+ return resultMap;
6291
+ }
6292
+ const allIdsToFetch = /* @__PURE__ */ new Set();
6293
+ const recordRelationMap = /* @__PURE__ */ new Map();
6294
+ for (const record of records) {
6295
+ const idToAttr = /* @__PURE__ */ new Map();
6296
+ for (const attr of relationAttrs) {
6297
+ const relatedId = this.extractSingleId(record.values[attr.name]);
6298
+ if (relatedId) {
6299
+ allIdsToFetch.add(relatedId);
6300
+ idToAttr.set(relatedId, attr.name);
6301
+ }
6302
+ }
6303
+ recordRelationMap.set(record.id, idToAttr);
6304
+ }
6305
+ if (allIdsToFetch.size === 0) {
6306
+ return resultMap;
6307
+ }
6308
+ const relatedRecords = await this.adapter.objectRecords.findByIds([...allIdsToFetch]);
6309
+ const relatedRecordMap = new Map(relatedRecords.map((r) => [r.id, r]));
6310
+ for (const record of records) {
6311
+ const result = resultMap.get(record.id);
6312
+ const idToAttr = recordRelationMap.get(record.id);
6313
+ for (const [relatedId, attrName] of idToAttr) {
6314
+ const relatedRecord = relatedRecordMap.get(relatedId);
6315
+ if (relatedRecord) {
6316
+ result[attrName] = relatedRecord.values;
6317
+ }
6318
+ }
6319
+ }
6320
+ return resultMap;
6321
+ }
6322
+ /**
6323
+ * Flatten resolved relations for formula evaluation
6324
+ *
6325
+ * Converts nested structure to dot-notation keys:
6326
+ * { company: { name: "Acme" } } → { "company.name": "Acme" }
6327
+ *
6328
+ * @param resolved - Resolved relations from resolveRelationValues
6329
+ * @returns Flattened values suitable for formula evaluation
6330
+ */
6331
+ flattenResolvedRelations(resolved) {
6332
+ const flat = {};
6333
+ for (const [relationName, values] of Object.entries(resolved)) {
6334
+ for (const [key, value] of Object.entries(values)) {
6335
+ flat[`${relationName}.${key}`] = value;
6336
+ }
6337
+ }
6338
+ return flat;
6339
+ }
6340
+ /**
6341
+ * Extract a single relation ID from a value
6342
+ * Handles both single (string) and multi (array) relations
6343
+ * @internal
6344
+ */
6345
+ extractSingleId(value) {
6346
+ if (typeof value === "string" && value.length > 0) {
6347
+ return value;
6348
+ }
6349
+ if (Array.isArray(value) && value.length > 0 && typeof value[0] === "string") {
6350
+ return value[0];
6351
+ }
6352
+ return null;
6353
+ }
6354
+ };
6355
+
6356
+ // src/runtime/services/rollup-scheduler.ts
6357
+ var RollupScheduler = class {
6358
+ constructor(adapter, getSchemaById, options) {
6359
+ this.adapter = adapter;
6360
+ this.getSchemaById = getSchemaById;
6361
+ this.pending = /* @__PURE__ */ new Map();
6362
+ this.rollupService = new RollupService(adapter);
6363
+ this.debounceMs = options?.debounceMs ?? 100;
6364
+ this.maxPending = options?.maxPending ?? 100;
6365
+ }
6366
+ /**
6367
+ * Schedule a rollup recalculation for a parent record
6368
+ *
6369
+ * If the same record is already pending, the timer is reset.
6370
+ * If too many recalculations are pending, triggers an immediate flush.
6371
+ *
6372
+ * @param parentId - ID of the parent record to recalculate
6373
+ * @param parentObjectId - Object ID of the parent
6374
+ * @param tenantId - Tenant ID
6375
+ */
6376
+ scheduleRecalculation(parentId, parentObjectId, tenantId) {
6377
+ const key = `${parentId}:${parentObjectId}`;
6378
+ const existing = this.pending.get(key);
6379
+ if (existing) {
6380
+ clearTimeout(existing.timeout);
6381
+ }
6382
+ const timeout = setTimeout(async () => {
6383
+ await this.executeRecalculation(parentId, parentObjectId, tenantId);
6384
+ this.pending.delete(key);
6385
+ }, this.debounceMs);
6386
+ this.pending.set(key, { parentId, parentObjectId, tenantId, timeout });
6387
+ if (this.pending.size >= this.maxPending) {
6388
+ this.flush();
6389
+ }
6390
+ }
6391
+ /**
6392
+ * Execute all pending recalculations immediately
6393
+ */
6394
+ async flush() {
6395
+ const entries = Array.from(this.pending.entries());
6396
+ for (const [, entry] of entries) {
6397
+ clearTimeout(entry.timeout);
6398
+ }
6399
+ this.pending.clear();
6400
+ const tenantMap = /* @__PURE__ */ new Map();
6401
+ for (const [, entry] of entries) {
6402
+ tenantMap.set(entry.parentId, entry.tenantId);
6403
+ }
6404
+ const allIds = entries.map(([, entry]) => entry.parentId);
6405
+ const records = await this.adapter.objectRecords.findByIds(allIds);
6406
+ for (const record of records) {
6407
+ const schema = await this.getSchemaById(record.objectId);
6408
+ const tenantId = tenantMap.get(record.id);
6409
+ if (schema && tenantId) {
6410
+ await this.rollupService.recalculateAndUpdate(record, schema, tenantId);
6411
+ }
6412
+ }
6413
+ }
6414
+ /**
6415
+ * Execute a single recalculation
6416
+ */
6417
+ async executeRecalculation(parentId, parentObjectId, tenantId) {
6418
+ const record = await this.adapter.objectRecords.findById(parentId);
6419
+ if (!record) return;
6420
+ const schema = await this.getSchemaById(parentObjectId);
6421
+ if (!schema) return;
6422
+ await this.rollupService.recalculateAndUpdate(record, schema, tenantId);
6423
+ }
6424
+ /**
6425
+ * Get number of pending recalculations
6426
+ */
6427
+ get pendingCount() {
6428
+ return this.pending.size;
6429
+ }
6430
+ /**
6431
+ * Clear all pending recalculations without executing them
6432
+ */
6433
+ clear() {
6434
+ for (const [, entry] of this.pending) {
6435
+ clearTimeout(entry.timeout);
6436
+ }
6437
+ this.pending.clear();
6438
+ }
6439
+ };
6440
+
5442
6441
  // src/runtime/services/user-profile.service.ts
5443
6442
  var UserProfileService = class {
5444
6443
  constructor(adapter, tenantId, options) {
@@ -6136,7 +7135,8 @@ async function syncAttributes(adapter, nativeObject, dbObject, existingObject, r
6136
7135
  hidden: attr.hidden,
6137
7136
  archived: attr.archived,
6138
7137
  deprecated: attr.deprecated,
6139
- defaultValue: attr.defaultValue,
7138
+ // Formula and rollup attributes don't have defaultValue
7139
+ defaultValue: "defaultValue" in attr ? attr.defaultValue : void 0,
6140
7140
  config: extractAttributeConfig(attr)
6141
7141
  });
6142
7142
  if (existingAttr) {
@@ -6215,26 +7215,54 @@ var NoopGeocodingAdapter = class {
6215
7215
  FlowService,
6216
7216
  GeocodingService,
6217
7217
  GlobalSearchService,
7218
+ InvalidPathError,
7219
+ MaxDepthExceededError,
6218
7220
  NoopGeocodingAdapter,
6219
7221
  NoopHookRegistry,
6220
7222
  ObjectSchemaService,
6221
7223
  PermissionService,
6222
7224
  RecordService,
7225
+ RelationResolverService,
6223
7226
  RelationService,
7227
+ RollupScheduler,
7228
+ RollupService,
6224
7229
  UserProfileService,
6225
7230
  UserService,
6226
7231
  ViewService,
6227
7232
  buildAuditChanges,
6228
7233
  createMockAdapter,
7234
+ createRollupHooks,
6229
7235
  enrichValuesWithSelectLabels,
7236
+ evaluateFormula,
7237
+ evaluateFormulaAttribute,
7238
+ evaluateFormulaAttributeWithRelations,
7239
+ evaluateFormulaWithRelations,
7240
+ evaluateFormulaWithResult,
6230
7241
  extractAttributeNames,
7242
+ extractFormulaVariables,
7243
+ extractRelationNames,
7244
+ extractRelationReferences,
7245
+ flattenRelationsForEval,
7246
+ formatFormulaResult,
7247
+ getPathDepth,
7248
+ getRelationPath,
6231
7249
  getSyncPreview,
7250
+ getTargetAttributeName,
6232
7251
  getViewSyncPreview,
7252
+ hasRelationReferences,
6233
7253
  isLabelExpression,
7254
+ parsePath,
7255
+ pathHasManyCardinality,
7256
+ registerAllRollupHooks,
6234
7257
  renderLabelExpression,
7258
+ resolveMultiplePaths,
7259
+ resolveSingleValue,
6235
7260
  syncAll,
6236
7261
  syncNativeObjects,
6237
7262
  syncNativeViews,
7263
+ traversePath,
7264
+ validateFormulaExpression,
7265
+ validatePath,
6238
7266
  verifyNativeObjectsSync,
6239
7267
  verifyNativeViewsSync
6240
7268
  });