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