@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/index.js
CHANGED
|
@@ -159,6 +159,9 @@ __export(index_exports, {
|
|
|
159
159
|
PermissionService: () => PermissionService,
|
|
160
160
|
ProtectedResourceError: () => ProtectedResourceError,
|
|
161
161
|
ProtectedRoleError: () => ProtectedRoleError,
|
|
162
|
+
QueryBuilder: () => QueryBuilder,
|
|
163
|
+
QueryMultipleResultsError: () => QueryMultipleResultsError,
|
|
164
|
+
QueryNoResultError: () => QueryNoResultError,
|
|
162
165
|
RecordNotFoundError: () => RecordNotFoundError,
|
|
163
166
|
RecordReferencedError: () => RecordReferencedError,
|
|
164
167
|
RecordService: () => RecordService,
|
|
@@ -167,6 +170,8 @@ __export(index_exports, {
|
|
|
167
170
|
RoleNotFoundError: () => RoleNotFoundError,
|
|
168
171
|
RollupScheduler: () => RollupScheduler,
|
|
169
172
|
RollupService: () => RollupService,
|
|
173
|
+
SHORTCUT_TO_FILTER_OPERATOR: () => SHORTCUT_TO_FILTER_OPERATOR,
|
|
174
|
+
SYSTEM_FIELD_NAMES: () => SYSTEM_FIELD_NAMES,
|
|
170
175
|
SYSTEM_RESOURCES: () => SYSTEM_RESOURCES,
|
|
171
176
|
SYSTEM_RESOURCE_LABELS: () => SYSTEM_RESOURCE_LABELS,
|
|
172
177
|
SchemaError: () => SchemaError,
|
|
@@ -189,6 +194,7 @@ __export(index_exports, {
|
|
|
189
194
|
createCheckboxValidator: () => createCheckboxValidator,
|
|
190
195
|
createCurrencyValidator: () => createCurrencyValidator,
|
|
191
196
|
createDateValidator: () => createDateValidator,
|
|
197
|
+
createDefaultState: () => createDefaultState,
|
|
192
198
|
createDraftValidator: () => createDraftValidator,
|
|
193
199
|
createFileValidator: () => createFileValidator,
|
|
194
200
|
createFormulaValidator: () => createFormulaValidator,
|
|
@@ -199,9 +205,9 @@ __export(index_exports, {
|
|
|
199
205
|
createNumberValidator: () => createNumberValidator,
|
|
200
206
|
createObjectValidator: () => createObjectValidator,
|
|
201
207
|
createPhoneValidator: () => createPhoneValidator,
|
|
208
|
+
createQueryBuilder: () => createQueryBuilder,
|
|
202
209
|
createRatingValidator: () => createRatingValidator,
|
|
203
210
|
createRelationValidator: () => createRelationValidator,
|
|
204
|
-
createRollupHooks: () => createRollupHooks,
|
|
205
211
|
createRollupValidator: () => createRollupValidator,
|
|
206
212
|
createSelectValidator: () => createSelectValidator,
|
|
207
213
|
createSingleRelationValidator: () => createSingleRelationValidator,
|
|
@@ -229,6 +235,8 @@ __export(index_exports, {
|
|
|
229
235
|
flow: () => flow,
|
|
230
236
|
formatAttributeValue: () => formatAttributeValue,
|
|
231
237
|
formatFormulaResult: () => formatFormulaResult,
|
|
238
|
+
formatRecord: () => formatRecord,
|
|
239
|
+
formatRecords: () => formatRecords,
|
|
232
240
|
formula: () => formula,
|
|
233
241
|
formulaConfigSchema: () => formulaConfigSchema,
|
|
234
242
|
generateId: () => generateId,
|
|
@@ -273,7 +281,6 @@ __export(index_exports, {
|
|
|
273
281
|
phoneConfigSchema: () => phoneConfigSchema,
|
|
274
282
|
rating: () => rating,
|
|
275
283
|
ratingConfigSchema: () => ratingConfigSchema,
|
|
276
|
-
registerAllRollupHooks: () => registerAllRollupHooks,
|
|
277
284
|
registry: () => registry,
|
|
278
285
|
relation: () => relation,
|
|
279
286
|
relationConfigSchema: () => relationConfigSchema,
|
|
@@ -484,6 +491,9 @@ var NoopGeocodingAdapter = class {
|
|
|
484
491
|
}
|
|
485
492
|
};
|
|
486
493
|
|
|
494
|
+
// src/types/inference.ts
|
|
495
|
+
var SYSTEM_FIELD_NAMES = ["id", "createdAt", "updatedAt"];
|
|
496
|
+
|
|
487
497
|
// src/types/views.ts
|
|
488
498
|
function isFormTab(tab) {
|
|
489
499
|
return tab.type === "form";
|
|
@@ -891,6 +901,7 @@ var VALID_ICONS = new Set(import_constants2.ICONS);
|
|
|
891
901
|
var ATTRIBUTE_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
892
902
|
var MAX_NAME_LENGTH = 63;
|
|
893
903
|
var MAX_LABEL_LENGTH = 128;
|
|
904
|
+
var RESERVED_ATTRIBUTE_NAMES = ["id", "createdAt", "updatedAt", "metadata"];
|
|
894
905
|
function validateAttributeName(name) {
|
|
895
906
|
if (!name || name.length === 0) {
|
|
896
907
|
throw new Error("[AttributeBuilder] Attribute name cannot be empty");
|
|
@@ -905,6 +916,12 @@ function validateAttributeName(name) {
|
|
|
905
916
|
"[AttributeBuilder] 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'"
|
|
906
917
|
);
|
|
907
918
|
}
|
|
919
|
+
if (RESERVED_ATTRIBUTE_NAMES.includes(name)) {
|
|
920
|
+
throw new Error(
|
|
921
|
+
`[AttributeBuilder] "${name}" is a reserved name and cannot be used as an attribute name.
|
|
922
|
+
Reserved names: ${RESERVED_ATTRIBUTE_NAMES.join(", ")}`
|
|
923
|
+
);
|
|
924
|
+
}
|
|
908
925
|
}
|
|
909
926
|
function validateLabel(label) {
|
|
910
927
|
if (!label || label.length === 0) {
|
|
@@ -1628,6 +1645,14 @@ var RollupAttributeBuilder = class extends BaseAttributeBuilder {
|
|
|
1628
1645
|
this.attr.decimals = value;
|
|
1629
1646
|
return this;
|
|
1630
1647
|
}
|
|
1648
|
+
/**
|
|
1649
|
+
* Display the rollup value using the target attribute's type formatter
|
|
1650
|
+
* instead of the default rollup-specific formatting
|
|
1651
|
+
*/
|
|
1652
|
+
showAsOriginal() {
|
|
1653
|
+
this.attr.showAsOriginal = true;
|
|
1654
|
+
return this;
|
|
1655
|
+
}
|
|
1631
1656
|
/**
|
|
1632
1657
|
* Set multi-level path for traversing nested relations (Phase 4+)
|
|
1633
1658
|
* @param path - Dot notation path (e.g., "orders.items")
|
|
@@ -2965,10 +2990,12 @@ var rollupConfigSchema = baseConfigSchema.extend({
|
|
|
2965
2990
|
"countValues",
|
|
2966
2991
|
"countEmpty",
|
|
2967
2992
|
"percentEmpty",
|
|
2968
|
-
"percentFilled"
|
|
2993
|
+
"percentFilled",
|
|
2994
|
+
"collect"
|
|
2969
2995
|
]),
|
|
2970
2996
|
decimals: import_zod4.z.number().int().min(0).max(10).optional(),
|
|
2971
|
-
|
|
2997
|
+
showAsOriginal: import_zod4.z.boolean().optional(),
|
|
2998
|
+
targetAttributeType: import_zod4.z.string().optional()
|
|
2972
2999
|
});
|
|
2973
3000
|
var attributeConfigSchemas = {
|
|
2974
3001
|
text: textConfigSchema,
|
|
@@ -3049,7 +3076,7 @@ function createCheckboxValidator(_attr) {
|
|
|
3049
3076
|
return import_zod4.z.boolean();
|
|
3050
3077
|
}
|
|
3051
3078
|
function createDateValidator(attr) {
|
|
3052
|
-
return import_zod4.z.
|
|
3079
|
+
return import_zod4.z.coerce.date({ message: `${attr.label} must be a valid ISO date` });
|
|
3053
3080
|
}
|
|
3054
3081
|
function createPhoneValidator(_attr) {
|
|
3055
3082
|
return import_zod4.z.object({
|
|
@@ -3155,7 +3182,7 @@ function createRelationValidator(attr) {
|
|
|
3155
3182
|
return createSingleRelationValidator(attr);
|
|
3156
3183
|
}
|
|
3157
3184
|
function createRatingValidator(attr) {
|
|
3158
|
-
let schema = import_zod4.z.number().
|
|
3185
|
+
let schema = import_zod4.z.number().min(0);
|
|
3159
3186
|
if (attr.max !== void 0) {
|
|
3160
3187
|
schema = schema.max(attr.max, `${attr.label} must be at most ${attr.max}`);
|
|
3161
3188
|
}
|
|
@@ -3328,6 +3355,477 @@ function computeRecordStatus(objectDef, data) {
|
|
|
3328
3355
|
return isRecordComplete(objectDef, data) ? "complete" : "draft";
|
|
3329
3356
|
}
|
|
3330
3357
|
|
|
3358
|
+
// src/runtime/client/types.ts
|
|
3359
|
+
function formatRecord(record) {
|
|
3360
|
+
return {
|
|
3361
|
+
id: record.id,
|
|
3362
|
+
createdAt: record.createdAt,
|
|
3363
|
+
updatedAt: record.updatedAt,
|
|
3364
|
+
metadata: record.metadata ?? {},
|
|
3365
|
+
...record.values
|
|
3366
|
+
};
|
|
3367
|
+
}
|
|
3368
|
+
function formatRecords(records) {
|
|
3369
|
+
return records.map((r) => formatRecord(r));
|
|
3370
|
+
}
|
|
3371
|
+
function createDefaultState(objectName) {
|
|
3372
|
+
return {
|
|
3373
|
+
objectName,
|
|
3374
|
+
filters: [],
|
|
3375
|
+
combinator: "and",
|
|
3376
|
+
sorts: [],
|
|
3377
|
+
raw: false,
|
|
3378
|
+
includeDeleted: false
|
|
3379
|
+
};
|
|
3380
|
+
}
|
|
3381
|
+
var SHORTCUT_TO_FILTER_OPERATOR = {
|
|
3382
|
+
eq: "is",
|
|
3383
|
+
neq: "is_not",
|
|
3384
|
+
gt: "gt",
|
|
3385
|
+
gte: "gte",
|
|
3386
|
+
lt: "lt",
|
|
3387
|
+
lte: "lte",
|
|
3388
|
+
contains: "contains",
|
|
3389
|
+
notContains: "not_contains",
|
|
3390
|
+
startsWith: "starts_with",
|
|
3391
|
+
endsWith: "ends_with",
|
|
3392
|
+
isEmpty: "is_empty",
|
|
3393
|
+
isNotEmpty: "is_not_empty"
|
|
3394
|
+
};
|
|
3395
|
+
var QueryNoResultError = class extends Error {
|
|
3396
|
+
constructor(objectName, filters) {
|
|
3397
|
+
const filterInfo = filters?.length ? ` with filters: ${JSON.stringify(filters)}` : "";
|
|
3398
|
+
super(`No record found in "${objectName}"${filterInfo}`);
|
|
3399
|
+
this.name = "QueryNoResultError";
|
|
3400
|
+
this.objectName = objectName;
|
|
3401
|
+
this.filters = filters;
|
|
3402
|
+
}
|
|
3403
|
+
};
|
|
3404
|
+
var QueryMultipleResultsError = class extends Error {
|
|
3405
|
+
constructor(objectName, count) {
|
|
3406
|
+
super(
|
|
3407
|
+
`Expected single record in "${objectName}", but found ${count}. Use first() or add more filters.`
|
|
3408
|
+
);
|
|
3409
|
+
this.name = "QueryMultipleResultsError";
|
|
3410
|
+
this.objectName = objectName;
|
|
3411
|
+
this.count = count;
|
|
3412
|
+
}
|
|
3413
|
+
};
|
|
3414
|
+
|
|
3415
|
+
// src/runtime/client/query-builder.ts
|
|
3416
|
+
var QueryBuilder = class _QueryBuilder {
|
|
3417
|
+
constructor(recordService, adapter, objectName, state) {
|
|
3418
|
+
this.objectId = null;
|
|
3419
|
+
this.recordService = recordService;
|
|
3420
|
+
this.adapter = adapter;
|
|
3421
|
+
this.state = {
|
|
3422
|
+
...createDefaultState(objectName),
|
|
3423
|
+
...state
|
|
3424
|
+
};
|
|
3425
|
+
}
|
|
3426
|
+
/**
|
|
3427
|
+
* Clone the builder with new state (immutability pattern)
|
|
3428
|
+
*/
|
|
3429
|
+
clone(updates) {
|
|
3430
|
+
return new _QueryBuilder(this.recordService, this.adapter, this.state.objectName, {
|
|
3431
|
+
...this.state,
|
|
3432
|
+
...updates
|
|
3433
|
+
});
|
|
3434
|
+
}
|
|
3435
|
+
/**
|
|
3436
|
+
* Resolve object name to object ID (cached)
|
|
3437
|
+
*/
|
|
3438
|
+
async resolveObjectId() {
|
|
3439
|
+
if (this.objectId) return this.objectId;
|
|
3440
|
+
const tenantId = this.recordService.tenantId;
|
|
3441
|
+
const dbObject = await this.adapter.objects.findByName(tenantId, this.state.objectName);
|
|
3442
|
+
if (!dbObject) {
|
|
3443
|
+
throw new Error(`Object "${this.state.objectName}" not found`);
|
|
3444
|
+
}
|
|
3445
|
+
this.objectId = dbObject.id;
|
|
3446
|
+
return dbObject.id;
|
|
3447
|
+
}
|
|
3448
|
+
// ============================================================================
|
|
3449
|
+
// FILTER METHODS
|
|
3450
|
+
// ============================================================================
|
|
3451
|
+
/**
|
|
3452
|
+
* Add a filter rule with explicit operator
|
|
3453
|
+
*
|
|
3454
|
+
* @example
|
|
3455
|
+
* ```typescript
|
|
3456
|
+
* qb.where('status', 'is', 'active')
|
|
3457
|
+
* .where('price', 'gte', 100)
|
|
3458
|
+
* ```
|
|
3459
|
+
*/
|
|
3460
|
+
where(attribute, operator, value) {
|
|
3461
|
+
const rule = { attribute, operator, value };
|
|
3462
|
+
return this.clone({
|
|
3463
|
+
filters: [...this.state.filters, rule]
|
|
3464
|
+
});
|
|
3465
|
+
}
|
|
3466
|
+
/**
|
|
3467
|
+
* Set filter combinator (default: 'and')
|
|
3468
|
+
*/
|
|
3469
|
+
or() {
|
|
3470
|
+
return this.clone({ combinator: "or" });
|
|
3471
|
+
}
|
|
3472
|
+
// ============================================================================
|
|
3473
|
+
// SHORTCUT OPERATORS
|
|
3474
|
+
// ============================================================================
|
|
3475
|
+
/**
|
|
3476
|
+
* Equal (is)
|
|
3477
|
+
*/
|
|
3478
|
+
eq(attribute, value) {
|
|
3479
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.eq, value);
|
|
3480
|
+
}
|
|
3481
|
+
/**
|
|
3482
|
+
* Not equal (is_not)
|
|
3483
|
+
*/
|
|
3484
|
+
neq(attribute, value) {
|
|
3485
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.neq, value);
|
|
3486
|
+
}
|
|
3487
|
+
/**
|
|
3488
|
+
* Greater than
|
|
3489
|
+
*/
|
|
3490
|
+
gt(attribute, value) {
|
|
3491
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.gt, value);
|
|
3492
|
+
}
|
|
3493
|
+
/**
|
|
3494
|
+
* Greater than or equal
|
|
3495
|
+
*/
|
|
3496
|
+
gte(attribute, value) {
|
|
3497
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.gte, value);
|
|
3498
|
+
}
|
|
3499
|
+
/**
|
|
3500
|
+
* Less than
|
|
3501
|
+
*/
|
|
3502
|
+
lt(attribute, value) {
|
|
3503
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.lt, value);
|
|
3504
|
+
}
|
|
3505
|
+
/**
|
|
3506
|
+
* Less than or equal
|
|
3507
|
+
*/
|
|
3508
|
+
lte(attribute, value) {
|
|
3509
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.lte, value);
|
|
3510
|
+
}
|
|
3511
|
+
/**
|
|
3512
|
+
* Contains (text/relation)
|
|
3513
|
+
*/
|
|
3514
|
+
contains(attribute, value) {
|
|
3515
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.contains, value);
|
|
3516
|
+
}
|
|
3517
|
+
/**
|
|
3518
|
+
* Not contains
|
|
3519
|
+
*/
|
|
3520
|
+
notContains(attribute, value) {
|
|
3521
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.notContains, value);
|
|
3522
|
+
}
|
|
3523
|
+
/**
|
|
3524
|
+
* Starts with (text)
|
|
3525
|
+
*/
|
|
3526
|
+
startsWith(attribute, value) {
|
|
3527
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.startsWith, value);
|
|
3528
|
+
}
|
|
3529
|
+
/**
|
|
3530
|
+
* Ends with (text)
|
|
3531
|
+
*/
|
|
3532
|
+
endsWith(attribute, value) {
|
|
3533
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.endsWith, value);
|
|
3534
|
+
}
|
|
3535
|
+
/**
|
|
3536
|
+
* Is empty
|
|
3537
|
+
*/
|
|
3538
|
+
isEmpty(attribute) {
|
|
3539
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.isEmpty, null);
|
|
3540
|
+
}
|
|
3541
|
+
/**
|
|
3542
|
+
* Is not empty
|
|
3543
|
+
*/
|
|
3544
|
+
isNotEmpty(attribute) {
|
|
3545
|
+
return this.where(attribute, SHORTCUT_TO_FILTER_OPERATOR.isNotEmpty, null);
|
|
3546
|
+
}
|
|
3547
|
+
/**
|
|
3548
|
+
* In list (any_of)
|
|
3549
|
+
*/
|
|
3550
|
+
in(attribute, values) {
|
|
3551
|
+
return this.where(attribute, "any_of", values);
|
|
3552
|
+
}
|
|
3553
|
+
/**
|
|
3554
|
+
* Not in list (none_of)
|
|
3555
|
+
*/
|
|
3556
|
+
notIn(attribute, values) {
|
|
3557
|
+
return this.where(attribute, "none_of", values);
|
|
3558
|
+
}
|
|
3559
|
+
// ============================================================================
|
|
3560
|
+
// SORT & PAGINATION
|
|
3561
|
+
// ============================================================================
|
|
3562
|
+
/**
|
|
3563
|
+
* Add sort rule
|
|
3564
|
+
*/
|
|
3565
|
+
orderBy(attribute, direction = "asc") {
|
|
3566
|
+
return this.clone({
|
|
3567
|
+
sorts: [...this.state.sorts, { attribute, direction }]
|
|
3568
|
+
});
|
|
3569
|
+
}
|
|
3570
|
+
/**
|
|
3571
|
+
* Set limit
|
|
3572
|
+
*/
|
|
3573
|
+
limit(count) {
|
|
3574
|
+
return this.clone({ limit: count });
|
|
3575
|
+
}
|
|
3576
|
+
/**
|
|
3577
|
+
* Set offset
|
|
3578
|
+
*/
|
|
3579
|
+
offset(count) {
|
|
3580
|
+
return this.clone({ offset: count });
|
|
3581
|
+
}
|
|
3582
|
+
/**
|
|
3583
|
+
* Include soft-deleted records
|
|
3584
|
+
*/
|
|
3585
|
+
withDeleted() {
|
|
3586
|
+
return this.clone({ includeDeleted: true });
|
|
3587
|
+
}
|
|
3588
|
+
// ============================================================================
|
|
3589
|
+
// GROUP BY (KANBAN)
|
|
3590
|
+
// ============================================================================
|
|
3591
|
+
/**
|
|
3592
|
+
* Group results by an attribute (for Kanban views).
|
|
3593
|
+
* Only status and select attributes are supported.
|
|
3594
|
+
*
|
|
3595
|
+
* @example
|
|
3596
|
+
* ```typescript
|
|
3597
|
+
* const grouped = await qb
|
|
3598
|
+
* .groupBy('status')
|
|
3599
|
+
* .fetchGrouped();
|
|
3600
|
+
* // → { groups: { 'pending': [...], 'active': [...] }, total: 50 }
|
|
3601
|
+
* ```
|
|
3602
|
+
*/
|
|
3603
|
+
groupBy(attribute) {
|
|
3604
|
+
return this.clone({ groupBy: attribute });
|
|
3605
|
+
}
|
|
3606
|
+
// ============================================================================
|
|
3607
|
+
// RAW MODE
|
|
3608
|
+
// ============================================================================
|
|
3609
|
+
/**
|
|
3610
|
+
* Return raw ObjectRecord instead of formatted record.
|
|
3611
|
+
* Includes all metadata (objectId, label, completionStatus, etc.)
|
|
3612
|
+
*/
|
|
3613
|
+
raw() {
|
|
3614
|
+
return this.clone({ raw: true });
|
|
3615
|
+
}
|
|
3616
|
+
// ============================================================================
|
|
3617
|
+
// TERMINATION METHODS (READ)
|
|
3618
|
+
// ============================================================================
|
|
3619
|
+
/**
|
|
3620
|
+
* Fetch records with current filters
|
|
3621
|
+
* Returns formatted records by default, raw ObjectRecords if raw() was called
|
|
3622
|
+
*/
|
|
3623
|
+
async fetch() {
|
|
3624
|
+
const objectId = await this.resolveObjectId();
|
|
3625
|
+
const filters = this.state.filters.length > 0 ? { combinator: this.state.combinator, rules: this.state.filters } : void 0;
|
|
3626
|
+
const result = await this.recordService.listRecords(objectId, {
|
|
3627
|
+
filters,
|
|
3628
|
+
sorts: this.state.sorts.length > 0 ? this.state.sorts : void 0,
|
|
3629
|
+
limit: this.state.limit,
|
|
3630
|
+
offset: this.state.offset,
|
|
3631
|
+
includeDeleted: this.state.includeDeleted
|
|
3632
|
+
});
|
|
3633
|
+
if (this.state.raw) {
|
|
3634
|
+
return result;
|
|
3635
|
+
}
|
|
3636
|
+
return {
|
|
3637
|
+
records: formatRecords(result.records),
|
|
3638
|
+
total: result.total
|
|
3639
|
+
};
|
|
3640
|
+
}
|
|
3641
|
+
/**
|
|
3642
|
+
* Get a single record (throws if 0 or >1 results)
|
|
3643
|
+
*/
|
|
3644
|
+
async single() {
|
|
3645
|
+
const result = await this.limit(2).fetch();
|
|
3646
|
+
if (result.records.length === 0) {
|
|
3647
|
+
throw new QueryNoResultError(this.state.objectName, this.state.filters);
|
|
3648
|
+
}
|
|
3649
|
+
if (result.records.length > 1) {
|
|
3650
|
+
throw new QueryMultipleResultsError(this.state.objectName, result.total);
|
|
3651
|
+
}
|
|
3652
|
+
return result.records[0];
|
|
3653
|
+
}
|
|
3654
|
+
/**
|
|
3655
|
+
* Get first record or null
|
|
3656
|
+
*/
|
|
3657
|
+
async first() {
|
|
3658
|
+
const result = await this.limit(1).fetch();
|
|
3659
|
+
return result.records[0] ?? null;
|
|
3660
|
+
}
|
|
3661
|
+
/**
|
|
3662
|
+
* Get record by ID
|
|
3663
|
+
*/
|
|
3664
|
+
async findById(id) {
|
|
3665
|
+
const record = await this.recordService.getRecord(id);
|
|
3666
|
+
if (!record) return null;
|
|
3667
|
+
if (this.state.raw) {
|
|
3668
|
+
return record;
|
|
3669
|
+
}
|
|
3670
|
+
return formatRecord(record);
|
|
3671
|
+
}
|
|
3672
|
+
/**
|
|
3673
|
+
* Count records matching filters
|
|
3674
|
+
*/
|
|
3675
|
+
async count() {
|
|
3676
|
+
const result = await this.limit(0).fetch();
|
|
3677
|
+
return result.total;
|
|
3678
|
+
}
|
|
3679
|
+
/**
|
|
3680
|
+
* Fetch records grouped by the groupBy attribute (for Kanban views).
|
|
3681
|
+
* Requires groupBy() to be called first.
|
|
3682
|
+
*
|
|
3683
|
+
* @example
|
|
3684
|
+
* ```typescript
|
|
3685
|
+
* const result = await schema.from('tasks')
|
|
3686
|
+
* .groupBy('status')
|
|
3687
|
+
* .fetchGrouped();
|
|
3688
|
+
* // → { groups: { 'todo': [...], 'in_progress': [...], 'done': [...] }, total: 50 }
|
|
3689
|
+
* ```
|
|
3690
|
+
*/
|
|
3691
|
+
async fetchGrouped() {
|
|
3692
|
+
if (!this.state.groupBy) {
|
|
3693
|
+
throw new Error("groupBy() must be called before fetchGrouped()");
|
|
3694
|
+
}
|
|
3695
|
+
const result = await this.fetch();
|
|
3696
|
+
const groupByAttr = this.state.groupBy;
|
|
3697
|
+
const groups = {};
|
|
3698
|
+
for (const record of result.records) {
|
|
3699
|
+
const value = record[groupByAttr];
|
|
3700
|
+
const key = value != null ? String(value) : "__null__";
|
|
3701
|
+
if (!groups[key]) {
|
|
3702
|
+
groups[key] = [];
|
|
3703
|
+
}
|
|
3704
|
+
groups[key].push(record);
|
|
3705
|
+
}
|
|
3706
|
+
return {
|
|
3707
|
+
groups,
|
|
3708
|
+
total: result.total
|
|
3709
|
+
};
|
|
3710
|
+
}
|
|
3711
|
+
// ============================================================================
|
|
3712
|
+
// TERMINATION METHODS (WRITE)
|
|
3713
|
+
// ============================================================================
|
|
3714
|
+
/**
|
|
3715
|
+
* Create a new record
|
|
3716
|
+
* System fields (id, createdAt, updatedAt) are managed automatically.
|
|
3717
|
+
*
|
|
3718
|
+
* @example
|
|
3719
|
+
* ```typescript
|
|
3720
|
+
* const product = await schema.from('products')
|
|
3721
|
+
* .insert({ name: 'iPhone', price: 999 });
|
|
3722
|
+
*
|
|
3723
|
+
* // Allow draft (missing required fields)
|
|
3724
|
+
* const draft = await schema.from('products')
|
|
3725
|
+
* .insert({ name: 'Draft' }, { allowDraft: true });
|
|
3726
|
+
*
|
|
3727
|
+
* // With custom metadata
|
|
3728
|
+
* const withMeta = await schema.from('products')
|
|
3729
|
+
* .insert({ name: 'iPhone' }, { metadata: { externalId: 'ext-123' } });
|
|
3730
|
+
* ```
|
|
3731
|
+
*/
|
|
3732
|
+
async insert(data, options) {
|
|
3733
|
+
const objectId = await this.resolveObjectId();
|
|
3734
|
+
const record = await this.recordService.createRecord(
|
|
3735
|
+
objectId,
|
|
3736
|
+
data,
|
|
3737
|
+
{
|
|
3738
|
+
allowDraft: options?.allowDraft,
|
|
3739
|
+
validate: options?.validate,
|
|
3740
|
+
metadata: options?.metadata
|
|
3741
|
+
}
|
|
3742
|
+
);
|
|
3743
|
+
if (this.state.raw) {
|
|
3744
|
+
return record;
|
|
3745
|
+
}
|
|
3746
|
+
return formatRecord(record);
|
|
3747
|
+
}
|
|
3748
|
+
/**
|
|
3749
|
+
* Update records matching filters (expects exactly 1 record by default)
|
|
3750
|
+
* Use with eq('id', ...) for single record update.
|
|
3751
|
+
* System fields (id, createdAt, updatedAt) are managed automatically.
|
|
3752
|
+
*
|
|
3753
|
+
* @example
|
|
3754
|
+
* ```typescript
|
|
3755
|
+
* const updated = await schema.from('products')
|
|
3756
|
+
* .eq('id', 'rec-123')
|
|
3757
|
+
* .update({ price: 899 });
|
|
3758
|
+
*
|
|
3759
|
+
* // Update with metadata (replaces existing metadata)
|
|
3760
|
+
* const withMeta = await schema.from('products')
|
|
3761
|
+
* .eq('id', 'rec-123')
|
|
3762
|
+
* .update({ price: 899 }, { metadata: { synced: true } });
|
|
3763
|
+
* ```
|
|
3764
|
+
*/
|
|
3765
|
+
async update(data, options) {
|
|
3766
|
+
const existing = await this.single();
|
|
3767
|
+
const recordId = this.state.raw ? existing.id : existing.id;
|
|
3768
|
+
const record = await this.recordService.updateRecord(
|
|
3769
|
+
recordId,
|
|
3770
|
+
data,
|
|
3771
|
+
{
|
|
3772
|
+
partial: true,
|
|
3773
|
+
metadata: options?.metadata
|
|
3774
|
+
}
|
|
3775
|
+
);
|
|
3776
|
+
if (this.state.raw) {
|
|
3777
|
+
return record;
|
|
3778
|
+
}
|
|
3779
|
+
return formatRecord(record);
|
|
3780
|
+
}
|
|
3781
|
+
/**
|
|
3782
|
+
* Delete record matching filters (expects exactly 1 record)
|
|
3783
|
+
*
|
|
3784
|
+
* @example
|
|
3785
|
+
* ```typescript
|
|
3786
|
+
* await schema.from('products')
|
|
3787
|
+
* .eq('id', 'rec-123')
|
|
3788
|
+
* .delete();
|
|
3789
|
+
* ```
|
|
3790
|
+
*/
|
|
3791
|
+
async delete() {
|
|
3792
|
+
const existing = await this.single();
|
|
3793
|
+
const recordId = this.state.raw ? existing.id : existing.id;
|
|
3794
|
+
await this.recordService.deleteRecord(recordId);
|
|
3795
|
+
}
|
|
3796
|
+
/**
|
|
3797
|
+
* Upsert (update if exists by ID, insert otherwise)
|
|
3798
|
+
* Provide 'id' for lookup. System fields (createdAt, updatedAt) are managed automatically.
|
|
3799
|
+
*
|
|
3800
|
+
* @example
|
|
3801
|
+
* ```typescript
|
|
3802
|
+
* // Insert if not exists, update if exists
|
|
3803
|
+
* const product = await schema.from('products')
|
|
3804
|
+
* .upsert({ id: 'rec-123', name: 'iPhone', price: 999 });
|
|
3805
|
+
*
|
|
3806
|
+
* // With metadata
|
|
3807
|
+
* const withMeta = await schema.from('products')
|
|
3808
|
+
* .upsert({ name: 'iPhone' }, { metadata: { source: 'import' } });
|
|
3809
|
+
* ```
|
|
3810
|
+
*/
|
|
3811
|
+
async upsert(data, options) {
|
|
3812
|
+
const { id, ...rest } = data;
|
|
3813
|
+
const writeData = rest;
|
|
3814
|
+
if (id) {
|
|
3815
|
+
const existing = await this.findById(id);
|
|
3816
|
+
if (existing) {
|
|
3817
|
+
return this.eq("id", id).update(writeData, {
|
|
3818
|
+
metadata: options?.metadata
|
|
3819
|
+
});
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
return this.insert(writeData, options);
|
|
3823
|
+
}
|
|
3824
|
+
};
|
|
3825
|
+
function createQueryBuilder(recordService, adapter, objectName) {
|
|
3826
|
+
return new QueryBuilder(recordService, adapter, objectName);
|
|
3827
|
+
}
|
|
3828
|
+
|
|
3331
3829
|
// src/runtime/formula/evaluator.ts
|
|
3332
3830
|
var import_expr_eval = require("expr-eval");
|
|
3333
3831
|
function createFormulaParser() {
|
|
@@ -3712,260 +4210,6 @@ var NoopHookRegistry = class {
|
|
|
3712
4210
|
}
|
|
3713
4211
|
};
|
|
3714
4212
|
|
|
3715
|
-
// src/runtime/services/rollup.service.ts
|
|
3716
|
-
var RollupService = class {
|
|
3717
|
-
constructor(adapter) {
|
|
3718
|
-
this.adapter = adapter;
|
|
3719
|
-
}
|
|
3720
|
-
/**
|
|
3721
|
-
* Calculate a rollup value for a record
|
|
3722
|
-
*
|
|
3723
|
-
* @param recordId - ID of the parent record
|
|
3724
|
-
* @param rollupAttr - Rollup attribute definition
|
|
3725
|
-
* @param schema - Schema of the parent object
|
|
3726
|
-
* @returns Computed rollup value
|
|
3727
|
-
*
|
|
3728
|
-
* @example
|
|
3729
|
-
* ```typescript
|
|
3730
|
-
* // Sum all order amounts for a company
|
|
3731
|
-
* const totalOrders = await rollupService.calculate(
|
|
3732
|
-
* "company-123",
|
|
3733
|
-
* {
|
|
3734
|
-
* type: "rollup",
|
|
3735
|
-
* name: "totalOrders",
|
|
3736
|
-
* relationAttribute: "orders",
|
|
3737
|
-
* targetAttribute: "amount",
|
|
3738
|
-
* function: "sum",
|
|
3739
|
-
* ...
|
|
3740
|
-
* },
|
|
3741
|
-
* companySchema
|
|
3742
|
-
* );
|
|
3743
|
-
* ```
|
|
3744
|
-
*/
|
|
3745
|
-
async calculate(recordId, rollupAttr, schema, tenantId) {
|
|
3746
|
-
const relationAttr = schema.attributes.find(
|
|
3747
|
-
(a) => a.type === "relation" && a.name === rollupAttr.relationAttribute
|
|
3748
|
-
);
|
|
3749
|
-
if (!relationAttr) {
|
|
3750
|
-
return { value: null, recordCount: 0 };
|
|
3751
|
-
}
|
|
3752
|
-
const targetObjectName = relationAttr.targets[0]?.object;
|
|
3753
|
-
if (!targetObjectName) {
|
|
3754
|
-
return { value: null, recordCount: 0 };
|
|
3755
|
-
}
|
|
3756
|
-
const targetObject = await this.adapter.objects.findByName(tenantId, targetObjectName);
|
|
3757
|
-
if (!targetObject) {
|
|
3758
|
-
return { value: null, recordCount: 0 };
|
|
3759
|
-
}
|
|
3760
|
-
const relatedRecords = await this.adapter.objectRecords.findByRelation(
|
|
3761
|
-
tenantId,
|
|
3762
|
-
targetObject.id,
|
|
3763
|
-
rollupAttr.relationAttribute,
|
|
3764
|
-
recordId
|
|
3765
|
-
);
|
|
3766
|
-
if (relatedRecords.length === 0) {
|
|
3767
|
-
return { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 };
|
|
3768
|
-
}
|
|
3769
|
-
const values = relatedRecords.map((r) => r.values[rollupAttr.targetAttribute]).filter((v) => v !== void 0);
|
|
3770
|
-
const aggregated = this.aggregate(values, rollupAttr.function);
|
|
3771
|
-
const result = typeof aggregated === "number" && rollupAttr.decimals !== void 0 ? Number(aggregated.toFixed(rollupAttr.decimals)) : aggregated;
|
|
3772
|
-
return { value: result, recordCount: relatedRecords.length };
|
|
3773
|
-
}
|
|
3774
|
-
/**
|
|
3775
|
-
* Calculate rollup values for multiple records (batched)
|
|
3776
|
-
*
|
|
3777
|
-
* More efficient than calling calculate() for each record individually.
|
|
3778
|
-
*/
|
|
3779
|
-
async calculateForMany(recordIds, rollupAttr, schema, tenantId) {
|
|
3780
|
-
const results = /* @__PURE__ */ new Map();
|
|
3781
|
-
for (const id of recordIds) {
|
|
3782
|
-
results.set(id, { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 });
|
|
3783
|
-
}
|
|
3784
|
-
await Promise.all(
|
|
3785
|
-
recordIds.map(async (id) => {
|
|
3786
|
-
const result = await this.calculate(id, rollupAttr, schema, tenantId);
|
|
3787
|
-
results.set(id, result);
|
|
3788
|
-
})
|
|
3789
|
-
);
|
|
3790
|
-
return results;
|
|
3791
|
-
}
|
|
3792
|
-
/**
|
|
3793
|
-
* Apply aggregation function to a set of values
|
|
3794
|
-
*/
|
|
3795
|
-
aggregate(values, fn) {
|
|
3796
|
-
switch (fn) {
|
|
3797
|
-
case "sum":
|
|
3798
|
-
return this.sumNumbers(values);
|
|
3799
|
-
case "avg":
|
|
3800
|
-
return this.averageNumbers(values);
|
|
3801
|
-
case "min":
|
|
3802
|
-
return this.minNumbers(values);
|
|
3803
|
-
case "max":
|
|
3804
|
-
return this.maxNumbers(values);
|
|
3805
|
-
case "count":
|
|
3806
|
-
return values.length;
|
|
3807
|
-
case "countValues":
|
|
3808
|
-
return values.filter((v) => v != null && v !== "").length;
|
|
3809
|
-
case "countEmpty":
|
|
3810
|
-
return values.filter((v) => v == null || v === "").length;
|
|
3811
|
-
case "percentFilled": {
|
|
3812
|
-
if (values.length === 0) return 0;
|
|
3813
|
-
const filled = values.filter((v) => v != null && v !== "").length;
|
|
3814
|
-
return filled / values.length * 100;
|
|
3815
|
-
}
|
|
3816
|
-
case "percentEmpty": {
|
|
3817
|
-
if (values.length === 0) return 0;
|
|
3818
|
-
const empty = values.filter((v) => v == null || v === "").length;
|
|
3819
|
-
return empty / values.length * 100;
|
|
3820
|
-
}
|
|
3821
|
-
}
|
|
3822
|
-
}
|
|
3823
|
-
/**
|
|
3824
|
-
* Get the default empty value for a rollup function
|
|
3825
|
-
*/
|
|
3826
|
-
getEmptyValue(fn) {
|
|
3827
|
-
switch (fn) {
|
|
3828
|
-
case "sum":
|
|
3829
|
-
case "count":
|
|
3830
|
-
case "countValues":
|
|
3831
|
-
case "countEmpty":
|
|
3832
|
-
case "percentEmpty":
|
|
3833
|
-
case "percentFilled":
|
|
3834
|
-
return 0;
|
|
3835
|
-
case "avg":
|
|
3836
|
-
case "min":
|
|
3837
|
-
case "max":
|
|
3838
|
-
return null;
|
|
3839
|
-
}
|
|
3840
|
-
}
|
|
3841
|
-
/**
|
|
3842
|
-
* Sum numeric values
|
|
3843
|
-
*/
|
|
3844
|
-
sumNumbers(values) {
|
|
3845
|
-
return values.filter((v) => typeof v === "number" && !Number.isNaN(v)).reduce((sum, n) => sum + n, 0);
|
|
3846
|
-
}
|
|
3847
|
-
/**
|
|
3848
|
-
* Average numeric values
|
|
3849
|
-
*/
|
|
3850
|
-
averageNumbers(values) {
|
|
3851
|
-
const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
|
|
3852
|
-
if (nums.length === 0) return null;
|
|
3853
|
-
return nums.reduce((sum, n) => sum + n, 0) / nums.length;
|
|
3854
|
-
}
|
|
3855
|
-
/**
|
|
3856
|
-
* Minimum numeric value
|
|
3857
|
-
*/
|
|
3858
|
-
minNumbers(values) {
|
|
3859
|
-
const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
|
|
3860
|
-
if (nums.length === 0) return null;
|
|
3861
|
-
return Math.min(...nums);
|
|
3862
|
-
}
|
|
3863
|
-
/**
|
|
3864
|
-
* Maximum numeric value
|
|
3865
|
-
*/
|
|
3866
|
-
maxNumbers(values) {
|
|
3867
|
-
const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
|
|
3868
|
-
if (nums.length === 0) return null;
|
|
3869
|
-
return Math.max(...nums);
|
|
3870
|
-
}
|
|
3871
|
-
/**
|
|
3872
|
-
* Recalculate all rollup attributes for a record and update it
|
|
3873
|
-
*
|
|
3874
|
-
* Called after related records change to keep rollups up-to-date.
|
|
3875
|
-
*/
|
|
3876
|
-
async recalculateAndUpdate(record, schema, tenantId) {
|
|
3877
|
-
const rollupAttrs = schema.attributes.filter((a) => a.type === "rollup");
|
|
3878
|
-
if (rollupAttrs.length === 0) {
|
|
3879
|
-
return record;
|
|
3880
|
-
}
|
|
3881
|
-
const updates = {};
|
|
3882
|
-
for (const attr of rollupAttrs) {
|
|
3883
|
-
const result = await this.calculate(record.id, attr, schema, tenantId);
|
|
3884
|
-
updates[attr.name] = result.value;
|
|
3885
|
-
}
|
|
3886
|
-
return await this.adapter.objectRecords.update(record.id, updates);
|
|
3887
|
-
}
|
|
3888
|
-
/**
|
|
3889
|
-
* Find parent records that need rollup recalculation when a child record changes
|
|
3890
|
-
*
|
|
3891
|
-
* Used by hooks to determine which parent records to recalculate after
|
|
3892
|
-
* a child record is created, updated, or deleted.
|
|
3893
|
-
*
|
|
3894
|
-
* @param changedRecord - The record that was modified
|
|
3895
|
-
* @param changedSchema - Schema of the changed record's object
|
|
3896
|
-
* @returns Array of parent record IDs that need recalculation
|
|
3897
|
-
*/
|
|
3898
|
-
async findAffectedParentRecords(changedRecord, changedSchema) {
|
|
3899
|
-
const affectedIds = [];
|
|
3900
|
-
const relationAttrs = changedSchema.attributes.filter(
|
|
3901
|
-
(a) => a.type === "relation"
|
|
3902
|
-
);
|
|
3903
|
-
for (const attr of relationAttrs) {
|
|
3904
|
-
const relatedId = changedRecord.values[attr.name];
|
|
3905
|
-
if (typeof relatedId === "string" && relatedId.length > 0) {
|
|
3906
|
-
affectedIds.push(relatedId);
|
|
3907
|
-
} else if (Array.isArray(relatedId)) {
|
|
3908
|
-
affectedIds.push(...relatedId.filter((id) => typeof id === "string"));
|
|
3909
|
-
}
|
|
3910
|
-
}
|
|
3911
|
-
return [...new Set(affectedIds)];
|
|
3912
|
-
}
|
|
3913
|
-
};
|
|
3914
|
-
|
|
3915
|
-
// src/runtime/hooks/rollup.hooks.ts
|
|
3916
|
-
function createRollupHooks(objectName, config) {
|
|
3917
|
-
const rollupService = new RollupService(config.adapter);
|
|
3918
|
-
async function recalculateParentRollups(ctx) {
|
|
3919
|
-
const schema = await config.getSchemaById(ctx.objectId);
|
|
3920
|
-
if (!schema) return;
|
|
3921
|
-
const affectedParentIds = await rollupService.findAffectedParentRecords(ctx.record, schema);
|
|
3922
|
-
if (affectedParentIds.length === 0) return;
|
|
3923
|
-
const parentRecords = await config.adapter.objectRecords.findByIds(affectedParentIds);
|
|
3924
|
-
for (const parentRecord of parentRecords) {
|
|
3925
|
-
const parentSchema = await config.getSchemaById(parentRecord.objectId);
|
|
3926
|
-
if (!parentSchema) continue;
|
|
3927
|
-
const hasRollups = parentSchema.attributes.some(
|
|
3928
|
-
(a) => a.type === "rollup"
|
|
3929
|
-
);
|
|
3930
|
-
if (hasRollups) {
|
|
3931
|
-
await rollupService.recalculateAndUpdate(parentRecord, parentSchema, config.tenantId);
|
|
3932
|
-
}
|
|
3933
|
-
}
|
|
3934
|
-
}
|
|
3935
|
-
return [
|
|
3936
|
-
{
|
|
3937
|
-
type: "afterCreate",
|
|
3938
|
-
objectName,
|
|
3939
|
-
handler: recalculateParentRollups,
|
|
3940
|
-
priority: 100
|
|
3941
|
-
// Run after other hooks
|
|
3942
|
-
},
|
|
3943
|
-
{
|
|
3944
|
-
type: "afterUpdate",
|
|
3945
|
-
objectName,
|
|
3946
|
-
handler: recalculateParentRollups,
|
|
3947
|
-
priority: 100
|
|
3948
|
-
},
|
|
3949
|
-
{
|
|
3950
|
-
type: "afterDelete",
|
|
3951
|
-
objectName,
|
|
3952
|
-
handler: recalculateParentRollups,
|
|
3953
|
-
priority: 100
|
|
3954
|
-
}
|
|
3955
|
-
];
|
|
3956
|
-
}
|
|
3957
|
-
function registerAllRollupHooks(hookRegistry, schemas, config) {
|
|
3958
|
-
for (const schema of schemas) {
|
|
3959
|
-
const hasRelations = schema.attributes.some((a) => a.type === "relation");
|
|
3960
|
-
if (hasRelations && hookRegistry.register) {
|
|
3961
|
-
const hooks = createRollupHooks(schema.name, config);
|
|
3962
|
-
for (const hook of hooks) {
|
|
3963
|
-
hookRegistry.register(hook);
|
|
3964
|
-
}
|
|
3965
|
-
}
|
|
3966
|
-
}
|
|
3967
|
-
}
|
|
3968
|
-
|
|
3969
4213
|
// src/runtime/template.ts
|
|
3970
4214
|
var pipes = {
|
|
3971
4215
|
/** Convert to uppercase */
|
|
@@ -4423,6 +4667,7 @@ function createMockObjectRecordsRepository(stores) {
|
|
|
4423
4667
|
label: data.label,
|
|
4424
4668
|
completionStatus: data.completionStatus,
|
|
4425
4669
|
values: data.data,
|
|
4670
|
+
metadata: data.metadata ?? {},
|
|
4426
4671
|
createdAt: /* @__PURE__ */ new Date(),
|
|
4427
4672
|
updatedAt: /* @__PURE__ */ new Date()
|
|
4428
4673
|
};
|
|
@@ -4435,11 +4680,13 @@ function createMockObjectRecordsRepository(stores) {
|
|
|
4435
4680
|
if (!existing) {
|
|
4436
4681
|
return Promise.reject(new Error(`ObjectRecord ${id} not found`));
|
|
4437
4682
|
}
|
|
4438
|
-
const { __completionStatus, __label, ...valueData } = data;
|
|
4683
|
+
const { __completionStatus, __label, __metadata, ...valueData } = data;
|
|
4439
4684
|
const updated = {
|
|
4440
4685
|
...existing,
|
|
4441
4686
|
label: __label ?? existing.label,
|
|
4442
4687
|
completionStatus: __completionStatus ?? existing.completionStatus,
|
|
4688
|
+
// Metadata uses replace behavior (not merge)
|
|
4689
|
+
metadata: __metadata !== void 0 ? __metadata : existing.metadata,
|
|
4443
4690
|
values: { ...existing.values, ...valueData },
|
|
4444
4691
|
updatedAt: /* @__PURE__ */ new Date()
|
|
4445
4692
|
};
|
|
@@ -6117,7 +6364,7 @@ var GlobalSearchService = class {
|
|
|
6117
6364
|
};
|
|
6118
6365
|
|
|
6119
6366
|
// src/runtime/services/object-schema.service.ts
|
|
6120
|
-
var
|
|
6367
|
+
var _ObjectSchemaService = class _ObjectSchemaService {
|
|
6121
6368
|
constructor(adapter, nativeRegistry, options) {
|
|
6122
6369
|
this.adapter = adapter;
|
|
6123
6370
|
this.nativeRegistry = nativeRegistry;
|
|
@@ -6540,6 +6787,14 @@ var ObjectSchemaService = class {
|
|
|
6540
6787
|
"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'"
|
|
6541
6788
|
);
|
|
6542
6789
|
}
|
|
6790
|
+
if (_ObjectSchemaService.RESERVED_ATTRIBUTE_NAMES.includes(
|
|
6791
|
+
name
|
|
6792
|
+
)) {
|
|
6793
|
+
throw new Error(
|
|
6794
|
+
`"${name}" is a reserved name and cannot be used as an attribute name.
|
|
6795
|
+
Reserved names: ${_ObjectSchemaService.RESERVED_ATTRIBUTE_NAMES.join(", ")}`
|
|
6796
|
+
);
|
|
6797
|
+
}
|
|
6543
6798
|
}
|
|
6544
6799
|
/**
|
|
6545
6800
|
* Get complete object schema (system + custom attributes)
|
|
@@ -6786,6 +7041,17 @@ var ObjectSchemaService = class {
|
|
|
6786
7041
|
return baseAttr;
|
|
6787
7042
|
}
|
|
6788
7043
|
};
|
|
7044
|
+
/**
|
|
7045
|
+
* Reserved attribute names that cannot be used.
|
|
7046
|
+
* These names conflict with system fields on ObjectRecord.
|
|
7047
|
+
*/
|
|
7048
|
+
_ObjectSchemaService.RESERVED_ATTRIBUTE_NAMES = [
|
|
7049
|
+
"id",
|
|
7050
|
+
"createdAt",
|
|
7051
|
+
"updatedAt",
|
|
7052
|
+
"metadata"
|
|
7053
|
+
];
|
|
7054
|
+
var ObjectSchemaService = _ObjectSchemaService;
|
|
6789
7055
|
|
|
6790
7056
|
// src/runtime/services/permission.service.ts
|
|
6791
7057
|
var PermissionService = class {
|
|
@@ -7492,6 +7758,328 @@ var RelationService = class {
|
|
|
7492
7758
|
}
|
|
7493
7759
|
};
|
|
7494
7760
|
|
|
7761
|
+
// src/runtime/services/rollup.service.ts
|
|
7762
|
+
var RollupService = class {
|
|
7763
|
+
constructor(adapter) {
|
|
7764
|
+
this.adapter = adapter;
|
|
7765
|
+
}
|
|
7766
|
+
/**
|
|
7767
|
+
* Calculate a rollup value for a record
|
|
7768
|
+
*
|
|
7769
|
+
* @param recordId - ID of the parent record
|
|
7770
|
+
* @param rollupAttr - Rollup attribute definition
|
|
7771
|
+
* @param schema - Schema of the parent object
|
|
7772
|
+
* @returns Computed rollup value
|
|
7773
|
+
*
|
|
7774
|
+
* @example
|
|
7775
|
+
* ```typescript
|
|
7776
|
+
* // Sum all order amounts for a company
|
|
7777
|
+
* const totalOrders = await rollupService.calculate(
|
|
7778
|
+
* "company-123",
|
|
7779
|
+
* {
|
|
7780
|
+
* type: "rollup",
|
|
7781
|
+
* name: "totalOrders",
|
|
7782
|
+
* relationAttribute: "orders",
|
|
7783
|
+
* targetAttribute: "amount",
|
|
7784
|
+
* function: "sum",
|
|
7785
|
+
* ...
|
|
7786
|
+
* },
|
|
7787
|
+
* companySchema
|
|
7788
|
+
* );
|
|
7789
|
+
* ```
|
|
7790
|
+
*/
|
|
7791
|
+
async calculate(recordId, rollupAttr, schema) {
|
|
7792
|
+
const relationAttr = schema.attributes.find(
|
|
7793
|
+
(a) => a.type === "relation" && a.name === rollupAttr.relationAttribute
|
|
7794
|
+
);
|
|
7795
|
+
if (relationAttr) {
|
|
7796
|
+
return this.calculateForward(recordId, rollupAttr);
|
|
7797
|
+
}
|
|
7798
|
+
return this.calculateReverse(recordId, rollupAttr, schema);
|
|
7799
|
+
}
|
|
7800
|
+
/**
|
|
7801
|
+
* Forward pattern: this record has a relation attribute pointing to other records
|
|
7802
|
+
* Example: entreprise222 has relation "entreprises" → companies, rollup collects from companies
|
|
7803
|
+
*/
|
|
7804
|
+
async calculateForward(recordId, rollupAttr) {
|
|
7805
|
+
const record = await this.adapter.objectRecords.findById(recordId);
|
|
7806
|
+
if (!record) {
|
|
7807
|
+
return { value: null, recordCount: 0 };
|
|
7808
|
+
}
|
|
7809
|
+
const relationValue = record.values[rollupAttr.relationAttribute];
|
|
7810
|
+
let relatedIds = [];
|
|
7811
|
+
if (typeof relationValue === "string" && relationValue.length > 0) {
|
|
7812
|
+
relatedIds = [relationValue];
|
|
7813
|
+
} else if (Array.isArray(relationValue)) {
|
|
7814
|
+
relatedIds = relationValue.filter((id) => typeof id === "string" && id.length > 0);
|
|
7815
|
+
}
|
|
7816
|
+
if (relatedIds.length === 0) {
|
|
7817
|
+
return { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 };
|
|
7818
|
+
}
|
|
7819
|
+
const relatedRecords = await this.adapter.objectRecords.findByIds(relatedIds);
|
|
7820
|
+
if (relatedRecords.length === 0) {
|
|
7821
|
+
return { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 };
|
|
7822
|
+
}
|
|
7823
|
+
return this.aggregateValues(relatedRecords, rollupAttr);
|
|
7824
|
+
}
|
|
7825
|
+
/**
|
|
7826
|
+
* Reverse pattern: other records have a relation pointing to this record
|
|
7827
|
+
* Example: Company has rollup on "orders", Order has relation "company" → companies
|
|
7828
|
+
*/
|
|
7829
|
+
async calculateReverse(recordId, rollupAttr, schema) {
|
|
7830
|
+
const record = await this.adapter.objectRecords.findById(recordId);
|
|
7831
|
+
if (!record) {
|
|
7832
|
+
return { value: null, recordCount: 0 };
|
|
7833
|
+
}
|
|
7834
|
+
const parentObject = await this.adapter.objects.findById(record.objectId);
|
|
7835
|
+
if (!parentObject) {
|
|
7836
|
+
return { value: null, recordCount: 0 };
|
|
7837
|
+
}
|
|
7838
|
+
const tenantId = parentObject.tenantId;
|
|
7839
|
+
const sourceObjectName = rollupAttr.relationAttribute;
|
|
7840
|
+
const sourceObject = await this.adapter.objects.findByName(tenantId, sourceObjectName);
|
|
7841
|
+
if (!sourceObject) {
|
|
7842
|
+
return { value: null, recordCount: 0 };
|
|
7843
|
+
}
|
|
7844
|
+
const sourceAttributes = await this.adapter.attributes.findByObjectId(sourceObject.id);
|
|
7845
|
+
const reverseRelationAttr = sourceAttributes.find((attr) => {
|
|
7846
|
+
if (attr.type !== "relation") return false;
|
|
7847
|
+
const relationConfig = attr.config;
|
|
7848
|
+
return relationConfig?.targets?.some((t) => t.object === schema.name);
|
|
7849
|
+
});
|
|
7850
|
+
if (!reverseRelationAttr) {
|
|
7851
|
+
return { value: null, recordCount: 0 };
|
|
7852
|
+
}
|
|
7853
|
+
const relatedRecords = await this.adapter.objectRecords.findByRelation(
|
|
7854
|
+
tenantId,
|
|
7855
|
+
sourceObject.id,
|
|
7856
|
+
reverseRelationAttr.name,
|
|
7857
|
+
recordId
|
|
7858
|
+
);
|
|
7859
|
+
if (relatedRecords.length === 0) {
|
|
7860
|
+
return { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 };
|
|
7861
|
+
}
|
|
7862
|
+
return this.aggregateValues(relatedRecords, rollupAttr);
|
|
7863
|
+
}
|
|
7864
|
+
/**
|
|
7865
|
+
* Extract and aggregate values from related records
|
|
7866
|
+
*/
|
|
7867
|
+
aggregateValues(relatedRecords, rollupAttr) {
|
|
7868
|
+
const values = relatedRecords.map((r) => r.values[rollupAttr.targetAttribute]).filter((v) => v !== void 0 && v !== null);
|
|
7869
|
+
const aggregated = this.aggregate(values, rollupAttr.function);
|
|
7870
|
+
const result = typeof aggregated === "number" && rollupAttr.decimals !== void 0 ? Number(aggregated.toFixed(rollupAttr.decimals)) : aggregated;
|
|
7871
|
+
return { value: result, recordCount: relatedRecords.length };
|
|
7872
|
+
}
|
|
7873
|
+
/**
|
|
7874
|
+
* Calculate rollup values for multiple records (batched)
|
|
7875
|
+
*
|
|
7876
|
+
* More efficient than calling calculate() for each record individually.
|
|
7877
|
+
*/
|
|
7878
|
+
async calculateForMany(recordIds, rollupAttr, schema) {
|
|
7879
|
+
const results = /* @__PURE__ */ new Map();
|
|
7880
|
+
for (const id of recordIds) {
|
|
7881
|
+
results.set(id, { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 });
|
|
7882
|
+
}
|
|
7883
|
+
await Promise.all(
|
|
7884
|
+
recordIds.map(async (id) => {
|
|
7885
|
+
const result = await this.calculate(id, rollupAttr, schema);
|
|
7886
|
+
results.set(id, result);
|
|
7887
|
+
})
|
|
7888
|
+
);
|
|
7889
|
+
return results;
|
|
7890
|
+
}
|
|
7891
|
+
/**
|
|
7892
|
+
* Apply aggregation function to a set of values
|
|
7893
|
+
*/
|
|
7894
|
+
aggregate(values, fn) {
|
|
7895
|
+
switch (fn) {
|
|
7896
|
+
case "sum":
|
|
7897
|
+
return this.sumNumbers(values);
|
|
7898
|
+
case "avg":
|
|
7899
|
+
return this.averageNumbers(values);
|
|
7900
|
+
case "min":
|
|
7901
|
+
return this.minNumbers(values);
|
|
7902
|
+
case "max":
|
|
7903
|
+
return this.maxNumbers(values);
|
|
7904
|
+
case "count":
|
|
7905
|
+
return values.length;
|
|
7906
|
+
case "countValues":
|
|
7907
|
+
return values.filter((v) => v != null && v !== "").length;
|
|
7908
|
+
case "countEmpty":
|
|
7909
|
+
return values.filter((v) => v == null || v === "").length;
|
|
7910
|
+
case "percentFilled": {
|
|
7911
|
+
if (values.length === 0) return 0;
|
|
7912
|
+
const filled = values.filter((v) => v != null && v !== "").length;
|
|
7913
|
+
return filled / values.length * 100;
|
|
7914
|
+
}
|
|
7915
|
+
case "percentEmpty": {
|
|
7916
|
+
if (values.length === 0) return 0;
|
|
7917
|
+
const empty = values.filter((v) => v == null || v === "").length;
|
|
7918
|
+
return empty / values.length * 100;
|
|
7919
|
+
}
|
|
7920
|
+
case "collect":
|
|
7921
|
+
return values;
|
|
7922
|
+
}
|
|
7923
|
+
}
|
|
7924
|
+
/**
|
|
7925
|
+
* Get the default empty value for a rollup function
|
|
7926
|
+
*/
|
|
7927
|
+
getEmptyValue(fn) {
|
|
7928
|
+
switch (fn) {
|
|
7929
|
+
case "sum":
|
|
7930
|
+
case "count":
|
|
7931
|
+
case "countValues":
|
|
7932
|
+
case "countEmpty":
|
|
7933
|
+
case "percentEmpty":
|
|
7934
|
+
case "percentFilled":
|
|
7935
|
+
return 0;
|
|
7936
|
+
case "avg":
|
|
7937
|
+
case "min":
|
|
7938
|
+
case "max":
|
|
7939
|
+
return null;
|
|
7940
|
+
case "collect":
|
|
7941
|
+
return [];
|
|
7942
|
+
}
|
|
7943
|
+
}
|
|
7944
|
+
/**
|
|
7945
|
+
* Sum numeric values
|
|
7946
|
+
*/
|
|
7947
|
+
sumNumbers(values) {
|
|
7948
|
+
return values.filter((v) => typeof v === "number" && !Number.isNaN(v)).reduce((sum, n) => sum + n, 0);
|
|
7949
|
+
}
|
|
7950
|
+
/**
|
|
7951
|
+
* Average numeric values
|
|
7952
|
+
*/
|
|
7953
|
+
averageNumbers(values) {
|
|
7954
|
+
const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
|
|
7955
|
+
if (nums.length === 0) return null;
|
|
7956
|
+
return nums.reduce((sum, n) => sum + n, 0) / nums.length;
|
|
7957
|
+
}
|
|
7958
|
+
/**
|
|
7959
|
+
* Minimum numeric value
|
|
7960
|
+
*/
|
|
7961
|
+
minNumbers(values) {
|
|
7962
|
+
const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
|
|
7963
|
+
if (nums.length === 0) return null;
|
|
7964
|
+
return Math.min(...nums);
|
|
7965
|
+
}
|
|
7966
|
+
/**
|
|
7967
|
+
* Maximum numeric value
|
|
7968
|
+
*/
|
|
7969
|
+
maxNumbers(values) {
|
|
7970
|
+
const nums = values.filter((v) => typeof v === "number" && !Number.isNaN(v));
|
|
7971
|
+
if (nums.length === 0) return null;
|
|
7972
|
+
return Math.max(...nums);
|
|
7973
|
+
}
|
|
7974
|
+
/**
|
|
7975
|
+
* Recalculate all rollup attributes for a record and update it
|
|
7976
|
+
*
|
|
7977
|
+
* Called after related records change to keep rollups up-to-date.
|
|
7978
|
+
*/
|
|
7979
|
+
async recalculateAndUpdate(record, schema) {
|
|
7980
|
+
const rollupAttrs = schema.attributes.filter((a) => a.type === "rollup");
|
|
7981
|
+
if (rollupAttrs.length === 0) {
|
|
7982
|
+
return record;
|
|
7983
|
+
}
|
|
7984
|
+
const updates = {};
|
|
7985
|
+
for (const attr of rollupAttrs) {
|
|
7986
|
+
const result = await this.calculate(record.id, attr, schema);
|
|
7987
|
+
updates[attr.name] = result.value;
|
|
7988
|
+
}
|
|
7989
|
+
return await this.adapter.objectRecords.update(record.id, updates);
|
|
7990
|
+
}
|
|
7991
|
+
/**
|
|
7992
|
+
* Find parent records that need rollup recalculation when a child record changes
|
|
7993
|
+
*
|
|
7994
|
+
* Used by hooks to determine which parent records to recalculate after
|
|
7995
|
+
* a child record is created, updated, or deleted.
|
|
7996
|
+
*
|
|
7997
|
+
* @param changedRecord - The record that was modified
|
|
7998
|
+
* @param changedSchema - Schema of the changed record's object
|
|
7999
|
+
* @returns Array of parent record IDs that need recalculation
|
|
8000
|
+
*/
|
|
8001
|
+
async findAffectedParentRecords(changedRecord, changedSchema) {
|
|
8002
|
+
const affectedIds = [];
|
|
8003
|
+
const relationAttrs = changedSchema.attributes.filter(
|
|
8004
|
+
(a) => a.type === "relation"
|
|
8005
|
+
);
|
|
8006
|
+
for (const attr of relationAttrs) {
|
|
8007
|
+
const relatedId = changedRecord.values[attr.name];
|
|
8008
|
+
if (typeof relatedId === "string" && relatedId.length > 0) {
|
|
8009
|
+
affectedIds.push(relatedId);
|
|
8010
|
+
} else if (Array.isArray(relatedId)) {
|
|
8011
|
+
affectedIds.push(...relatedId.filter((id) => typeof id === "string"));
|
|
8012
|
+
}
|
|
8013
|
+
}
|
|
8014
|
+
return [...new Set(affectedIds)];
|
|
8015
|
+
}
|
|
8016
|
+
/**
|
|
8017
|
+
* Find records that have forward rollups pointing to the modified record.
|
|
8018
|
+
*
|
|
8019
|
+
* Forward rollups are rollups where the record has a relation attribute
|
|
8020
|
+
* pointing to another object, and the rollup aggregates values from that target.
|
|
8021
|
+
* When the target record changes, we need to recalculate these rollups.
|
|
8022
|
+
*
|
|
8023
|
+
* Example: Order has relation "company" → Company, and rollup "capitalSocial"
|
|
8024
|
+
* aggregating from the Company. When Company.capitalSocial changes,
|
|
8025
|
+
* all Orders pointing to that Company need their rollup recalculated.
|
|
8026
|
+
*
|
|
8027
|
+
* @param changedRecord - The record that was modified
|
|
8028
|
+
* @param changedSchema - Schema of the changed record's object
|
|
8029
|
+
* @returns Array of records that need their forward rollups recalculated
|
|
8030
|
+
*/
|
|
8031
|
+
async findRecordsWithForwardRollup(changedRecord, changedSchema) {
|
|
8032
|
+
const affectedRecords = [];
|
|
8033
|
+
const recordObject = await this.adapter.objects.findById(changedRecord.objectId);
|
|
8034
|
+
if (!recordObject) {
|
|
8035
|
+
return affectedRecords;
|
|
8036
|
+
}
|
|
8037
|
+
const tenantId = recordObject.tenantId;
|
|
8038
|
+
const allObjects = await this.adapter.objects.list(tenantId);
|
|
8039
|
+
for (const obj of allObjects) {
|
|
8040
|
+
if (obj.id === changedRecord.objectId) {
|
|
8041
|
+
continue;
|
|
8042
|
+
}
|
|
8043
|
+
const attributes = await this.adapter.attributes.findByObjectId(obj.id);
|
|
8044
|
+
const rollupAttrs = attributes.filter((a) => a.type === "rollup");
|
|
8045
|
+
if (rollupAttrs.length === 0) {
|
|
8046
|
+
continue;
|
|
8047
|
+
}
|
|
8048
|
+
for (const rollupDbAttr of rollupAttrs) {
|
|
8049
|
+
const rollupConfig = rollupDbAttr.config;
|
|
8050
|
+
if (!rollupConfig?.relationAttribute) {
|
|
8051
|
+
continue;
|
|
8052
|
+
}
|
|
8053
|
+
const relationAttr = attributes.find(
|
|
8054
|
+
(a) => a.type === "relation" && a.name === rollupConfig.relationAttribute
|
|
8055
|
+
);
|
|
8056
|
+
if (!relationAttr) {
|
|
8057
|
+
continue;
|
|
8058
|
+
}
|
|
8059
|
+
const relationConfig = relationAttr.config;
|
|
8060
|
+
const targetsChangedObject = relationConfig?.targets?.some(
|
|
8061
|
+
(t) => t.object === changedSchema.name
|
|
8062
|
+
);
|
|
8063
|
+
if (!targetsChangedObject) {
|
|
8064
|
+
continue;
|
|
8065
|
+
}
|
|
8066
|
+
const recordsPointingToChanged = await this.adapter.objectRecords.findByRelation(
|
|
8067
|
+
tenantId,
|
|
8068
|
+
obj.id,
|
|
8069
|
+
relationAttr.name,
|
|
8070
|
+
changedRecord.id
|
|
8071
|
+
);
|
|
8072
|
+
affectedRecords.push(...recordsPointingToChanged);
|
|
8073
|
+
}
|
|
8074
|
+
}
|
|
8075
|
+
const uniqueRecords = /* @__PURE__ */ new Map();
|
|
8076
|
+
for (const record of affectedRecords) {
|
|
8077
|
+
uniqueRecords.set(record.id, record);
|
|
8078
|
+
}
|
|
8079
|
+
return Array.from(uniqueRecords.values());
|
|
8080
|
+
}
|
|
8081
|
+
};
|
|
8082
|
+
|
|
7495
8083
|
// src/runtime/services/user.service.ts
|
|
7496
8084
|
var UserService = class {
|
|
7497
8085
|
constructor(adapter, tenantId) {
|
|
@@ -7614,9 +8202,14 @@ var RecordService = class {
|
|
|
7614
8202
|
constructor(adapter, tenantId, options) {
|
|
7615
8203
|
this.adapter = adapter;
|
|
7616
8204
|
this.tenantId = tenantId;
|
|
7617
|
-
this.schemaService = new ObjectSchemaService(adapter, registry
|
|
8205
|
+
this.schemaService = new ObjectSchemaService(adapter, registry, {
|
|
8206
|
+
userId: options?.userId,
|
|
8207
|
+
userEmail: options?.userEmail,
|
|
8208
|
+
auditService: options?.auditService
|
|
8209
|
+
});
|
|
7618
8210
|
this.relationService = new RelationService(adapter, registry);
|
|
7619
8211
|
this.userService = new UserService(adapter, tenantId);
|
|
8212
|
+
this.rollupService = new RollupService(adapter);
|
|
7620
8213
|
this.hookRegistry = options?.hookRegistry ?? new NoopHookRegistry();
|
|
7621
8214
|
this.permissionService = options?.permissionService;
|
|
7622
8215
|
this.auditService = options?.auditService ?? (adapter.audit ? new AuditService(adapter, tenantId) : void 0);
|
|
@@ -7768,7 +8361,8 @@ var RecordService = class {
|
|
|
7768
8361
|
objectId,
|
|
7769
8362
|
data,
|
|
7770
8363
|
label,
|
|
7771
|
-
completionStatus
|
|
8364
|
+
completionStatus,
|
|
8365
|
+
metadata: options?.metadata
|
|
7772
8366
|
});
|
|
7773
8367
|
if (!options?.skipHooks) {
|
|
7774
8368
|
const afterCtx = {
|
|
@@ -7778,6 +8372,7 @@ var RecordService = class {
|
|
|
7778
8372
|
};
|
|
7779
8373
|
await this.hookRegistry.execute("afterCreate", schema.name, afterCtx);
|
|
7780
8374
|
}
|
|
8375
|
+
await this.recalculateParentRollups(record, schema);
|
|
7781
8376
|
if (this.auditService && this.userId) {
|
|
7782
8377
|
await this.auditService.logRecordAction({
|
|
7783
8378
|
action: "record.created",
|
|
@@ -7881,11 +8476,15 @@ var RecordService = class {
|
|
|
7881
8476
|
}
|
|
7882
8477
|
const completionStatus = computeRecordStatus(schema, mergedData);
|
|
7883
8478
|
const label = await this.computeLabel(schema, mergedData);
|
|
7884
|
-
const
|
|
8479
|
+
const updatePayload = {
|
|
7885
8480
|
...data,
|
|
7886
8481
|
__completionStatus: completionStatus,
|
|
7887
8482
|
__label: label
|
|
7888
|
-
}
|
|
8483
|
+
};
|
|
8484
|
+
if (options?.metadata !== void 0) {
|
|
8485
|
+
updatePayload.__metadata = options.metadata;
|
|
8486
|
+
}
|
|
8487
|
+
const updated = await this.adapter.objectRecords.update(recordId, updatePayload);
|
|
7889
8488
|
if (!options?.skipHooks) {
|
|
7890
8489
|
const afterCtx = {
|
|
7891
8490
|
...hookCtx,
|
|
@@ -7893,6 +8492,7 @@ var RecordService = class {
|
|
|
7893
8492
|
};
|
|
7894
8493
|
await this.hookRegistry.execute("afterUpdate", schema.name, afterCtx);
|
|
7895
8494
|
}
|
|
8495
|
+
await this.recalculateParentRollups(updated, schema);
|
|
7896
8496
|
if (this.auditService && this.userId && changedAttributes.length > 0) {
|
|
7897
8497
|
const changes = changedAttributes.map((attr) => ({
|
|
7898
8498
|
field: attr,
|
|
@@ -8020,6 +8620,7 @@ var RecordService = class {
|
|
|
8020
8620
|
if (!options?.skipHooks) {
|
|
8021
8621
|
await this.hookRegistry.execute("afterDelete", schema.name, hookCtx);
|
|
8022
8622
|
}
|
|
8623
|
+
await this.recalculateParentRollups(record, schema);
|
|
8023
8624
|
if (this.auditService && this.userId) {
|
|
8024
8625
|
await this.auditService.logRecordAction({
|
|
8025
8626
|
action: "record.deleted",
|
|
@@ -8149,6 +8750,47 @@ var RecordService = class {
|
|
|
8149
8750
|
}
|
|
8150
8751
|
return records.map((record) => this.enrichWithFormulas(record, schema));
|
|
8151
8752
|
}
|
|
8753
|
+
/**
|
|
8754
|
+
* Recalculate rollups after a record changes
|
|
8755
|
+
*
|
|
8756
|
+
* This handles three cases:
|
|
8757
|
+
* 1. The record itself has rollups (e.g., aggregating from related records it points to)
|
|
8758
|
+
* 2. Parent records have rollups that aggregate from this record (reverse pattern)
|
|
8759
|
+
* 3. Records that have forward rollups pointing to this record (forward pattern)
|
|
8760
|
+
*
|
|
8761
|
+
* @param record - The record that was modified
|
|
8762
|
+
* @param schema - Schema of the record's object
|
|
8763
|
+
* @internal
|
|
8764
|
+
*/
|
|
8765
|
+
async recalculateParentRollups(record, schema) {
|
|
8766
|
+
const ownRollupAttrs = schema.attributes.filter(
|
|
8767
|
+
(a) => a.type === "rollup"
|
|
8768
|
+
);
|
|
8769
|
+
if (ownRollupAttrs.length > 0) {
|
|
8770
|
+
await this.rollupService.recalculateAndUpdate(record, schema);
|
|
8771
|
+
}
|
|
8772
|
+
const affectedParentIds = await this.rollupService.findAffectedParentRecords(record, schema);
|
|
8773
|
+
if (affectedParentIds.length > 0) {
|
|
8774
|
+
const parentRecords = await this.adapter.objectRecords.findByIds(affectedParentIds);
|
|
8775
|
+
for (const parentRecord of parentRecords) {
|
|
8776
|
+
const parentSchema = await this.schemaService.getObjectSchema(parentRecord.objectId);
|
|
8777
|
+
const rollupAttrs = parentSchema.attributes.filter(
|
|
8778
|
+
(a) => a.type === "rollup"
|
|
8779
|
+
);
|
|
8780
|
+
if (rollupAttrs.length > 0) {
|
|
8781
|
+
await this.rollupService.recalculateAndUpdate(parentRecord, parentSchema);
|
|
8782
|
+
}
|
|
8783
|
+
}
|
|
8784
|
+
}
|
|
8785
|
+
const affectedForwardRecords = await this.rollupService.findRecordsWithForwardRollup(
|
|
8786
|
+
record,
|
|
8787
|
+
schema
|
|
8788
|
+
);
|
|
8789
|
+
for (const forwardRecord of affectedForwardRecords) {
|
|
8790
|
+
const forwardSchema = await this.schemaService.getObjectSchema(forwardRecord.objectId);
|
|
8791
|
+
await this.rollupService.recalculateAndUpdate(forwardRecord, forwardSchema);
|
|
8792
|
+
}
|
|
8793
|
+
}
|
|
8152
8794
|
/**
|
|
8153
8795
|
* Permanently delete a record (hard delete)
|
|
8154
8796
|
*
|
|
@@ -8422,7 +9064,7 @@ var RollupScheduler = class {
|
|
|
8422
9064
|
clearTimeout(existing.timeout);
|
|
8423
9065
|
}
|
|
8424
9066
|
const timeout = setTimeout(async () => {
|
|
8425
|
-
await this.executeRecalculation(parentId, parentObjectId
|
|
9067
|
+
await this.executeRecalculation(parentId, parentObjectId);
|
|
8426
9068
|
this.pending.delete(key);
|
|
8427
9069
|
}, this.debounceMs);
|
|
8428
9070
|
this.pending.set(key, { parentId, parentObjectId, tenantId, timeout });
|
|
@@ -8449,19 +9091,19 @@ var RollupScheduler = class {
|
|
|
8449
9091
|
const schema = await this.getSchemaById(record.objectId);
|
|
8450
9092
|
const tenantId = tenantMap.get(record.id);
|
|
8451
9093
|
if (schema && tenantId) {
|
|
8452
|
-
await this.rollupService.recalculateAndUpdate(record, schema
|
|
9094
|
+
await this.rollupService.recalculateAndUpdate(record, schema);
|
|
8453
9095
|
}
|
|
8454
9096
|
}
|
|
8455
9097
|
}
|
|
8456
9098
|
/**
|
|
8457
9099
|
* Execute a single recalculation
|
|
8458
9100
|
*/
|
|
8459
|
-
async executeRecalculation(parentId, parentObjectId
|
|
9101
|
+
async executeRecalculation(parentId, parentObjectId) {
|
|
8460
9102
|
const record = await this.adapter.objectRecords.findById(parentId);
|
|
8461
9103
|
if (!record) return;
|
|
8462
9104
|
const schema = await this.getSchemaById(parentObjectId);
|
|
8463
9105
|
if (!schema) return;
|
|
8464
|
-
await this.rollupService.recalculateAndUpdate(record, schema
|
|
9106
|
+
await this.rollupService.recalculateAndUpdate(record, schema);
|
|
8465
9107
|
}
|
|
8466
9108
|
/**
|
|
8467
9109
|
* Get number of pending recalculations
|
|
@@ -9275,6 +9917,9 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
9275
9917
|
PermissionService,
|
|
9276
9918
|
ProtectedResourceError,
|
|
9277
9919
|
ProtectedRoleError,
|
|
9920
|
+
QueryBuilder,
|
|
9921
|
+
QueryMultipleResultsError,
|
|
9922
|
+
QueryNoResultError,
|
|
9278
9923
|
RecordNotFoundError,
|
|
9279
9924
|
RecordReferencedError,
|
|
9280
9925
|
RecordService,
|
|
@@ -9283,6 +9928,8 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
9283
9928
|
RoleNotFoundError,
|
|
9284
9929
|
RollupScheduler,
|
|
9285
9930
|
RollupService,
|
|
9931
|
+
SHORTCUT_TO_FILTER_OPERATOR,
|
|
9932
|
+
SYSTEM_FIELD_NAMES,
|
|
9286
9933
|
SYSTEM_RESOURCES,
|
|
9287
9934
|
SYSTEM_RESOURCE_LABELS,
|
|
9288
9935
|
SchemaError,
|
|
@@ -9305,6 +9952,7 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
9305
9952
|
createCheckboxValidator,
|
|
9306
9953
|
createCurrencyValidator,
|
|
9307
9954
|
createDateValidator,
|
|
9955
|
+
createDefaultState,
|
|
9308
9956
|
createDraftValidator,
|
|
9309
9957
|
createFileValidator,
|
|
9310
9958
|
createFormulaValidator,
|
|
@@ -9315,9 +9963,9 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
9315
9963
|
createNumberValidator,
|
|
9316
9964
|
createObjectValidator,
|
|
9317
9965
|
createPhoneValidator,
|
|
9966
|
+
createQueryBuilder,
|
|
9318
9967
|
createRatingValidator,
|
|
9319
9968
|
createRelationValidator,
|
|
9320
|
-
createRollupHooks,
|
|
9321
9969
|
createRollupValidator,
|
|
9322
9970
|
createSelectValidator,
|
|
9323
9971
|
createSingleRelationValidator,
|
|
@@ -9345,6 +9993,8 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
9345
9993
|
flow,
|
|
9346
9994
|
formatAttributeValue,
|
|
9347
9995
|
formatFormulaResult,
|
|
9996
|
+
formatRecord,
|
|
9997
|
+
formatRecords,
|
|
9348
9998
|
formula,
|
|
9349
9999
|
formulaConfigSchema,
|
|
9350
10000
|
generateId,
|
|
@@ -9389,7 +10039,6 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
|
|
|
9389
10039
|
phoneConfigSchema,
|
|
9390
10040
|
rating,
|
|
9391
10041
|
ratingConfigSchema,
|
|
9392
|
-
registerAllRollupHooks,
|
|
9393
10042
|
registry,
|
|
9394
10043
|
relation,
|
|
9395
10044
|
relationConfigSchema,
|