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