@stndrds/schema 0.1.0-alpha.18 → 0.1.0-alpha.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-4SW4V3YU.mjs +8854 -0
- package/dist/chunk-7GSIXU67.mjs +8820 -0
- package/dist/chunk-BKOV4XVX.mjs +8816 -0
- package/dist/chunk-BQUYTOB3.mjs +8815 -0
- package/dist/chunk-BSAR4UKK.mjs +8851 -0
- package/dist/chunk-DPOXIK3M.mjs +8816 -0
- package/dist/chunk-DU2HLOQH.mjs +8813 -0
- package/dist/chunk-I4TTNDN7.mjs +8910 -0
- package/dist/chunk-JD2MVLTW.mjs +8814 -0
- package/dist/chunk-KDHA2N7D.mjs +8927 -0
- package/dist/chunk-MX4X4UEN.mjs +8854 -0
- package/dist/chunk-OJWVSONX.mjs +8900 -0
- package/dist/chunk-PMYSW54L.mjs +8855 -0
- package/dist/chunk-SD2UXOSF.mjs +8814 -0
- package/dist/chunk-Y5KIRAND.mjs +8916 -0
- package/dist/chunk-YLVZT3H4.mjs +8793 -0
- package/dist/chunk-ZU5SR355.mjs +8862 -0
- package/dist/index.d.mts +119 -6
- package/dist/index.d.ts +119 -6
- package/dist/index.js +1215 -11
- package/dist/index.mjs +83 -2
- package/dist/runtime-09fOm3VO.d.mts +6383 -0
- package/dist/runtime-09fOm3VO.d.ts +6383 -0
- package/dist/runtime-BMHqETm7.d.mts +6450 -0
- package/dist/runtime-BMHqETm7.d.ts +6450 -0
- package/dist/runtime-BpsaNQPs.d.mts +6397 -0
- package/dist/runtime-BpsaNQPs.d.ts +6397 -0
- package/dist/runtime-CHAkwiwa.d.mts +6450 -0
- package/dist/runtime-CHAkwiwa.d.ts +6450 -0
- package/dist/runtime-CPggQqoW.d.mts +6428 -0
- package/dist/runtime-CPggQqoW.d.ts +6428 -0
- package/dist/runtime-CfVfZtoq.d.mts +6437 -0
- package/dist/runtime-CfVfZtoq.d.ts +6437 -0
- package/dist/runtime-DSMMZRLE.d.mts +6430 -0
- package/dist/runtime-DSMMZRLE.d.ts +6430 -0
- package/dist/runtime-xtAfMn_f.d.mts +6436 -0
- package/dist/runtime-xtAfMn_f.d.ts +6436 -0
- package/dist/runtime.d.mts +1 -1
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +1075 -9
- package/dist/runtime.mjs +53 -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,7 @@ __export(index_exports, {
|
|
|
195
201
|
createPhoneValidator: () => createPhoneValidator,
|
|
196
202
|
createRatingValidator: () => createRatingValidator,
|
|
197
203
|
createRelationValidator: () => createRelationValidator,
|
|
204
|
+
createRollupValidator: () => createRollupValidator,
|
|
198
205
|
createSelectValidator: () => createSelectValidator,
|
|
199
206
|
createSingleRelationValidator: () => createSingleRelationValidator,
|
|
200
207
|
createStatusValidator: () => createStatusValidator,
|
|
@@ -206,18 +213,34 @@ __export(index_exports, {
|
|
|
206
213
|
date: () => date,
|
|
207
214
|
dateConfigSchema: () => dateConfigSchema,
|
|
208
215
|
enrichValuesWithSelectLabels: () => enrichValuesWithSelectLabels,
|
|
216
|
+
evaluateFormula: () => evaluateFormula,
|
|
217
|
+
evaluateFormulaAttribute: () => evaluateFormulaAttribute,
|
|
218
|
+
evaluateFormulaAttributeWithRelations: () => evaluateFormulaAttributeWithRelations,
|
|
219
|
+
evaluateFormulaWithRelations: () => evaluateFormulaWithRelations,
|
|
220
|
+
evaluateFormulaWithResult: () => evaluateFormulaWithResult,
|
|
209
221
|
extractAttributeNames: () => extractAttributeNames,
|
|
222
|
+
extractFormulaVariables: () => extractFormulaVariables,
|
|
223
|
+
extractRelationNames: () => extractRelationNames,
|
|
224
|
+
extractRelationReferences: () => extractRelationReferences,
|
|
210
225
|
file: () => file,
|
|
211
226
|
fileConfigSchema: () => fileConfigSchema,
|
|
227
|
+
flattenRelationsForEval: () => flattenRelationsForEval,
|
|
212
228
|
flow: () => flow,
|
|
213
229
|
formatAttributeValue: () => formatAttributeValue,
|
|
230
|
+
formatFormulaResult: () => formatFormulaResult,
|
|
231
|
+
formula: () => formula,
|
|
232
|
+
formulaConfigSchema: () => formulaConfigSchema,
|
|
214
233
|
generateId: () => generateId,
|
|
215
234
|
generatePrefixedId: () => generatePrefixedId,
|
|
216
235
|
getAttributeConfigSchema: () => getAttributeConfigSchema,
|
|
217
236
|
getMissingRequiredAttributes: () => getMissingRequiredAttributes,
|
|
237
|
+
getPathDepth: () => getPathDepth,
|
|
238
|
+
getRelationPath: () => getRelationPath,
|
|
218
239
|
getSyncPreview: () => getSyncPreview,
|
|
240
|
+
getTargetAttributeName: () => getTargetAttributeName,
|
|
219
241
|
getViewSyncPreview: () => getViewSyncPreview,
|
|
220
242
|
group: () => group,
|
|
243
|
+
hasRelationReferences: () => hasRelationReferences,
|
|
221
244
|
isActivityTab: () => isActivityTab,
|
|
222
245
|
isAdvancedFilterState: () => isAdvancedFilterState,
|
|
223
246
|
isCustomTab: () => isCustomTab,
|
|
@@ -243,6 +266,8 @@ __export(index_exports, {
|
|
|
243
266
|
numberConfigSchema: () => numberConfigSchema,
|
|
244
267
|
object: () => object,
|
|
245
268
|
parseAttributeConfig: () => parseAttributeConfig,
|
|
269
|
+
parsePath: () => parsePath,
|
|
270
|
+
pathHasManyCardinality: () => pathHasManyCardinality,
|
|
246
271
|
phone: () => phone,
|
|
247
272
|
phoneConfigSchema: () => phoneConfigSchema,
|
|
248
273
|
rating: () => rating,
|
|
@@ -251,6 +276,10 @@ __export(index_exports, {
|
|
|
251
276
|
relation: () => relation,
|
|
252
277
|
relationConfigSchema: () => relationConfigSchema,
|
|
253
278
|
renderLabelExpression: () => renderLabelExpression,
|
|
279
|
+
resolveMultiplePaths: () => resolveMultiplePaths,
|
|
280
|
+
resolveSingleValue: () => resolveSingleValue,
|
|
281
|
+
rollup: () => rollup,
|
|
282
|
+
rollupConfigSchema: () => rollupConfigSchema,
|
|
254
283
|
safeParseAttributeConfig: () => safeParseAttributeConfig,
|
|
255
284
|
select: () => select,
|
|
256
285
|
selectConfigSchema: () => selectConfigSchema,
|
|
@@ -267,14 +296,17 @@ __export(index_exports, {
|
|
|
267
296
|
timestampConfigSchema: () => timestampConfigSchema,
|
|
268
297
|
toAdvancedFilterState: () => toAdvancedFilterState,
|
|
269
298
|
toSimpleFilterState: () => toSimpleFilterState,
|
|
299
|
+
traversePath: () => traversePath,
|
|
270
300
|
user: () => user,
|
|
271
301
|
userConfigSchema: () => userConfigSchema,
|
|
272
302
|
validateAttribute: () => validateAttribute,
|
|
273
303
|
validateAttributeConfig: () => validateAttributeConfig,
|
|
274
304
|
validateDraft: () => validateDraft,
|
|
275
305
|
validateDraftOrThrow: () => validateDraftOrThrow,
|
|
306
|
+
validateFormulaExpression: () => validateFormulaExpression,
|
|
276
307
|
validateObject: () => validateObject,
|
|
277
308
|
validateObjectOrThrow: () => validateObjectOrThrow,
|
|
309
|
+
validatePath: () => validatePath,
|
|
278
310
|
verifyNativeObjectsSync: () => verifyNativeObjectsSync,
|
|
279
311
|
verifyNativeViewsSync: () => verifyNativeViewsSync,
|
|
280
312
|
view: () => view,
|
|
@@ -397,7 +429,24 @@ var OPERATORS_BY_TYPE = {
|
|
|
397
429
|
file: ["is_empty", "is_not_empty"],
|
|
398
430
|
user: ["contains", "not_contains", "is_empty", "is_not_empty"],
|
|
399
431
|
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"]
|
|
432
|
+
rating: ["eq", "neq", "lt", "gt", "lte", "gte", "is_empty", "is_not_empty"],
|
|
433
|
+
// Formula: operators depend on returnType, but we provide a general set
|
|
434
|
+
formula: [
|
|
435
|
+
"is",
|
|
436
|
+
"is_not",
|
|
437
|
+
"eq",
|
|
438
|
+
"neq",
|
|
439
|
+
"lt",
|
|
440
|
+
"gt",
|
|
441
|
+
"lte",
|
|
442
|
+
"gte",
|
|
443
|
+
"contains",
|
|
444
|
+
"not_contains",
|
|
445
|
+
"is_empty",
|
|
446
|
+
"is_not_empty"
|
|
447
|
+
],
|
|
448
|
+
// Rollup: typically numeric aggregations
|
|
449
|
+
rollup: ["eq", "neq", "lt", "gt", "lte", "gte", "is_empty", "is_not_empty"]
|
|
401
450
|
};
|
|
402
451
|
var NO_VALUE_OPERATORS = [
|
|
403
452
|
"is_empty",
|
|
@@ -956,7 +1005,9 @@ var BaseAttributeBuilder = class {
|
|
|
956
1005
|
return this;
|
|
957
1006
|
}
|
|
958
1007
|
defaultValue(value) {
|
|
959
|
-
this.attr.
|
|
1008
|
+
if ("defaultValue" in this.attr || !["formula", "rollup"].includes(this.attr.type)) {
|
|
1009
|
+
this.attr.defaultValue = value;
|
|
1010
|
+
}
|
|
960
1011
|
return this;
|
|
961
1012
|
}
|
|
962
1013
|
metadata(value) {
|
|
@@ -1498,6 +1549,113 @@ var RatingAttributeBuilder = class extends BaseAttributeBuilder {
|
|
|
1498
1549
|
function rating(config) {
|
|
1499
1550
|
return new RatingAttributeBuilder(config.name, config.label);
|
|
1500
1551
|
}
|
|
1552
|
+
var FormulaAttributeBuilder = class extends BaseAttributeBuilder {
|
|
1553
|
+
constructor(name, label) {
|
|
1554
|
+
super("formula", name, label);
|
|
1555
|
+
this.attr.required = false;
|
|
1556
|
+
}
|
|
1557
|
+
/**
|
|
1558
|
+
* Set the formula expression
|
|
1559
|
+
* @param expr - Expression to evaluate (e.g., "price * quantity")
|
|
1560
|
+
*/
|
|
1561
|
+
expression(expr) {
|
|
1562
|
+
this.attr.expression = expr;
|
|
1563
|
+
return this;
|
|
1564
|
+
}
|
|
1565
|
+
/**
|
|
1566
|
+
* Set the expected return type for formatting
|
|
1567
|
+
* @param type - Return type: "text", "number", "boolean", or "date"
|
|
1568
|
+
*/
|
|
1569
|
+
returns(type) {
|
|
1570
|
+
this.attr.returnType = type;
|
|
1571
|
+
return this;
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Set decimal places for number results
|
|
1575
|
+
*/
|
|
1576
|
+
decimals(value) {
|
|
1577
|
+
this.attr.decimals = value;
|
|
1578
|
+
return this;
|
|
1579
|
+
}
|
|
1580
|
+
// Override to prevent making formulas required
|
|
1581
|
+
// biome-ignore lint/suspicious/noExplicitAny: intentional override
|
|
1582
|
+
required() {
|
|
1583
|
+
console.warn("Formula attributes cannot be required (they are read-only)");
|
|
1584
|
+
return this;
|
|
1585
|
+
}
|
|
1586
|
+
// biome-ignore lint/suspicious/noExplicitAny: intentional override
|
|
1587
|
+
optional() {
|
|
1588
|
+
return this;
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1591
|
+
function formula(config) {
|
|
1592
|
+
return new FormulaAttributeBuilder(config.name, config.label);
|
|
1593
|
+
}
|
|
1594
|
+
var RollupAttributeBuilder = class extends BaseAttributeBuilder {
|
|
1595
|
+
constructor(name, label) {
|
|
1596
|
+
super("rollup", name, label);
|
|
1597
|
+
this.attr.required = false;
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* Set the relation attribute to aggregate from
|
|
1601
|
+
* @param relationName - Name of the relation attribute on this object
|
|
1602
|
+
*/
|
|
1603
|
+
from(relationName) {
|
|
1604
|
+
this.attr.relationAttribute = relationName;
|
|
1605
|
+
return this;
|
|
1606
|
+
}
|
|
1607
|
+
/**
|
|
1608
|
+
* Set the target attribute to aggregate
|
|
1609
|
+
* @param attrName - Name of the attribute on the related object
|
|
1610
|
+
*/
|
|
1611
|
+
aggregate(attrName) {
|
|
1612
|
+
this.attr.targetAttribute = attrName;
|
|
1613
|
+
return this;
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Set the aggregation function
|
|
1617
|
+
*/
|
|
1618
|
+
using(fn) {
|
|
1619
|
+
this.attr.function = fn;
|
|
1620
|
+
return this;
|
|
1621
|
+
}
|
|
1622
|
+
/**
|
|
1623
|
+
* Set decimal places for numeric results
|
|
1624
|
+
*/
|
|
1625
|
+
decimals(value) {
|
|
1626
|
+
this.attr.decimals = value;
|
|
1627
|
+
return this;
|
|
1628
|
+
}
|
|
1629
|
+
/**
|
|
1630
|
+
* Display the rollup value using the target attribute's type formatter
|
|
1631
|
+
* instead of the default rollup-specific formatting
|
|
1632
|
+
*/
|
|
1633
|
+
showAsOriginal() {
|
|
1634
|
+
this.attr.showAsOriginal = true;
|
|
1635
|
+
return this;
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* Set multi-level path for traversing nested relations (Phase 4+)
|
|
1639
|
+
* @param path - Dot notation path (e.g., "orders.items")
|
|
1640
|
+
*/
|
|
1641
|
+
path(relationPath) {
|
|
1642
|
+
this.attr.relationPath = relationPath;
|
|
1643
|
+
return this;
|
|
1644
|
+
}
|
|
1645
|
+
// Override to prevent making rollups required
|
|
1646
|
+
// biome-ignore lint/suspicious/noExplicitAny: intentional override
|
|
1647
|
+
required() {
|
|
1648
|
+
console.warn("Rollup attributes cannot be required (they are read-only)");
|
|
1649
|
+
return this;
|
|
1650
|
+
}
|
|
1651
|
+
// biome-ignore lint/suspicious/noExplicitAny: intentional override
|
|
1652
|
+
optional() {
|
|
1653
|
+
return this;
|
|
1654
|
+
}
|
|
1655
|
+
};
|
|
1656
|
+
function rollup(config) {
|
|
1657
|
+
return new RollupAttributeBuilder(config.name, config.label);
|
|
1658
|
+
}
|
|
1501
1659
|
|
|
1502
1660
|
// src/builders/flow-builder.ts
|
|
1503
1661
|
var import_zod = require("zod");
|
|
@@ -2794,6 +2952,32 @@ var ratingConfigSchema = baseConfigSchema.extend({
|
|
|
2794
2952
|
max: import_zod4.z.number().int().min(1).optional(),
|
|
2795
2953
|
iconType: import_zod4.z.enum(["star", "heart", "thumbs", "number"]).optional()
|
|
2796
2954
|
});
|
|
2955
|
+
var formulaConfigSchema = baseConfigSchema.extend({
|
|
2956
|
+
expression: import_zod4.z.string().min(1),
|
|
2957
|
+
returnType: import_zod4.z.enum(["text", "number", "boolean", "date"]),
|
|
2958
|
+
decimals: import_zod4.z.number().int().min(0).max(10).optional(),
|
|
2959
|
+
allowRelations: import_zod4.z.boolean().optional()
|
|
2960
|
+
});
|
|
2961
|
+
var rollupConfigSchema = baseConfigSchema.extend({
|
|
2962
|
+
relationAttribute: import_zod4.z.string().min(1).optional(),
|
|
2963
|
+
relationPath: import_zod4.z.string().optional(),
|
|
2964
|
+
targetAttribute: import_zod4.z.string().min(1),
|
|
2965
|
+
function: import_zod4.z.enum([
|
|
2966
|
+
"sum",
|
|
2967
|
+
"count",
|
|
2968
|
+
"avg",
|
|
2969
|
+
"min",
|
|
2970
|
+
"max",
|
|
2971
|
+
"countValues",
|
|
2972
|
+
"countEmpty",
|
|
2973
|
+
"percentEmpty",
|
|
2974
|
+
"percentFilled",
|
|
2975
|
+
"collect"
|
|
2976
|
+
]),
|
|
2977
|
+
decimals: import_zod4.z.number().int().min(0).max(10).optional(),
|
|
2978
|
+
showAsOriginal: import_zod4.z.boolean().optional(),
|
|
2979
|
+
targetAttributeType: import_zod4.z.string().optional()
|
|
2980
|
+
});
|
|
2797
2981
|
var attributeConfigSchemas = {
|
|
2798
2982
|
text: textConfigSchema,
|
|
2799
2983
|
textarea: textareaConfigSchema,
|
|
@@ -2810,7 +2994,9 @@ var attributeConfigSchemas = {
|
|
|
2810
2994
|
file: fileConfigSchema,
|
|
2811
2995
|
user: userConfigSchema,
|
|
2812
2996
|
relation: relationConfigSchema,
|
|
2813
|
-
rating: ratingConfigSchema
|
|
2997
|
+
rating: ratingConfigSchema,
|
|
2998
|
+
formula: formulaConfigSchema,
|
|
2999
|
+
rollup: rollupConfigSchema
|
|
2814
3000
|
};
|
|
2815
3001
|
function getAttributeConfigSchema(type) {
|
|
2816
3002
|
return attributeConfigSchemas[type];
|
|
@@ -2983,6 +3169,12 @@ function createRatingValidator(attr) {
|
|
|
2983
3169
|
}
|
|
2984
3170
|
return schema;
|
|
2985
3171
|
}
|
|
3172
|
+
function createFormulaValidator(_attr) {
|
|
3173
|
+
return import_zod4.z.unknown();
|
|
3174
|
+
}
|
|
3175
|
+
function createRollupValidator(_attr) {
|
|
3176
|
+
return import_zod4.z.unknown();
|
|
3177
|
+
}
|
|
2986
3178
|
function createAttributeValidator(attr) {
|
|
2987
3179
|
switch (attr.type) {
|
|
2988
3180
|
case "text":
|
|
@@ -3015,6 +3207,10 @@ function createAttributeValidator(attr) {
|
|
|
3015
3207
|
return createRelationValidator(attr);
|
|
3016
3208
|
case "rating":
|
|
3017
3209
|
return createRatingValidator(attr);
|
|
3210
|
+
case "formula":
|
|
3211
|
+
return createFormulaValidator(attr);
|
|
3212
|
+
case "rollup":
|
|
3213
|
+
return createRollupValidator(attr);
|
|
3018
3214
|
default:
|
|
3019
3215
|
return import_zod4.z.unknown();
|
|
3020
3216
|
}
|
|
@@ -3140,6 +3336,384 @@ function computeRecordStatus(objectDef, data) {
|
|
|
3140
3336
|
return isRecordComplete(objectDef, data) ? "complete" : "draft";
|
|
3141
3337
|
}
|
|
3142
3338
|
|
|
3339
|
+
// src/runtime/formula/evaluator.ts
|
|
3340
|
+
var import_expr_eval = require("expr-eval");
|
|
3341
|
+
function createFormulaParser() {
|
|
3342
|
+
const parser = new import_expr_eval.Parser();
|
|
3343
|
+
parser.functions.IF = (condition, thenValue, elseValue) => condition ? thenValue : elseValue;
|
|
3344
|
+
parser.functions.AND = (...args) => args.every(Boolean);
|
|
3345
|
+
parser.functions.OR = (...args) => args.some(Boolean);
|
|
3346
|
+
parser.functions.NOT = (value) => !value;
|
|
3347
|
+
parser.functions.EMPTY = (value) => value === null || value === void 0 || value === "";
|
|
3348
|
+
parser.functions.COALESCE = (...args) => args.find((a) => a != null) ?? null;
|
|
3349
|
+
parser.functions.DEFAULT = (value, defaultValue) => value == null || value === "" ? defaultValue : value;
|
|
3350
|
+
parser.functions.CONCAT = (...args) => args.filter((a) => a != null).map(String).join("");
|
|
3351
|
+
parser.functions.UPPER = (value) => String(value ?? "").toUpperCase();
|
|
3352
|
+
parser.functions.LOWER = (value) => String(value ?? "").toLowerCase();
|
|
3353
|
+
parser.functions.TRIM = (value) => String(value ?? "").trim();
|
|
3354
|
+
parser.functions.LENGTH = (value) => String(value ?? "").length;
|
|
3355
|
+
parser.functions.LEFT = (value, count) => String(value ?? "").slice(0, count);
|
|
3356
|
+
parser.functions.RIGHT = (value, count) => String(value ?? "").slice(-count);
|
|
3357
|
+
parser.functions.REPLACE = (value, search, replacement) => String(value ?? "").replace(new RegExp(search, "g"), replacement);
|
|
3358
|
+
parser.functions.CONTAINS = (value, search) => String(value ?? "").toLowerCase().includes(String(search).toLowerCase());
|
|
3359
|
+
parser.functions.ROUND = (value, decimals = 0) => {
|
|
3360
|
+
if (typeof value !== "number" || Number.isNaN(value)) return null;
|
|
3361
|
+
const factor = 10 ** decimals;
|
|
3362
|
+
return Math.round(value * factor) / factor;
|
|
3363
|
+
};
|
|
3364
|
+
parser.functions.FLOOR = (value) => {
|
|
3365
|
+
if (typeof value !== "number" || Number.isNaN(value)) return null;
|
|
3366
|
+
return Math.floor(value);
|
|
3367
|
+
};
|
|
3368
|
+
parser.functions.CEIL = (value) => {
|
|
3369
|
+
if (typeof value !== "number" || Number.isNaN(value)) return null;
|
|
3370
|
+
return Math.ceil(value);
|
|
3371
|
+
};
|
|
3372
|
+
parser.functions.ABS = (value) => {
|
|
3373
|
+
if (typeof value !== "number" || Number.isNaN(value)) return null;
|
|
3374
|
+
return Math.abs(value);
|
|
3375
|
+
};
|
|
3376
|
+
parser.functions.MIN = (...args) => {
|
|
3377
|
+
const nums = args.filter((n) => typeof n === "number" && !Number.isNaN(n));
|
|
3378
|
+
return nums.length > 0 ? Math.min(...nums) : null;
|
|
3379
|
+
};
|
|
3380
|
+
parser.functions.MAX = (...args) => {
|
|
3381
|
+
const nums = args.filter((n) => typeof n === "number" && !Number.isNaN(n));
|
|
3382
|
+
return nums.length > 0 ? Math.max(...nums) : null;
|
|
3383
|
+
};
|
|
3384
|
+
parser.functions.POW = (base, exponent) => {
|
|
3385
|
+
if (typeof base !== "number" || typeof exponent !== "number") return null;
|
|
3386
|
+
return base ** exponent;
|
|
3387
|
+
};
|
|
3388
|
+
parser.functions.MOD = (a, b) => {
|
|
3389
|
+
if (typeof a !== "number" || typeof b !== "number" || b === 0) return null;
|
|
3390
|
+
return a % b;
|
|
3391
|
+
};
|
|
3392
|
+
parser.functions.NOW = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
3393
|
+
parser.functions.TODAY = () => (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
3394
|
+
parser.functions.YEAR = (value) => {
|
|
3395
|
+
const date2 = parseDate(value);
|
|
3396
|
+
return date2 ? date2.getFullYear() : null;
|
|
3397
|
+
};
|
|
3398
|
+
parser.functions.MONTH = (value) => {
|
|
3399
|
+
const date2 = parseDate(value);
|
|
3400
|
+
return date2 ? date2.getMonth() + 1 : null;
|
|
3401
|
+
};
|
|
3402
|
+
parser.functions.DAY = (value) => {
|
|
3403
|
+
const date2 = parseDate(value);
|
|
3404
|
+
return date2 ? date2.getDate() : null;
|
|
3405
|
+
};
|
|
3406
|
+
parser.functions.DATE_DIFF = (date1, date2, unit = "days") => {
|
|
3407
|
+
const d1 = parseDate(date1);
|
|
3408
|
+
const d2 = parseDate(date2);
|
|
3409
|
+
if (d1 === null || d2 === null) return null;
|
|
3410
|
+
const diffMs = d1.getTime() - d2.getTime();
|
|
3411
|
+
const MS_PER_DAY = 1e3 * 60 * 60 * 24;
|
|
3412
|
+
const conversions = {
|
|
3413
|
+
years: MS_PER_DAY * 365,
|
|
3414
|
+
months: MS_PER_DAY * 30,
|
|
3415
|
+
weeks: MS_PER_DAY * 7,
|
|
3416
|
+
days: MS_PER_DAY,
|
|
3417
|
+
hours: 1e3 * 60 * 60,
|
|
3418
|
+
minutes: 1e3 * 60
|
|
3419
|
+
};
|
|
3420
|
+
const divisor = conversions[unit] ?? MS_PER_DAY;
|
|
3421
|
+
return diffMs / divisor;
|
|
3422
|
+
};
|
|
3423
|
+
return parser;
|
|
3424
|
+
}
|
|
3425
|
+
function parseDate(value) {
|
|
3426
|
+
if (!value) return null;
|
|
3427
|
+
if (value instanceof Date) return value;
|
|
3428
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
3429
|
+
const date2 = new Date(value);
|
|
3430
|
+
return Number.isNaN(date2.getTime()) ? null : date2;
|
|
3431
|
+
}
|
|
3432
|
+
return null;
|
|
3433
|
+
}
|
|
3434
|
+
var formulaParser = createFormulaParser();
|
|
3435
|
+
function evaluateFormula(expression, values) {
|
|
3436
|
+
try {
|
|
3437
|
+
const parsed = formulaParser.parse(expression);
|
|
3438
|
+
return parsed.evaluate(values);
|
|
3439
|
+
} catch {
|
|
3440
|
+
return null;
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
function evaluateFormulaWithResult(expression, values) {
|
|
3444
|
+
try {
|
|
3445
|
+
const parsed = formulaParser.parse(expression);
|
|
3446
|
+
const value = parsed.evaluate(values);
|
|
3447
|
+
return { value };
|
|
3448
|
+
} catch (error) {
|
|
3449
|
+
return {
|
|
3450
|
+
value: null,
|
|
3451
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
3452
|
+
};
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
function formatFormulaResult(value, returnType, decimals) {
|
|
3456
|
+
if (value === null || value === void 0) {
|
|
3457
|
+
return null;
|
|
3458
|
+
}
|
|
3459
|
+
switch (returnType) {
|
|
3460
|
+
case "number":
|
|
3461
|
+
return formatNumberResult(value, decimals);
|
|
3462
|
+
case "boolean":
|
|
3463
|
+
return Boolean(value);
|
|
3464
|
+
case "date":
|
|
3465
|
+
return formatDateResult(value);
|
|
3466
|
+
case "text":
|
|
3467
|
+
return String(value);
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
function formatNumberResult(value, decimals) {
|
|
3471
|
+
const num = typeof value === "number" ? value : Number(value);
|
|
3472
|
+
if (Number.isNaN(num)) return null;
|
|
3473
|
+
return decimals !== void 0 ? Number(num.toFixed(decimals)) : num;
|
|
3474
|
+
}
|
|
3475
|
+
function formatDateResult(value) {
|
|
3476
|
+
if (value instanceof Date) return value.toISOString();
|
|
3477
|
+
if (typeof value === "string") return value;
|
|
3478
|
+
return null;
|
|
3479
|
+
}
|
|
3480
|
+
function evaluateFormulaAttribute(attr, values) {
|
|
3481
|
+
const raw = evaluateFormula(attr.expression, values);
|
|
3482
|
+
return formatFormulaResult(raw, attr.returnType, attr.decimals);
|
|
3483
|
+
}
|
|
3484
|
+
function validateFormulaExpression(expression) {
|
|
3485
|
+
try {
|
|
3486
|
+
formulaParser.parse(expression);
|
|
3487
|
+
return { valid: true };
|
|
3488
|
+
} catch (error) {
|
|
3489
|
+
return {
|
|
3490
|
+
valid: false,
|
|
3491
|
+
error: error instanceof Error ? error.message : "Invalid expression"
|
|
3492
|
+
};
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
function extractFormulaVariables(expression) {
|
|
3496
|
+
try {
|
|
3497
|
+
const parsed = formulaParser.parse(expression);
|
|
3498
|
+
return parsed.variables();
|
|
3499
|
+
} catch {
|
|
3500
|
+
return [];
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
var RELATION_REF_PATTERN = /\b([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)\b/g;
|
|
3504
|
+
function extractRelationReferences(expression) {
|
|
3505
|
+
const regex = new RegExp(RELATION_REF_PATTERN.source, "g");
|
|
3506
|
+
const matches = expression.matchAll(regex);
|
|
3507
|
+
return [...matches].map((m) => m[0]);
|
|
3508
|
+
}
|
|
3509
|
+
function extractRelationNames(expression) {
|
|
3510
|
+
const refs = extractRelationReferences(expression);
|
|
3511
|
+
const names = refs.map((ref) => ref.split(".")[0]);
|
|
3512
|
+
return [...new Set(names)];
|
|
3513
|
+
}
|
|
3514
|
+
function hasRelationReferences(expression) {
|
|
3515
|
+
const regex = new RegExp(RELATION_REF_PATTERN.source);
|
|
3516
|
+
return regex.test(expression);
|
|
3517
|
+
}
|
|
3518
|
+
function flattenRelationsForEval(resolvedRelations) {
|
|
3519
|
+
const flat = {};
|
|
3520
|
+
for (const [relationName, values] of Object.entries(resolvedRelations)) {
|
|
3521
|
+
for (const [key, value] of Object.entries(values)) {
|
|
3522
|
+
flat[`${relationName}.${key}`] = value;
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
return flat;
|
|
3526
|
+
}
|
|
3527
|
+
async function evaluateFormulaWithRelations(expression, record, schema, resolver) {
|
|
3528
|
+
const relationNames = extractRelationNames(expression);
|
|
3529
|
+
if (relationNames.length === 0) {
|
|
3530
|
+
return evaluateFormula(expression, record.values);
|
|
3531
|
+
}
|
|
3532
|
+
const resolvedRelations = await resolver.resolveRelationValues(record, schema, relationNames);
|
|
3533
|
+
const flattenedRelations = flattenRelationsForEval(resolvedRelations);
|
|
3534
|
+
const allValues = {
|
|
3535
|
+
...record.values,
|
|
3536
|
+
...flattenedRelations
|
|
3537
|
+
};
|
|
3538
|
+
return evaluateFormula(expression, allValues);
|
|
3539
|
+
}
|
|
3540
|
+
async function evaluateFormulaAttributeWithRelations(attr, record, schema, resolver) {
|
|
3541
|
+
const raw = await evaluateFormulaWithRelations(attr.expression, record, schema, resolver);
|
|
3542
|
+
return formatFormulaResult(raw, attr.returnType, attr.decimals);
|
|
3543
|
+
}
|
|
3544
|
+
|
|
3545
|
+
// src/runtime/formula/path-parser.ts
|
|
3546
|
+
var InvalidPathError = class extends Error {
|
|
3547
|
+
constructor(path, segment, reason) {
|
|
3548
|
+
super(`Invalid path "${path}" at "${segment}": ${reason}`);
|
|
3549
|
+
this.path = path;
|
|
3550
|
+
this.segment = segment;
|
|
3551
|
+
this.reason = reason;
|
|
3552
|
+
this.name = "InvalidPathError";
|
|
3553
|
+
}
|
|
3554
|
+
};
|
|
3555
|
+
var MaxDepthExceededError = class extends Error {
|
|
3556
|
+
constructor(path, maxDepth) {
|
|
3557
|
+
super(`Path "${path}" exceeds maximum depth of ${maxDepth}`);
|
|
3558
|
+
this.path = path;
|
|
3559
|
+
this.maxDepth = maxDepth;
|
|
3560
|
+
this.name = "MaxDepthExceededError";
|
|
3561
|
+
}
|
|
3562
|
+
};
|
|
3563
|
+
async function parsePath(path, startSchema, getSchema, maxDepth = 5) {
|
|
3564
|
+
const segments = path.split(".");
|
|
3565
|
+
if (segments.length === 0 || segments.length === 1 && segments[0] === "") {
|
|
3566
|
+
throw new InvalidPathError(path, path, "Path cannot be empty");
|
|
3567
|
+
}
|
|
3568
|
+
if (segments.length > maxDepth) {
|
|
3569
|
+
throw new MaxDepthExceededError(path, maxDepth);
|
|
3570
|
+
}
|
|
3571
|
+
const result = [];
|
|
3572
|
+
let currentSchema = startSchema;
|
|
3573
|
+
for (let i = 0; i < segments.length; i++) {
|
|
3574
|
+
const segmentName = segments[i];
|
|
3575
|
+
const isLastSegment = i === segments.length - 1;
|
|
3576
|
+
const attr = currentSchema.attributes.find((a) => a.name === segmentName);
|
|
3577
|
+
if (!attr) {
|
|
3578
|
+
throw new InvalidPathError(
|
|
3579
|
+
path,
|
|
3580
|
+
segmentName,
|
|
3581
|
+
`Attribute "${segmentName}" not found in object "${currentSchema.name}"`
|
|
3582
|
+
);
|
|
3583
|
+
}
|
|
3584
|
+
if (attr.type === "relation") {
|
|
3585
|
+
const relationAttr = attr;
|
|
3586
|
+
const targetObject = relationAttr.targets[0]?.object;
|
|
3587
|
+
if (!targetObject) {
|
|
3588
|
+
throw new InvalidPathError(path, segmentName, "Relation has no target object");
|
|
3589
|
+
}
|
|
3590
|
+
result.push({
|
|
3591
|
+
name: segmentName,
|
|
3592
|
+
type: "relation",
|
|
3593
|
+
cardinality: relationAttr.cardinality,
|
|
3594
|
+
targetObject
|
|
3595
|
+
});
|
|
3596
|
+
if (!isLastSegment) {
|
|
3597
|
+
const nextSchema = await getSchema(targetObject);
|
|
3598
|
+
if (!nextSchema) {
|
|
3599
|
+
throw new InvalidPathError(
|
|
3600
|
+
path,
|
|
3601
|
+
segmentName,
|
|
3602
|
+
`Target object "${targetObject}" schema not found`
|
|
3603
|
+
);
|
|
3604
|
+
}
|
|
3605
|
+
currentSchema = nextSchema;
|
|
3606
|
+
}
|
|
3607
|
+
} else {
|
|
3608
|
+
if (!isLastSegment) {
|
|
3609
|
+
throw new InvalidPathError(
|
|
3610
|
+
path,
|
|
3611
|
+
segmentName,
|
|
3612
|
+
`"${segmentName}" is not a relation but has segments after it`
|
|
3613
|
+
);
|
|
3614
|
+
}
|
|
3615
|
+
result.push({
|
|
3616
|
+
name: segmentName,
|
|
3617
|
+
type: "attribute"
|
|
3618
|
+
});
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
return result;
|
|
3622
|
+
}
|
|
3623
|
+
async function validatePath(path, startSchema, getSchema, maxDepth = 5) {
|
|
3624
|
+
try {
|
|
3625
|
+
await parsePath(path, startSchema, getSchema, maxDepth);
|
|
3626
|
+
return true;
|
|
3627
|
+
} catch {
|
|
3628
|
+
return false;
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
function pathHasManyCardinality(segments) {
|
|
3632
|
+
return segments.some((s) => s.type === "relation" && s.cardinality === "many");
|
|
3633
|
+
}
|
|
3634
|
+
function getPathDepth(segments) {
|
|
3635
|
+
return segments.filter((s) => s.type === "relation").length;
|
|
3636
|
+
}
|
|
3637
|
+
function getTargetAttributeName(path) {
|
|
3638
|
+
const parts = path.split(".");
|
|
3639
|
+
return parts[parts.length - 1];
|
|
3640
|
+
}
|
|
3641
|
+
function getRelationPath(path) {
|
|
3642
|
+
const parts = path.split(".");
|
|
3643
|
+
if (parts.length <= 1) return null;
|
|
3644
|
+
return parts.slice(0, -1).join(".");
|
|
3645
|
+
}
|
|
3646
|
+
|
|
3647
|
+
// src/runtime/formula/path-traversal.ts
|
|
3648
|
+
async function traversePath(record, path, startSchemaName, adapter, getSchema, options) {
|
|
3649
|
+
const maxDepth = options?.maxDepth ?? 5;
|
|
3650
|
+
const startSchema = await getSchema(startSchemaName);
|
|
3651
|
+
if (!startSchema) {
|
|
3652
|
+
return { values: [], recordCounts: [0] };
|
|
3653
|
+
}
|
|
3654
|
+
const segments = await parsePath(path, startSchema, getSchema, maxDepth);
|
|
3655
|
+
if (segments.length > maxDepth) {
|
|
3656
|
+
throw new MaxDepthExceededError(path, maxDepth);
|
|
3657
|
+
}
|
|
3658
|
+
return await executeTraversal([record], segments, adapter, 0);
|
|
3659
|
+
}
|
|
3660
|
+
async function executeTraversal(records, segments, adapter, depth) {
|
|
3661
|
+
const recordCounts = [records.length];
|
|
3662
|
+
if (depth >= segments.length || records.length === 0) {
|
|
3663
|
+
return { values: [], recordCounts };
|
|
3664
|
+
}
|
|
3665
|
+
const segment = segments[depth];
|
|
3666
|
+
const isLastSegment = depth === segments.length - 1;
|
|
3667
|
+
if (segment.type === "attribute") {
|
|
3668
|
+
const values = records.map((r) => r.values[segment.name]).filter((v) => v !== void 0);
|
|
3669
|
+
return { values, recordCounts };
|
|
3670
|
+
}
|
|
3671
|
+
const relatedIds = collectRelatedIds(records, segment.name);
|
|
3672
|
+
if (relatedIds.length === 0) {
|
|
3673
|
+
return { values: [], recordCounts };
|
|
3674
|
+
}
|
|
3675
|
+
const relatedRecords = await adapter.objectRecords.findByIds(relatedIds);
|
|
3676
|
+
recordCounts.push(relatedRecords.length);
|
|
3677
|
+
if (isLastSegment) {
|
|
3678
|
+
return { values: relatedIds, recordCounts };
|
|
3679
|
+
}
|
|
3680
|
+
const nestedResult = await executeTraversal(relatedRecords, segments, adapter, depth + 1);
|
|
3681
|
+
return {
|
|
3682
|
+
values: nestedResult.values,
|
|
3683
|
+
recordCounts: [...recordCounts, ...nestedResult.recordCounts.slice(1)]
|
|
3684
|
+
};
|
|
3685
|
+
}
|
|
3686
|
+
function collectRelatedIds(records, relationName) {
|
|
3687
|
+
const ids = [];
|
|
3688
|
+
for (const record of records) {
|
|
3689
|
+
const value = record.values[relationName];
|
|
3690
|
+
if (typeof value === "string" && value.length > 0) {
|
|
3691
|
+
ids.push(value);
|
|
3692
|
+
} else if (Array.isArray(value)) {
|
|
3693
|
+
for (const id of value) {
|
|
3694
|
+
if (typeof id === "string" && id.length > 0) {
|
|
3695
|
+
ids.push(id);
|
|
3696
|
+
}
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
return [...new Set(ids)];
|
|
3701
|
+
}
|
|
3702
|
+
async function resolveSingleValue(record, path, startSchemaName, adapter, getSchema) {
|
|
3703
|
+
const result = await traversePath(record, path, startSchemaName, adapter, getSchema);
|
|
3704
|
+
return result.values[0] ?? null;
|
|
3705
|
+
}
|
|
3706
|
+
async function resolveMultiplePaths(record, paths, startSchemaName, adapter, getSchema) {
|
|
3707
|
+
const results = /* @__PURE__ */ new Map();
|
|
3708
|
+
await Promise.all(
|
|
3709
|
+
paths.map(async (path) => {
|
|
3710
|
+
const result = await traversePath(record, path, startSchemaName, adapter, getSchema);
|
|
3711
|
+
results.set(path, result.values);
|
|
3712
|
+
})
|
|
3713
|
+
);
|
|
3714
|
+
return results;
|
|
3715
|
+
}
|
|
3716
|
+
|
|
3143
3717
|
// src/runtime/hooks/hook.registry.ts
|
|
3144
3718
|
var NoopHookRegistry = class {
|
|
3145
3719
|
async execute() {
|
|
@@ -3803,6 +4377,26 @@ function createMockObjectRecordsRepository(stores) {
|
|
|
3803
4377
|
}
|
|
3804
4378
|
}
|
|
3805
4379
|
return Promise.resolve(updated);
|
|
4380
|
+
},
|
|
4381
|
+
findByRelation(objectName, relationAttributeName, targetRecordId) {
|
|
4382
|
+
const obj = Array.from(stores.objects.values()).find((o) => o.name === objectName);
|
|
4383
|
+
if (!obj) return Promise.resolve([]);
|
|
4384
|
+
const results = [];
|
|
4385
|
+
for (const record of stores.objectRecords.values()) {
|
|
4386
|
+
if (record.objectId !== obj.id) continue;
|
|
4387
|
+
const value = record.values[relationAttributeName];
|
|
4388
|
+
let matches = false;
|
|
4389
|
+
if (typeof value === "string" && value === targetRecordId) {
|
|
4390
|
+
matches = true;
|
|
4391
|
+
} else if (Array.isArray(value) && value.includes(targetRecordId)) {
|
|
4392
|
+
matches = true;
|
|
4393
|
+
}
|
|
4394
|
+
if (matches) {
|
|
4395
|
+
const { tenantId: _, ...publicRecord } = record;
|
|
4396
|
+
results.push(publicRecord);
|
|
4397
|
+
}
|
|
4398
|
+
}
|
|
4399
|
+
return Promise.resolve(results);
|
|
3806
4400
|
}
|
|
3807
4401
|
};
|
|
3808
4402
|
}
|
|
@@ -6652,6 +7246,263 @@ var RelationService = class {
|
|
|
6652
7246
|
}
|
|
6653
7247
|
};
|
|
6654
7248
|
|
|
7249
|
+
// src/runtime/services/rollup.service.ts
|
|
7250
|
+
var RollupService = class {
|
|
7251
|
+
constructor(adapter) {
|
|
7252
|
+
this.adapter = adapter;
|
|
7253
|
+
}
|
|
7254
|
+
/**
|
|
7255
|
+
* Calculate a rollup value for a record
|
|
7256
|
+
*
|
|
7257
|
+
* @param recordId - ID of the parent record
|
|
7258
|
+
* @param rollupAttr - Rollup attribute definition
|
|
7259
|
+
* @param schema - Schema of the parent object
|
|
7260
|
+
* @returns Computed rollup value
|
|
7261
|
+
*
|
|
7262
|
+
* @example
|
|
7263
|
+
* ```typescript
|
|
7264
|
+
* // Sum all order amounts for a company
|
|
7265
|
+
* const totalOrders = await rollupService.calculate(
|
|
7266
|
+
* "company-123",
|
|
7267
|
+
* {
|
|
7268
|
+
* type: "rollup",
|
|
7269
|
+
* name: "totalOrders",
|
|
7270
|
+
* relationAttribute: "orders",
|
|
7271
|
+
* targetAttribute: "amount",
|
|
7272
|
+
* function: "sum",
|
|
7273
|
+
* ...
|
|
7274
|
+
* },
|
|
7275
|
+
* companySchema
|
|
7276
|
+
* );
|
|
7277
|
+
* ```
|
|
7278
|
+
*/
|
|
7279
|
+
async calculate(recordId, rollupAttr, schema) {
|
|
7280
|
+
const relationAttr = schema.attributes.find(
|
|
7281
|
+
(a) => a.type === "relation" && a.name === rollupAttr.relationAttribute
|
|
7282
|
+
);
|
|
7283
|
+
if (relationAttr) {
|
|
7284
|
+
return this.calculateForward(recordId, rollupAttr);
|
|
7285
|
+
}
|
|
7286
|
+
return this.calculateReverse(recordId, rollupAttr, schema);
|
|
7287
|
+
}
|
|
7288
|
+
/**
|
|
7289
|
+
* Forward pattern: this record has a relation attribute pointing to other records
|
|
7290
|
+
* Example: entreprise222 has relation "entreprises" → companies, rollup collects from companies
|
|
7291
|
+
*/
|
|
7292
|
+
async calculateForward(recordId, rollupAttr) {
|
|
7293
|
+
const record = await this.adapter.objectRecords.findById(recordId);
|
|
7294
|
+
if (!record) {
|
|
7295
|
+
return { value: null, recordCount: 0 };
|
|
7296
|
+
}
|
|
7297
|
+
const relationValue = record.values[rollupAttr.relationAttribute];
|
|
7298
|
+
let relatedIds = [];
|
|
7299
|
+
if (typeof relationValue === "string" && relationValue.length > 0) {
|
|
7300
|
+
relatedIds = [relationValue];
|
|
7301
|
+
} else if (Array.isArray(relationValue)) {
|
|
7302
|
+
relatedIds = relationValue.filter((id) => typeof id === "string" && id.length > 0);
|
|
7303
|
+
}
|
|
7304
|
+
if (relatedIds.length === 0) {
|
|
7305
|
+
return { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 };
|
|
7306
|
+
}
|
|
7307
|
+
const relatedRecords = await this.adapter.objectRecords.findByIds(relatedIds);
|
|
7308
|
+
if (relatedRecords.length === 0) {
|
|
7309
|
+
return { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 };
|
|
7310
|
+
}
|
|
7311
|
+
return this.aggregateValues(relatedRecords, rollupAttr);
|
|
7312
|
+
}
|
|
7313
|
+
/**
|
|
7314
|
+
* Reverse pattern: other records have a relation pointing to this record
|
|
7315
|
+
* Example: Company has rollup on "orders", Order has relation "company" → companies
|
|
7316
|
+
*/
|
|
7317
|
+
async calculateReverse(recordId, rollupAttr, schema) {
|
|
7318
|
+
const record = await this.adapter.objectRecords.findById(recordId);
|
|
7319
|
+
if (!record) {
|
|
7320
|
+
return { value: null, recordCount: 0 };
|
|
7321
|
+
}
|
|
7322
|
+
const parentObject = await this.adapter.objects.findById(record.objectId);
|
|
7323
|
+
if (!parentObject) {
|
|
7324
|
+
return { value: null, recordCount: 0 };
|
|
7325
|
+
}
|
|
7326
|
+
const tenantId = parentObject.tenantId;
|
|
7327
|
+
const sourceObjectName = rollupAttr.relationAttribute;
|
|
7328
|
+
const sourceObject = await this.adapter.objects.findByName(tenantId, sourceObjectName);
|
|
7329
|
+
if (!sourceObject) {
|
|
7330
|
+
return { value: null, recordCount: 0 };
|
|
7331
|
+
}
|
|
7332
|
+
const sourceAttributes = await this.adapter.attributes.findByObjectId(sourceObject.id);
|
|
7333
|
+
const reverseRelationAttr = sourceAttributes.find((attr) => {
|
|
7334
|
+
if (attr.type !== "relation") return false;
|
|
7335
|
+
const relationConfig = attr.config;
|
|
7336
|
+
return relationConfig?.targets?.some((t) => t.object === schema.name);
|
|
7337
|
+
});
|
|
7338
|
+
if (!reverseRelationAttr) {
|
|
7339
|
+
return { value: null, recordCount: 0 };
|
|
7340
|
+
}
|
|
7341
|
+
const relatedRecords = await this.adapter.objectRecords.findByRelation(
|
|
7342
|
+
tenantId,
|
|
7343
|
+
sourceObject.id,
|
|
7344
|
+
reverseRelationAttr.name,
|
|
7345
|
+
recordId
|
|
7346
|
+
);
|
|
7347
|
+
if (relatedRecords.length === 0) {
|
|
7348
|
+
return { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 };
|
|
7349
|
+
}
|
|
7350
|
+
return this.aggregateValues(relatedRecords, rollupAttr);
|
|
7351
|
+
}
|
|
7352
|
+
/**
|
|
7353
|
+
* Extract and aggregate values from related records
|
|
7354
|
+
*/
|
|
7355
|
+
aggregateValues(relatedRecords, rollupAttr) {
|
|
7356
|
+
const values = relatedRecords.map((r) => r.values[rollupAttr.targetAttribute]).filter((v) => v !== void 0 && v !== null);
|
|
7357
|
+
const aggregated = this.aggregate(values, rollupAttr.function);
|
|
7358
|
+
const result = typeof aggregated === "number" && rollupAttr.decimals !== void 0 ? Number(aggregated.toFixed(rollupAttr.decimals)) : aggregated;
|
|
7359
|
+
return { value: result, recordCount: relatedRecords.length };
|
|
7360
|
+
}
|
|
7361
|
+
/**
|
|
7362
|
+
* Calculate rollup values for multiple records (batched)
|
|
7363
|
+
*
|
|
7364
|
+
* More efficient than calling calculate() for each record individually.
|
|
7365
|
+
*/
|
|
7366
|
+
async calculateForMany(recordIds, rollupAttr, schema) {
|
|
7367
|
+
const results = /* @__PURE__ */ new Map();
|
|
7368
|
+
for (const id of recordIds) {
|
|
7369
|
+
results.set(id, { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 });
|
|
7370
|
+
}
|
|
7371
|
+
await Promise.all(
|
|
7372
|
+
recordIds.map(async (id) => {
|
|
7373
|
+
const result = await this.calculate(id, rollupAttr, schema);
|
|
7374
|
+
results.set(id, result);
|
|
7375
|
+
})
|
|
7376
|
+
);
|
|
7377
|
+
return results;
|
|
7378
|
+
}
|
|
7379
|
+
/**
|
|
7380
|
+
* Apply aggregation function to a set of values
|
|
7381
|
+
*/
|
|
7382
|
+
aggregate(values, fn) {
|
|
7383
|
+
switch (fn) {
|
|
7384
|
+
case "sum":
|
|
7385
|
+
return this.sumNumbers(values);
|
|
7386
|
+
case "avg":
|
|
7387
|
+
return this.averageNumbers(values);
|
|
7388
|
+
case "min":
|
|
7389
|
+
return this.minNumbers(values);
|
|
7390
|
+
case "max":
|
|
7391
|
+
return this.maxNumbers(values);
|
|
7392
|
+
case "count":
|
|
7393
|
+
return values.length;
|
|
7394
|
+
case "countValues":
|
|
7395
|
+
return values.filter((v) => v != null && v !== "").length;
|
|
7396
|
+
case "countEmpty":
|
|
7397
|
+
return values.filter((v) => v == null || v === "").length;
|
|
7398
|
+
case "percentFilled": {
|
|
7399
|
+
if (values.length === 0) return 0;
|
|
7400
|
+
const filled = values.filter((v) => v != null && v !== "").length;
|
|
7401
|
+
return filled / values.length * 100;
|
|
7402
|
+
}
|
|
7403
|
+
case "percentEmpty": {
|
|
7404
|
+
if (values.length === 0) return 0;
|
|
7405
|
+
const empty = values.filter((v) => v == null || v === "").length;
|
|
7406
|
+
return empty / values.length * 100;
|
|
7407
|
+
}
|
|
7408
|
+
case "collect":
|
|
7409
|
+
return values;
|
|
7410
|
+
}
|
|
7411
|
+
}
|
|
7412
|
+
/**
|
|
7413
|
+
* Get the default empty value for a rollup function
|
|
7414
|
+
*/
|
|
7415
|
+
getEmptyValue(fn) {
|
|
7416
|
+
switch (fn) {
|
|
7417
|
+
case "sum":
|
|
7418
|
+
case "count":
|
|
7419
|
+
case "countValues":
|
|
7420
|
+
case "countEmpty":
|
|
7421
|
+
case "percentEmpty":
|
|
7422
|
+
case "percentFilled":
|
|
7423
|
+
return 0;
|
|
7424
|
+
case "avg":
|
|
7425
|
+
case "min":
|
|
7426
|
+
case "max":
|
|
7427
|
+
return null;
|
|
7428
|
+
case "collect":
|
|
7429
|
+
return [];
|
|
7430
|
+
}
|
|
7431
|
+
}
|
|
7432
|
+
/**
|
|
7433
|
+
* Sum numeric values
|
|
7434
|
+
*/
|
|
7435
|
+
sumNumbers(values) {
|
|
7436
|
+
return values.filter((v) => typeof v === "number" && !Number.isNaN(v)).reduce((sum, n) => sum + n, 0);
|
|
7437
|
+
}
|
|
7438
|
+
/**
|
|
7439
|
+
* Average numeric values
|
|
7440
|
+
*/
|
|
7441
|
+
averageNumbers(values) {
|
|
7442
|
+
const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
|
|
7443
|
+
if (nums.length === 0) return null;
|
|
7444
|
+
return nums.reduce((sum, n) => sum + n, 0) / nums.length;
|
|
7445
|
+
}
|
|
7446
|
+
/**
|
|
7447
|
+
* Minimum numeric value
|
|
7448
|
+
*/
|
|
7449
|
+
minNumbers(values) {
|
|
7450
|
+
const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
|
|
7451
|
+
if (nums.length === 0) return null;
|
|
7452
|
+
return Math.min(...nums);
|
|
7453
|
+
}
|
|
7454
|
+
/**
|
|
7455
|
+
* Maximum numeric value
|
|
7456
|
+
*/
|
|
7457
|
+
maxNumbers(values) {
|
|
7458
|
+
const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
|
|
7459
|
+
if (nums.length === 0) return null;
|
|
7460
|
+
return Math.max(...nums);
|
|
7461
|
+
}
|
|
7462
|
+
/**
|
|
7463
|
+
* Recalculate all rollup attributes for a record and update it
|
|
7464
|
+
*
|
|
7465
|
+
* Called after related records change to keep rollups up-to-date.
|
|
7466
|
+
*/
|
|
7467
|
+
async recalculateAndUpdate(record, schema) {
|
|
7468
|
+
const rollupAttrs = schema.attributes.filter((a) => a.type === "rollup");
|
|
7469
|
+
if (rollupAttrs.length === 0) {
|
|
7470
|
+
return record;
|
|
7471
|
+
}
|
|
7472
|
+
const updates = {};
|
|
7473
|
+
for (const attr of rollupAttrs) {
|
|
7474
|
+
const result = await this.calculate(record.id, attr, schema);
|
|
7475
|
+
updates[attr.name] = result.value;
|
|
7476
|
+
}
|
|
7477
|
+
return await this.adapter.objectRecords.update(record.id, updates);
|
|
7478
|
+
}
|
|
7479
|
+
/**
|
|
7480
|
+
* Find parent records that need rollup recalculation when a child record changes
|
|
7481
|
+
*
|
|
7482
|
+
* Used by hooks to determine which parent records to recalculate after
|
|
7483
|
+
* a child record is created, updated, or deleted.
|
|
7484
|
+
*
|
|
7485
|
+
* @param changedRecord - The record that was modified
|
|
7486
|
+
* @param changedSchema - Schema of the changed record's object
|
|
7487
|
+
* @returns Array of parent record IDs that need recalculation
|
|
7488
|
+
*/
|
|
7489
|
+
async findAffectedParentRecords(changedRecord, changedSchema) {
|
|
7490
|
+
const affectedIds = [];
|
|
7491
|
+
const relationAttrs = changedSchema.attributes.filter(
|
|
7492
|
+
(a) => a.type === "relation"
|
|
7493
|
+
);
|
|
7494
|
+
for (const attr of relationAttrs) {
|
|
7495
|
+
const relatedId = changedRecord.values[attr.name];
|
|
7496
|
+
if (typeof relatedId === "string" && relatedId.length > 0) {
|
|
7497
|
+
affectedIds.push(relatedId);
|
|
7498
|
+
} else if (Array.isArray(relatedId)) {
|
|
7499
|
+
affectedIds.push(...relatedId.filter((id) => typeof id === "string"));
|
|
7500
|
+
}
|
|
7501
|
+
}
|
|
7502
|
+
return [...new Set(affectedIds)];
|
|
7503
|
+
}
|
|
7504
|
+
};
|
|
7505
|
+
|
|
6655
7506
|
// src/runtime/services/user.service.ts
|
|
6656
7507
|
var UserService = class {
|
|
6657
7508
|
constructor(adapter, tenantId) {
|
|
@@ -6777,6 +7628,7 @@ var RecordService = class {
|
|
|
6777
7628
|
this.schemaService = new ObjectSchemaService(adapter, registry);
|
|
6778
7629
|
this.relationService = new RelationService(adapter, registry);
|
|
6779
7630
|
this.userService = new UserService(adapter, tenantId);
|
|
7631
|
+
this.rollupService = new RollupService(adapter);
|
|
6780
7632
|
this.hookRegistry = options?.hookRegistry ?? new NoopHookRegistry();
|
|
6781
7633
|
this.permissionService = options?.permissionService;
|
|
6782
7634
|
this.auditService = options?.auditService ?? (adapter.audit ? new AuditService(adapter, tenantId) : void 0);
|
|
@@ -6938,6 +7790,7 @@ var RecordService = class {
|
|
|
6938
7790
|
};
|
|
6939
7791
|
await this.hookRegistry.execute("afterCreate", schema.name, afterCtx);
|
|
6940
7792
|
}
|
|
7793
|
+
await this.recalculateParentRollups(record, schema);
|
|
6941
7794
|
if (this.auditService && this.userId) {
|
|
6942
7795
|
await this.auditService.logRecordAction({
|
|
6943
7796
|
action: "record.created",
|
|
@@ -6964,13 +7817,17 @@ var RecordService = class {
|
|
|
6964
7817
|
if (!record) {
|
|
6965
7818
|
return null;
|
|
6966
7819
|
}
|
|
7820
|
+
const schema = await this.schemaService.getObjectSchema(record.objectId);
|
|
7821
|
+
let enrichedRecord = record;
|
|
7822
|
+
if (!options?.skipFormulas) {
|
|
7823
|
+
enrichedRecord = this.enrichWithFormulas(record, schema);
|
|
7824
|
+
}
|
|
6967
7825
|
if (options?.includeSchema) {
|
|
6968
|
-
const
|
|
6969
|
-
const recordWithSchema = record;
|
|
7826
|
+
const recordWithSchema = enrichedRecord;
|
|
6970
7827
|
recordWithSchema.schema = schema;
|
|
6971
7828
|
return recordWithSchema;
|
|
6972
7829
|
}
|
|
6973
|
-
return
|
|
7830
|
+
return enrichedRecord;
|
|
6974
7831
|
}
|
|
6975
7832
|
/**
|
|
6976
7833
|
* Get a record by ID or throw if not found
|
|
@@ -7049,6 +7906,7 @@ var RecordService = class {
|
|
|
7049
7906
|
};
|
|
7050
7907
|
await this.hookRegistry.execute("afterUpdate", schema.name, afterCtx);
|
|
7051
7908
|
}
|
|
7909
|
+
await this.recalculateParentRollups(updated, schema);
|
|
7052
7910
|
if (this.auditService && this.userId && changedAttributes.length > 0) {
|
|
7053
7911
|
const changes = changedAttributes.map((attr) => ({
|
|
7054
7912
|
field: attr,
|
|
@@ -7176,6 +8034,7 @@ var RecordService = class {
|
|
|
7176
8034
|
if (!options?.skipHooks) {
|
|
7177
8035
|
await this.hookRegistry.execute("afterDelete", schema.name, hookCtx);
|
|
7178
8036
|
}
|
|
8037
|
+
await this.recalculateParentRollups(record, schema);
|
|
7179
8038
|
if (this.auditService && this.userId) {
|
|
7180
8039
|
await this.auditService.logRecordAction({
|
|
7181
8040
|
action: "record.deleted",
|
|
@@ -7265,6 +8124,79 @@ var RecordService = class {
|
|
|
7265
8124
|
timestamp: /* @__PURE__ */ new Date()
|
|
7266
8125
|
};
|
|
7267
8126
|
}
|
|
8127
|
+
/**
|
|
8128
|
+
* Enrich a record with computed formula values
|
|
8129
|
+
*
|
|
8130
|
+
* Formula attributes are calculated at read-time from the record's values.
|
|
8131
|
+
* This method adds the computed values to the record's values object.
|
|
8132
|
+
*
|
|
8133
|
+
* @param record - The record to enrich
|
|
8134
|
+
* @param schema - The object schema containing attribute definitions
|
|
8135
|
+
* @returns Record with formula values computed
|
|
8136
|
+
* @internal
|
|
8137
|
+
*/
|
|
8138
|
+
enrichWithFormulas(record, schema) {
|
|
8139
|
+
const formulaAttrs = schema.attributes.filter(
|
|
8140
|
+
(a) => a.type === "formula"
|
|
8141
|
+
);
|
|
8142
|
+
if (formulaAttrs.length === 0) {
|
|
8143
|
+
return record;
|
|
8144
|
+
}
|
|
8145
|
+
const enrichedValues = { ...record.values };
|
|
8146
|
+
for (const attr of formulaAttrs) {
|
|
8147
|
+
enrichedValues[attr.name] = evaluateFormulaAttribute(attr, record.values);
|
|
8148
|
+
}
|
|
8149
|
+
return {
|
|
8150
|
+
...record,
|
|
8151
|
+
values: enrichedValues
|
|
8152
|
+
};
|
|
8153
|
+
}
|
|
8154
|
+
/**
|
|
8155
|
+
* Enrich multiple records with computed formula values
|
|
8156
|
+
* @internal
|
|
8157
|
+
*/
|
|
8158
|
+
enrichRecordsWithFormulas(records, schema) {
|
|
8159
|
+
const formulaAttrs = schema.attributes.filter(
|
|
8160
|
+
(a) => a.type === "formula"
|
|
8161
|
+
);
|
|
8162
|
+
if (formulaAttrs.length === 0) {
|
|
8163
|
+
return records;
|
|
8164
|
+
}
|
|
8165
|
+
return records.map((record) => this.enrichWithFormulas(record, schema));
|
|
8166
|
+
}
|
|
8167
|
+
/**
|
|
8168
|
+
* Recalculate rollups after a record changes
|
|
8169
|
+
*
|
|
8170
|
+
* This handles two cases:
|
|
8171
|
+
* 1. The record itself has rollups (e.g., aggregating from related records it points to)
|
|
8172
|
+
* 2. Parent records have rollups that aggregate from this record
|
|
8173
|
+
*
|
|
8174
|
+
* @param record - The record that was modified
|
|
8175
|
+
* @param schema - Schema of the record's object
|
|
8176
|
+
* @internal
|
|
8177
|
+
*/
|
|
8178
|
+
async recalculateParentRollups(record, schema) {
|
|
8179
|
+
const ownRollupAttrs = schema.attributes.filter(
|
|
8180
|
+
(a) => a.type === "rollup"
|
|
8181
|
+
);
|
|
8182
|
+
if (ownRollupAttrs.length > 0) {
|
|
8183
|
+
await this.rollupService.recalculateAndUpdate(record, schema);
|
|
8184
|
+
}
|
|
8185
|
+
const affectedParentIds = await this.rollupService.findAffectedParentRecords(record, schema);
|
|
8186
|
+
if (affectedParentIds.length === 0) {
|
|
8187
|
+
return;
|
|
8188
|
+
}
|
|
8189
|
+
const parentRecords = await this.adapter.objectRecords.findByIds(affectedParentIds);
|
|
8190
|
+
for (const parentRecord of parentRecords) {
|
|
8191
|
+
const parentSchema = await this.schemaService.getObjectSchema(parentRecord.objectId);
|
|
8192
|
+
const rollupAttrs = parentSchema.attributes.filter(
|
|
8193
|
+
(a) => a.type === "rollup"
|
|
8194
|
+
);
|
|
8195
|
+
if (rollupAttrs.length > 0) {
|
|
8196
|
+
await this.rollupService.recalculateAndUpdate(parentRecord, parentSchema);
|
|
8197
|
+
}
|
|
8198
|
+
}
|
|
8199
|
+
}
|
|
7268
8200
|
/**
|
|
7269
8201
|
* Permanently delete a record (hard delete)
|
|
7270
8202
|
*
|
|
@@ -7293,11 +8225,18 @@ var RecordService = class {
|
|
|
7293
8225
|
* @returns Records and total count
|
|
7294
8226
|
*/
|
|
7295
8227
|
async listRecords(objectId, options) {
|
|
8228
|
+
const schema = await this.schemaService.getObjectSchema(objectId);
|
|
7296
8229
|
if (this.permissionService && this.userId) {
|
|
7297
|
-
const schema = await this.schemaService.getObjectSchema(objectId);
|
|
7298
8230
|
await this.checkPermission(schema.name, "read");
|
|
7299
8231
|
}
|
|
7300
|
-
|
|
8232
|
+
const result = await this.adapter.objectRecords.list(this.tenantId, objectId, options);
|
|
8233
|
+
if (!options?.skipFormulas) {
|
|
8234
|
+
return {
|
|
8235
|
+
records: this.enrichRecordsWithFormulas(result.records, schema),
|
|
8236
|
+
total: result.total
|
|
8237
|
+
};
|
|
8238
|
+
}
|
|
8239
|
+
return result;
|
|
7301
8240
|
}
|
|
7302
8241
|
/**
|
|
7303
8242
|
* Search records using full-text search
|
|
@@ -7308,11 +8247,18 @@ var RecordService = class {
|
|
|
7308
8247
|
* @returns Matching records and total count
|
|
7309
8248
|
*/
|
|
7310
8249
|
async searchRecords(objectId, query, options) {
|
|
8250
|
+
const schema = await this.schemaService.getObjectSchema(objectId);
|
|
7311
8251
|
if (this.permissionService && this.userId) {
|
|
7312
|
-
const schema = await this.schemaService.getObjectSchema(objectId);
|
|
7313
8252
|
await this.checkPermission(schema.name, "read");
|
|
7314
8253
|
}
|
|
7315
|
-
|
|
8254
|
+
const result = await this.adapter.objectRecords.search(this.tenantId, objectId, query, options);
|
|
8255
|
+
if (!options?.skipFormulas) {
|
|
8256
|
+
return {
|
|
8257
|
+
records: this.enrichRecordsWithFormulas(result.records, schema),
|
|
8258
|
+
total: result.total
|
|
8259
|
+
};
|
|
8260
|
+
}
|
|
8261
|
+
return result;
|
|
7316
8262
|
}
|
|
7317
8263
|
/**
|
|
7318
8264
|
* Validate data against object schema without saving
|
|
@@ -7357,6 +8303,231 @@ var RecordService = class {
|
|
|
7357
8303
|
}
|
|
7358
8304
|
};
|
|
7359
8305
|
|
|
8306
|
+
// src/runtime/services/relation-resolver.service.ts
|
|
8307
|
+
var RelationResolverService = class {
|
|
8308
|
+
constructor(adapter) {
|
|
8309
|
+
this.adapter = adapter;
|
|
8310
|
+
}
|
|
8311
|
+
/**
|
|
8312
|
+
* Resolve values from related records for formula evaluation
|
|
8313
|
+
*
|
|
8314
|
+
* Phase 2: Supports 1 level of relation traversal only
|
|
8315
|
+
*
|
|
8316
|
+
* @param record - The source record
|
|
8317
|
+
* @param schema - Schema of the source object
|
|
8318
|
+
* @param relationNames - Names of relation attributes to resolve
|
|
8319
|
+
* @returns Map of relation name to related record's values
|
|
8320
|
+
*
|
|
8321
|
+
* @example
|
|
8322
|
+
* ```typescript
|
|
8323
|
+
* // For an order with company relation
|
|
8324
|
+
* const resolved = await resolver.resolveRelationValues(
|
|
8325
|
+
* orderRecord,
|
|
8326
|
+
* orderSchema,
|
|
8327
|
+
* ["company"]
|
|
8328
|
+
* );
|
|
8329
|
+
* // → { company: { id: "...", name: "Acme Corp", ... } }
|
|
8330
|
+
* ```
|
|
8331
|
+
*/
|
|
8332
|
+
async resolveRelationValues(record, schema, relationNames) {
|
|
8333
|
+
const result = {};
|
|
8334
|
+
const relationAttrs = schema.attributes.filter(
|
|
8335
|
+
(a) => a.type === "relation" && relationNames.includes(a.name)
|
|
8336
|
+
);
|
|
8337
|
+
if (relationAttrs.length === 0) {
|
|
8338
|
+
return result;
|
|
8339
|
+
}
|
|
8340
|
+
const idsToFetch = [];
|
|
8341
|
+
const attrIdMap = /* @__PURE__ */ new Map();
|
|
8342
|
+
for (const attr of relationAttrs) {
|
|
8343
|
+
const relatedId = this.extractSingleId(record.values[attr.name]);
|
|
8344
|
+
if (relatedId) {
|
|
8345
|
+
idsToFetch.push(relatedId);
|
|
8346
|
+
attrIdMap.set(relatedId, attr.name);
|
|
8347
|
+
}
|
|
8348
|
+
}
|
|
8349
|
+
if (idsToFetch.length === 0) {
|
|
8350
|
+
return result;
|
|
8351
|
+
}
|
|
8352
|
+
const relatedRecords = await this.adapter.objectRecords.findByIds(idsToFetch);
|
|
8353
|
+
for (const relatedRecord of relatedRecords) {
|
|
8354
|
+
const attrName = attrIdMap.get(relatedRecord.id);
|
|
8355
|
+
if (attrName) {
|
|
8356
|
+
result[attrName] = relatedRecord.values;
|
|
8357
|
+
}
|
|
8358
|
+
}
|
|
8359
|
+
return result;
|
|
8360
|
+
}
|
|
8361
|
+
/**
|
|
8362
|
+
* Resolve relation values for multiple records (batched)
|
|
8363
|
+
*
|
|
8364
|
+
* Optimized for list operations - fetches all related records in one batch
|
|
8365
|
+
*
|
|
8366
|
+
* @param records - Source records
|
|
8367
|
+
* @param schema - Schema of the source object
|
|
8368
|
+
* @param relationNames - Names of relation attributes to resolve
|
|
8369
|
+
* @returns Map of record ID to resolved relations
|
|
8370
|
+
*/
|
|
8371
|
+
async resolveRelationValuesForMany(records, schema, relationNames) {
|
|
8372
|
+
const resultMap = /* @__PURE__ */ new Map();
|
|
8373
|
+
for (const record of records) {
|
|
8374
|
+
resultMap.set(record.id, {});
|
|
8375
|
+
}
|
|
8376
|
+
const relationAttrs = schema.attributes.filter(
|
|
8377
|
+
(a) => a.type === "relation" && relationNames.includes(a.name)
|
|
8378
|
+
);
|
|
8379
|
+
if (relationAttrs.length === 0) {
|
|
8380
|
+
return resultMap;
|
|
8381
|
+
}
|
|
8382
|
+
const allIdsToFetch = /* @__PURE__ */ new Set();
|
|
8383
|
+
const recordRelationMap = /* @__PURE__ */ new Map();
|
|
8384
|
+
for (const record of records) {
|
|
8385
|
+
const idToAttr = /* @__PURE__ */ new Map();
|
|
8386
|
+
for (const attr of relationAttrs) {
|
|
8387
|
+
const relatedId = this.extractSingleId(record.values[attr.name]);
|
|
8388
|
+
if (relatedId) {
|
|
8389
|
+
allIdsToFetch.add(relatedId);
|
|
8390
|
+
idToAttr.set(relatedId, attr.name);
|
|
8391
|
+
}
|
|
8392
|
+
}
|
|
8393
|
+
recordRelationMap.set(record.id, idToAttr);
|
|
8394
|
+
}
|
|
8395
|
+
if (allIdsToFetch.size === 0) {
|
|
8396
|
+
return resultMap;
|
|
8397
|
+
}
|
|
8398
|
+
const relatedRecords = await this.adapter.objectRecords.findByIds([...allIdsToFetch]);
|
|
8399
|
+
const relatedRecordMap = new Map(relatedRecords.map((r) => [r.id, r]));
|
|
8400
|
+
for (const record of records) {
|
|
8401
|
+
const result = resultMap.get(record.id);
|
|
8402
|
+
const idToAttr = recordRelationMap.get(record.id);
|
|
8403
|
+
for (const [relatedId, attrName] of idToAttr) {
|
|
8404
|
+
const relatedRecord = relatedRecordMap.get(relatedId);
|
|
8405
|
+
if (relatedRecord) {
|
|
8406
|
+
result[attrName] = relatedRecord.values;
|
|
8407
|
+
}
|
|
8408
|
+
}
|
|
8409
|
+
}
|
|
8410
|
+
return resultMap;
|
|
8411
|
+
}
|
|
8412
|
+
/**
|
|
8413
|
+
* Flatten resolved relations for formula evaluation
|
|
8414
|
+
*
|
|
8415
|
+
* Converts nested structure to dot-notation keys:
|
|
8416
|
+
* { company: { name: "Acme" } } → { "company.name": "Acme" }
|
|
8417
|
+
*
|
|
8418
|
+
* @param resolved - Resolved relations from resolveRelationValues
|
|
8419
|
+
* @returns Flattened values suitable for formula evaluation
|
|
8420
|
+
*/
|
|
8421
|
+
flattenResolvedRelations(resolved) {
|
|
8422
|
+
const flat = {};
|
|
8423
|
+
for (const [relationName, values] of Object.entries(resolved)) {
|
|
8424
|
+
for (const [key, value] of Object.entries(values)) {
|
|
8425
|
+
flat[`${relationName}.${key}`] = value;
|
|
8426
|
+
}
|
|
8427
|
+
}
|
|
8428
|
+
return flat;
|
|
8429
|
+
}
|
|
8430
|
+
/**
|
|
8431
|
+
* Extract a single relation ID from a value
|
|
8432
|
+
* Handles both single (string) and multi (array) relations
|
|
8433
|
+
* @internal
|
|
8434
|
+
*/
|
|
8435
|
+
extractSingleId(value) {
|
|
8436
|
+
if (typeof value === "string" && value.length > 0) {
|
|
8437
|
+
return value;
|
|
8438
|
+
}
|
|
8439
|
+
if (Array.isArray(value) && value.length > 0 && typeof value[0] === "string") {
|
|
8440
|
+
return value[0];
|
|
8441
|
+
}
|
|
8442
|
+
return null;
|
|
8443
|
+
}
|
|
8444
|
+
};
|
|
8445
|
+
|
|
8446
|
+
// src/runtime/services/rollup-scheduler.ts
|
|
8447
|
+
var RollupScheduler = class {
|
|
8448
|
+
constructor(adapter, getSchemaById, options) {
|
|
8449
|
+
this.adapter = adapter;
|
|
8450
|
+
this.getSchemaById = getSchemaById;
|
|
8451
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
8452
|
+
this.rollupService = new RollupService(adapter);
|
|
8453
|
+
this.debounceMs = options?.debounceMs ?? 100;
|
|
8454
|
+
this.maxPending = options?.maxPending ?? 100;
|
|
8455
|
+
}
|
|
8456
|
+
/**
|
|
8457
|
+
* Schedule a rollup recalculation for a parent record
|
|
8458
|
+
*
|
|
8459
|
+
* If the same record is already pending, the timer is reset.
|
|
8460
|
+
* If too many recalculations are pending, triggers an immediate flush.
|
|
8461
|
+
*
|
|
8462
|
+
* @param parentId - ID of the parent record to recalculate
|
|
8463
|
+
* @param parentObjectId - Object ID of the parent
|
|
8464
|
+
* @param tenantId - Tenant ID
|
|
8465
|
+
*/
|
|
8466
|
+
scheduleRecalculation(parentId, parentObjectId, tenantId) {
|
|
8467
|
+
const key = `${parentId}:${parentObjectId}`;
|
|
8468
|
+
const existing = this.pending.get(key);
|
|
8469
|
+
if (existing) {
|
|
8470
|
+
clearTimeout(existing.timeout);
|
|
8471
|
+
}
|
|
8472
|
+
const timeout = setTimeout(async () => {
|
|
8473
|
+
await this.executeRecalculation(parentId, parentObjectId);
|
|
8474
|
+
this.pending.delete(key);
|
|
8475
|
+
}, this.debounceMs);
|
|
8476
|
+
this.pending.set(key, { parentId, parentObjectId, tenantId, timeout });
|
|
8477
|
+
if (this.pending.size >= this.maxPending) {
|
|
8478
|
+
this.flush();
|
|
8479
|
+
}
|
|
8480
|
+
}
|
|
8481
|
+
/**
|
|
8482
|
+
* Execute all pending recalculations immediately
|
|
8483
|
+
*/
|
|
8484
|
+
async flush() {
|
|
8485
|
+
const entries = Array.from(this.pending.entries());
|
|
8486
|
+
for (const [, entry] of entries) {
|
|
8487
|
+
clearTimeout(entry.timeout);
|
|
8488
|
+
}
|
|
8489
|
+
this.pending.clear();
|
|
8490
|
+
const tenantMap = /* @__PURE__ */ new Map();
|
|
8491
|
+
for (const [, entry] of entries) {
|
|
8492
|
+
tenantMap.set(entry.parentId, entry.tenantId);
|
|
8493
|
+
}
|
|
8494
|
+
const allIds = entries.map(([, entry]) => entry.parentId);
|
|
8495
|
+
const records = await this.adapter.objectRecords.findByIds(allIds);
|
|
8496
|
+
for (const record of records) {
|
|
8497
|
+
const schema = await this.getSchemaById(record.objectId);
|
|
8498
|
+
const tenantId = tenantMap.get(record.id);
|
|
8499
|
+
if (schema && tenantId) {
|
|
8500
|
+
await this.rollupService.recalculateAndUpdate(record, schema);
|
|
8501
|
+
}
|
|
8502
|
+
}
|
|
8503
|
+
}
|
|
8504
|
+
/**
|
|
8505
|
+
* Execute a single recalculation
|
|
8506
|
+
*/
|
|
8507
|
+
async executeRecalculation(parentId, parentObjectId) {
|
|
8508
|
+
const record = await this.adapter.objectRecords.findById(parentId);
|
|
8509
|
+
if (!record) return;
|
|
8510
|
+
const schema = await this.getSchemaById(parentObjectId);
|
|
8511
|
+
if (!schema) return;
|
|
8512
|
+
await this.rollupService.recalculateAndUpdate(record, schema);
|
|
8513
|
+
}
|
|
8514
|
+
/**
|
|
8515
|
+
* Get number of pending recalculations
|
|
8516
|
+
*/
|
|
8517
|
+
get pendingCount() {
|
|
8518
|
+
return this.pending.size;
|
|
8519
|
+
}
|
|
8520
|
+
/**
|
|
8521
|
+
* Clear all pending recalculations without executing them
|
|
8522
|
+
*/
|
|
8523
|
+
clear() {
|
|
8524
|
+
for (const [, entry] of this.pending) {
|
|
8525
|
+
clearTimeout(entry.timeout);
|
|
8526
|
+
}
|
|
8527
|
+
this.pending.clear();
|
|
8528
|
+
}
|
|
8529
|
+
};
|
|
8530
|
+
|
|
7360
8531
|
// src/runtime/services/user-profile.service.ts
|
|
7361
8532
|
var UserProfileService = class {
|
|
7362
8533
|
constructor(adapter, tenantId, options) {
|
|
@@ -8054,7 +9225,8 @@ async function syncAttributes(adapter, nativeObject, dbObject, existingObject, r
|
|
|
8054
9225
|
hidden: attr.hidden,
|
|
8055
9226
|
archived: attr.archived,
|
|
8056
9227
|
deprecated: attr.deprecated,
|
|
8057
|
-
|
|
9228
|
+
// Formula and rollup attributes don't have defaultValue
|
|
9229
|
+
defaultValue: "defaultValue" in attr ? attr.defaultValue : void 0,
|
|
8058
9230
|
config: extractAttributeConfig(attr)
|
|
8059
9231
|
});
|
|
8060
9232
|
if (existingAttr) {
|
|
@@ -8136,6 +9308,8 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
8136
9308
|
GeocodingService,
|
|
8137
9309
|
GlobalSearchService,
|
|
8138
9310
|
GroupBuilder,
|
|
9311
|
+
InvalidPathError,
|
|
9312
|
+
MaxDepthExceededError,
|
|
8139
9313
|
NO_VALUE_OPERATORS,
|
|
8140
9314
|
NoopGeocodingAdapter,
|
|
8141
9315
|
NoopHookRegistry,
|
|
@@ -8152,8 +9326,11 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
8152
9326
|
RecordNotFoundError,
|
|
8153
9327
|
RecordReferencedError,
|
|
8154
9328
|
RecordService,
|
|
9329
|
+
RelationResolverService,
|
|
8155
9330
|
RelationService,
|
|
8156
9331
|
RoleNotFoundError,
|
|
9332
|
+
RollupScheduler,
|
|
9333
|
+
RollupService,
|
|
8157
9334
|
SYSTEM_RESOURCES,
|
|
8158
9335
|
SYSTEM_RESOURCE_LABELS,
|
|
8159
9336
|
SchemaError,
|
|
@@ -8178,6 +9355,7 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
8178
9355
|
createDateValidator,
|
|
8179
9356
|
createDraftValidator,
|
|
8180
9357
|
createFileValidator,
|
|
9358
|
+
createFormulaValidator,
|
|
8181
9359
|
createLocationValidator,
|
|
8182
9360
|
createMockAdapter,
|
|
8183
9361
|
createMultiRelationValidator,
|
|
@@ -8187,6 +9365,7 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
8187
9365
|
createPhoneValidator,
|
|
8188
9366
|
createRatingValidator,
|
|
8189
9367
|
createRelationValidator,
|
|
9368
|
+
createRollupValidator,
|
|
8190
9369
|
createSelectValidator,
|
|
8191
9370
|
createSingleRelationValidator,
|
|
8192
9371
|
createStatusValidator,
|
|
@@ -8198,18 +9377,34 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
8198
9377
|
date,
|
|
8199
9378
|
dateConfigSchema,
|
|
8200
9379
|
enrichValuesWithSelectLabels,
|
|
9380
|
+
evaluateFormula,
|
|
9381
|
+
evaluateFormulaAttribute,
|
|
9382
|
+
evaluateFormulaAttributeWithRelations,
|
|
9383
|
+
evaluateFormulaWithRelations,
|
|
9384
|
+
evaluateFormulaWithResult,
|
|
8201
9385
|
extractAttributeNames,
|
|
9386
|
+
extractFormulaVariables,
|
|
9387
|
+
extractRelationNames,
|
|
9388
|
+
extractRelationReferences,
|
|
8202
9389
|
file,
|
|
8203
9390
|
fileConfigSchema,
|
|
9391
|
+
flattenRelationsForEval,
|
|
8204
9392
|
flow,
|
|
8205
9393
|
formatAttributeValue,
|
|
9394
|
+
formatFormulaResult,
|
|
9395
|
+
formula,
|
|
9396
|
+
formulaConfigSchema,
|
|
8206
9397
|
generateId,
|
|
8207
9398
|
generatePrefixedId,
|
|
8208
9399
|
getAttributeConfigSchema,
|
|
8209
9400
|
getMissingRequiredAttributes,
|
|
9401
|
+
getPathDepth,
|
|
9402
|
+
getRelationPath,
|
|
8210
9403
|
getSyncPreview,
|
|
9404
|
+
getTargetAttributeName,
|
|
8211
9405
|
getViewSyncPreview,
|
|
8212
9406
|
group,
|
|
9407
|
+
hasRelationReferences,
|
|
8213
9408
|
isActivityTab,
|
|
8214
9409
|
isAdvancedFilterState,
|
|
8215
9410
|
isCustomTab,
|
|
@@ -8235,6 +9430,8 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
8235
9430
|
numberConfigSchema,
|
|
8236
9431
|
object,
|
|
8237
9432
|
parseAttributeConfig,
|
|
9433
|
+
parsePath,
|
|
9434
|
+
pathHasManyCardinality,
|
|
8238
9435
|
phone,
|
|
8239
9436
|
phoneConfigSchema,
|
|
8240
9437
|
rating,
|
|
@@ -8243,6 +9440,10 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
8243
9440
|
relation,
|
|
8244
9441
|
relationConfigSchema,
|
|
8245
9442
|
renderLabelExpression,
|
|
9443
|
+
resolveMultiplePaths,
|
|
9444
|
+
resolveSingleValue,
|
|
9445
|
+
rollup,
|
|
9446
|
+
rollupConfigSchema,
|
|
8246
9447
|
safeParseAttributeConfig,
|
|
8247
9448
|
select,
|
|
8248
9449
|
selectConfigSchema,
|
|
@@ -8259,14 +9460,17 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
8259
9460
|
timestampConfigSchema,
|
|
8260
9461
|
toAdvancedFilterState,
|
|
8261
9462
|
toSimpleFilterState,
|
|
9463
|
+
traversePath,
|
|
8262
9464
|
user,
|
|
8263
9465
|
userConfigSchema,
|
|
8264
9466
|
validateAttribute,
|
|
8265
9467
|
validateAttributeConfig,
|
|
8266
9468
|
validateDraft,
|
|
8267
9469
|
validateDraftOrThrow,
|
|
9470
|
+
validateFormulaExpression,
|
|
8268
9471
|
validateObject,
|
|
8269
9472
|
validateObjectOrThrow,
|
|
9473
|
+
validatePath,
|
|
8270
9474
|
verifyNativeObjectsSync,
|
|
8271
9475
|
verifyNativeViewsSync,
|
|
8272
9476
|
view,
|