@stndrds/schema 0.1.0-alpha.17 → 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-2PBQNUTS.mjs +7400 -0
- package/dist/chunk-4D476S25.mjs +7713 -0
- package/dist/chunk-BKOV4XVX.mjs +8816 -0
- package/dist/chunk-CWPOGEK2.mjs +7707 -0
- package/dist/chunk-DPOXIK3M.mjs +8816 -0
- package/dist/chunk-DU2HLOQH.mjs +8813 -0
- package/dist/chunk-EWNSOD22.mjs +7707 -0
- package/dist/chunk-JD2MVLTW.mjs +8814 -0
- package/dist/chunk-KZSM5546.mjs +7706 -0
- package/dist/chunk-L7AQ7SZC.mjs +7432 -0
- package/dist/chunk-LZW2MI43.mjs +7404 -0
- package/dist/chunk-SD2UXOSF.mjs +8814 -0
- package/dist/chunk-V5QXU2OK.mjs +82 -0
- package/dist/chunk-XMGMXAHW.mjs +7706 -0
- package/dist/chunk-YLVZT3H4.mjs +8793 -0
- package/dist/default-roles-AKWIWTLE.mjs +20 -0
- package/dist/index.d.mts +127 -8
- package/dist/index.d.ts +127 -8
- package/dist/index.js +1787 -182
- package/dist/index.mjs +90 -3
- 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-DWhSpnn4.d.mts +5256 -0
- package/dist/runtime-DWhSpnn4.d.ts +5256 -0
- package/dist/runtime-Dw2exb5q.d.mts +5601 -0
- package/dist/runtime-Dw2exb5q.d.ts +5601 -0
- package/dist/runtime-DySuNV6Y.d.mts +5245 -0
- package/dist/runtime-DySuNV6Y.d.ts +5245 -0
- package/dist/runtime-DzdsFCyL.d.mts +5601 -0
- package/dist/runtime-DzdsFCyL.d.ts +5601 -0
- package/dist/runtime-uXhTgSIx.d.mts +5260 -0
- package/dist/runtime-uXhTgSIx.d.ts +5260 -0
- package/dist/runtime.d.mts +1 -1
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +1520 -59
- package/dist/runtime.mjs +59 -1
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -59,21 +59,25 @@ var init_default_roles = __esm({
|
|
|
59
59
|
/** Tenant settings and configuration */
|
|
60
60
|
SETTINGS: "settings",
|
|
61
61
|
/** Audit logs and activity trail */
|
|
62
|
-
AUDIT: "audit"
|
|
62
|
+
AUDIT: "audit",
|
|
63
|
+
/** File uploads and storage management */
|
|
64
|
+
FILES: "files"
|
|
63
65
|
};
|
|
64
66
|
ALL_SYSTEM_RESOURCES = [
|
|
65
67
|
SYSTEM_RESOURCES.USERS,
|
|
66
68
|
SYSTEM_RESOURCES.SCHEMA,
|
|
67
69
|
SYSTEM_RESOURCES.ROLES,
|
|
68
70
|
SYSTEM_RESOURCES.SETTINGS,
|
|
69
|
-
SYSTEM_RESOURCES.AUDIT
|
|
71
|
+
SYSTEM_RESOURCES.AUDIT,
|
|
72
|
+
SYSTEM_RESOURCES.FILES
|
|
70
73
|
];
|
|
71
74
|
SYSTEM_RESOURCE_LABELS = {
|
|
72
75
|
users: "Users",
|
|
73
76
|
schema: "Schema",
|
|
74
77
|
roles: "Roles & Permissions",
|
|
75
78
|
settings: "Settings",
|
|
76
|
-
audit: "Audit Logs"
|
|
79
|
+
audit: "Audit Logs",
|
|
80
|
+
files: "Files"
|
|
77
81
|
};
|
|
78
82
|
DEFAULT_ROLES = {
|
|
79
83
|
/** Full platform access - can manage everything */
|
|
@@ -140,6 +144,8 @@ __export(index_exports, {
|
|
|
140
144
|
GeocodingService: () => GeocodingService,
|
|
141
145
|
GlobalSearchService: () => GlobalSearchService,
|
|
142
146
|
GroupBuilder: () => GroupBuilder,
|
|
147
|
+
InvalidPathError: () => InvalidPathError,
|
|
148
|
+
MaxDepthExceededError: () => MaxDepthExceededError,
|
|
143
149
|
NO_VALUE_OPERATORS: () => NO_VALUE_OPERATORS,
|
|
144
150
|
NoopGeocodingAdapter: () => NoopGeocodingAdapter,
|
|
145
151
|
NoopHookRegistry: () => NoopHookRegistry,
|
|
@@ -156,8 +162,11 @@ __export(index_exports, {
|
|
|
156
162
|
RecordNotFoundError: () => RecordNotFoundError,
|
|
157
163
|
RecordReferencedError: () => RecordReferencedError,
|
|
158
164
|
RecordService: () => RecordService,
|
|
165
|
+
RelationResolverService: () => RelationResolverService,
|
|
159
166
|
RelationService: () => RelationService,
|
|
160
167
|
RoleNotFoundError: () => RoleNotFoundError,
|
|
168
|
+
RollupScheduler: () => RollupScheduler,
|
|
169
|
+
RollupService: () => RollupService,
|
|
161
170
|
SYSTEM_RESOURCES: () => SYSTEM_RESOURCES,
|
|
162
171
|
SYSTEM_RESOURCE_LABELS: () => SYSTEM_RESOURCE_LABELS,
|
|
163
172
|
SchemaError: () => SchemaError,
|
|
@@ -167,6 +176,7 @@ __export(index_exports, {
|
|
|
167
176
|
TableTabConfig: () => TableTabConfig,
|
|
168
177
|
UserProfileNotFoundError: () => UserProfileNotFoundError,
|
|
169
178
|
UserProfileService: () => UserProfileService,
|
|
179
|
+
UserService: () => UserService,
|
|
170
180
|
ValidationError: () => ValidationError,
|
|
171
181
|
ViewBuilder: () => ViewBuilder,
|
|
172
182
|
ViewService: () => ViewService,
|
|
@@ -181,6 +191,7 @@ __export(index_exports, {
|
|
|
181
191
|
createDateValidator: () => createDateValidator,
|
|
182
192
|
createDraftValidator: () => createDraftValidator,
|
|
183
193
|
createFileValidator: () => createFileValidator,
|
|
194
|
+
createFormulaValidator: () => createFormulaValidator,
|
|
184
195
|
createLocationValidator: () => createLocationValidator,
|
|
185
196
|
createMockAdapter: () => createMockAdapter,
|
|
186
197
|
createMultiRelationValidator: () => createMultiRelationValidator,
|
|
@@ -190,6 +201,8 @@ __export(index_exports, {
|
|
|
190
201
|
createPhoneValidator: () => createPhoneValidator,
|
|
191
202
|
createRatingValidator: () => createRatingValidator,
|
|
192
203
|
createRelationValidator: () => createRelationValidator,
|
|
204
|
+
createRollupHooks: () => createRollupHooks,
|
|
205
|
+
createRollupValidator: () => createRollupValidator,
|
|
193
206
|
createSelectValidator: () => createSelectValidator,
|
|
194
207
|
createSingleRelationValidator: () => createSingleRelationValidator,
|
|
195
208
|
createStatusValidator: () => createStatusValidator,
|
|
@@ -201,18 +214,34 @@ __export(index_exports, {
|
|
|
201
214
|
date: () => date,
|
|
202
215
|
dateConfigSchema: () => dateConfigSchema,
|
|
203
216
|
enrichValuesWithSelectLabels: () => enrichValuesWithSelectLabels,
|
|
217
|
+
evaluateFormula: () => evaluateFormula,
|
|
218
|
+
evaluateFormulaAttribute: () => evaluateFormulaAttribute,
|
|
219
|
+
evaluateFormulaAttributeWithRelations: () => evaluateFormulaAttributeWithRelations,
|
|
220
|
+
evaluateFormulaWithRelations: () => evaluateFormulaWithRelations,
|
|
221
|
+
evaluateFormulaWithResult: () => evaluateFormulaWithResult,
|
|
204
222
|
extractAttributeNames: () => extractAttributeNames,
|
|
223
|
+
extractFormulaVariables: () => extractFormulaVariables,
|
|
224
|
+
extractRelationNames: () => extractRelationNames,
|
|
225
|
+
extractRelationReferences: () => extractRelationReferences,
|
|
205
226
|
file: () => file,
|
|
206
227
|
fileConfigSchema: () => fileConfigSchema,
|
|
228
|
+
flattenRelationsForEval: () => flattenRelationsForEval,
|
|
207
229
|
flow: () => flow,
|
|
208
230
|
formatAttributeValue: () => formatAttributeValue,
|
|
231
|
+
formatFormulaResult: () => formatFormulaResult,
|
|
232
|
+
formula: () => formula,
|
|
233
|
+
formulaConfigSchema: () => formulaConfigSchema,
|
|
209
234
|
generateId: () => generateId,
|
|
210
235
|
generatePrefixedId: () => generatePrefixedId,
|
|
211
236
|
getAttributeConfigSchema: () => getAttributeConfigSchema,
|
|
212
237
|
getMissingRequiredAttributes: () => getMissingRequiredAttributes,
|
|
238
|
+
getPathDepth: () => getPathDepth,
|
|
239
|
+
getRelationPath: () => getRelationPath,
|
|
213
240
|
getSyncPreview: () => getSyncPreview,
|
|
241
|
+
getTargetAttributeName: () => getTargetAttributeName,
|
|
214
242
|
getViewSyncPreview: () => getViewSyncPreview,
|
|
215
243
|
group: () => group,
|
|
244
|
+
hasRelationReferences: () => hasRelationReferences,
|
|
216
245
|
isActivityTab: () => isActivityTab,
|
|
217
246
|
isAdvancedFilterState: () => isAdvancedFilterState,
|
|
218
247
|
isCustomTab: () => isCustomTab,
|
|
@@ -238,14 +267,21 @@ __export(index_exports, {
|
|
|
238
267
|
numberConfigSchema: () => numberConfigSchema,
|
|
239
268
|
object: () => object,
|
|
240
269
|
parseAttributeConfig: () => parseAttributeConfig,
|
|
270
|
+
parsePath: () => parsePath,
|
|
271
|
+
pathHasManyCardinality: () => pathHasManyCardinality,
|
|
241
272
|
phone: () => phone,
|
|
242
273
|
phoneConfigSchema: () => phoneConfigSchema,
|
|
243
274
|
rating: () => rating,
|
|
244
275
|
ratingConfigSchema: () => ratingConfigSchema,
|
|
276
|
+
registerAllRollupHooks: () => registerAllRollupHooks,
|
|
245
277
|
registry: () => registry,
|
|
246
278
|
relation: () => relation,
|
|
247
279
|
relationConfigSchema: () => relationConfigSchema,
|
|
248
280
|
renderLabelExpression: () => renderLabelExpression,
|
|
281
|
+
resolveMultiplePaths: () => resolveMultiplePaths,
|
|
282
|
+
resolveSingleValue: () => resolveSingleValue,
|
|
283
|
+
rollup: () => rollup,
|
|
284
|
+
rollupConfigSchema: () => rollupConfigSchema,
|
|
249
285
|
safeParseAttributeConfig: () => safeParseAttributeConfig,
|
|
250
286
|
select: () => select,
|
|
251
287
|
selectConfigSchema: () => selectConfigSchema,
|
|
@@ -262,14 +298,17 @@ __export(index_exports, {
|
|
|
262
298
|
timestampConfigSchema: () => timestampConfigSchema,
|
|
263
299
|
toAdvancedFilterState: () => toAdvancedFilterState,
|
|
264
300
|
toSimpleFilterState: () => toSimpleFilterState,
|
|
301
|
+
traversePath: () => traversePath,
|
|
265
302
|
user: () => user,
|
|
266
303
|
userConfigSchema: () => userConfigSchema,
|
|
267
304
|
validateAttribute: () => validateAttribute,
|
|
268
305
|
validateAttributeConfig: () => validateAttributeConfig,
|
|
269
306
|
validateDraft: () => validateDraft,
|
|
270
307
|
validateDraftOrThrow: () => validateDraftOrThrow,
|
|
308
|
+
validateFormulaExpression: () => validateFormulaExpression,
|
|
271
309
|
validateObject: () => validateObject,
|
|
272
310
|
validateObjectOrThrow: () => validateObjectOrThrow,
|
|
311
|
+
validatePath: () => validatePath,
|
|
273
312
|
verifyNativeObjectsSync: () => verifyNativeObjectsSync,
|
|
274
313
|
verifyNativeViewsSync: () => verifyNativeViewsSync,
|
|
275
314
|
view: () => view,
|
|
@@ -392,7 +431,24 @@ var OPERATORS_BY_TYPE = {
|
|
|
392
431
|
file: ["is_empty", "is_not_empty"],
|
|
393
432
|
user: ["contains", "not_contains", "is_empty", "is_not_empty"],
|
|
394
433
|
relation: ["any_of", "none_of", "contains", "not_contains", "is_empty", "is_not_empty"],
|
|
395
|
-
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"]
|
|
396
452
|
};
|
|
397
453
|
var NO_VALUE_OPERATORS = [
|
|
398
454
|
"is_empty",
|
|
@@ -469,6 +525,13 @@ function formatCheckbox(value) {
|
|
|
469
525
|
function formatNumber(value, attribute) {
|
|
470
526
|
if (typeof value !== "number") return String(value);
|
|
471
527
|
const decimals = attribute.decimals;
|
|
528
|
+
if (attribute.unit === "percentage") {
|
|
529
|
+
return value.toLocaleString(void 0, {
|
|
530
|
+
style: "percent",
|
|
531
|
+
minimumFractionDigits: decimals,
|
|
532
|
+
maximumFractionDigits: decimals
|
|
533
|
+
});
|
|
534
|
+
}
|
|
472
535
|
return value.toLocaleString(void 0, {
|
|
473
536
|
minimumFractionDigits: decimals,
|
|
474
537
|
maximumFractionDigits: decimals
|
|
@@ -640,6 +703,7 @@ var SchemaErrorCode = {
|
|
|
640
703
|
// Protected Resources
|
|
641
704
|
PROTECTED_OBJECT: "SCHEMA_PROTECTED_OBJECT",
|
|
642
705
|
PROTECTED_ATTRIBUTE: "SCHEMA_PROTECTED_ATTRIBUTE",
|
|
706
|
+
PROTECTED_VIEW: "SCHEMA_PROTECTED_VIEW",
|
|
643
707
|
PROTECTED_ROLE: "SCHEMA_PROTECTED_ROLE",
|
|
644
708
|
// Permissions
|
|
645
709
|
FORBIDDEN: "SCHEMA_FORBIDDEN",
|
|
@@ -728,7 +792,7 @@ var ValidationError = class _ValidationError extends SchemaError {
|
|
|
728
792
|
};
|
|
729
793
|
var ProtectedResourceError = class extends SchemaError {
|
|
730
794
|
constructor(resourceType, resourceName, operation) {
|
|
731
|
-
const code = resourceType === "object" ? SchemaErrorCode.PROTECTED_OBJECT : SchemaErrorCode.PROTECTED_ATTRIBUTE;
|
|
795
|
+
const code = resourceType === "object" ? SchemaErrorCode.PROTECTED_OBJECT : resourceType === "view" ? SchemaErrorCode.PROTECTED_VIEW : SchemaErrorCode.PROTECTED_ATTRIBUTE;
|
|
732
796
|
super(`Cannot ${operation} system ${resourceType} "${resourceName}"`, code, {
|
|
733
797
|
resourceType,
|
|
734
798
|
resourceName,
|
|
@@ -943,7 +1007,9 @@ var BaseAttributeBuilder = class {
|
|
|
943
1007
|
return this;
|
|
944
1008
|
}
|
|
945
1009
|
defaultValue(value) {
|
|
946
|
-
this.attr.
|
|
1010
|
+
if ("defaultValue" in this.attr || !["formula", "rollup"].includes(this.attr.type)) {
|
|
1011
|
+
this.attr.defaultValue = value;
|
|
1012
|
+
}
|
|
947
1013
|
return this;
|
|
948
1014
|
}
|
|
949
1015
|
metadata(value) {
|
|
@@ -1324,6 +1390,13 @@ var FileAttributeBuilder = class extends BaseAttributeBuilder {
|
|
|
1324
1390
|
this.attr.verification = config;
|
|
1325
1391
|
return this;
|
|
1326
1392
|
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Allow multiple files to be uploaded
|
|
1395
|
+
*/
|
|
1396
|
+
multiple() {
|
|
1397
|
+
this.attr.multiple = true;
|
|
1398
|
+
return this;
|
|
1399
|
+
}
|
|
1327
1400
|
required() {
|
|
1328
1401
|
this.setRequired(true);
|
|
1329
1402
|
return this;
|
|
@@ -1344,6 +1417,13 @@ var UserAttributeBuilder = class extends BaseAttributeBuilder {
|
|
|
1344
1417
|
this.attr.allowedRoles = roles;
|
|
1345
1418
|
return this;
|
|
1346
1419
|
}
|
|
1420
|
+
/**
|
|
1421
|
+
* Allow multiple users to be selected
|
|
1422
|
+
*/
|
|
1423
|
+
multiple() {
|
|
1424
|
+
this.attr.multiple = true;
|
|
1425
|
+
return this;
|
|
1426
|
+
}
|
|
1347
1427
|
required() {
|
|
1348
1428
|
this.setRequired(true);
|
|
1349
1429
|
return this;
|
|
@@ -1471,6 +1551,105 @@ var RatingAttributeBuilder = class extends BaseAttributeBuilder {
|
|
|
1471
1551
|
function rating(config) {
|
|
1472
1552
|
return new RatingAttributeBuilder(config.name, config.label);
|
|
1473
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
|
+
}
|
|
1474
1653
|
|
|
1475
1654
|
// src/builders/flow-builder.ts
|
|
1476
1655
|
var import_zod = require("zod");
|
|
@@ -2750,10 +2929,12 @@ var fileConfigSchema = baseConfigSchema.extend({
|
|
|
2750
2929
|
maxFiles: import_zod4.z.number().int().min(1).optional(),
|
|
2751
2930
|
maxSize: import_zod4.z.number().int().min(1).optional(),
|
|
2752
2931
|
allowedTypes: import_zod4.z.array(import_zod4.z.string()).optional(),
|
|
2753
|
-
verification: fileVerificationConfigSchema.optional()
|
|
2932
|
+
verification: fileVerificationConfigSchema.optional(),
|
|
2933
|
+
multiple: import_zod4.z.boolean().optional()
|
|
2754
2934
|
});
|
|
2755
2935
|
var userConfigSchema = baseConfigSchema.extend({
|
|
2756
|
-
allowedRoles: import_zod4.z.array(import_zod4.z.string()).optional()
|
|
2936
|
+
allowedRoles: import_zod4.z.array(import_zod4.z.string()).optional(),
|
|
2937
|
+
multiple: import_zod4.z.boolean().optional()
|
|
2757
2938
|
});
|
|
2758
2939
|
var relationConfigSchema = baseConfigSchema.extend({
|
|
2759
2940
|
targets: import_zod4.z.array(relationTargetSchema).min(1),
|
|
@@ -2765,6 +2946,30 @@ var ratingConfigSchema = baseConfigSchema.extend({
|
|
|
2765
2946
|
max: import_zod4.z.number().int().min(1).optional(),
|
|
2766
2947
|
iconType: import_zod4.z.enum(["star", "heart", "thumbs", "number"]).optional()
|
|
2767
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
|
+
});
|
|
2768
2973
|
var attributeConfigSchemas = {
|
|
2769
2974
|
text: textConfigSchema,
|
|
2770
2975
|
textarea: textareaConfigSchema,
|
|
@@ -2781,7 +2986,9 @@ var attributeConfigSchemas = {
|
|
|
2781
2986
|
file: fileConfigSchema,
|
|
2782
2987
|
user: userConfigSchema,
|
|
2783
2988
|
relation: relationConfigSchema,
|
|
2784
|
-
rating: ratingConfigSchema
|
|
2989
|
+
rating: ratingConfigSchema,
|
|
2990
|
+
formula: formulaConfigSchema,
|
|
2991
|
+
rollup: rollupConfigSchema
|
|
2785
2992
|
};
|
|
2786
2993
|
function getAttributeConfigSchema(type) {
|
|
2787
2994
|
return attributeConfigSchemas[type];
|
|
@@ -2891,20 +3098,39 @@ function createLocationValidator(_attr) {
|
|
|
2891
3098
|
function createTimestampValidator(_attr) {
|
|
2892
3099
|
return import_zod4.z.number().int().positive();
|
|
2893
3100
|
}
|
|
2894
|
-
function createFileValidator(
|
|
2895
|
-
|
|
3101
|
+
function createFileValidator(attr) {
|
|
3102
|
+
const uuidSchema = import_zod4.z.uuid({
|
|
3103
|
+
message: `${attr.label} must be a valid file ID`
|
|
3104
|
+
});
|
|
3105
|
+
if (attr.multiple) {
|
|
3106
|
+
let arraySchema = import_zod4.z.array(uuidSchema);
|
|
3107
|
+
if (attr.maxFiles) {
|
|
3108
|
+
arraySchema = arraySchema.max(
|
|
3109
|
+
attr.maxFiles,
|
|
3110
|
+
`${attr.label} cannot have more than ${attr.maxFiles} file${attr.maxFiles > 1 ? "s" : ""}`
|
|
3111
|
+
);
|
|
3112
|
+
}
|
|
3113
|
+
return arraySchema;
|
|
3114
|
+
}
|
|
3115
|
+
return uuidSchema;
|
|
2896
3116
|
}
|
|
2897
|
-
function createUserValidator(
|
|
2898
|
-
|
|
3117
|
+
function createUserValidator(attr) {
|
|
3118
|
+
const uuidSchema = import_zod4.z.uuid({
|
|
3119
|
+
message: `${attr.label} must be a valid user ID`
|
|
3120
|
+
});
|
|
3121
|
+
if (attr.multiple) {
|
|
3122
|
+
return import_zod4.z.array(uuidSchema);
|
|
3123
|
+
}
|
|
3124
|
+
return uuidSchema;
|
|
2899
3125
|
}
|
|
2900
3126
|
function createSingleRelationValidator(attr) {
|
|
2901
|
-
const uuidSchema = import_zod4.z.
|
|
3127
|
+
const uuidSchema = import_zod4.z.uuid({
|
|
2902
3128
|
message: `${attr.label} must be a valid record ID`
|
|
2903
3129
|
});
|
|
2904
3130
|
return import_zod4.z.union([uuidSchema, import_zod4.z.null()]);
|
|
2905
3131
|
}
|
|
2906
3132
|
function createMultiRelationValidator(attr) {
|
|
2907
|
-
const uuidSchema = import_zod4.z.
|
|
3133
|
+
const uuidSchema = import_zod4.z.uuid({
|
|
2908
3134
|
message: `Each ${attr.label} item must be a valid record ID`
|
|
2909
3135
|
});
|
|
2910
3136
|
let arraySchema = import_zod4.z.array(uuidSchema);
|
|
@@ -2935,6 +3161,12 @@ function createRatingValidator(attr) {
|
|
|
2935
3161
|
}
|
|
2936
3162
|
return schema;
|
|
2937
3163
|
}
|
|
3164
|
+
function createFormulaValidator(_attr) {
|
|
3165
|
+
return import_zod4.z.unknown();
|
|
3166
|
+
}
|
|
3167
|
+
function createRollupValidator(_attr) {
|
|
3168
|
+
return import_zod4.z.unknown();
|
|
3169
|
+
}
|
|
2938
3170
|
function createAttributeValidator(attr) {
|
|
2939
3171
|
switch (attr.type) {
|
|
2940
3172
|
case "text":
|
|
@@ -2967,6 +3199,10 @@ function createAttributeValidator(attr) {
|
|
|
2967
3199
|
return createRelationValidator(attr);
|
|
2968
3200
|
case "rating":
|
|
2969
3201
|
return createRatingValidator(attr);
|
|
3202
|
+
case "formula":
|
|
3203
|
+
return createFormulaValidator(attr);
|
|
3204
|
+
case "rollup":
|
|
3205
|
+
return createRollupValidator(attr);
|
|
2970
3206
|
default:
|
|
2971
3207
|
return import_zod4.z.unknown();
|
|
2972
3208
|
}
|
|
@@ -3092,139 +3328,771 @@ function computeRecordStatus(objectDef, data) {
|
|
|
3092
3328
|
return isRecordComplete(objectDef, data) ? "complete" : "draft";
|
|
3093
3329
|
}
|
|
3094
3330
|
|
|
3095
|
-
// src/runtime/
|
|
3096
|
-
var
|
|
3097
|
-
|
|
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;
|
|
3098
3423
|
}
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
// src/runtime/template.ts
|
|
3102
|
-
var pipes = {
|
|
3103
|
-
/** Convert to uppercase */
|
|
3104
|
-
UPPER: (v) => String(v).toUpperCase(),
|
|
3105
|
-
/** Convert to lowercase */
|
|
3106
|
-
LOWER: (v) => String(v).toLowerCase(),
|
|
3107
|
-
/** Capitalize first letter of each word */
|
|
3108
|
-
capitalize: (v) => String(v).replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
3109
|
-
/** Trim whitespace from both ends */
|
|
3110
|
-
trim: (v) => String(v).trim()
|
|
3111
|
-
};
|
|
3112
|
-
function getValue(obj, path) {
|
|
3113
|
-
return path.split(".").reduce((acc, key) => {
|
|
3114
|
-
if (acc == null || typeof acc !== "object") return void 0;
|
|
3115
|
-
return acc[key];
|
|
3116
|
-
}, obj);
|
|
3424
|
+
return null;
|
|
3117
3425
|
}
|
|
3118
|
-
var
|
|
3119
|
-
function
|
|
3120
|
-
|
|
3121
|
-
const
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
const pipeName = parts[i].trim();
|
|
3127
|
-
const fn = pipes[pipeName];
|
|
3128
|
-
if (fn) {
|
|
3129
|
-
value = fn(String(value));
|
|
3130
|
-
}
|
|
3131
|
-
}
|
|
3132
|
-
return String(value);
|
|
3133
|
-
}).trim();
|
|
3134
|
-
return result || fallback;
|
|
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
|
+
}
|
|
3135
3434
|
}
|
|
3136
|
-
function
|
|
3137
|
-
|
|
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
|
+
}
|
|
3138
3446
|
}
|
|
3139
|
-
function
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
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);
|
|
3149
3460
|
}
|
|
3150
|
-
return names;
|
|
3151
3461
|
}
|
|
3152
|
-
function
|
|
3153
|
-
|
|
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;
|
|
3154
3466
|
}
|
|
3155
|
-
function
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
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;
|
|
3166
3515
|
}
|
|
3167
3516
|
}
|
|
3168
|
-
return
|
|
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);
|
|
3169
3535
|
}
|
|
3170
3536
|
|
|
3171
|
-
// src/runtime/
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
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");
|
|
3182
3581
|
}
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
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
|
+
);
|
|
3189
3596
|
}
|
|
3597
|
+
currentSchema = nextSchema;
|
|
3190
3598
|
}
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
label: data.label,
|
|
3199
|
-
pluralLabel: data.pluralLabel,
|
|
3200
|
-
description: data.description,
|
|
3201
|
-
icon: data.icon,
|
|
3202
|
-
labelExpression: data.labelExpression,
|
|
3203
|
-
system: data.system ?? false,
|
|
3204
|
-
metadata: data.metadata,
|
|
3205
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
3206
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
3207
|
-
};
|
|
3208
|
-
stores.objects.set(obj.id, obj);
|
|
3209
|
-
return Promise.resolve(obj);
|
|
3210
|
-
},
|
|
3211
|
-
update(id, data) {
|
|
3212
|
-
const existing = stores.objects.get(id);
|
|
3213
|
-
if (!existing) {
|
|
3214
|
-
return Promise.reject(new Error(`Object ${id} not found`));
|
|
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
|
+
);
|
|
3215
3606
|
}
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
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
|
+
|
|
3709
|
+
// src/runtime/hooks/hook.registry.ts
|
|
3710
|
+
var NoopHookRegistry = class {
|
|
3711
|
+
async execute() {
|
|
3712
|
+
}
|
|
3713
|
+
};
|
|
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
|
+
|
|
3969
|
+
// src/runtime/template.ts
|
|
3970
|
+
var pipes = {
|
|
3971
|
+
/** Convert to uppercase */
|
|
3972
|
+
UPPER: (v) => String(v).toUpperCase(),
|
|
3973
|
+
/** Convert to lowercase */
|
|
3974
|
+
LOWER: (v) => String(v).toLowerCase(),
|
|
3975
|
+
/** Capitalize first letter of each word */
|
|
3976
|
+
capitalize: (v) => String(v).replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
3977
|
+
/** Trim whitespace from both ends */
|
|
3978
|
+
trim: (v) => String(v).trim()
|
|
3979
|
+
};
|
|
3980
|
+
function getValue(obj, path) {
|
|
3981
|
+
return path.split(".").reduce((acc, key) => {
|
|
3982
|
+
if (acc == null || typeof acc !== "object") return void 0;
|
|
3983
|
+
return acc[key];
|
|
3984
|
+
}, obj);
|
|
3985
|
+
}
|
|
3986
|
+
var DEFAULT_LABEL_FALLBACK = "(Untitled)";
|
|
3987
|
+
function renderLabelExpression(template, values, fallback = DEFAULT_LABEL_FALLBACK) {
|
|
3988
|
+
const result = template.replace(/\{\{\s*([^}]+)\s*\}\}/g, (_, expr) => {
|
|
3989
|
+
const parts = expr.split("|").map((s) => s.trim());
|
|
3990
|
+
const path = parts[0];
|
|
3991
|
+
let value = getValue(values, path);
|
|
3992
|
+
if (value == null || value === "") return "";
|
|
3993
|
+
for (let i = 1; i < parts.length; i++) {
|
|
3994
|
+
const pipeName = parts[i].trim();
|
|
3995
|
+
const fn = pipes[pipeName];
|
|
3996
|
+
if (fn) {
|
|
3997
|
+
value = fn(String(value));
|
|
3998
|
+
}
|
|
3999
|
+
}
|
|
4000
|
+
return String(value);
|
|
4001
|
+
}).trim();
|
|
4002
|
+
return result || fallback;
|
|
4003
|
+
}
|
|
4004
|
+
function isLabelExpression(value) {
|
|
4005
|
+
return /\{\{\s*\S+.*\}\}/.test(value);
|
|
4006
|
+
}
|
|
4007
|
+
function extractAttributeNames(template) {
|
|
4008
|
+
const names = [];
|
|
4009
|
+
const regex = /\{\{\s*([^|}]+)/g;
|
|
4010
|
+
let match;
|
|
4011
|
+
while ((match = regex.exec(template)) !== null) {
|
|
4012
|
+
const path = match[1].trim();
|
|
4013
|
+
const rootName = path.split(".")[0];
|
|
4014
|
+
if (rootName && !names.includes(rootName)) {
|
|
4015
|
+
names.push(rootName);
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
4018
|
+
return names;
|
|
4019
|
+
}
|
|
4020
|
+
function hasOptions(attr) {
|
|
4021
|
+
return "options" in attr && Array.isArray(attr.options) && attr.options.length > 0;
|
|
4022
|
+
}
|
|
4023
|
+
function enrichValuesWithSelectLabels(values, attributes) {
|
|
4024
|
+
const enriched = { ...values };
|
|
4025
|
+
for (const attr of attributes) {
|
|
4026
|
+
const value = values[attr.name];
|
|
4027
|
+
if (value == null) continue;
|
|
4028
|
+
const isSelectLike = attr.type === "select" || attr.type === "status" || attr.type === "multiselect";
|
|
4029
|
+
if (!(isSelectLike && hasOptions(attr))) continue;
|
|
4030
|
+
if (attr.type === "multiselect" && Array.isArray(value) && value.length === 0) continue;
|
|
4031
|
+
const formatted = formatAttributeValue(value, attr);
|
|
4032
|
+
if (formatted && formatted !== EMPTY_VALUE_PLACEHOLDER) {
|
|
4033
|
+
enriched[attr.name] = formatted;
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
4036
|
+
return enriched;
|
|
4037
|
+
}
|
|
4038
|
+
|
|
4039
|
+
// src/runtime/mock-adapter.ts
|
|
4040
|
+
function createMockObjectsRepository(stores) {
|
|
4041
|
+
return {
|
|
4042
|
+
findById(id) {
|
|
4043
|
+
return Promise.resolve(stores.objects.get(id) ?? null);
|
|
4044
|
+
},
|
|
4045
|
+
findByName(tenantId, name) {
|
|
4046
|
+
for (const obj of stores.objects.values()) {
|
|
4047
|
+
if (obj.tenantId === tenantId && obj.name === name) {
|
|
4048
|
+
return Promise.resolve(obj);
|
|
4049
|
+
}
|
|
4050
|
+
}
|
|
4051
|
+
return Promise.resolve(null);
|
|
4052
|
+
},
|
|
4053
|
+
findSystemByName(name) {
|
|
4054
|
+
for (const obj of stores.objects.values()) {
|
|
4055
|
+
if (obj.system && obj.name === name) {
|
|
4056
|
+
return Promise.resolve(obj);
|
|
4057
|
+
}
|
|
4058
|
+
}
|
|
4059
|
+
return Promise.resolve(null);
|
|
4060
|
+
},
|
|
4061
|
+
create(data) {
|
|
4062
|
+
const obj = {
|
|
4063
|
+
id: generateId(),
|
|
4064
|
+
tenantId: data.tenantId,
|
|
4065
|
+
name: data.name,
|
|
4066
|
+
label: data.label,
|
|
4067
|
+
pluralLabel: data.pluralLabel,
|
|
4068
|
+
description: data.description,
|
|
4069
|
+
icon: data.icon,
|
|
4070
|
+
labelExpression: data.labelExpression,
|
|
4071
|
+
system: data.system ?? false,
|
|
4072
|
+
metadata: data.metadata,
|
|
4073
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
4074
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
4075
|
+
};
|
|
4076
|
+
stores.objects.set(obj.id, obj);
|
|
4077
|
+
return Promise.resolve(obj);
|
|
4078
|
+
},
|
|
4079
|
+
update(id, data) {
|
|
4080
|
+
const existing = stores.objects.get(id);
|
|
4081
|
+
if (!existing) {
|
|
4082
|
+
return Promise.reject(new Error(`Object ${id} not found`));
|
|
4083
|
+
}
|
|
4084
|
+
const updated = {
|
|
4085
|
+
...existing,
|
|
4086
|
+
...data,
|
|
4087
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
4088
|
+
};
|
|
4089
|
+
stores.objects.set(id, updated);
|
|
4090
|
+
return Promise.resolve(updated);
|
|
4091
|
+
},
|
|
4092
|
+
delete(id) {
|
|
4093
|
+
stores.objects.delete(id);
|
|
4094
|
+
return Promise.resolve();
|
|
4095
|
+
},
|
|
3228
4096
|
list(tenantId) {
|
|
3229
4097
|
const results = Array.from(stores.objects.values()).filter(
|
|
3230
4098
|
(obj) => obj.tenantId === tenantId
|
|
@@ -3755,6 +4623,26 @@ function createMockObjectRecordsRepository(stores) {
|
|
|
3755
4623
|
}
|
|
3756
4624
|
}
|
|
3757
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);
|
|
3758
4646
|
}
|
|
3759
4647
|
};
|
|
3760
4648
|
}
|
|
@@ -4174,6 +5062,24 @@ var AuditService = class {
|
|
|
4174
5062
|
};
|
|
4175
5063
|
await this.log(entry);
|
|
4176
5064
|
}
|
|
5065
|
+
/**
|
|
5066
|
+
* Log a file action (upload, update, delete)
|
|
5067
|
+
*/
|
|
5068
|
+
async logFileAction(params) {
|
|
5069
|
+
const entry = {
|
|
5070
|
+
tenantId: this.tenantId,
|
|
5071
|
+
actorId: params.actorId,
|
|
5072
|
+
actorEmail: params.actorEmail,
|
|
5073
|
+
actorType: "user",
|
|
5074
|
+
action: params.action,
|
|
5075
|
+
resourceType: "file",
|
|
5076
|
+
resourceId: params.fileId,
|
|
5077
|
+
resourceLabel: params.fileName,
|
|
5078
|
+
changes: params.changes ? this.redactSensitiveFields(params.changes) : void 0,
|
|
5079
|
+
metadata: params.metadata
|
|
5080
|
+
};
|
|
5081
|
+
await this.log(entry);
|
|
5082
|
+
}
|
|
4177
5083
|
/**
|
|
4178
5084
|
* Log a system action (no actor)
|
|
4179
5085
|
*/
|
|
@@ -4323,21 +5229,105 @@ var AuditService = class {
|
|
|
4323
5229
|
|
|
4324
5230
|
// src/runtime/services/file.service.ts
|
|
4325
5231
|
var FileService = class {
|
|
4326
|
-
constructor(adapter, tenantId) {
|
|
5232
|
+
constructor(adapter, tenantId, options) {
|
|
4327
5233
|
this.adapter = adapter;
|
|
4328
5234
|
this.tenantId = tenantId;
|
|
5235
|
+
this.auditService = options?.auditService ?? (adapter.audit ? new AuditService(adapter, tenantId) : void 0);
|
|
5236
|
+
this.userId = options?.userId;
|
|
5237
|
+
this.userEmail = options?.userEmail;
|
|
4329
5238
|
}
|
|
5239
|
+
// ============================================================================
|
|
5240
|
+
// UPLOAD (requires StorageAdapter)
|
|
5241
|
+
// ============================================================================
|
|
4330
5242
|
/**
|
|
4331
|
-
*
|
|
5243
|
+
* Upload a file to storage and create metadata record.
|
|
4332
5244
|
*
|
|
4333
|
-
*
|
|
5245
|
+
* This method orchestrates:
|
|
5246
|
+
* 1. Upload to storage (via StorageAdapter)
|
|
5247
|
+
* 2. Create file metadata in database
|
|
5248
|
+
* 3. Audit log the operation
|
|
5249
|
+
*
|
|
5250
|
+
* Requires `adapter.storage` to be configured.
|
|
5251
|
+
*
|
|
5252
|
+
* @param input - File content and metadata
|
|
4334
5253
|
* @returns Created file record
|
|
5254
|
+
* @throws Error if StorageAdapter is not configured
|
|
4335
5255
|
*
|
|
4336
5256
|
* @example
|
|
4337
5257
|
* ```typescript
|
|
4338
|
-
* const
|
|
5258
|
+
* const file = await service.uploadFile({
|
|
5259
|
+
* content: fileBuffer,
|
|
5260
|
+
* fileName: "document.pdf",
|
|
5261
|
+
* mimeType: "application/pdf",
|
|
5262
|
+
* size: 12345,
|
|
5263
|
+
* uploadedBy: "user-123",
|
|
5264
|
+
* visibility: "private",
|
|
5265
|
+
* folderPath: "/documents",
|
|
5266
|
+
* tags: ["contract", "2025"],
|
|
5267
|
+
* });
|
|
5268
|
+
* ```
|
|
5269
|
+
*/
|
|
5270
|
+
async uploadFile(input) {
|
|
5271
|
+
if (!this.adapter.storage) {
|
|
5272
|
+
throw new Error(
|
|
5273
|
+
"StorageAdapter is not configured. Provide adapter.storage to use uploadFile()."
|
|
5274
|
+
);
|
|
5275
|
+
}
|
|
5276
|
+
const uploadResult = await this.adapter.storage.upload({
|
|
5277
|
+
content: input.content,
|
|
5278
|
+
fileName: input.fileName,
|
|
5279
|
+
mimeType: input.mimeType,
|
|
5280
|
+
size: input.size,
|
|
5281
|
+
tenantId: this.tenantId,
|
|
5282
|
+
folderPath: input.folderPath
|
|
5283
|
+
});
|
|
5284
|
+
const file2 = await this.adapter.files.create({
|
|
5285
|
+
tenantId: this.tenantId,
|
|
5286
|
+
name: input.fileName,
|
|
5287
|
+
originalName: input.fileName,
|
|
5288
|
+
mimeType: input.mimeType,
|
|
5289
|
+
size: input.size,
|
|
5290
|
+
storageProvider: uploadResult.storageProvider,
|
|
5291
|
+
storagePath: uploadResult.storagePath,
|
|
5292
|
+
storageBucket: uploadResult.storageBucket,
|
|
5293
|
+
url: uploadResult.url,
|
|
5294
|
+
uploadedBy: input.uploadedBy,
|
|
5295
|
+
folderPath: input.folderPath,
|
|
5296
|
+
tags: input.tags,
|
|
5297
|
+
visibility: input.visibility ?? "private",
|
|
5298
|
+
allowedUsers: input.allowedUsers
|
|
5299
|
+
});
|
|
5300
|
+
if (this.auditService && this.userId) {
|
|
5301
|
+
await this.auditService.logFileAction({
|
|
5302
|
+
action: "file.uploaded",
|
|
5303
|
+
actorId: this.userId,
|
|
5304
|
+
actorEmail: this.userEmail,
|
|
5305
|
+
fileId: file2.id,
|
|
5306
|
+
fileName: file2.name,
|
|
5307
|
+
metadata: {
|
|
5308
|
+
mimeType: file2.mimeType,
|
|
5309
|
+
size: file2.size,
|
|
5310
|
+
visibility: file2.visibility
|
|
5311
|
+
}
|
|
5312
|
+
});
|
|
5313
|
+
}
|
|
5314
|
+
return file2;
|
|
5315
|
+
}
|
|
5316
|
+
// ============================================================================
|
|
5317
|
+
// CREATE (metadata only, for external storage)
|
|
5318
|
+
// ============================================================================
|
|
5319
|
+
/**
|
|
5320
|
+
* Create a new file record (after upload to storage).
|
|
5321
|
+
*
|
|
5322
|
+
* Use this method when handling storage externally (e.g., with Multer + S3).
|
|
5323
|
+
* For integrated upload, use `uploadFile()` instead.
|
|
4339
5324
|
*
|
|
4340
|
-
*
|
|
5325
|
+
* @param data - File metadata
|
|
5326
|
+
* @returns Created file record
|
|
5327
|
+
*
|
|
5328
|
+
* @example
|
|
5329
|
+
* ```typescript
|
|
5330
|
+
* // After uploading to S3 with Multer
|
|
4341
5331
|
* const file = await service.createFile({
|
|
4342
5332
|
* tenantId: "tenant-123",
|
|
4343
5333
|
* name: "contract-2025.pdf",
|
|
@@ -4359,8 +5349,26 @@ var FileService = class {
|
|
|
4359
5349
|
`Tenant mismatch: service initialized with "${this.tenantId}" but data has "${data.tenantId}"`
|
|
4360
5350
|
);
|
|
4361
5351
|
}
|
|
4362
|
-
|
|
5352
|
+
const file2 = await this.adapter.files.create(data);
|
|
5353
|
+
if (this.auditService && this.userId) {
|
|
5354
|
+
await this.auditService.logFileAction({
|
|
5355
|
+
action: "file.uploaded",
|
|
5356
|
+
actorId: this.userId,
|
|
5357
|
+
actorEmail: this.userEmail,
|
|
5358
|
+
fileId: file2.id,
|
|
5359
|
+
fileName: file2.name,
|
|
5360
|
+
metadata: {
|
|
5361
|
+
mimeType: file2.mimeType,
|
|
5362
|
+
size: file2.size,
|
|
5363
|
+
visibility: file2.visibility
|
|
5364
|
+
}
|
|
5365
|
+
});
|
|
5366
|
+
}
|
|
5367
|
+
return file2;
|
|
4363
5368
|
}
|
|
5369
|
+
// ============================================================================
|
|
5370
|
+
// READ
|
|
5371
|
+
// ============================================================================
|
|
4364
5372
|
/**
|
|
4365
5373
|
* Get file by ID
|
|
4366
5374
|
*/
|
|
@@ -4381,6 +5389,9 @@ var FileService = class {
|
|
|
4381
5389
|
}
|
|
4382
5390
|
return file2;
|
|
4383
5391
|
}
|
|
5392
|
+
// ============================================================================
|
|
5393
|
+
// UPDATE
|
|
5394
|
+
// ============================================================================
|
|
4384
5395
|
/**
|
|
4385
5396
|
* Update file metadata
|
|
4386
5397
|
*
|
|
@@ -4389,18 +5400,44 @@ var FileService = class {
|
|
|
4389
5400
|
* @returns Updated file
|
|
4390
5401
|
*/
|
|
4391
5402
|
async updateFile(fileId, data) {
|
|
4392
|
-
await this.getFileOrThrow(fileId);
|
|
4393
|
-
|
|
5403
|
+
const existingFile = await this.getFileOrThrow(fileId);
|
|
5404
|
+
const updatedFile = await this.adapter.files.update(fileId, data);
|
|
5405
|
+
if (this.auditService && this.userId) {
|
|
5406
|
+
const changes = [];
|
|
5407
|
+
for (const key of Object.keys(data)) {
|
|
5408
|
+
if (data[key] !== void 0 && data[key] !== existingFile[key]) {
|
|
5409
|
+
changes.push({
|
|
5410
|
+
field: key,
|
|
5411
|
+
oldValue: existingFile[key],
|
|
5412
|
+
newValue: data[key]
|
|
5413
|
+
});
|
|
5414
|
+
}
|
|
5415
|
+
}
|
|
5416
|
+
if (changes.length > 0) {
|
|
5417
|
+
await this.auditService.logFileAction({
|
|
5418
|
+
action: "file.updated",
|
|
5419
|
+
actorId: this.userId,
|
|
5420
|
+
actorEmail: this.userEmail,
|
|
5421
|
+
fileId,
|
|
5422
|
+
fileName: updatedFile.name,
|
|
5423
|
+
changes
|
|
5424
|
+
});
|
|
5425
|
+
}
|
|
5426
|
+
}
|
|
5427
|
+
return updatedFile;
|
|
4394
5428
|
}
|
|
5429
|
+
// ============================================================================
|
|
5430
|
+
// DELETE
|
|
5431
|
+
// ============================================================================
|
|
4395
5432
|
/**
|
|
4396
|
-
* Delete file (soft delete)
|
|
5433
|
+
* Delete file (soft delete by default)
|
|
4397
5434
|
*
|
|
4398
5435
|
* @param fileId - File UUID
|
|
4399
5436
|
* @param options - Delete options
|
|
4400
5437
|
*/
|
|
4401
5438
|
async deleteFile(fileId, options) {
|
|
5439
|
+
const file2 = await this.getFileOrThrow(fileId);
|
|
4402
5440
|
if (options?.checkOwnership && options.userId) {
|
|
4403
|
-
const file2 = await this.getFileOrThrow(fileId);
|
|
4404
5441
|
if (file2.uploadedBy !== options.userId) {
|
|
4405
5442
|
throw new Error("You can only delete files you uploaded");
|
|
4406
5443
|
}
|
|
@@ -4410,7 +5447,84 @@ var FileService = class {
|
|
|
4410
5447
|
} else {
|
|
4411
5448
|
await this.adapter.files.delete(fileId);
|
|
4412
5449
|
}
|
|
5450
|
+
if (this.auditService && this.userId) {
|
|
5451
|
+
await this.auditService.logFileAction({
|
|
5452
|
+
action: "file.deleted",
|
|
5453
|
+
actorId: this.userId,
|
|
5454
|
+
actorEmail: this.userEmail,
|
|
5455
|
+
fileId,
|
|
5456
|
+
fileName: file2.name
|
|
5457
|
+
});
|
|
5458
|
+
}
|
|
4413
5459
|
}
|
|
5460
|
+
/**
|
|
5461
|
+
* Delete file from both storage and database.
|
|
5462
|
+
*
|
|
5463
|
+
* Requires `adapter.storage` to be configured.
|
|
5464
|
+
*
|
|
5465
|
+
* @param fileId - File UUID
|
|
5466
|
+
* @param options - Delete options
|
|
5467
|
+
* @throws Error if StorageAdapter is not configured
|
|
5468
|
+
*/
|
|
5469
|
+
async deleteFileWithStorage(fileId, options) {
|
|
5470
|
+
if (!this.adapter.storage) {
|
|
5471
|
+
throw new Error(
|
|
5472
|
+
"StorageAdapter is not configured. Provide adapter.storage to use deleteFileWithStorage()."
|
|
5473
|
+
);
|
|
5474
|
+
}
|
|
5475
|
+
const file2 = await this.getFileOrThrow(fileId);
|
|
5476
|
+
await this.adapter.storage.delete(file2.storagePath);
|
|
5477
|
+
if (options?.hard) {
|
|
5478
|
+
await this.adapter.files.hardDelete(fileId);
|
|
5479
|
+
} else {
|
|
5480
|
+
await this.adapter.files.delete(fileId);
|
|
5481
|
+
}
|
|
5482
|
+
if (this.auditService && this.userId) {
|
|
5483
|
+
await this.auditService.logFileAction({
|
|
5484
|
+
action: "file.deleted",
|
|
5485
|
+
actorId: this.userId,
|
|
5486
|
+
actorEmail: this.userEmail,
|
|
5487
|
+
fileId,
|
|
5488
|
+
fileName: file2.name,
|
|
5489
|
+
metadata: { deletedFromStorage: true }
|
|
5490
|
+
});
|
|
5491
|
+
}
|
|
5492
|
+
}
|
|
5493
|
+
/**
|
|
5494
|
+
* Delete multiple files
|
|
5495
|
+
*
|
|
5496
|
+
* @param fileIds - Array of file UUIDs
|
|
5497
|
+
* @param options - Delete options
|
|
5498
|
+
*/
|
|
5499
|
+
async bulkDelete(fileIds, options) {
|
|
5500
|
+
for (const fileId of fileIds) {
|
|
5501
|
+
const file2 = await this.getFile(fileId);
|
|
5502
|
+
if (!file2) {
|
|
5503
|
+
continue;
|
|
5504
|
+
}
|
|
5505
|
+
if (options?.deleteFromStorage && this.adapter.storage) {
|
|
5506
|
+
await this.adapter.storage.delete(file2.storagePath);
|
|
5507
|
+
}
|
|
5508
|
+
if (options?.hard) {
|
|
5509
|
+
await this.adapter.files.hardDelete(fileId);
|
|
5510
|
+
} else {
|
|
5511
|
+
await this.adapter.files.delete(fileId);
|
|
5512
|
+
}
|
|
5513
|
+
if (this.auditService && this.userId) {
|
|
5514
|
+
await this.auditService.logFileAction({
|
|
5515
|
+
action: "file.deleted",
|
|
5516
|
+
actorId: this.userId,
|
|
5517
|
+
actorEmail: this.userEmail,
|
|
5518
|
+
fileId,
|
|
5519
|
+
fileName: file2.name,
|
|
5520
|
+
metadata: { deletedFromStorage: options?.deleteFromStorage ?? false }
|
|
5521
|
+
});
|
|
5522
|
+
}
|
|
5523
|
+
}
|
|
5524
|
+
}
|
|
5525
|
+
// ============================================================================
|
|
5526
|
+
// LIST
|
|
5527
|
+
// ============================================================================
|
|
4414
5528
|
/**
|
|
4415
5529
|
* List files for the tenant
|
|
4416
5530
|
*/
|
|
@@ -4418,16 +5532,84 @@ var FileService = class {
|
|
|
4418
5532
|
return await this.adapter.files.list(this.tenantId, options);
|
|
4419
5533
|
}
|
|
4420
5534
|
/**
|
|
4421
|
-
* List files by folder
|
|
5535
|
+
* List files by folder
|
|
5536
|
+
*/
|
|
5537
|
+
async listFilesByFolder(folderPath) {
|
|
5538
|
+
return await this.adapter.files.findByFolder(this.tenantId, folderPath);
|
|
5539
|
+
}
|
|
5540
|
+
/**
|
|
5541
|
+
* List files uploaded by a specific user
|
|
5542
|
+
*/
|
|
5543
|
+
async listFilesByUploader(uploadedBy) {
|
|
5544
|
+
return await this.adapter.files.findByUploader(uploadedBy);
|
|
5545
|
+
}
|
|
5546
|
+
// ============================================================================
|
|
5547
|
+
// SIGNED URL
|
|
5548
|
+
// ============================================================================
|
|
5549
|
+
/**
|
|
5550
|
+
* Get a signed URL for private file access.
|
|
5551
|
+
*
|
|
5552
|
+
* Checks access permissions before generating URL.
|
|
5553
|
+
* Requires `adapter.storage` to be configured.
|
|
5554
|
+
*
|
|
5555
|
+
* @param fileId - File UUID
|
|
5556
|
+
* @param userId - User requesting access
|
|
5557
|
+
* @param options - Signed URL options
|
|
5558
|
+
* @returns Signed URL
|
|
5559
|
+
* @throws Error if user doesn't have access or StorageAdapter is not configured
|
|
5560
|
+
*
|
|
5561
|
+
* @example
|
|
5562
|
+
* ```typescript
|
|
5563
|
+
* const url = await service.getSignedUrl("file-123", "user-456", {
|
|
5564
|
+
* expiresIn: 3600, // 1 hour
|
|
5565
|
+
* });
|
|
5566
|
+
* ```
|
|
5567
|
+
*/
|
|
5568
|
+
async getSignedUrl(fileId, userId, options) {
|
|
5569
|
+
const file2 = await this.getFileOrThrow(fileId);
|
|
5570
|
+
const hasAccess = await this.checkAccess(fileId, userId);
|
|
5571
|
+
if (!hasAccess) {
|
|
5572
|
+
throw new Error("Access denied to this file");
|
|
5573
|
+
}
|
|
5574
|
+
if (file2.visibility === "public") {
|
|
5575
|
+
return file2.url;
|
|
5576
|
+
}
|
|
5577
|
+
if (!this.adapter.storage) {
|
|
5578
|
+
return file2.url;
|
|
5579
|
+
}
|
|
5580
|
+
return await this.adapter.storage.getSignedUrl(file2.storagePath, options);
|
|
5581
|
+
}
|
|
5582
|
+
// ============================================================================
|
|
5583
|
+
// ACCESS CONTROL
|
|
5584
|
+
// ============================================================================
|
|
5585
|
+
/**
|
|
5586
|
+
* Check if user has access to a file
|
|
5587
|
+
*
|
|
5588
|
+
* @param fileId - File UUID
|
|
5589
|
+
* @param userId - User ID to check
|
|
5590
|
+
* @returns true if user can access the file
|
|
4422
5591
|
*/
|
|
4423
|
-
async
|
|
4424
|
-
|
|
5592
|
+
async checkAccess(fileId, userId) {
|
|
5593
|
+
const file2 = await this.getFile(fileId);
|
|
5594
|
+
if (!file2) {
|
|
5595
|
+
return false;
|
|
5596
|
+
}
|
|
5597
|
+
if (file2.visibility === "public") {
|
|
5598
|
+
return true;
|
|
5599
|
+
}
|
|
5600
|
+
if (file2.uploadedBy === userId) {
|
|
5601
|
+
return true;
|
|
5602
|
+
}
|
|
5603
|
+
if (file2.visibility === "restricted") {
|
|
5604
|
+
return file2.allowedUsers?.includes(userId) ?? false;
|
|
5605
|
+
}
|
|
5606
|
+
return false;
|
|
4425
5607
|
}
|
|
4426
5608
|
/**
|
|
4427
|
-
*
|
|
5609
|
+
* @deprecated Use checkAccess() instead
|
|
4428
5610
|
*/
|
|
4429
|
-
|
|
4430
|
-
return
|
|
5611
|
+
canAccess(fileId, userId) {
|
|
5612
|
+
return this.checkAccess(fileId, userId);
|
|
4431
5613
|
}
|
|
4432
5614
|
/**
|
|
4433
5615
|
* Change file visibility
|
|
@@ -4472,29 +5654,9 @@ var FileService = class {
|
|
|
4472
5654
|
allowedUsers: newAllowed
|
|
4473
5655
|
});
|
|
4474
5656
|
}
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
* @param fileId - File UUID
|
|
4479
|
-
* @param userId - User ID to check
|
|
4480
|
-
* @returns true if user can access the file
|
|
4481
|
-
*/
|
|
4482
|
-
async canAccess(fileId, userId) {
|
|
4483
|
-
const file2 = await this.getFile(fileId);
|
|
4484
|
-
if (!file2) {
|
|
4485
|
-
return false;
|
|
4486
|
-
}
|
|
4487
|
-
if (file2.visibility === "public") {
|
|
4488
|
-
return true;
|
|
4489
|
-
}
|
|
4490
|
-
if (file2.uploadedBy === userId) {
|
|
4491
|
-
return true;
|
|
4492
|
-
}
|
|
4493
|
-
if (file2.visibility === "restricted") {
|
|
4494
|
-
return file2.allowedUsers?.includes(userId) ?? false;
|
|
4495
|
-
}
|
|
4496
|
-
return false;
|
|
4497
|
-
}
|
|
5657
|
+
// ============================================================================
|
|
5658
|
+
// ORGANIZATION
|
|
5659
|
+
// ============================================================================
|
|
4498
5660
|
/**
|
|
4499
5661
|
* Move file to different folder
|
|
4500
5662
|
*/
|
|
@@ -6330,6 +7492,123 @@ var RelationService = class {
|
|
|
6330
7492
|
}
|
|
6331
7493
|
};
|
|
6332
7494
|
|
|
7495
|
+
// src/runtime/services/user.service.ts
|
|
7496
|
+
var UserService = class {
|
|
7497
|
+
constructor(adapter, tenantId) {
|
|
7498
|
+
this.adapter = adapter;
|
|
7499
|
+
this.tenantId = tenantId;
|
|
7500
|
+
}
|
|
7501
|
+
/**
|
|
7502
|
+
* Validate all user attributes in the data
|
|
7503
|
+
*
|
|
7504
|
+
* @param schema - Object schema containing attribute definitions
|
|
7505
|
+
* @param data - Record data to validate
|
|
7506
|
+
* @returns Validation result with errors if any
|
|
7507
|
+
*
|
|
7508
|
+
* @example
|
|
7509
|
+
* ```typescript
|
|
7510
|
+
* const result = await userService.validateUsers(schema, {
|
|
7511
|
+
* assignee: "user-123",
|
|
7512
|
+
* watchers: ["user-456", "user-789"]
|
|
7513
|
+
* });
|
|
7514
|
+
*
|
|
7515
|
+
* if (!result.valid) {
|
|
7516
|
+
* console.log(result.errors);
|
|
7517
|
+
* // [{ attribute: "assignee", message: "User not found", invalidIds: ["user-123"] }]
|
|
7518
|
+
* }
|
|
7519
|
+
* ```
|
|
7520
|
+
*/
|
|
7521
|
+
async validateUsers(schema, data) {
|
|
7522
|
+
const errors = [];
|
|
7523
|
+
const userAttrs = schema.attributes.filter(
|
|
7524
|
+
(attr) => attr.type === "user"
|
|
7525
|
+
);
|
|
7526
|
+
for (const attr of userAttrs) {
|
|
7527
|
+
const value = data[attr.name];
|
|
7528
|
+
if (value === void 0 || value === null) {
|
|
7529
|
+
continue;
|
|
7530
|
+
}
|
|
7531
|
+
const attrErrors = await this.validateUserAttribute(attr, value);
|
|
7532
|
+
errors.push(...attrErrors);
|
|
7533
|
+
}
|
|
7534
|
+
return {
|
|
7535
|
+
valid: errors.length === 0,
|
|
7536
|
+
errors
|
|
7537
|
+
};
|
|
7538
|
+
}
|
|
7539
|
+
/**
|
|
7540
|
+
* Validate a single user attribute value
|
|
7541
|
+
*/
|
|
7542
|
+
async validateUserAttribute(attr, value) {
|
|
7543
|
+
const errors = [];
|
|
7544
|
+
const ids = this.extractIds(attr, value);
|
|
7545
|
+
if (ids.length === 0) {
|
|
7546
|
+
return errors;
|
|
7547
|
+
}
|
|
7548
|
+
const invalidIds = [];
|
|
7549
|
+
const roleErrors = [];
|
|
7550
|
+
for (const id of ids) {
|
|
7551
|
+
const user2 = await this.adapter.userProfiles.findById(id);
|
|
7552
|
+
if (!user2) {
|
|
7553
|
+
invalidIds.push(id);
|
|
7554
|
+
continue;
|
|
7555
|
+
}
|
|
7556
|
+
if (user2.tenantId !== this.tenantId) {
|
|
7557
|
+
invalidIds.push(id);
|
|
7558
|
+
continue;
|
|
7559
|
+
}
|
|
7560
|
+
if (attr.allowedRoles && attr.allowedRoles.length > 0) {
|
|
7561
|
+
if (!attr.allowedRoles.includes(user2.role)) {
|
|
7562
|
+
roleErrors.push(id);
|
|
7563
|
+
}
|
|
7564
|
+
}
|
|
7565
|
+
}
|
|
7566
|
+
if (invalidIds.length > 0) {
|
|
7567
|
+
errors.push({
|
|
7568
|
+
attribute: attr.name,
|
|
7569
|
+
message: `Invalid or non-existent users for ${attr.label}`,
|
|
7570
|
+
invalidIds
|
|
7571
|
+
});
|
|
7572
|
+
}
|
|
7573
|
+
if (roleErrors.length > 0) {
|
|
7574
|
+
errors.push({
|
|
7575
|
+
attribute: attr.name,
|
|
7576
|
+
message: `Users do not have required role for ${attr.label}. Allowed roles: ${attr.allowedRoles?.join(", ")}`,
|
|
7577
|
+
invalidIds: roleErrors
|
|
7578
|
+
});
|
|
7579
|
+
}
|
|
7580
|
+
return errors;
|
|
7581
|
+
}
|
|
7582
|
+
/**
|
|
7583
|
+
* Extract IDs from user value based on multiple flag
|
|
7584
|
+
*/
|
|
7585
|
+
extractIds(attr, value) {
|
|
7586
|
+
if (attr.multiple) {
|
|
7587
|
+
if (!Array.isArray(value)) {
|
|
7588
|
+
return [];
|
|
7589
|
+
}
|
|
7590
|
+
return value.filter((v) => typeof v === "string" && v !== "");
|
|
7591
|
+
}
|
|
7592
|
+
if (typeof value === "string" && value !== "") {
|
|
7593
|
+
return [value];
|
|
7594
|
+
}
|
|
7595
|
+
return [];
|
|
7596
|
+
}
|
|
7597
|
+
/**
|
|
7598
|
+
* Validate users and throw if invalid
|
|
7599
|
+
*/
|
|
7600
|
+
async validateUsersOrThrow(schema, data) {
|
|
7601
|
+
const result = await this.validateUsers(schema, data);
|
|
7602
|
+
if (!result.valid) {
|
|
7603
|
+
const errors = result.errors.map((e) => ({
|
|
7604
|
+
path: [e.attribute],
|
|
7605
|
+
message: e.message
|
|
7606
|
+
}));
|
|
7607
|
+
throw new ValidationError("User validation failed", errors);
|
|
7608
|
+
}
|
|
7609
|
+
}
|
|
7610
|
+
};
|
|
7611
|
+
|
|
6333
7612
|
// src/runtime/services/record.service.ts
|
|
6334
7613
|
var RecordService = class {
|
|
6335
7614
|
constructor(adapter, tenantId, options) {
|
|
@@ -6337,6 +7616,7 @@ var RecordService = class {
|
|
|
6337
7616
|
this.tenantId = tenantId;
|
|
6338
7617
|
this.schemaService = new ObjectSchemaService(adapter, registry);
|
|
6339
7618
|
this.relationService = new RelationService(adapter, registry);
|
|
7619
|
+
this.userService = new UserService(adapter, tenantId);
|
|
6340
7620
|
this.hookRegistry = options?.hookRegistry ?? new NoopHookRegistry();
|
|
6341
7621
|
this.permissionService = options?.permissionService;
|
|
6342
7622
|
this.auditService = options?.auditService ?? (adapter.audit ? new AuditService(adapter, tenantId) : void 0);
|
|
@@ -6477,6 +7757,9 @@ var RecordService = class {
|
|
|
6477
7757
|
if (!options?.skipRelationValidation) {
|
|
6478
7758
|
await this.relationService.validateRelationsOrThrow(schema, data);
|
|
6479
7759
|
}
|
|
7760
|
+
if (!options?.skipUserValidation) {
|
|
7761
|
+
await this.userService.validateUsersOrThrow(schema, data);
|
|
7762
|
+
}
|
|
6480
7763
|
}
|
|
6481
7764
|
const completionStatus = computeRecordStatus(schema, data);
|
|
6482
7765
|
const label = await this.computeLabel(schema, data);
|
|
@@ -6521,13 +7804,17 @@ var RecordService = class {
|
|
|
6521
7804
|
if (!record) {
|
|
6522
7805
|
return null;
|
|
6523
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
|
+
}
|
|
6524
7812
|
if (options?.includeSchema) {
|
|
6525
|
-
const
|
|
6526
|
-
const recordWithSchema = record;
|
|
7813
|
+
const recordWithSchema = enrichedRecord;
|
|
6527
7814
|
recordWithSchema.schema = schema;
|
|
6528
7815
|
return recordWithSchema;
|
|
6529
7816
|
}
|
|
6530
|
-
return
|
|
7817
|
+
return enrichedRecord;
|
|
6531
7818
|
}
|
|
6532
7819
|
/**
|
|
6533
7820
|
* Get a record by ID or throw if not found
|
|
@@ -6588,6 +7875,9 @@ var RecordService = class {
|
|
|
6588
7875
|
if (!options?.skipRelationValidation) {
|
|
6589
7876
|
await this.relationService.validateRelationsOrThrow(schema, data);
|
|
6590
7877
|
}
|
|
7878
|
+
if (!options?.skipUserValidation) {
|
|
7879
|
+
await this.userService.validateUsersOrThrow(schema, data);
|
|
7880
|
+
}
|
|
6591
7881
|
}
|
|
6592
7882
|
const completionStatus = computeRecordStatus(schema, mergedData);
|
|
6593
7883
|
const label = await this.computeLabel(schema, mergedData);
|
|
@@ -6819,6 +8109,46 @@ var RecordService = class {
|
|
|
6819
8109
|
timestamp: /* @__PURE__ */ new Date()
|
|
6820
8110
|
};
|
|
6821
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
|
+
}
|
|
6822
8152
|
/**
|
|
6823
8153
|
* Permanently delete a record (hard delete)
|
|
6824
8154
|
*
|
|
@@ -6847,11 +8177,18 @@ var RecordService = class {
|
|
|
6847
8177
|
* @returns Records and total count
|
|
6848
8178
|
*/
|
|
6849
8179
|
async listRecords(objectId, options) {
|
|
8180
|
+
const schema = await this.schemaService.getObjectSchema(objectId);
|
|
6850
8181
|
if (this.permissionService && this.userId) {
|
|
6851
|
-
const schema = await this.schemaService.getObjectSchema(objectId);
|
|
6852
8182
|
await this.checkPermission(schema.name, "read");
|
|
6853
8183
|
}
|
|
6854
|
-
|
|
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;
|
|
6855
8192
|
}
|
|
6856
8193
|
/**
|
|
6857
8194
|
* Search records using full-text search
|
|
@@ -6862,11 +8199,18 @@ var RecordService = class {
|
|
|
6862
8199
|
* @returns Matching records and total count
|
|
6863
8200
|
*/
|
|
6864
8201
|
async searchRecords(objectId, query, options) {
|
|
8202
|
+
const schema = await this.schemaService.getObjectSchema(objectId);
|
|
6865
8203
|
if (this.permissionService && this.userId) {
|
|
6866
|
-
const schema = await this.schemaService.getObjectSchema(objectId);
|
|
6867
8204
|
await this.checkPermission(schema.name, "read");
|
|
6868
8205
|
}
|
|
6869
|
-
|
|
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;
|
|
6870
8214
|
}
|
|
6871
8215
|
/**
|
|
6872
8216
|
* Validate data against object schema without saving
|
|
@@ -6911,6 +8255,231 @@ var RecordService = class {
|
|
|
6911
8255
|
}
|
|
6912
8256
|
};
|
|
6913
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
|
+
|
|
6914
8483
|
// src/runtime/services/user-profile.service.ts
|
|
6915
8484
|
var UserProfileService = class {
|
|
6916
8485
|
constructor(adapter, tenantId, options) {
|
|
@@ -7245,7 +8814,7 @@ var ViewService = class {
|
|
|
7245
8814
|
throw new NotFoundError("View", viewId);
|
|
7246
8815
|
}
|
|
7247
8816
|
if (dbView.system) {
|
|
7248
|
-
throw new ProtectedResourceError("
|
|
8817
|
+
throw new ProtectedResourceError("view", dbView.name, "modify");
|
|
7249
8818
|
}
|
|
7250
8819
|
const updated = await this.adapter.views.update(viewId, {
|
|
7251
8820
|
label: input.label,
|
|
@@ -7268,7 +8837,7 @@ var ViewService = class {
|
|
|
7268
8837
|
throw new NotFoundError("View", viewId);
|
|
7269
8838
|
}
|
|
7270
8839
|
if (dbView.system) {
|
|
7271
|
-
throw new ProtectedResourceError("
|
|
8840
|
+
throw new ProtectedResourceError("view", dbView.name, "delete");
|
|
7272
8841
|
}
|
|
7273
8842
|
await this.adapter.views.delete(viewId);
|
|
7274
8843
|
}
|
|
@@ -7608,7 +9177,8 @@ async function syncAttributes(adapter, nativeObject, dbObject, existingObject, r
|
|
|
7608
9177
|
hidden: attr.hidden,
|
|
7609
9178
|
archived: attr.archived,
|
|
7610
9179
|
deprecated: attr.deprecated,
|
|
7611
|
-
|
|
9180
|
+
// Formula and rollup attributes don't have defaultValue
|
|
9181
|
+
defaultValue: "defaultValue" in attr ? attr.defaultValue : void 0,
|
|
7612
9182
|
config: extractAttributeConfig(attr)
|
|
7613
9183
|
});
|
|
7614
9184
|
if (existingAttr) {
|
|
@@ -7690,6 +9260,8 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
7690
9260
|
GeocodingService,
|
|
7691
9261
|
GlobalSearchService,
|
|
7692
9262
|
GroupBuilder,
|
|
9263
|
+
InvalidPathError,
|
|
9264
|
+
MaxDepthExceededError,
|
|
7693
9265
|
NO_VALUE_OPERATORS,
|
|
7694
9266
|
NoopGeocodingAdapter,
|
|
7695
9267
|
NoopHookRegistry,
|
|
@@ -7706,8 +9278,11 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
7706
9278
|
RecordNotFoundError,
|
|
7707
9279
|
RecordReferencedError,
|
|
7708
9280
|
RecordService,
|
|
9281
|
+
RelationResolverService,
|
|
7709
9282
|
RelationService,
|
|
7710
9283
|
RoleNotFoundError,
|
|
9284
|
+
RollupScheduler,
|
|
9285
|
+
RollupService,
|
|
7711
9286
|
SYSTEM_RESOURCES,
|
|
7712
9287
|
SYSTEM_RESOURCE_LABELS,
|
|
7713
9288
|
SchemaError,
|
|
@@ -7717,6 +9292,7 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
7717
9292
|
TableTabConfig,
|
|
7718
9293
|
UserProfileNotFoundError,
|
|
7719
9294
|
UserProfileService,
|
|
9295
|
+
UserService,
|
|
7720
9296
|
ValidationError,
|
|
7721
9297
|
ViewBuilder,
|
|
7722
9298
|
ViewService,
|
|
@@ -7731,6 +9307,7 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
7731
9307
|
createDateValidator,
|
|
7732
9308
|
createDraftValidator,
|
|
7733
9309
|
createFileValidator,
|
|
9310
|
+
createFormulaValidator,
|
|
7734
9311
|
createLocationValidator,
|
|
7735
9312
|
createMockAdapter,
|
|
7736
9313
|
createMultiRelationValidator,
|
|
@@ -7740,6 +9317,8 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
7740
9317
|
createPhoneValidator,
|
|
7741
9318
|
createRatingValidator,
|
|
7742
9319
|
createRelationValidator,
|
|
9320
|
+
createRollupHooks,
|
|
9321
|
+
createRollupValidator,
|
|
7743
9322
|
createSelectValidator,
|
|
7744
9323
|
createSingleRelationValidator,
|
|
7745
9324
|
createStatusValidator,
|
|
@@ -7751,18 +9330,34 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
7751
9330
|
date,
|
|
7752
9331
|
dateConfigSchema,
|
|
7753
9332
|
enrichValuesWithSelectLabels,
|
|
9333
|
+
evaluateFormula,
|
|
9334
|
+
evaluateFormulaAttribute,
|
|
9335
|
+
evaluateFormulaAttributeWithRelations,
|
|
9336
|
+
evaluateFormulaWithRelations,
|
|
9337
|
+
evaluateFormulaWithResult,
|
|
7754
9338
|
extractAttributeNames,
|
|
9339
|
+
extractFormulaVariables,
|
|
9340
|
+
extractRelationNames,
|
|
9341
|
+
extractRelationReferences,
|
|
7755
9342
|
file,
|
|
7756
9343
|
fileConfigSchema,
|
|
9344
|
+
flattenRelationsForEval,
|
|
7757
9345
|
flow,
|
|
7758
9346
|
formatAttributeValue,
|
|
9347
|
+
formatFormulaResult,
|
|
9348
|
+
formula,
|
|
9349
|
+
formulaConfigSchema,
|
|
7759
9350
|
generateId,
|
|
7760
9351
|
generatePrefixedId,
|
|
7761
9352
|
getAttributeConfigSchema,
|
|
7762
9353
|
getMissingRequiredAttributes,
|
|
9354
|
+
getPathDepth,
|
|
9355
|
+
getRelationPath,
|
|
7763
9356
|
getSyncPreview,
|
|
9357
|
+
getTargetAttributeName,
|
|
7764
9358
|
getViewSyncPreview,
|
|
7765
9359
|
group,
|
|
9360
|
+
hasRelationReferences,
|
|
7766
9361
|
isActivityTab,
|
|
7767
9362
|
isAdvancedFilterState,
|
|
7768
9363
|
isCustomTab,
|
|
@@ -7788,14 +9383,21 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
7788
9383
|
numberConfigSchema,
|
|
7789
9384
|
object,
|
|
7790
9385
|
parseAttributeConfig,
|
|
9386
|
+
parsePath,
|
|
9387
|
+
pathHasManyCardinality,
|
|
7791
9388
|
phone,
|
|
7792
9389
|
phoneConfigSchema,
|
|
7793
9390
|
rating,
|
|
7794
9391
|
ratingConfigSchema,
|
|
9392
|
+
registerAllRollupHooks,
|
|
7795
9393
|
registry,
|
|
7796
9394
|
relation,
|
|
7797
9395
|
relationConfigSchema,
|
|
7798
9396
|
renderLabelExpression,
|
|
9397
|
+
resolveMultiplePaths,
|
|
9398
|
+
resolveSingleValue,
|
|
9399
|
+
rollup,
|
|
9400
|
+
rollupConfigSchema,
|
|
7799
9401
|
safeParseAttributeConfig,
|
|
7800
9402
|
select,
|
|
7801
9403
|
selectConfigSchema,
|
|
@@ -7812,14 +9414,17 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
7812
9414
|
timestampConfigSchema,
|
|
7813
9415
|
toAdvancedFilterState,
|
|
7814
9416
|
toSimpleFilterState,
|
|
9417
|
+
traversePath,
|
|
7815
9418
|
user,
|
|
7816
9419
|
userConfigSchema,
|
|
7817
9420
|
validateAttribute,
|
|
7818
9421
|
validateAttributeConfig,
|
|
7819
9422
|
validateDraft,
|
|
7820
9423
|
validateDraftOrThrow,
|
|
9424
|
+
validateFormulaExpression,
|
|
7821
9425
|
validateObject,
|
|
7822
9426
|
validateObjectOrThrow,
|
|
9427
|
+
validatePath,
|
|
7823
9428
|
verifyNativeObjectsSync,
|
|
7824
9429
|
verifyNativeViewsSync,
|
|
7825
9430
|
view,
|