@stndrds/schema 0.1.0-alpha.19 → 0.1.0-alpha.21
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-3RVLIIUK.mjs +9257 -0
- package/dist/chunk-4SW4V3YU.mjs +8854 -0
- package/dist/chunk-6FNFXEQY.mjs +9316 -0
- package/dist/chunk-6TJR4WDK.mjs +9262 -0
- package/dist/chunk-7GSIXU67.mjs +8820 -0
- package/dist/chunk-BQUYTOB3.mjs +8815 -0
- package/dist/chunk-BSAR4UKK.mjs +8851 -0
- package/dist/chunk-F27AKBTL.mjs +9452 -0
- package/dist/chunk-HSNBVKQU.mjs +9316 -0
- package/dist/chunk-I4TTNDN7.mjs +8910 -0
- package/dist/chunk-JJLODIBK.mjs +9389 -0
- package/dist/chunk-KDHA2N7D.mjs +8927 -0
- package/dist/chunk-MAXEN3GU.mjs +9266 -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-Y5KIRAND.mjs +8916 -0
- package/dist/chunk-ZD3BFM25.mjs +9316 -0
- package/dist/chunk-ZU5SR355.mjs +8862 -0
- package/dist/index.d.mts +8 -197
- package/dist/index.d.ts +8 -197
- package/dist/index.js +921 -272
- package/dist/index.mjs +21 -5
- package/dist/runtime-09fOm3VO.d.mts +6383 -0
- package/dist/runtime-09fOm3VO.d.ts +6383 -0
- package/dist/runtime-0gaIqXql.d.mts +7074 -0
- package/dist/runtime-0gaIqXql.d.ts +7074 -0
- package/dist/runtime-3u5doYAK.d.mts +6934 -0
- package/dist/runtime-3u5doYAK.d.ts +6934 -0
- package/dist/runtime-B6mOrMV4.d.mts +6985 -0
- package/dist/runtime-B6mOrMV4.d.ts +6985 -0
- package/dist/runtime-BMHqETm7.d.mts +6450 -0
- package/dist/runtime-BMHqETm7.d.ts +6450 -0
- package/dist/runtime-BNML-fPx.d.mts +6969 -0
- package/dist/runtime-BNML-fPx.d.ts +6969 -0
- package/dist/runtime-BPdsKDJo.d.mts +6969 -0
- package/dist/runtime-BPdsKDJo.d.ts +6969 -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-CfVfZtoq.d.mts +6437 -0
- package/dist/runtime-CfVfZtoq.d.ts +6437 -0
- package/dist/runtime-D1jyS1lS.d.mts +7008 -0
- package/dist/runtime-D1jyS1lS.d.ts +7008 -0
- package/dist/runtime-DjEUAeL4.d.mts +6713 -0
- package/dist/runtime-DjEUAeL4.d.ts +6713 -0
- package/dist/runtime-rTM9ddEe.d.mts +6934 -0
- package/dist/runtime-rTM9ddEe.d.ts +6934 -0
- package/dist/runtime-xLzAkAnp.d.mts +6969 -0
- package/dist/runtime-xLzAkAnp.d.ts +6969 -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 +901 -272
- package/dist/runtime.mjs +17 -5
- package/package.json +2 -2
package/dist/runtime.js
CHANGED
|
@@ -134,17 +134,22 @@ __export(runtime_exports, {
|
|
|
134
134
|
NoopHookRegistry: () => NoopHookRegistry,
|
|
135
135
|
ObjectSchemaService: () => ObjectSchemaService,
|
|
136
136
|
PermissionService: () => PermissionService,
|
|
137
|
+
QueryBuilder: () => QueryBuilder,
|
|
138
|
+
QueryMultipleResultsError: () => QueryMultipleResultsError,
|
|
139
|
+
QueryNoResultError: () => QueryNoResultError,
|
|
137
140
|
RecordService: () => RecordService,
|
|
138
141
|
RelationResolverService: () => RelationResolverService,
|
|
139
142
|
RelationService: () => RelationService,
|
|
140
143
|
RollupScheduler: () => RollupScheduler,
|
|
141
144
|
RollupService: () => RollupService,
|
|
145
|
+
SHORTCUT_TO_FILTER_OPERATOR: () => SHORTCUT_TO_FILTER_OPERATOR,
|
|
142
146
|
UserProfileService: () => UserProfileService,
|
|
143
147
|
UserService: () => UserService,
|
|
144
148
|
ViewService: () => ViewService,
|
|
145
149
|
buildAuditChanges: () => buildAuditChanges,
|
|
150
|
+
createDefaultState: () => createDefaultState,
|
|
146
151
|
createMockAdapter: () => createMockAdapter,
|
|
147
|
-
|
|
152
|
+
createQueryBuilder: () => createQueryBuilder,
|
|
148
153
|
enrichValuesWithSelectLabels: () => enrichValuesWithSelectLabels,
|
|
149
154
|
evaluateFormula: () => evaluateFormula,
|
|
150
155
|
evaluateFormulaAttribute: () => evaluateFormulaAttribute,
|
|
@@ -157,6 +162,8 @@ __export(runtime_exports, {
|
|
|
157
162
|
extractRelationReferences: () => extractRelationReferences,
|
|
158
163
|
flattenRelationsForEval: () => flattenRelationsForEval,
|
|
159
164
|
formatFormulaResult: () => formatFormulaResult,
|
|
165
|
+
formatRecord: () => formatRecord,
|
|
166
|
+
formatRecords: () => formatRecords,
|
|
160
167
|
getPathDepth: () => getPathDepth,
|
|
161
168
|
getRelationPath: () => getRelationPath,
|
|
162
169
|
getSyncPreview: () => getSyncPreview,
|
|
@@ -166,7 +173,6 @@ __export(runtime_exports, {
|
|
|
166
173
|
isLabelExpression: () => isLabelExpression,
|
|
167
174
|
parsePath: () => parsePath,
|
|
168
175
|
pathHasManyCardinality: () => pathHasManyCardinality,
|
|
169
|
-
registerAllRollupHooks: () => registerAllRollupHooks,
|
|
170
176
|
renderLabelExpression: () => renderLabelExpression,
|
|
171
177
|
resolveMultiplePaths: () => resolveMultiplePaths,
|
|
172
178
|
resolveSingleValue: () => resolveSingleValue,
|
|
@@ -181,6 +187,477 @@ __export(runtime_exports, {
|
|
|
181
187
|
});
|
|
182
188
|
module.exports = __toCommonJS(runtime_exports);
|
|
183
189
|
|
|
190
|
+
// src/runtime/client/types.ts
|
|
191
|
+
function formatRecord(record) {
|
|
192
|
+
return {
|
|
193
|
+
id: record.id,
|
|
194
|
+
createdAt: record.createdAt,
|
|
195
|
+
updatedAt: record.updatedAt,
|
|
196
|
+
metadata: record.metadata ?? {},
|
|
197
|
+
...record.values
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function formatRecords(records) {
|
|
201
|
+
return records.map((r) => formatRecord(r));
|
|
202
|
+
}
|
|
203
|
+
function createDefaultState(objectName) {
|
|
204
|
+
return {
|
|
205
|
+
objectName,
|
|
206
|
+
filters: [],
|
|
207
|
+
combinator: "and",
|
|
208
|
+
sorts: [],
|
|
209
|
+
raw: false,
|
|
210
|
+
includeDeleted: false
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
var SHORTCUT_TO_FILTER_OPERATOR = {
|
|
214
|
+
eq: "is",
|
|
215
|
+
neq: "is_not",
|
|
216
|
+
gt: "gt",
|
|
217
|
+
gte: "gte",
|
|
218
|
+
lt: "lt",
|
|
219
|
+
lte: "lte",
|
|
220
|
+
contains: "contains",
|
|
221
|
+
notContains: "not_contains",
|
|
222
|
+
startsWith: "starts_with",
|
|
223
|
+
endsWith: "ends_with",
|
|
224
|
+
isEmpty: "is_empty",
|
|
225
|
+
isNotEmpty: "is_not_empty"
|
|
226
|
+
};
|
|
227
|
+
var QueryNoResultError = class extends Error {
|
|
228
|
+
constructor(objectName, filters) {
|
|
229
|
+
const filterInfo = filters?.length ? ` with filters: ${JSON.stringify(filters)}` : "";
|
|
230
|
+
super(`No record found in "${objectName}"${filterInfo}`);
|
|
231
|
+
this.name = "QueryNoResultError";
|
|
232
|
+
this.objectName = objectName;
|
|
233
|
+
this.filters = filters;
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
var QueryMultipleResultsError = class extends Error {
|
|
237
|
+
constructor(objectName, count) {
|
|
238
|
+
super(
|
|
239
|
+
`Expected single record in "${objectName}", but found ${count}. Use first() or add more filters.`
|
|
240
|
+
);
|
|
241
|
+
this.name = "QueryMultipleResultsError";
|
|
242
|
+
this.objectName = objectName;
|
|
243
|
+
this.count = count;
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// src/runtime/client/query-builder.ts
|
|
248
|
+
var QueryBuilder = class _QueryBuilder {
|
|
249
|
+
constructor(recordService, adapter, objectName, state) {
|
|
250
|
+
this.objectId = null;
|
|
251
|
+
this.recordService = recordService;
|
|
252
|
+
this.adapter = adapter;
|
|
253
|
+
this.state = {
|
|
254
|
+
...createDefaultState(objectName),
|
|
255
|
+
...state
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Clone the builder with new state (immutability pattern)
|
|
260
|
+
*/
|
|
261
|
+
clone(updates) {
|
|
262
|
+
return new _QueryBuilder(this.recordService, this.adapter, this.state.objectName, {
|
|
263
|
+
...this.state,
|
|
264
|
+
...updates
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Resolve object name to object ID (cached)
|
|
269
|
+
*/
|
|
270
|
+
async resolveObjectId() {
|
|
271
|
+
if (this.objectId) return this.objectId;
|
|
272
|
+
const tenantId = this.recordService.tenantId;
|
|
273
|
+
const dbObject = await this.adapter.objects.findByName(tenantId, this.state.objectName);
|
|
274
|
+
if (!dbObject) {
|
|
275
|
+
throw new Error(`Object "${this.state.objectName}" not found`);
|
|
276
|
+
}
|
|
277
|
+
this.objectId = dbObject.id;
|
|
278
|
+
return dbObject.id;
|
|
279
|
+
}
|
|
280
|
+
// ============================================================================
|
|
281
|
+
// FILTER METHODS
|
|
282
|
+
// ============================================================================
|
|
283
|
+
/**
|
|
284
|
+
* Add a filter rule with explicit operator
|
|
285
|
+
*
|
|
286
|
+
* @example
|
|
287
|
+
* ```typescript
|
|
288
|
+
* qb.where('status', 'is', 'active')
|
|
289
|
+
* .where('price', 'gte', 100)
|
|
290
|
+
* ```
|
|
291
|
+
*/
|
|
292
|
+
where(attribute, operator, value) {
|
|
293
|
+
const rule = { attribute, operator, value };
|
|
294
|
+
return this.clone({
|
|
295
|
+
filters: [...this.state.filters, rule]
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Set filter combinator (default: 'and')
|
|
300
|
+
*/
|
|
301
|
+
or() {
|
|
302
|
+
return this.clone({ combinator: "or" });
|
|
303
|
+
}
|
|
304
|
+
// ============================================================================
|
|
305
|
+
// SHORTCUT OPERATORS
|
|
306
|
+
// ============================================================================
|
|
307
|
+
/**
|
|
308
|
+
* Equal (is)
|
|
309
|
+
*/
|
|
310
|
+
eq(attribute, value) {
|
|
311
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.eq, value);
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Not equal (is_not)
|
|
315
|
+
*/
|
|
316
|
+
neq(attribute, value) {
|
|
317
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.neq, value);
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Greater than
|
|
321
|
+
*/
|
|
322
|
+
gt(attribute, value) {
|
|
323
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.gt, value);
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Greater than or equal
|
|
327
|
+
*/
|
|
328
|
+
gte(attribute, value) {
|
|
329
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.gte, value);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Less than
|
|
333
|
+
*/
|
|
334
|
+
lt(attribute, value) {
|
|
335
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.lt, value);
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Less than or equal
|
|
339
|
+
*/
|
|
340
|
+
lte(attribute, value) {
|
|
341
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.lte, value);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Contains (text/relation)
|
|
345
|
+
*/
|
|
346
|
+
contains(attribute, value) {
|
|
347
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.contains, value);
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Not contains
|
|
351
|
+
*/
|
|
352
|
+
notContains(attribute, value) {
|
|
353
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.notContains, value);
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Starts with (text)
|
|
357
|
+
*/
|
|
358
|
+
startsWith(attribute, value) {
|
|
359
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.startsWith, value);
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Ends with (text)
|
|
363
|
+
*/
|
|
364
|
+
endsWith(attribute, value) {
|
|
365
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.endsWith, value);
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Is empty
|
|
369
|
+
*/
|
|
370
|
+
isEmpty(attribute) {
|
|
371
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.isEmpty, null);
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Is not empty
|
|
375
|
+
*/
|
|
376
|
+
isNotEmpty(attribute) {
|
|
377
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.isNotEmpty, null);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* In list (any_of)
|
|
381
|
+
*/
|
|
382
|
+
in(attribute, values) {
|
|
383
|
+
return this.where(attribute, "any_of", values);
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Not in list (none_of)
|
|
387
|
+
*/
|
|
388
|
+
notIn(attribute, values) {
|
|
389
|
+
return this.where(attribute, "none_of", values);
|
|
390
|
+
}
|
|
391
|
+
// ============================================================================
|
|
392
|
+
// SORT & PAGINATION
|
|
393
|
+
// ============================================================================
|
|
394
|
+
/**
|
|
395
|
+
* Add sort rule
|
|
396
|
+
*/
|
|
397
|
+
orderBy(attribute, direction = "asc") {
|
|
398
|
+
return this.clone({
|
|
399
|
+
sorts: [...this.state.sorts, { attribute, direction }]
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Set limit
|
|
404
|
+
*/
|
|
405
|
+
limit(count) {
|
|
406
|
+
return this.clone({ limit: count });
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Set offset
|
|
410
|
+
*/
|
|
411
|
+
offset(count) {
|
|
412
|
+
return this.clone({ offset: count });
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Include soft-deleted records
|
|
416
|
+
*/
|
|
417
|
+
withDeleted() {
|
|
418
|
+
return this.clone({ includeDeleted: true });
|
|
419
|
+
}
|
|
420
|
+
// ============================================================================
|
|
421
|
+
// GROUP BY (KANBAN)
|
|
422
|
+
// ============================================================================
|
|
423
|
+
/**
|
|
424
|
+
* Group results by an attribute (for Kanban views).
|
|
425
|
+
* Only status and select attributes are supported.
|
|
426
|
+
*
|
|
427
|
+
* @example
|
|
428
|
+
* ```typescript
|
|
429
|
+
* const grouped = await qb
|
|
430
|
+
* .groupBy('status')
|
|
431
|
+
* .fetchGrouped();
|
|
432
|
+
* // → { groups: { 'pending': [...], 'active': [...] }, total: 50 }
|
|
433
|
+
* ```
|
|
434
|
+
*/
|
|
435
|
+
groupBy(attribute) {
|
|
436
|
+
return this.clone({ groupBy: attribute });
|
|
437
|
+
}
|
|
438
|
+
// ============================================================================
|
|
439
|
+
// RAW MODE
|
|
440
|
+
// ============================================================================
|
|
441
|
+
/**
|
|
442
|
+
* Return raw ObjectRecord instead of formatted record.
|
|
443
|
+
* Includes all metadata (objectId, label, completionStatus, etc.)
|
|
444
|
+
*/
|
|
445
|
+
raw() {
|
|
446
|
+
return this.clone({ raw: true });
|
|
447
|
+
}
|
|
448
|
+
// ============================================================================
|
|
449
|
+
// TERMINATION METHODS (READ)
|
|
450
|
+
// ============================================================================
|
|
451
|
+
/**
|
|
452
|
+
* Fetch records with current filters
|
|
453
|
+
* Returns formatted records by default, raw ObjectRecords if raw() was called
|
|
454
|
+
*/
|
|
455
|
+
async fetch() {
|
|
456
|
+
const objectId = await this.resolveObjectId();
|
|
457
|
+
const filters = this.state.filters.length > 0 ? { combinator: this.state.combinator, rules: this.state.filters } : void 0;
|
|
458
|
+
const result = await this.recordService.listRecords(objectId, {
|
|
459
|
+
filters,
|
|
460
|
+
sorts: this.state.sorts.length > 0 ? this.state.sorts : void 0,
|
|
461
|
+
limit: this.state.limit,
|
|
462
|
+
offset: this.state.offset,
|
|
463
|
+
includeDeleted: this.state.includeDeleted
|
|
464
|
+
});
|
|
465
|
+
if (this.state.raw) {
|
|
466
|
+
return result;
|
|
467
|
+
}
|
|
468
|
+
return {
|
|
469
|
+
records: formatRecords(result.records),
|
|
470
|
+
total: result.total
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Get a single record (throws if 0 or >1 results)
|
|
475
|
+
*/
|
|
476
|
+
async single() {
|
|
477
|
+
const result = await this.limit(2).fetch();
|
|
478
|
+
if (result.records.length === 0) {
|
|
479
|
+
throw new QueryNoResultError(this.state.objectName, this.state.filters);
|
|
480
|
+
}
|
|
481
|
+
if (result.records.length > 1) {
|
|
482
|
+
throw new QueryMultipleResultsError(this.state.objectName, result.total);
|
|
483
|
+
}
|
|
484
|
+
return result.records[0];
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Get first record or null
|
|
488
|
+
*/
|
|
489
|
+
async first() {
|
|
490
|
+
const result = await this.limit(1).fetch();
|
|
491
|
+
return result.records[0] ?? null;
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Get record by ID
|
|
495
|
+
*/
|
|
496
|
+
async findById(id) {
|
|
497
|
+
const record = await this.recordService.getRecord(id);
|
|
498
|
+
if (!record) return null;
|
|
499
|
+
if (this.state.raw) {
|
|
500
|
+
return record;
|
|
501
|
+
}
|
|
502
|
+
return formatRecord(record);
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Count records matching filters
|
|
506
|
+
*/
|
|
507
|
+
async count() {
|
|
508
|
+
const result = await this.limit(0).fetch();
|
|
509
|
+
return result.total;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Fetch records grouped by the groupBy attribute (for Kanban views).
|
|
513
|
+
* Requires groupBy() to be called first.
|
|
514
|
+
*
|
|
515
|
+
* @example
|
|
516
|
+
* ```typescript
|
|
517
|
+
* const result = await schema.from('tasks')
|
|
518
|
+
* .groupBy('status')
|
|
519
|
+
* .fetchGrouped();
|
|
520
|
+
* // → { groups: { 'todo': [...], 'in_progress': [...], 'done': [...] }, total: 50 }
|
|
521
|
+
* ```
|
|
522
|
+
*/
|
|
523
|
+
async fetchGrouped() {
|
|
524
|
+
if (!this.state.groupBy) {
|
|
525
|
+
throw new Error("groupBy() must be called before fetchGrouped()");
|
|
526
|
+
}
|
|
527
|
+
const result = await this.fetch();
|
|
528
|
+
const groupByAttr = this.state.groupBy;
|
|
529
|
+
const groups = {};
|
|
530
|
+
for (const record of result.records) {
|
|
531
|
+
const value = record[groupByAttr];
|
|
532
|
+
const key = value != null ? String(value) : "__null__";
|
|
533
|
+
if (!groups[key]) {
|
|
534
|
+
groups[key] = [];
|
|
535
|
+
}
|
|
536
|
+
groups[key].push(record);
|
|
537
|
+
}
|
|
538
|
+
return {
|
|
539
|
+
groups,
|
|
540
|
+
total: result.total
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
// ============================================================================
|
|
544
|
+
// TERMINATION METHODS (WRITE)
|
|
545
|
+
// ============================================================================
|
|
546
|
+
/**
|
|
547
|
+
* Create a new record
|
|
548
|
+
* System fields (id, createdAt, updatedAt) are managed automatically.
|
|
549
|
+
*
|
|
550
|
+
* @example
|
|
551
|
+
* ```typescript
|
|
552
|
+
* const product = await schema.from('products')
|
|
553
|
+
* .insert({ name: 'iPhone', price: 999 });
|
|
554
|
+
*
|
|
555
|
+
* // Allow draft (missing required fields)
|
|
556
|
+
* const draft = await schema.from('products')
|
|
557
|
+
* .insert({ name: 'Draft' }, { allowDraft: true });
|
|
558
|
+
*
|
|
559
|
+
* // With custom metadata
|
|
560
|
+
* const withMeta = await schema.from('products')
|
|
561
|
+
* .insert({ name: 'iPhone' }, { metadata: { externalId: 'ext-123' } });
|
|
562
|
+
* ```
|
|
563
|
+
*/
|
|
564
|
+
async insert(data, options) {
|
|
565
|
+
const objectId = await this.resolveObjectId();
|
|
566
|
+
const record = await this.recordService.createRecord(
|
|
567
|
+
objectId,
|
|
568
|
+
data,
|
|
569
|
+
{
|
|
570
|
+
allowDraft: options?.allowDraft,
|
|
571
|
+
validate: options?.validate,
|
|
572
|
+
metadata: options?.metadata
|
|
573
|
+
}
|
|
574
|
+
);
|
|
575
|
+
if (this.state.raw) {
|
|
576
|
+
return record;
|
|
577
|
+
}
|
|
578
|
+
return formatRecord(record);
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Update records matching filters (expects exactly 1 record by default)
|
|
582
|
+
* Use with eq('id', ...) for single record update.
|
|
583
|
+
* System fields (id, createdAt, updatedAt) are managed automatically.
|
|
584
|
+
*
|
|
585
|
+
* @example
|
|
586
|
+
* ```typescript
|
|
587
|
+
* const updated = await schema.from('products')
|
|
588
|
+
* .eq('id', 'rec-123')
|
|
589
|
+
* .update({ price: 899 });
|
|
590
|
+
*
|
|
591
|
+
* // Update with metadata (replaces existing metadata)
|
|
592
|
+
* const withMeta = await schema.from('products')
|
|
593
|
+
* .eq('id', 'rec-123')
|
|
594
|
+
* .update({ price: 899 }, { metadata: { synced: true } });
|
|
595
|
+
* ```
|
|
596
|
+
*/
|
|
597
|
+
async update(data, options) {
|
|
598
|
+
const existing = await this.single();
|
|
599
|
+
const recordId = this.state.raw ? existing.id : existing.id;
|
|
600
|
+
const record = await this.recordService.updateRecord(
|
|
601
|
+
recordId,
|
|
602
|
+
data,
|
|
603
|
+
{
|
|
604
|
+
partial: true,
|
|
605
|
+
metadata: options?.metadata
|
|
606
|
+
}
|
|
607
|
+
);
|
|
608
|
+
if (this.state.raw) {
|
|
609
|
+
return record;
|
|
610
|
+
}
|
|
611
|
+
return formatRecord(record);
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Delete record matching filters (expects exactly 1 record)
|
|
615
|
+
*
|
|
616
|
+
* @example
|
|
617
|
+
* ```typescript
|
|
618
|
+
* await schema.from('products')
|
|
619
|
+
* .eq('id', 'rec-123')
|
|
620
|
+
* .delete();
|
|
621
|
+
* ```
|
|
622
|
+
*/
|
|
623
|
+
async delete() {
|
|
624
|
+
const existing = await this.single();
|
|
625
|
+
const recordId = this.state.raw ? existing.id : existing.id;
|
|
626
|
+
await this.recordService.deleteRecord(recordId);
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Upsert (update if exists by ID, insert otherwise)
|
|
630
|
+
* Provide 'id' for lookup. System fields (createdAt, updatedAt) are managed automatically.
|
|
631
|
+
*
|
|
632
|
+
* @example
|
|
633
|
+
* ```typescript
|
|
634
|
+
* // Insert if not exists, update if exists
|
|
635
|
+
* const product = await schema.from('products')
|
|
636
|
+
* .upsert({ id: 'rec-123', name: 'iPhone', price: 999 });
|
|
637
|
+
*
|
|
638
|
+
* // With metadata
|
|
639
|
+
* const withMeta = await schema.from('products')
|
|
640
|
+
* .upsert({ name: 'iPhone' }, { metadata: { source: 'import' } });
|
|
641
|
+
* ```
|
|
642
|
+
*/
|
|
643
|
+
async upsert(data, options) {
|
|
644
|
+
const { id, ...rest } = data;
|
|
645
|
+
const writeData = rest;
|
|
646
|
+
if (id) {
|
|
647
|
+
const existing = await this.findById(id);
|
|
648
|
+
if (existing) {
|
|
649
|
+
return this.eq("id", id).update(writeData, {
|
|
650
|
+
metadata: options?.metadata
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return this.insert(writeData, options);
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
function createQueryBuilder(recordService, adapter, objectName) {
|
|
658
|
+
return new QueryBuilder(recordService, adapter, objectName);
|
|
659
|
+
}
|
|
660
|
+
|
|
184
661
|
// src/runtime/formula/evaluator.ts
|
|
185
662
|
var import_expr_eval = require("expr-eval");
|
|
186
663
|
function createFormulaParser() {
|
|
@@ -565,260 +1042,6 @@ var NoopHookRegistry = class {
|
|
|
565
1042
|
}
|
|
566
1043
|
};
|
|
567
1044
|
|
|
568
|
-
// src/runtime/services/rollup.service.ts
|
|
569
|
-
var RollupService = class {
|
|
570
|
-
constructor(adapter) {
|
|
571
|
-
this.adapter = adapter;
|
|
572
|
-
}
|
|
573
|
-
/**
|
|
574
|
-
* Calculate a rollup value for a record
|
|
575
|
-
*
|
|
576
|
-
* @param recordId - ID of the parent record
|
|
577
|
-
* @param rollupAttr - Rollup attribute definition
|
|
578
|
-
* @param schema - Schema of the parent object
|
|
579
|
-
* @returns Computed rollup value
|
|
580
|
-
*
|
|
581
|
-
* @example
|
|
582
|
-
* ```typescript
|
|
583
|
-
* // Sum all order amounts for a company
|
|
584
|
-
* const totalOrders = await rollupService.calculate(
|
|
585
|
-
* "company-123",
|
|
586
|
-
* {
|
|
587
|
-
* type: "rollup",
|
|
588
|
-
* name: "totalOrders",
|
|
589
|
-
* relationAttribute: "orders",
|
|
590
|
-
* targetAttribute: "amount",
|
|
591
|
-
* function: "sum",
|
|
592
|
-
* ...
|
|
593
|
-
* },
|
|
594
|
-
* companySchema
|
|
595
|
-
* );
|
|
596
|
-
* ```
|
|
597
|
-
*/
|
|
598
|
-
async calculate(recordId, rollupAttr, schema, tenantId) {
|
|
599
|
-
const relationAttr = schema.attributes.find(
|
|
600
|
-
(a) => a.type === "relation" && a.name === rollupAttr.relationAttribute
|
|
601
|
-
);
|
|
602
|
-
if (!relationAttr) {
|
|
603
|
-
return { value: null, recordCount: 0 };
|
|
604
|
-
}
|
|
605
|
-
const targetObjectName = relationAttr.targets[0]?.object;
|
|
606
|
-
if (!targetObjectName) {
|
|
607
|
-
return { value: null, recordCount: 0 };
|
|
608
|
-
}
|
|
609
|
-
const targetObject = await this.adapter.objects.findByName(tenantId, targetObjectName);
|
|
610
|
-
if (!targetObject) {
|
|
611
|
-
return { value: null, recordCount: 0 };
|
|
612
|
-
}
|
|
613
|
-
const relatedRecords = await this.adapter.objectRecords.findByRelation(
|
|
614
|
-
tenantId,
|
|
615
|
-
targetObject.id,
|
|
616
|
-
rollupAttr.relationAttribute,
|
|
617
|
-
recordId
|
|
618
|
-
);
|
|
619
|
-
if (relatedRecords.length === 0) {
|
|
620
|
-
return { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 };
|
|
621
|
-
}
|
|
622
|
-
const values = relatedRecords.map((r) => r.values[rollupAttr.targetAttribute]).filter((v) => v !== void 0);
|
|
623
|
-
const aggregated = this.aggregate(values, rollupAttr.function);
|
|
624
|
-
const result = typeof aggregated === "number" && rollupAttr.decimals !== void 0 ? Number(aggregated.toFixed(rollupAttr.decimals)) : aggregated;
|
|
625
|
-
return { value: result, recordCount: relatedRecords.length };
|
|
626
|
-
}
|
|
627
|
-
/**
|
|
628
|
-
* Calculate rollup values for multiple records (batched)
|
|
629
|
-
*
|
|
630
|
-
* More efficient than calling calculate() for each record individually.
|
|
631
|
-
*/
|
|
632
|
-
async calculateForMany(recordIds, rollupAttr, schema, tenantId) {
|
|
633
|
-
const results = /* @__PURE__ */ new Map();
|
|
634
|
-
for (const id of recordIds) {
|
|
635
|
-
results.set(id, { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 });
|
|
636
|
-
}
|
|
637
|
-
await Promise.all(
|
|
638
|
-
recordIds.map(async (id) => {
|
|
639
|
-
const result = await this.calculate(id, rollupAttr, schema, tenantId);
|
|
640
|
-
results.set(id, result);
|
|
641
|
-
})
|
|
642
|
-
);
|
|
643
|
-
return results;
|
|
644
|
-
}
|
|
645
|
-
/**
|
|
646
|
-
* Apply aggregation function to a set of values
|
|
647
|
-
*/
|
|
648
|
-
aggregate(values, fn) {
|
|
649
|
-
switch (fn) {
|
|
650
|
-
case "sum":
|
|
651
|
-
return this.sumNumbers(values);
|
|
652
|
-
case "avg":
|
|
653
|
-
return this.averageNumbers(values);
|
|
654
|
-
case "min":
|
|
655
|
-
return this.minNumbers(values);
|
|
656
|
-
case "max":
|
|
657
|
-
return this.maxNumbers(values);
|
|
658
|
-
case "count":
|
|
659
|
-
return values.length;
|
|
660
|
-
case "countValues":
|
|
661
|
-
return values.filter((v) => v != null && v !== "").length;
|
|
662
|
-
case "countEmpty":
|
|
663
|
-
return values.filter((v) => v == null || v === "").length;
|
|
664
|
-
case "percentFilled": {
|
|
665
|
-
if (values.length === 0) return 0;
|
|
666
|
-
const filled = values.filter((v) => v != null && v !== "").length;
|
|
667
|
-
return filled / values.length * 100;
|
|
668
|
-
}
|
|
669
|
-
case "percentEmpty": {
|
|
670
|
-
if (values.length === 0) return 0;
|
|
671
|
-
const empty = values.filter((v) => v == null || v === "").length;
|
|
672
|
-
return empty / values.length * 100;
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
/**
|
|
677
|
-
* Get the default empty value for a rollup function
|
|
678
|
-
*/
|
|
679
|
-
getEmptyValue(fn) {
|
|
680
|
-
switch (fn) {
|
|
681
|
-
case "sum":
|
|
682
|
-
case "count":
|
|
683
|
-
case "countValues":
|
|
684
|
-
case "countEmpty":
|
|
685
|
-
case "percentEmpty":
|
|
686
|
-
case "percentFilled":
|
|
687
|
-
return 0;
|
|
688
|
-
case "avg":
|
|
689
|
-
case "min":
|
|
690
|
-
case "max":
|
|
691
|
-
return null;
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
/**
|
|
695
|
-
* Sum numeric values
|
|
696
|
-
*/
|
|
697
|
-
sumNumbers(values) {
|
|
698
|
-
return values.filter((v) => typeof v === "number" && !Number.isNaN(v)).reduce((sum, n) => sum + n, 0);
|
|
699
|
-
}
|
|
700
|
-
/**
|
|
701
|
-
* Average numeric values
|
|
702
|
-
*/
|
|
703
|
-
averageNumbers(values) {
|
|
704
|
-
const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
|
|
705
|
-
if (nums.length === 0) return null;
|
|
706
|
-
return nums.reduce((sum, n) => sum + n, 0) / nums.length;
|
|
707
|
-
}
|
|
708
|
-
/**
|
|
709
|
-
* Minimum numeric value
|
|
710
|
-
*/
|
|
711
|
-
minNumbers(values) {
|
|
712
|
-
const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
|
|
713
|
-
if (nums.length === 0) return null;
|
|
714
|
-
return Math.min(...nums);
|
|
715
|
-
}
|
|
716
|
-
/**
|
|
717
|
-
* Maximum numeric value
|
|
718
|
-
*/
|
|
719
|
-
maxNumbers(values) {
|
|
720
|
-
const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
|
|
721
|
-
if (nums.length === 0) return null;
|
|
722
|
-
return Math.max(...nums);
|
|
723
|
-
}
|
|
724
|
-
/**
|
|
725
|
-
* Recalculate all rollup attributes for a record and update it
|
|
726
|
-
*
|
|
727
|
-
* Called after related records change to keep rollups up-to-date.
|
|
728
|
-
*/
|
|
729
|
-
async recalculateAndUpdate(record, schema, tenantId) {
|
|
730
|
-
const rollupAttrs = schema.attributes.filter((a) => a.type === "rollup");
|
|
731
|
-
if (rollupAttrs.length === 0) {
|
|
732
|
-
return record;
|
|
733
|
-
}
|
|
734
|
-
const updates = {};
|
|
735
|
-
for (const attr of rollupAttrs) {
|
|
736
|
-
const result = await this.calculate(record.id, attr, schema, tenantId);
|
|
737
|
-
updates[attr.name] = result.value;
|
|
738
|
-
}
|
|
739
|
-
return await this.adapter.objectRecords.update(record.id, updates);
|
|
740
|
-
}
|
|
741
|
-
/**
|
|
742
|
-
* Find parent records that need rollup recalculation when a child record changes
|
|
743
|
-
*
|
|
744
|
-
* Used by hooks to determine which parent records to recalculate after
|
|
745
|
-
* a child record is created, updated, or deleted.
|
|
746
|
-
*
|
|
747
|
-
* @param changedRecord - The record that was modified
|
|
748
|
-
* @param changedSchema - Schema of the changed record's object
|
|
749
|
-
* @returns Array of parent record IDs that need recalculation
|
|
750
|
-
*/
|
|
751
|
-
async findAffectedParentRecords(changedRecord, changedSchema) {
|
|
752
|
-
const affectedIds = [];
|
|
753
|
-
const relationAttrs = changedSchema.attributes.filter(
|
|
754
|
-
(a) => a.type === "relation"
|
|
755
|
-
);
|
|
756
|
-
for (const attr of relationAttrs) {
|
|
757
|
-
const relatedId = changedRecord.values[attr.name];
|
|
758
|
-
if (typeof relatedId === "string" && relatedId.length > 0) {
|
|
759
|
-
affectedIds.push(relatedId);
|
|
760
|
-
} else if (Array.isArray(relatedId)) {
|
|
761
|
-
affectedIds.push(...relatedId.filter((id) => typeof id === "string"));
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
return [...new Set(affectedIds)];
|
|
765
|
-
}
|
|
766
|
-
};
|
|
767
|
-
|
|
768
|
-
// src/runtime/hooks/rollup.hooks.ts
|
|
769
|
-
function createRollupHooks(objectName, config) {
|
|
770
|
-
const rollupService = new RollupService(config.adapter);
|
|
771
|
-
async function recalculateParentRollups(ctx) {
|
|
772
|
-
const schema = await config.getSchemaById(ctx.objectId);
|
|
773
|
-
if (!schema) return;
|
|
774
|
-
const affectedParentIds = await rollupService.findAffectedParentRecords(ctx.record, schema);
|
|
775
|
-
if (affectedParentIds.length === 0) return;
|
|
776
|
-
const parentRecords = await config.adapter.objectRecords.findByIds(affectedParentIds);
|
|
777
|
-
for (const parentRecord of parentRecords) {
|
|
778
|
-
const parentSchema = await config.getSchemaById(parentRecord.objectId);
|
|
779
|
-
if (!parentSchema) continue;
|
|
780
|
-
const hasRollups = parentSchema.attributes.some(
|
|
781
|
-
(a) => a.type === "rollup"
|
|
782
|
-
);
|
|
783
|
-
if (hasRollups) {
|
|
784
|
-
await rollupService.recalculateAndUpdate(parentRecord, parentSchema, config.tenantId);
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
return [
|
|
789
|
-
{
|
|
790
|
-
type: "afterCreate",
|
|
791
|
-
objectName,
|
|
792
|
-
handler: recalculateParentRollups,
|
|
793
|
-
priority: 100
|
|
794
|
-
// Run after other hooks
|
|
795
|
-
},
|
|
796
|
-
{
|
|
797
|
-
type: "afterUpdate",
|
|
798
|
-
objectName,
|
|
799
|
-
handler: recalculateParentRollups,
|
|
800
|
-
priority: 100
|
|
801
|
-
},
|
|
802
|
-
{
|
|
803
|
-
type: "afterDelete",
|
|
804
|
-
objectName,
|
|
805
|
-
handler: recalculateParentRollups,
|
|
806
|
-
priority: 100
|
|
807
|
-
}
|
|
808
|
-
];
|
|
809
|
-
}
|
|
810
|
-
function registerAllRollupHooks(hookRegistry, schemas, config) {
|
|
811
|
-
for (const schema of schemas) {
|
|
812
|
-
const hasRelations = schema.attributes.some((a) => a.type === "relation");
|
|
813
|
-
if (hasRelations && hookRegistry.register) {
|
|
814
|
-
const hooks = createRollupHooks(schema.name, config);
|
|
815
|
-
for (const hook of hooks) {
|
|
816
|
-
hookRegistry.register(hook);
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
|
|
822
1045
|
// src/utils.ts
|
|
823
1046
|
function generateId() {
|
|
824
1047
|
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
@@ -1457,6 +1680,7 @@ function createMockObjectRecordsRepository(stores) {
|
|
|
1457
1680
|
label: data.label,
|
|
1458
1681
|
completionStatus: data.completionStatus,
|
|
1459
1682
|
values: data.data,
|
|
1683
|
+
metadata: data.metadata ?? {},
|
|
1460
1684
|
createdAt: /* @__PURE__ */ new Date(),
|
|
1461
1685
|
updatedAt: /* @__PURE__ */ new Date()
|
|
1462
1686
|
};
|
|
@@ -1469,11 +1693,13 @@ function createMockObjectRecordsRepository(stores) {
|
|
|
1469
1693
|
if (!existing) {
|
|
1470
1694
|
return Promise.reject(new Error(`ObjectRecord ${id} not found`));
|
|
1471
1695
|
}
|
|
1472
|
-
const { __completionStatus, __label, ...valueData } = data;
|
|
1696
|
+
const { __completionStatus, __label, __metadata, ...valueData } = data;
|
|
1473
1697
|
const updated = {
|
|
1474
1698
|
...existing,
|
|
1475
1699
|
label: __label ?? existing.label,
|
|
1476
1700
|
completionStatus: __completionStatus ?? existing.completionStatus,
|
|
1701
|
+
// Metadata uses replace behavior (not merge)
|
|
1702
|
+
metadata: __metadata !== void 0 ? __metadata : existing.metadata,
|
|
1477
1703
|
values: { ...existing.values, ...valueData },
|
|
1478
1704
|
updatedAt: /* @__PURE__ */ new Date()
|
|
1479
1705
|
};
|
|
@@ -3601,10 +3827,12 @@ var rollupConfigSchema = baseConfigSchema.extend({
|
|
|
3601
3827
|
"countValues",
|
|
3602
3828
|
"countEmpty",
|
|
3603
3829
|
"percentEmpty",
|
|
3604
|
-
"percentFilled"
|
|
3830
|
+
"percentFilled",
|
|
3831
|
+
"collect"
|
|
3605
3832
|
]),
|
|
3606
3833
|
decimals: import_zod4.z.number().int().min(0).max(10).optional(),
|
|
3607
|
-
|
|
3834
|
+
showAsOriginal: import_zod4.z.boolean().optional(),
|
|
3835
|
+
targetAttributeType: import_zod4.z.string().optional()
|
|
3608
3836
|
});
|
|
3609
3837
|
var attributeConfigSchemas = {
|
|
3610
3838
|
text: textConfigSchema,
|
|
@@ -3669,7 +3897,7 @@ function createCheckboxValidator(_attr) {
|
|
|
3669
3897
|
return import_zod4.z.boolean();
|
|
3670
3898
|
}
|
|
3671
3899
|
function createDateValidator(attr) {
|
|
3672
|
-
return import_zod4.z.
|
|
3900
|
+
return import_zod4.z.coerce.date({ message: `${attr.label} must be a valid ISO date` });
|
|
3673
3901
|
}
|
|
3674
3902
|
function createPhoneValidator(_attr) {
|
|
3675
3903
|
return import_zod4.z.object({
|
|
@@ -3775,7 +4003,7 @@ function createRelationValidator(attr) {
|
|
|
3775
4003
|
return createSingleRelationValidator(attr);
|
|
3776
4004
|
}
|
|
3777
4005
|
function createRatingValidator(attr) {
|
|
3778
|
-
let schema = import_zod4.z.number().
|
|
4006
|
+
let schema = import_zod4.z.number().min(0);
|
|
3779
4007
|
if (attr.max !== void 0) {
|
|
3780
4008
|
schema = schema.max(attr.max, `${attr.label} must be at most ${attr.max}`);
|
|
3781
4009
|
}
|
|
@@ -3929,7 +4157,7 @@ function computeRecordStatus(objectDef, data) {
|
|
|
3929
4157
|
}
|
|
3930
4158
|
|
|
3931
4159
|
// src/runtime/services/object-schema.service.ts
|
|
3932
|
-
var
|
|
4160
|
+
var _ObjectSchemaService = class _ObjectSchemaService {
|
|
3933
4161
|
constructor(adapter, nativeRegistry, options) {
|
|
3934
4162
|
this.adapter = adapter;
|
|
3935
4163
|
this.nativeRegistry = nativeRegistry;
|
|
@@ -4352,6 +4580,14 @@ var ObjectSchemaService = class {
|
|
|
4352
4580
|
"Invalid attribute name format.\nName must be a valid variable identifier:\n \u2705 Valid: 'firstName', 'first_name', 'FirstName', 'FIRST_NAME'\n \u274C Invalid: 'first-name', 'first name', '123name', 'first.name'"
|
|
4353
4581
|
);
|
|
4354
4582
|
}
|
|
4583
|
+
if (_ObjectSchemaService.RESERVED_ATTRIBUTE_NAMES.includes(
|
|
4584
|
+
name
|
|
4585
|
+
)) {
|
|
4586
|
+
throw new Error(
|
|
4587
|
+
`"${name}" is a reserved name and cannot be used as an attribute name.
|
|
4588
|
+
Reserved names: ${_ObjectSchemaService.RESERVED_ATTRIBUTE_NAMES.join(", ")}`
|
|
4589
|
+
);
|
|
4590
|
+
}
|
|
4355
4591
|
}
|
|
4356
4592
|
/**
|
|
4357
4593
|
* Get complete object schema (system + custom attributes)
|
|
@@ -4598,6 +4834,17 @@ var ObjectSchemaService = class {
|
|
|
4598
4834
|
return baseAttr;
|
|
4599
4835
|
}
|
|
4600
4836
|
};
|
|
4837
|
+
/**
|
|
4838
|
+
* Reserved attribute names that cannot be used.
|
|
4839
|
+
* These names conflict with system fields on ObjectRecord.
|
|
4840
|
+
*/
|
|
4841
|
+
_ObjectSchemaService.RESERVED_ATTRIBUTE_NAMES = [
|
|
4842
|
+
"id",
|
|
4843
|
+
"createdAt",
|
|
4844
|
+
"updatedAt",
|
|
4845
|
+
"metadata"
|
|
4846
|
+
];
|
|
4847
|
+
var ObjectSchemaService = _ObjectSchemaService;
|
|
4601
4848
|
|
|
4602
4849
|
// src/runtime/services/permission.service.ts
|
|
4603
4850
|
var PermissionService = class {
|
|
@@ -5450,6 +5697,328 @@ var RelationService = class {
|
|
|
5450
5697
|
}
|
|
5451
5698
|
};
|
|
5452
5699
|
|
|
5700
|
+
// src/runtime/services/rollup.service.ts
|
|
5701
|
+
var RollupService = class {
|
|
5702
|
+
constructor(adapter) {
|
|
5703
|
+
this.adapter = adapter;
|
|
5704
|
+
}
|
|
5705
|
+
/**
|
|
5706
|
+
* Calculate a rollup value for a record
|
|
5707
|
+
*
|
|
5708
|
+
* @param recordId - ID of the parent record
|
|
5709
|
+
* @param rollupAttr - Rollup attribute definition
|
|
5710
|
+
* @param schema - Schema of the parent object
|
|
5711
|
+
* @returns Computed rollup value
|
|
5712
|
+
*
|
|
5713
|
+
* @example
|
|
5714
|
+
* ```typescript
|
|
5715
|
+
* // Sum all order amounts for a company
|
|
5716
|
+
* const totalOrders = await rollupService.calculate(
|
|
5717
|
+
* "company-123",
|
|
5718
|
+
* {
|
|
5719
|
+
* type: "rollup",
|
|
5720
|
+
* name: "totalOrders",
|
|
5721
|
+
* relationAttribute: "orders",
|
|
5722
|
+
* targetAttribute: "amount",
|
|
5723
|
+
* function: "sum",
|
|
5724
|
+
* ...
|
|
5725
|
+
* },
|
|
5726
|
+
* companySchema
|
|
5727
|
+
* );
|
|
5728
|
+
* ```
|
|
5729
|
+
*/
|
|
5730
|
+
async calculate(recordId, rollupAttr, schema) {
|
|
5731
|
+
const relationAttr = schema.attributes.find(
|
|
5732
|
+
(a) => a.type === "relation" && a.name === rollupAttr.relationAttribute
|
|
5733
|
+
);
|
|
5734
|
+
if (relationAttr) {
|
|
5735
|
+
return this.calculateForward(recordId, rollupAttr);
|
|
5736
|
+
}
|
|
5737
|
+
return this.calculateReverse(recordId, rollupAttr, schema);
|
|
5738
|
+
}
|
|
5739
|
+
/**
|
|
5740
|
+
* Forward pattern: this record has a relation attribute pointing to other records
|
|
5741
|
+
* Example: entreprise222 has relation "entreprises" → companies, rollup collects from companies
|
|
5742
|
+
*/
|
|
5743
|
+
async calculateForward(recordId, rollupAttr) {
|
|
5744
|
+
const record = await this.adapter.objectRecords.findById(recordId);
|
|
5745
|
+
if (!record) {
|
|
5746
|
+
return { value: null, recordCount: 0 };
|
|
5747
|
+
}
|
|
5748
|
+
const relationValue = record.values[rollupAttr.relationAttribute];
|
|
5749
|
+
let relatedIds = [];
|
|
5750
|
+
if (typeof relationValue === "string" && relationValue.length > 0) {
|
|
5751
|
+
relatedIds = [relationValue];
|
|
5752
|
+
} else if (Array.isArray(relationValue)) {
|
|
5753
|
+
relatedIds = relationValue.filter((id) => typeof id === "string" && id.length > 0);
|
|
5754
|
+
}
|
|
5755
|
+
if (relatedIds.length === 0) {
|
|
5756
|
+
return { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 };
|
|
5757
|
+
}
|
|
5758
|
+
const relatedRecords = await this.adapter.objectRecords.findByIds(relatedIds);
|
|
5759
|
+
if (relatedRecords.length === 0) {
|
|
5760
|
+
return { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 };
|
|
5761
|
+
}
|
|
5762
|
+
return this.aggregateValues(relatedRecords, rollupAttr);
|
|
5763
|
+
}
|
|
5764
|
+
/**
|
|
5765
|
+
* Reverse pattern: other records have a relation pointing to this record
|
|
5766
|
+
* Example: Company has rollup on "orders", Order has relation "company" → companies
|
|
5767
|
+
*/
|
|
5768
|
+
async calculateReverse(recordId, rollupAttr, schema) {
|
|
5769
|
+
const record = await this.adapter.objectRecords.findById(recordId);
|
|
5770
|
+
if (!record) {
|
|
5771
|
+
return { value: null, recordCount: 0 };
|
|
5772
|
+
}
|
|
5773
|
+
const parentObject = await this.adapter.objects.findById(record.objectId);
|
|
5774
|
+
if (!parentObject) {
|
|
5775
|
+
return { value: null, recordCount: 0 };
|
|
5776
|
+
}
|
|
5777
|
+
const tenantId = parentObject.tenantId;
|
|
5778
|
+
const sourceObjectName = rollupAttr.relationAttribute;
|
|
5779
|
+
const sourceObject = await this.adapter.objects.findByName(tenantId, sourceObjectName);
|
|
5780
|
+
if (!sourceObject) {
|
|
5781
|
+
return { value: null, recordCount: 0 };
|
|
5782
|
+
}
|
|
5783
|
+
const sourceAttributes = await this.adapter.attributes.findByObjectId(sourceObject.id);
|
|
5784
|
+
const reverseRelationAttr = sourceAttributes.find((attr) => {
|
|
5785
|
+
if (attr.type !== "relation") return false;
|
|
5786
|
+
const relationConfig = attr.config;
|
|
5787
|
+
return relationConfig?.targets?.some((t) => t.object === schema.name);
|
|
5788
|
+
});
|
|
5789
|
+
if (!reverseRelationAttr) {
|
|
5790
|
+
return { value: null, recordCount: 0 };
|
|
5791
|
+
}
|
|
5792
|
+
const relatedRecords = await this.adapter.objectRecords.findByRelation(
|
|
5793
|
+
tenantId,
|
|
5794
|
+
sourceObject.id,
|
|
5795
|
+
reverseRelationAttr.name,
|
|
5796
|
+
recordId
|
|
5797
|
+
);
|
|
5798
|
+
if (relatedRecords.length === 0) {
|
|
5799
|
+
return { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 };
|
|
5800
|
+
}
|
|
5801
|
+
return this.aggregateValues(relatedRecords, rollupAttr);
|
|
5802
|
+
}
|
|
5803
|
+
/**
|
|
5804
|
+
* Extract and aggregate values from related records
|
|
5805
|
+
*/
|
|
5806
|
+
aggregateValues(relatedRecords, rollupAttr) {
|
|
5807
|
+
const values = relatedRecords.map((r) => r.values[rollupAttr.targetAttribute]).filter((v) => v !== void 0 && v !== null);
|
|
5808
|
+
const aggregated = this.aggregate(values, rollupAttr.function);
|
|
5809
|
+
const result = typeof aggregated === "number" && rollupAttr.decimals !== void 0 ? Number(aggregated.toFixed(rollupAttr.decimals)) : aggregated;
|
|
5810
|
+
return { value: result, recordCount: relatedRecords.length };
|
|
5811
|
+
}
|
|
5812
|
+
/**
|
|
5813
|
+
* Calculate rollup values for multiple records (batched)
|
|
5814
|
+
*
|
|
5815
|
+
* More efficient than calling calculate() for each record individually.
|
|
5816
|
+
*/
|
|
5817
|
+
async calculateForMany(recordIds, rollupAttr, schema) {
|
|
5818
|
+
const results = /* @__PURE__ */ new Map();
|
|
5819
|
+
for (const id of recordIds) {
|
|
5820
|
+
results.set(id, { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 });
|
|
5821
|
+
}
|
|
5822
|
+
await Promise.all(
|
|
5823
|
+
recordIds.map(async (id) => {
|
|
5824
|
+
const result = await this.calculate(id, rollupAttr, schema);
|
|
5825
|
+
results.set(id, result);
|
|
5826
|
+
})
|
|
5827
|
+
);
|
|
5828
|
+
return results;
|
|
5829
|
+
}
|
|
5830
|
+
/**
|
|
5831
|
+
* Apply aggregation function to a set of values
|
|
5832
|
+
*/
|
|
5833
|
+
aggregate(values, fn) {
|
|
5834
|
+
switch (fn) {
|
|
5835
|
+
case "sum":
|
|
5836
|
+
return this.sumNumbers(values);
|
|
5837
|
+
case "avg":
|
|
5838
|
+
return this.averageNumbers(values);
|
|
5839
|
+
case "min":
|
|
5840
|
+
return this.minNumbers(values);
|
|
5841
|
+
case "max":
|
|
5842
|
+
return this.maxNumbers(values);
|
|
5843
|
+
case "count":
|
|
5844
|
+
return values.length;
|
|
5845
|
+
case "countValues":
|
|
5846
|
+
return values.filter((v) => v != null && v !== "").length;
|
|
5847
|
+
case "countEmpty":
|
|
5848
|
+
return values.filter((v) => v == null || v === "").length;
|
|
5849
|
+
case "percentFilled": {
|
|
5850
|
+
if (values.length === 0) return 0;
|
|
5851
|
+
const filled = values.filter((v) => v != null && v !== "").length;
|
|
5852
|
+
return filled / values.length * 100;
|
|
5853
|
+
}
|
|
5854
|
+
case "percentEmpty": {
|
|
5855
|
+
if (values.length === 0) return 0;
|
|
5856
|
+
const empty = values.filter((v) => v == null || v === "").length;
|
|
5857
|
+
return empty / values.length * 100;
|
|
5858
|
+
}
|
|
5859
|
+
case "collect":
|
|
5860
|
+
return values;
|
|
5861
|
+
}
|
|
5862
|
+
}
|
|
5863
|
+
/**
|
|
5864
|
+
* Get the default empty value for a rollup function
|
|
5865
|
+
*/
|
|
5866
|
+
getEmptyValue(fn) {
|
|
5867
|
+
switch (fn) {
|
|
5868
|
+
case "sum":
|
|
5869
|
+
case "count":
|
|
5870
|
+
case "countValues":
|
|
5871
|
+
case "countEmpty":
|
|
5872
|
+
case "percentEmpty":
|
|
5873
|
+
case "percentFilled":
|
|
5874
|
+
return 0;
|
|
5875
|
+
case "avg":
|
|
5876
|
+
case "min":
|
|
5877
|
+
case "max":
|
|
5878
|
+
return null;
|
|
5879
|
+
case "collect":
|
|
5880
|
+
return [];
|
|
5881
|
+
}
|
|
5882
|
+
}
|
|
5883
|
+
/**
|
|
5884
|
+
* Sum numeric values
|
|
5885
|
+
*/
|
|
5886
|
+
sumNumbers(values) {
|
|
5887
|
+
return values.filter((v) => typeof v === "number" && !Number.isNaN(v)).reduce((sum, n) => sum + n, 0);
|
|
5888
|
+
}
|
|
5889
|
+
/**
|
|
5890
|
+
* Average numeric values
|
|
5891
|
+
*/
|
|
5892
|
+
averageNumbers(values) {
|
|
5893
|
+
const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
|
|
5894
|
+
if (nums.length === 0) return null;
|
|
5895
|
+
return nums.reduce((sum, n) => sum + n, 0) / nums.length;
|
|
5896
|
+
}
|
|
5897
|
+
/**
|
|
5898
|
+
* Minimum numeric value
|
|
5899
|
+
*/
|
|
5900
|
+
minNumbers(values) {
|
|
5901
|
+
const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
|
|
5902
|
+
if (nums.length === 0) return null;
|
|
5903
|
+
return Math.min(...nums);
|
|
5904
|
+
}
|
|
5905
|
+
/**
|
|
5906
|
+
* Maximum numeric value
|
|
5907
|
+
*/
|
|
5908
|
+
maxNumbers(values) {
|
|
5909
|
+
const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
|
|
5910
|
+
if (nums.length === 0) return null;
|
|
5911
|
+
return Math.max(...nums);
|
|
5912
|
+
}
|
|
5913
|
+
/**
|
|
5914
|
+
* Recalculate all rollup attributes for a record and update it
|
|
5915
|
+
*
|
|
5916
|
+
* Called after related records change to keep rollups up-to-date.
|
|
5917
|
+
*/
|
|
5918
|
+
async recalculateAndUpdate(record, schema) {
|
|
5919
|
+
const rollupAttrs = schema.attributes.filter((a) => a.type === "rollup");
|
|
5920
|
+
if (rollupAttrs.length === 0) {
|
|
5921
|
+
return record;
|
|
5922
|
+
}
|
|
5923
|
+
const updates = {};
|
|
5924
|
+
for (const attr of rollupAttrs) {
|
|
5925
|
+
const result = await this.calculate(record.id, attr, schema);
|
|
5926
|
+
updates[attr.name] = result.value;
|
|
5927
|
+
}
|
|
5928
|
+
return await this.adapter.objectRecords.update(record.id, updates);
|
|
5929
|
+
}
|
|
5930
|
+
/**
|
|
5931
|
+
* Find parent records that need rollup recalculation when a child record changes
|
|
5932
|
+
*
|
|
5933
|
+
* Used by hooks to determine which parent records to recalculate after
|
|
5934
|
+
* a child record is created, updated, or deleted.
|
|
5935
|
+
*
|
|
5936
|
+
* @param changedRecord - The record that was modified
|
|
5937
|
+
* @param changedSchema - Schema of the changed record's object
|
|
5938
|
+
* @returns Array of parent record IDs that need recalculation
|
|
5939
|
+
*/
|
|
5940
|
+
async findAffectedParentRecords(changedRecord, changedSchema) {
|
|
5941
|
+
const affectedIds = [];
|
|
5942
|
+
const relationAttrs = changedSchema.attributes.filter(
|
|
5943
|
+
(a) => a.type === "relation"
|
|
5944
|
+
);
|
|
5945
|
+
for (const attr of relationAttrs) {
|
|
5946
|
+
const relatedId = changedRecord.values[attr.name];
|
|
5947
|
+
if (typeof relatedId === "string" && relatedId.length > 0) {
|
|
5948
|
+
affectedIds.push(relatedId);
|
|
5949
|
+
} else if (Array.isArray(relatedId)) {
|
|
5950
|
+
affectedIds.push(...relatedId.filter((id) => typeof id === "string"));
|
|
5951
|
+
}
|
|
5952
|
+
}
|
|
5953
|
+
return [...new Set(affectedIds)];
|
|
5954
|
+
}
|
|
5955
|
+
/**
|
|
5956
|
+
* Find records that have forward rollups pointing to the modified record.
|
|
5957
|
+
*
|
|
5958
|
+
* Forward rollups are rollups where the record has a relation attribute
|
|
5959
|
+
* pointing to another object, and the rollup aggregates values from that target.
|
|
5960
|
+
* When the target record changes, we need to recalculate these rollups.
|
|
5961
|
+
*
|
|
5962
|
+
* Example: Order has relation "company" → Company, and rollup "capitalSocial"
|
|
5963
|
+
* aggregating from the Company. When Company.capitalSocial changes,
|
|
5964
|
+
* all Orders pointing to that Company need their rollup recalculated.
|
|
5965
|
+
*
|
|
5966
|
+
* @param changedRecord - The record that was modified
|
|
5967
|
+
* @param changedSchema - Schema of the changed record's object
|
|
5968
|
+
* @returns Array of records that need their forward rollups recalculated
|
|
5969
|
+
*/
|
|
5970
|
+
async findRecordsWithForwardRollup(changedRecord, changedSchema) {
|
|
5971
|
+
const affectedRecords = [];
|
|
5972
|
+
const recordObject = await this.adapter.objects.findById(changedRecord.objectId);
|
|
5973
|
+
if (!recordObject) {
|
|
5974
|
+
return affectedRecords;
|
|
5975
|
+
}
|
|
5976
|
+
const tenantId = recordObject.tenantId;
|
|
5977
|
+
const allObjects = await this.adapter.objects.list(tenantId);
|
|
5978
|
+
for (const obj of allObjects) {
|
|
5979
|
+
if (obj.id === changedRecord.objectId) {
|
|
5980
|
+
continue;
|
|
5981
|
+
}
|
|
5982
|
+
const attributes = await this.adapter.attributes.findByObjectId(obj.id);
|
|
5983
|
+
const rollupAttrs = attributes.filter((a) => a.type === "rollup");
|
|
5984
|
+
if (rollupAttrs.length === 0) {
|
|
5985
|
+
continue;
|
|
5986
|
+
}
|
|
5987
|
+
for (const rollupDbAttr of rollupAttrs) {
|
|
5988
|
+
const rollupConfig = rollupDbAttr.config;
|
|
5989
|
+
if (!rollupConfig?.relationAttribute) {
|
|
5990
|
+
continue;
|
|
5991
|
+
}
|
|
5992
|
+
const relationAttr = attributes.find(
|
|
5993
|
+
(a) => a.type === "relation" && a.name === rollupConfig.relationAttribute
|
|
5994
|
+
);
|
|
5995
|
+
if (!relationAttr) {
|
|
5996
|
+
continue;
|
|
5997
|
+
}
|
|
5998
|
+
const relationConfig = relationAttr.config;
|
|
5999
|
+
const targetsChangedObject = relationConfig?.targets?.some(
|
|
6000
|
+
(t) => t.object === changedSchema.name
|
|
6001
|
+
);
|
|
6002
|
+
if (!targetsChangedObject) {
|
|
6003
|
+
continue;
|
|
6004
|
+
}
|
|
6005
|
+
const recordsPointingToChanged = await this.adapter.objectRecords.findByRelation(
|
|
6006
|
+
tenantId,
|
|
6007
|
+
obj.id,
|
|
6008
|
+
relationAttr.name,
|
|
6009
|
+
changedRecord.id
|
|
6010
|
+
);
|
|
6011
|
+
affectedRecords.push(...recordsPointingToChanged);
|
|
6012
|
+
}
|
|
6013
|
+
}
|
|
6014
|
+
const uniqueRecords = /* @__PURE__ */ new Map();
|
|
6015
|
+
for (const record of affectedRecords) {
|
|
6016
|
+
uniqueRecords.set(record.id, record);
|
|
6017
|
+
}
|
|
6018
|
+
return Array.from(uniqueRecords.values());
|
|
6019
|
+
}
|
|
6020
|
+
};
|
|
6021
|
+
|
|
5453
6022
|
// src/runtime/services/user.service.ts
|
|
5454
6023
|
var UserService = class {
|
|
5455
6024
|
constructor(adapter, tenantId) {
|
|
@@ -5572,9 +6141,14 @@ var RecordService = class {
|
|
|
5572
6141
|
constructor(adapter, tenantId, options) {
|
|
5573
6142
|
this.adapter = adapter;
|
|
5574
6143
|
this.tenantId = tenantId;
|
|
5575
|
-
this.schemaService = new ObjectSchemaService(adapter, registry
|
|
6144
|
+
this.schemaService = new ObjectSchemaService(adapter, registry, {
|
|
6145
|
+
userId: options?.userId,
|
|
6146
|
+
userEmail: options?.userEmail,
|
|
6147
|
+
auditService: options?.auditService
|
|
6148
|
+
});
|
|
5576
6149
|
this.relationService = new RelationService(adapter, registry);
|
|
5577
6150
|
this.userService = new UserService(adapter, tenantId);
|
|
6151
|
+
this.rollupService = new RollupService(adapter);
|
|
5578
6152
|
this.hookRegistry = options?.hookRegistry ?? new NoopHookRegistry();
|
|
5579
6153
|
this.permissionService = options?.permissionService;
|
|
5580
6154
|
this.auditService = options?.auditService ?? (adapter.audit ? new AuditService(adapter, tenantId) : void 0);
|
|
@@ -5726,7 +6300,8 @@ var RecordService = class {
|
|
|
5726
6300
|
objectId,
|
|
5727
6301
|
data,
|
|
5728
6302
|
label,
|
|
5729
|
-
completionStatus
|
|
6303
|
+
completionStatus,
|
|
6304
|
+
metadata: options?.metadata
|
|
5730
6305
|
});
|
|
5731
6306
|
if (!options?.skipHooks) {
|
|
5732
6307
|
const afterCtx = {
|
|
@@ -5736,6 +6311,7 @@ var RecordService = class {
|
|
|
5736
6311
|
};
|
|
5737
6312
|
await this.hookRegistry.execute("afterCreate", schema.name, afterCtx);
|
|
5738
6313
|
}
|
|
6314
|
+
await this.recalculateParentRollups(record, schema);
|
|
5739
6315
|
if (this.auditService && this.userId) {
|
|
5740
6316
|
await this.auditService.logRecordAction({
|
|
5741
6317
|
action: "record.created",
|
|
@@ -5839,11 +6415,15 @@ var RecordService = class {
|
|
|
5839
6415
|
}
|
|
5840
6416
|
const completionStatus = computeRecordStatus(schema, mergedData);
|
|
5841
6417
|
const label = await this.computeLabel(schema, mergedData);
|
|
5842
|
-
const
|
|
6418
|
+
const updatePayload = {
|
|
5843
6419
|
...data,
|
|
5844
6420
|
__completionStatus: completionStatus,
|
|
5845
6421
|
__label: label
|
|
5846
|
-
}
|
|
6422
|
+
};
|
|
6423
|
+
if (options?.metadata !== void 0) {
|
|
6424
|
+
updatePayload.__metadata = options.metadata;
|
|
6425
|
+
}
|
|
6426
|
+
const updated = await this.adapter.objectRecords.update(recordId, updatePayload);
|
|
5847
6427
|
if (!options?.skipHooks) {
|
|
5848
6428
|
const afterCtx = {
|
|
5849
6429
|
...hookCtx,
|
|
@@ -5851,6 +6431,7 @@ var RecordService = class {
|
|
|
5851
6431
|
};
|
|
5852
6432
|
await this.hookRegistry.execute("afterUpdate", schema.name, afterCtx);
|
|
5853
6433
|
}
|
|
6434
|
+
await this.recalculateParentRollups(updated, schema);
|
|
5854
6435
|
if (this.auditService && this.userId && changedAttributes.length > 0) {
|
|
5855
6436
|
const changes = changedAttributes.map((attr) => ({
|
|
5856
6437
|
field: attr,
|
|
@@ -5978,6 +6559,7 @@ var RecordService = class {
|
|
|
5978
6559
|
if (!options?.skipHooks) {
|
|
5979
6560
|
await this.hookRegistry.execute("afterDelete", schema.name, hookCtx);
|
|
5980
6561
|
}
|
|
6562
|
+
await this.recalculateParentRollups(record, schema);
|
|
5981
6563
|
if (this.auditService && this.userId) {
|
|
5982
6564
|
await this.auditService.logRecordAction({
|
|
5983
6565
|
action: "record.deleted",
|
|
@@ -6107,6 +6689,47 @@ var RecordService = class {
|
|
|
6107
6689
|
}
|
|
6108
6690
|
return records.map((record) => this.enrichWithFormulas(record, schema));
|
|
6109
6691
|
}
|
|
6692
|
+
/**
|
|
6693
|
+
* Recalculate rollups after a record changes
|
|
6694
|
+
*
|
|
6695
|
+
* This handles three cases:
|
|
6696
|
+
* 1. The record itself has rollups (e.g., aggregating from related records it points to)
|
|
6697
|
+
* 2. Parent records have rollups that aggregate from this record (reverse pattern)
|
|
6698
|
+
* 3. Records that have forward rollups pointing to this record (forward pattern)
|
|
6699
|
+
*
|
|
6700
|
+
* @param record - The record that was modified
|
|
6701
|
+
* @param schema - Schema of the record's object
|
|
6702
|
+
* @internal
|
|
6703
|
+
*/
|
|
6704
|
+
async recalculateParentRollups(record, schema) {
|
|
6705
|
+
const ownRollupAttrs = schema.attributes.filter(
|
|
6706
|
+
(a) => a.type === "rollup"
|
|
6707
|
+
);
|
|
6708
|
+
if (ownRollupAttrs.length > 0) {
|
|
6709
|
+
await this.rollupService.recalculateAndUpdate(record, schema);
|
|
6710
|
+
}
|
|
6711
|
+
const affectedParentIds = await this.rollupService.findAffectedParentRecords(record, schema);
|
|
6712
|
+
if (affectedParentIds.length > 0) {
|
|
6713
|
+
const parentRecords = await this.adapter.objectRecords.findByIds(affectedParentIds);
|
|
6714
|
+
for (const parentRecord of parentRecords) {
|
|
6715
|
+
const parentSchema = await this.schemaService.getObjectSchema(parentRecord.objectId);
|
|
6716
|
+
const rollupAttrs = parentSchema.attributes.filter(
|
|
6717
|
+
(a) => a.type === "rollup"
|
|
6718
|
+
);
|
|
6719
|
+
if (rollupAttrs.length > 0) {
|
|
6720
|
+
await this.rollupService.recalculateAndUpdate(parentRecord, parentSchema);
|
|
6721
|
+
}
|
|
6722
|
+
}
|
|
6723
|
+
}
|
|
6724
|
+
const affectedForwardRecords = await this.rollupService.findRecordsWithForwardRollup(
|
|
6725
|
+
record,
|
|
6726
|
+
schema
|
|
6727
|
+
);
|
|
6728
|
+
for (const forwardRecord of affectedForwardRecords) {
|
|
6729
|
+
const forwardSchema = await this.schemaService.getObjectSchema(forwardRecord.objectId);
|
|
6730
|
+
await this.rollupService.recalculateAndUpdate(forwardRecord, forwardSchema);
|
|
6731
|
+
}
|
|
6732
|
+
}
|
|
6110
6733
|
/**
|
|
6111
6734
|
* Permanently delete a record (hard delete)
|
|
6112
6735
|
*
|
|
@@ -6380,7 +7003,7 @@ var RollupScheduler = class {
|
|
|
6380
7003
|
clearTimeout(existing.timeout);
|
|
6381
7004
|
}
|
|
6382
7005
|
const timeout = setTimeout(async () => {
|
|
6383
|
-
await this.executeRecalculation(parentId, parentObjectId
|
|
7006
|
+
await this.executeRecalculation(parentId, parentObjectId);
|
|
6384
7007
|
this.pending.delete(key);
|
|
6385
7008
|
}, this.debounceMs);
|
|
6386
7009
|
this.pending.set(key, { parentId, parentObjectId, tenantId, timeout });
|
|
@@ -6407,19 +7030,19 @@ var RollupScheduler = class {
|
|
|
6407
7030
|
const schema = await this.getSchemaById(record.objectId);
|
|
6408
7031
|
const tenantId = tenantMap.get(record.id);
|
|
6409
7032
|
if (schema && tenantId) {
|
|
6410
|
-
await this.rollupService.recalculateAndUpdate(record, schema
|
|
7033
|
+
await this.rollupService.recalculateAndUpdate(record, schema);
|
|
6411
7034
|
}
|
|
6412
7035
|
}
|
|
6413
7036
|
}
|
|
6414
7037
|
/**
|
|
6415
7038
|
* Execute a single recalculation
|
|
6416
7039
|
*/
|
|
6417
|
-
async executeRecalculation(parentId, parentObjectId
|
|
7040
|
+
async executeRecalculation(parentId, parentObjectId) {
|
|
6418
7041
|
const record = await this.adapter.objectRecords.findById(parentId);
|
|
6419
7042
|
if (!record) return;
|
|
6420
7043
|
const schema = await this.getSchemaById(parentObjectId);
|
|
6421
7044
|
if (!schema) return;
|
|
6422
|
-
await this.rollupService.recalculateAndUpdate(record, schema
|
|
7045
|
+
await this.rollupService.recalculateAndUpdate(record, schema);
|
|
6423
7046
|
}
|
|
6424
7047
|
/**
|
|
6425
7048
|
* Get number of pending recalculations
|
|
@@ -7221,17 +7844,22 @@ var NoopGeocodingAdapter = class {
|
|
|
7221
7844
|
NoopHookRegistry,
|
|
7222
7845
|
ObjectSchemaService,
|
|
7223
7846
|
PermissionService,
|
|
7847
|
+
QueryBuilder,
|
|
7848
|
+
QueryMultipleResultsError,
|
|
7849
|
+
QueryNoResultError,
|
|
7224
7850
|
RecordService,
|
|
7225
7851
|
RelationResolverService,
|
|
7226
7852
|
RelationService,
|
|
7227
7853
|
RollupScheduler,
|
|
7228
7854
|
RollupService,
|
|
7855
|
+
SHORTCUT_TO_FILTER_OPERATOR,
|
|
7229
7856
|
UserProfileService,
|
|
7230
7857
|
UserService,
|
|
7231
7858
|
ViewService,
|
|
7232
7859
|
buildAuditChanges,
|
|
7860
|
+
createDefaultState,
|
|
7233
7861
|
createMockAdapter,
|
|
7234
|
-
|
|
7862
|
+
createQueryBuilder,
|
|
7235
7863
|
enrichValuesWithSelectLabels,
|
|
7236
7864
|
evaluateFormula,
|
|
7237
7865
|
evaluateFormulaAttribute,
|
|
@@ -7244,6 +7872,8 @@ var NoopGeocodingAdapter = class {
|
|
|
7244
7872
|
extractRelationReferences,
|
|
7245
7873
|
flattenRelationsForEval,
|
|
7246
7874
|
formatFormulaResult,
|
|
7875
|
+
formatRecord,
|
|
7876
|
+
formatRecords,
|
|
7247
7877
|
getPathDepth,
|
|
7248
7878
|
getRelationPath,
|
|
7249
7879
|
getSyncPreview,
|
|
@@ -7253,7 +7883,6 @@ var NoopGeocodingAdapter = class {
|
|
|
7253
7883
|
isLabelExpression,
|
|
7254
7884
|
parsePath,
|
|
7255
7885
|
pathHasManyCardinality,
|
|
7256
|
-
registerAllRollupHooks,
|
|
7257
7886
|
renderLabelExpression,
|
|
7258
7887
|
resolveMultiplePaths,
|
|
7259
7888
|
resolveSingleValue,
|