@stndrds/schema 0.1.0-alpha.40 → 0.1.0-alpha.42
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-2C24E4Q2.mjs → chunk-MXYJTCA3.mjs} +490 -497
- package/dist/{chunk-UDJDDKGY.js → chunk-SWD6OPOV.js} +659 -666
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +22 -6
- package/dist/index.mjs +17 -1
- package/dist/{runtime-Dl-kGqgX.d.mts → runtime-CvRbtIhb.d.mts} +212 -307
- package/dist/{runtime-Dl-kGqgX.d.ts → runtime-CvRbtIhb.d.ts} +212 -307
- package/dist/runtime.d.mts +1 -1
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +18 -2
- package/dist/runtime.mjs +17 -1
- package/package.json +2 -2
|
@@ -482,7 +482,7 @@ var TenantContextError = class _TenantContextError extends Error {
|
|
|
482
482
|
}
|
|
483
483
|
};
|
|
484
484
|
|
|
485
|
-
// src/runtime/context/
|
|
485
|
+
// src/runtime/context/schema-context.ts
|
|
486
486
|
var browserStub = {
|
|
487
487
|
getStore: () => void 0,
|
|
488
488
|
run: (_store, callback) => callback()
|
|
@@ -520,8 +520,102 @@ function getStorage() {
|
|
|
520
520
|
storageInstance = browserStub;
|
|
521
521
|
return storageInstance;
|
|
522
522
|
}
|
|
523
|
-
function
|
|
523
|
+
function getSchemaFromContext(objectId) {
|
|
524
|
+
const ctx = getStorage().getStore();
|
|
525
|
+
return ctx?.objectsById.get(objectId);
|
|
526
|
+
}
|
|
527
|
+
function getSchemaByNameFromContext(objectName) {
|
|
528
|
+
const ctx = getStorage().getStore();
|
|
529
|
+
return ctx?.objectsByName.get(objectName);
|
|
530
|
+
}
|
|
531
|
+
function hasSchemaContext() {
|
|
532
|
+
return getStorage().getStore() !== void 0;
|
|
533
|
+
}
|
|
534
|
+
function getSchemaContext() {
|
|
535
|
+
return getStorage().getStore();
|
|
536
|
+
}
|
|
537
|
+
function addSchemaToContext(schema) {
|
|
524
538
|
const ctx = getStorage().getStore();
|
|
539
|
+
if (!ctx) {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
if (schema.id) {
|
|
543
|
+
ctx.objectsById.set(schema.id, schema);
|
|
544
|
+
}
|
|
545
|
+
ctx.objectsByName.set(schema.name, schema);
|
|
546
|
+
}
|
|
547
|
+
function buildSchemaContext(schemas) {
|
|
548
|
+
const objectsById = /* @__PURE__ */ new Map();
|
|
549
|
+
const objectsByName = /* @__PURE__ */ new Map();
|
|
550
|
+
for (const schema of schemas) {
|
|
551
|
+
if (schema.id) {
|
|
552
|
+
objectsById.set(schema.id, schema);
|
|
553
|
+
}
|
|
554
|
+
objectsByName.set(schema.name, schema);
|
|
555
|
+
}
|
|
556
|
+
return { objectsById, objectsByName };
|
|
557
|
+
}
|
|
558
|
+
function runWithSchemaContext(schemas, fn) {
|
|
559
|
+
const context = buildSchemaContext(schemas);
|
|
560
|
+
return getStorage().run(context, fn);
|
|
561
|
+
}
|
|
562
|
+
function runWithMergedSchemaContext(schemas, fn) {
|
|
563
|
+
const existing = getStorage().getStore();
|
|
564
|
+
const objectsById = new Map(existing?.objectsById);
|
|
565
|
+
const objectsByName = new Map(existing?.objectsByName);
|
|
566
|
+
for (const schema of schemas) {
|
|
567
|
+
if (schema.id) {
|
|
568
|
+
objectsById.set(schema.id, schema);
|
|
569
|
+
}
|
|
570
|
+
objectsByName.set(schema.name, schema);
|
|
571
|
+
}
|
|
572
|
+
const context = {
|
|
573
|
+
objectsById,
|
|
574
|
+
objectsByName
|
|
575
|
+
};
|
|
576
|
+
return getStorage().run(context, fn);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// src/runtime/context/tenant-context.ts
|
|
580
|
+
var browserStub2 = {
|
|
581
|
+
getStore: () => void 0,
|
|
582
|
+
run: (_store, callback) => callback()
|
|
583
|
+
};
|
|
584
|
+
var AsyncLocalStorageClass2 = null;
|
|
585
|
+
if (typeof process !== "undefined" && process.versions?.node) {
|
|
586
|
+
try {
|
|
587
|
+
if (typeof __require !== "undefined") {
|
|
588
|
+
const asyncHooks = __require("async_hooks");
|
|
589
|
+
AsyncLocalStorageClass2 = asyncHooks.AsyncLocalStorage;
|
|
590
|
+
}
|
|
591
|
+
} catch {
|
|
592
|
+
try {
|
|
593
|
+
const dynamicRequire = new Function(
|
|
594
|
+
"m",
|
|
595
|
+
'return typeof require!=="undefined"?require(m):null'
|
|
596
|
+
);
|
|
597
|
+
const asyncHooks = dynamicRequire("node:async_hooks");
|
|
598
|
+
if (asyncHooks) {
|
|
599
|
+
AsyncLocalStorageClass2 = asyncHooks.AsyncLocalStorage;
|
|
600
|
+
}
|
|
601
|
+
} catch {
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
var storageInstance2 = null;
|
|
606
|
+
function getStorage2() {
|
|
607
|
+
if (storageInstance2 !== null) {
|
|
608
|
+
return storageInstance2;
|
|
609
|
+
}
|
|
610
|
+
if (AsyncLocalStorageClass2) {
|
|
611
|
+
storageInstance2 = new AsyncLocalStorageClass2();
|
|
612
|
+
return storageInstance2;
|
|
613
|
+
}
|
|
614
|
+
storageInstance2 = browserStub2;
|
|
615
|
+
return storageInstance2;
|
|
616
|
+
}
|
|
617
|
+
function getContext() {
|
|
618
|
+
const ctx = getStorage2().getStore();
|
|
525
619
|
if (!ctx) {
|
|
526
620
|
throw new TenantContextError();
|
|
527
621
|
}
|
|
@@ -534,11 +628,11 @@ function getUserId() {
|
|
|
534
628
|
return getContext().userId;
|
|
535
629
|
}
|
|
536
630
|
function hasContext() {
|
|
537
|
-
return
|
|
631
|
+
return getStorage2().getStore() !== void 0;
|
|
538
632
|
}
|
|
539
633
|
function runWithContext(context, fn) {
|
|
540
634
|
const frozenContext = Object.freeze({ ...context });
|
|
541
|
-
return
|
|
635
|
+
return getStorage2().run(frozenContext, fn);
|
|
542
636
|
}
|
|
543
637
|
function withTenantContext(tenantId, fn, userId) {
|
|
544
638
|
return runWithContext({ tenantId, userId }, fn);
|
|
@@ -2315,8 +2409,14 @@ function formatCheckbox(value) {
|
|
|
2315
2409
|
function formatNumber(value, attribute) {
|
|
2316
2410
|
if (typeof value !== "number") return String(value);
|
|
2317
2411
|
const decimals = attribute.decimals;
|
|
2318
|
-
if (attribute.unit === "
|
|
2412
|
+
if (attribute.unit === "integer") {
|
|
2319
2413
|
return value.toLocaleString(void 0, {
|
|
2414
|
+
minimumFractionDigits: 0,
|
|
2415
|
+
maximumFractionDigits: 0
|
|
2416
|
+
});
|
|
2417
|
+
}
|
|
2418
|
+
if (attribute.unit === "percentage") {
|
|
2419
|
+
return (value / 100).toLocaleString(void 0, {
|
|
2320
2420
|
style: "percent",
|
|
2321
2421
|
minimumFractionDigits: decimals,
|
|
2322
2422
|
maximumFractionDigits: decimals
|
|
@@ -3200,11 +3300,11 @@ function createMockObjectRecordsRepository(stores) {
|
|
|
3200
3300
|
}
|
|
3201
3301
|
return Promise.resolve(updated);
|
|
3202
3302
|
},
|
|
3203
|
-
async batchRefreshLabels(objectId,
|
|
3303
|
+
async batchRefreshLabels(objectId, computeLabel2) {
|
|
3204
3304
|
let updated = 0;
|
|
3205
3305
|
for (const record of stores.objectRecords.values()) {
|
|
3206
3306
|
if (record.objectId === objectId) {
|
|
3207
|
-
const newLabel = await
|
|
3307
|
+
const newLabel = await computeLabel2(record.values);
|
|
3208
3308
|
if (newLabel !== record.label) {
|
|
3209
3309
|
record.label = newLabel;
|
|
3210
3310
|
record.updatedAt = /* @__PURE__ */ new Date();
|
|
@@ -4122,6 +4222,20 @@ var TenantAwareRepository = class {
|
|
|
4122
4222
|
return getUserId();
|
|
4123
4223
|
}
|
|
4124
4224
|
};
|
|
4225
|
+
var SchemaContextAwareRepository = class extends TenantAwareRepository {
|
|
4226
|
+
/**
|
|
4227
|
+
* Get an ObjectDefinition from context by its ID.
|
|
4228
|
+
*/
|
|
4229
|
+
getSchemaFromContext(objectId) {
|
|
4230
|
+
return getSchemaFromContext(objectId);
|
|
4231
|
+
}
|
|
4232
|
+
/**
|
|
4233
|
+
* Get an ObjectDefinition from context by its name.
|
|
4234
|
+
*/
|
|
4235
|
+
getSchemaByNameFromContext(objectName) {
|
|
4236
|
+
return getSchemaByNameFromContext(objectName);
|
|
4237
|
+
}
|
|
4238
|
+
};
|
|
4125
4239
|
|
|
4126
4240
|
// src/runtime/services/audit.service.ts
|
|
4127
4241
|
var SENSITIVE_PATTERNS = [
|
|
@@ -9349,6 +9463,8 @@ var RelationService = class extends TenantAwareService {
|
|
|
9349
9463
|
}
|
|
9350
9464
|
/**
|
|
9351
9465
|
* Validate a single relation attribute value
|
|
9466
|
+
*
|
|
9467
|
+
* Uses batch fetching (findByIds) to avoid N+1 query pattern.
|
|
9352
9468
|
*/
|
|
9353
9469
|
async validateRelationAttribute(attr, value) {
|
|
9354
9470
|
const errors = [];
|
|
@@ -9365,9 +9481,11 @@ var RelationService = class extends TenantAwareService {
|
|
|
9365
9481
|
});
|
|
9366
9482
|
return errors;
|
|
9367
9483
|
}
|
|
9484
|
+
const records = await this.adapter.objectRecords.findByIds(ids);
|
|
9485
|
+
const recordMap = new Map(records.map((r) => [r.id, r]));
|
|
9368
9486
|
const invalidIds = [];
|
|
9369
9487
|
for (const id of ids) {
|
|
9370
|
-
const record =
|
|
9488
|
+
const record = recordMap.get(id);
|
|
9371
9489
|
if (!record) {
|
|
9372
9490
|
invalidIds.push(id);
|
|
9373
9491
|
continue;
|
|
@@ -9681,22 +9799,37 @@ var RollupService = class {
|
|
|
9681
9799
|
return { value: null, recordCount: 0 };
|
|
9682
9800
|
}
|
|
9683
9801
|
const sourceObjectName = rollupAttr.relationAttribute;
|
|
9684
|
-
const
|
|
9685
|
-
|
|
9686
|
-
|
|
9802
|
+
const sourceSchema = getSchemaByNameFromContext(sourceObjectName);
|
|
9803
|
+
let sourceObjectId;
|
|
9804
|
+
let reverseRelationAttrName;
|
|
9805
|
+
if (sourceSchema?.id) {
|
|
9806
|
+
sourceObjectId = sourceSchema.id;
|
|
9807
|
+
const reverseRelationAttr = sourceSchema.attributes.find((attr) => {
|
|
9808
|
+
if (attr.type !== "relation") return false;
|
|
9809
|
+
const relationConfig = attr;
|
|
9810
|
+
return relationConfig?.targets?.some((t) => t.object === schema.name);
|
|
9811
|
+
});
|
|
9812
|
+
reverseRelationAttrName = reverseRelationAttr?.name;
|
|
9813
|
+
} else {
|
|
9814
|
+
const sourceObject = await this.adapter.objects.findByName(sourceObjectName);
|
|
9815
|
+
if (!sourceObject) {
|
|
9816
|
+
return { value: null, recordCount: 0 };
|
|
9817
|
+
}
|
|
9818
|
+
sourceObjectId = sourceObject.id;
|
|
9819
|
+
const sourceAttributes = await this.adapter.attributes.findByObjectId(sourceObject.id);
|
|
9820
|
+
const reverseRelationAttr = sourceAttributes.find((attr) => {
|
|
9821
|
+
if (attr.type !== "relation") return false;
|
|
9822
|
+
const relationConfig = attr.config;
|
|
9823
|
+
return relationConfig?.targets?.some((t) => t.object === schema.name);
|
|
9824
|
+
});
|
|
9825
|
+
reverseRelationAttrName = reverseRelationAttr?.name;
|
|
9687
9826
|
}
|
|
9688
|
-
|
|
9689
|
-
const reverseRelationAttr = sourceAttributes.find((attr) => {
|
|
9690
|
-
if (attr.type !== "relation") return false;
|
|
9691
|
-
const relationConfig = attr.config;
|
|
9692
|
-
return relationConfig?.targets?.some((t) => t.object === schema.name);
|
|
9693
|
-
});
|
|
9694
|
-
if (!reverseRelationAttr) {
|
|
9827
|
+
if (!reverseRelationAttrName) {
|
|
9695
9828
|
return { value: null, recordCount: 0 };
|
|
9696
9829
|
}
|
|
9697
9830
|
const relatedRecords = await this.adapter.objectRecords.findByRelation(
|
|
9698
|
-
|
|
9699
|
-
|
|
9831
|
+
sourceObjectId,
|
|
9832
|
+
reverseRelationAttrName,
|
|
9700
9833
|
recordId
|
|
9701
9834
|
);
|
|
9702
9835
|
if (relatedRecords.length === 0) {
|
|
@@ -10079,7 +10212,7 @@ var UserService = class extends TenantAwareService {
|
|
|
10079
10212
|
}
|
|
10080
10213
|
};
|
|
10081
10214
|
|
|
10082
|
-
// src/runtime/services/record.
|
|
10215
|
+
// src/runtime/services/record/defaults.ts
|
|
10083
10216
|
function applyDefaultValues(schema, data) {
|
|
10084
10217
|
const result = { ...data };
|
|
10085
10218
|
for (const attr of schema.attributes) {
|
|
@@ -10092,170 +10225,267 @@ function applyDefaultValues(schema, data) {
|
|
|
10092
10225
|
}
|
|
10093
10226
|
return result;
|
|
10094
10227
|
}
|
|
10095
|
-
|
|
10096
|
-
|
|
10097
|
-
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
auditService: options?.auditService
|
|
10101
|
-
});
|
|
10102
|
-
this.relationService = new RelationService(adapter, registry);
|
|
10103
|
-
this.userService = new UserService(adapter);
|
|
10104
|
-
this.rollupService = new RollupService(adapter);
|
|
10105
|
-
this.hookRegistry = options?.hookRegistry ?? new NoopHookRegistry();
|
|
10106
|
-
this.permissionService = options?.permissionService;
|
|
10107
|
-
this.auditService = options?.auditService ?? (adapter.audit ? new AuditService(adapter) : void 0);
|
|
10108
|
-
this.policyRegistry = options?.policyRegistry === null ? null : options?.policyRegistry ?? defaultPolicyRegistry;
|
|
10228
|
+
|
|
10229
|
+
// src/runtime/services/record/access.ts
|
|
10230
|
+
async function checkPermission(permissionService, userId, objectName, action) {
|
|
10231
|
+
if (permissionService && userId) {
|
|
10232
|
+
await permissionService.checkObjectAccess(userId, objectName, action);
|
|
10109
10233
|
}
|
|
10110
|
-
|
|
10111
|
-
|
|
10112
|
-
|
|
10113
|
-
|
|
10114
|
-
*/
|
|
10115
|
-
async checkPermission(objectName, action) {
|
|
10116
|
-
if (this.permissionService && this.userId) {
|
|
10117
|
-
await this.permissionService.checkObjectAccess(this.userId, objectName, action);
|
|
10118
|
-
}
|
|
10234
|
+
}
|
|
10235
|
+
function getPolicy(policyRegistry, userId, objectName) {
|
|
10236
|
+
if (!(policyRegistry && userId)) {
|
|
10237
|
+
return void 0;
|
|
10119
10238
|
}
|
|
10120
|
-
|
|
10121
|
-
|
|
10122
|
-
|
|
10123
|
-
|
|
10124
|
-
|
|
10125
|
-
|
|
10126
|
-
|
|
10127
|
-
|
|
10128
|
-
return this.policyRegistry.get(objectName);
|
|
10239
|
+
return policyRegistry.get(objectName);
|
|
10240
|
+
}
|
|
10241
|
+
function buildPolicyContext(objectName, userId, tenantId) {
|
|
10242
|
+
return { userId, tenantId, objectName };
|
|
10243
|
+
}
|
|
10244
|
+
function checkRecordAccess(policy, record, context) {
|
|
10245
|
+
if (!policy.canAccessRecord) {
|
|
10246
|
+
return true;
|
|
10129
10247
|
}
|
|
10130
|
-
|
|
10131
|
-
|
|
10132
|
-
|
|
10133
|
-
|
|
10134
|
-
|
|
10135
|
-
return {
|
|
10136
|
-
// biome-ignore lint/style/noNonNullAssertion: userId is guaranteed to be set
|
|
10137
|
-
userId: this.userId,
|
|
10138
|
-
tenantId: this.tenantId,
|
|
10139
|
-
objectName
|
|
10140
|
-
};
|
|
10248
|
+
return policy.canAccessRecord(context, record);
|
|
10249
|
+
}
|
|
10250
|
+
function checkRecordModifyOrThrow(policy, record, context) {
|
|
10251
|
+
if (!policy.canModifyRecord) {
|
|
10252
|
+
return;
|
|
10141
10253
|
}
|
|
10142
|
-
|
|
10143
|
-
|
|
10144
|
-
* Returns true if no policy exists or user can access.
|
|
10145
|
-
* @internal
|
|
10146
|
-
*/
|
|
10147
|
-
checkRecordAccess(policy, record) {
|
|
10148
|
-
if (!policy.canAccessRecord) {
|
|
10149
|
-
return true;
|
|
10150
|
-
}
|
|
10151
|
-
return policy.canAccessRecord(this.buildPolicyContext(policy.objectName), record);
|
|
10254
|
+
if (!policy.canModifyRecord(context, record)) {
|
|
10255
|
+
throw new PolicyViolationError(policy.objectName, "update", record.id);
|
|
10152
10256
|
}
|
|
10153
|
-
|
|
10154
|
-
|
|
10155
|
-
|
|
10156
|
-
|
|
10157
|
-
*/
|
|
10158
|
-
checkRecordModify(policy, record) {
|
|
10159
|
-
if (!policy.canModifyRecord) {
|
|
10160
|
-
return;
|
|
10161
|
-
}
|
|
10162
|
-
const canModify = policy.canModifyRecord(this.buildPolicyContext(policy.objectName), record);
|
|
10163
|
-
if (!canModify) {
|
|
10164
|
-
throw new PolicyViolationError(policy.objectName, "update", record.id);
|
|
10165
|
-
}
|
|
10257
|
+
}
|
|
10258
|
+
function checkRecordDeleteOrThrow(policy, record, context) {
|
|
10259
|
+
if (!policy.canDeleteRecord) {
|
|
10260
|
+
return;
|
|
10166
10261
|
}
|
|
10167
|
-
|
|
10168
|
-
|
|
10169
|
-
|
|
10170
|
-
|
|
10171
|
-
|
|
10172
|
-
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
|
|
10177
|
-
|
|
10178
|
-
|
|
10262
|
+
if (!policy.canDeleteRecord(context, record)) {
|
|
10263
|
+
throw new PolicyViolationError(policy.objectName, "delete", record.id);
|
|
10264
|
+
}
|
|
10265
|
+
}
|
|
10266
|
+
|
|
10267
|
+
// src/runtime/services/record/label.ts
|
|
10268
|
+
function extractRelationIds2(val) {
|
|
10269
|
+
if (typeof val === "string") return [val];
|
|
10270
|
+
if (Array.isArray(val) && typeof val[0] === "string") return [val[0]];
|
|
10271
|
+
return [];
|
|
10272
|
+
}
|
|
10273
|
+
async function resolveRelationLabels(relationAttrs, values, resolver) {
|
|
10274
|
+
const resolvedMap = /* @__PURE__ */ new Map();
|
|
10275
|
+
const idsWithAttrId = [];
|
|
10276
|
+
const idsWithoutAttrId = [];
|
|
10277
|
+
for (const attr of relationAttrs) {
|
|
10278
|
+
const ids = extractRelationIds2(values[attr.name]);
|
|
10279
|
+
if (ids.length > 0) {
|
|
10280
|
+
if (attr.id) {
|
|
10281
|
+
idsWithAttrId.push({ attrId: attr.id, ids });
|
|
10282
|
+
} else {
|
|
10283
|
+
idsWithoutAttrId.push(...ids);
|
|
10284
|
+
}
|
|
10179
10285
|
}
|
|
10180
10286
|
}
|
|
10181
|
-
|
|
10182
|
-
|
|
10183
|
-
|
|
10184
|
-
|
|
10185
|
-
|
|
10186
|
-
|
|
10187
|
-
|
|
10188
|
-
|
|
10189
|
-
|
|
10190
|
-
|
|
10191
|
-
|
|
10192
|
-
|
|
10193
|
-
|
|
10194
|
-
|
|
10195
|
-
|
|
10196
|
-
idsWithoutAttrId.push(...ids);
|
|
10287
|
+
await Promise.all(
|
|
10288
|
+
idsWithAttrId.map(async ({ attrId, ids }) => {
|
|
10289
|
+
const resolved = await resolver.resolveRelationIds(ids, attrId);
|
|
10290
|
+
for (const r of resolved) {
|
|
10291
|
+
resolvedMap.set(r.id, r.label);
|
|
10292
|
+
}
|
|
10293
|
+
})
|
|
10294
|
+
);
|
|
10295
|
+
if (idsWithoutAttrId.length > 0) {
|
|
10296
|
+
const uniqueIds = [...new Set(idsWithoutAttrId)].filter((id) => !resolvedMap.has(id));
|
|
10297
|
+
if (uniqueIds.length > 0) {
|
|
10298
|
+
const records = await resolver.findRecordLabels(uniqueIds);
|
|
10299
|
+
for (const record of records) {
|
|
10300
|
+
if (record.label) {
|
|
10301
|
+
resolvedMap.set(record.id, record.label);
|
|
10197
10302
|
}
|
|
10198
10303
|
}
|
|
10199
10304
|
}
|
|
10305
|
+
}
|
|
10306
|
+
return resolvedMap;
|
|
10307
|
+
}
|
|
10308
|
+
async function computeLabel(schema, values, resolver) {
|
|
10309
|
+
const attrNames = extractAttributeNames(schema.labelExpression);
|
|
10310
|
+
let enrichedValues = enrichValuesForDisplay(values, schema.attributes);
|
|
10311
|
+
const relationAttrs = schema.attributes.filter(
|
|
10312
|
+
(attr) => attr.type === "relation" && attrNames.includes(attr.name)
|
|
10313
|
+
);
|
|
10314
|
+
if (relationAttrs.length === 0) {
|
|
10315
|
+
return renderLabelExpression(schema.labelExpression, enrichedValues);
|
|
10316
|
+
}
|
|
10317
|
+
const resolvedMap = await resolveRelationLabels(relationAttrs, values, resolver);
|
|
10318
|
+
if (resolvedMap.size === 0) {
|
|
10319
|
+
return renderLabelExpression(schema.labelExpression, enrichedValues);
|
|
10320
|
+
}
|
|
10321
|
+
enrichedValues = { ...enrichedValues };
|
|
10322
|
+
for (const attr of relationAttrs) {
|
|
10323
|
+
const ids = extractRelationIds2(values[attr.name]);
|
|
10324
|
+
if (ids.length > 0 && resolvedMap.has(ids[0])) {
|
|
10325
|
+
enrichedValues[attr.name] = resolvedMap.get(ids[0]);
|
|
10326
|
+
}
|
|
10327
|
+
}
|
|
10328
|
+
return renderLabelExpression(schema.labelExpression, enrichedValues);
|
|
10329
|
+
}
|
|
10330
|
+
|
|
10331
|
+
// src/runtime/services/record/formula.ts
|
|
10332
|
+
function enrichWithFormulas(record, schema) {
|
|
10333
|
+
const formulaAttrs = schema.attributes.filter((a) => a.type === "formula");
|
|
10334
|
+
if (formulaAttrs.length === 0) {
|
|
10335
|
+
return record;
|
|
10336
|
+
}
|
|
10337
|
+
const enrichedValues = { ...record.values };
|
|
10338
|
+
for (const attr of formulaAttrs) {
|
|
10339
|
+
enrichedValues[attr.name] = evaluateFormulaAttribute(attr, record.values);
|
|
10340
|
+
}
|
|
10341
|
+
return {
|
|
10342
|
+
...record,
|
|
10343
|
+
values: enrichedValues
|
|
10344
|
+
};
|
|
10345
|
+
}
|
|
10346
|
+
function enrichRecordsWithFormulas(records, schema) {
|
|
10347
|
+
const formulaAttrs = schema.attributes.filter((a) => a.type === "formula");
|
|
10348
|
+
if (formulaAttrs.length === 0) {
|
|
10349
|
+
return records;
|
|
10350
|
+
}
|
|
10351
|
+
return records.map((record) => enrichWithFormulas(record, schema));
|
|
10352
|
+
}
|
|
10353
|
+
|
|
10354
|
+
// src/runtime/services/record/hook-context.ts
|
|
10355
|
+
function createGetChange(oldValues, newValues, changedAttributes) {
|
|
10356
|
+
return (attr) => ({
|
|
10357
|
+
oldValue: oldValues[attr],
|
|
10358
|
+
newValue: newValues[attr],
|
|
10359
|
+
changed: changedAttributes.includes(attr)
|
|
10360
|
+
});
|
|
10361
|
+
}
|
|
10362
|
+
function createContextForCreate(schema, tenantId, data, metadata) {
|
|
10363
|
+
const attributeNames = Object.keys(data);
|
|
10364
|
+
return {
|
|
10365
|
+
objectId: schema.id,
|
|
10366
|
+
objectName: schema.name,
|
|
10367
|
+
recordId: "",
|
|
10368
|
+
// Will be set after creation
|
|
10369
|
+
tenantId,
|
|
10370
|
+
record: null,
|
|
10371
|
+
// Will be set after creation
|
|
10372
|
+
oldValues: {},
|
|
10373
|
+
newValues: data,
|
|
10374
|
+
changedAttributes: attributeNames,
|
|
10375
|
+
// All attributes are "new"
|
|
10376
|
+
getChange: createGetChange({}, data, attributeNames),
|
|
10377
|
+
metadata: metadata ?? {},
|
|
10378
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
10379
|
+
};
|
|
10380
|
+
}
|
|
10381
|
+
function createContextForUpdate(schema, tenantId, existing, mergedData, changedAttributes, metadata) {
|
|
10382
|
+
const oldValuesSnapshot = { ...existing.values };
|
|
10383
|
+
return {
|
|
10384
|
+
objectId: schema.id,
|
|
10385
|
+
objectName: schema.name,
|
|
10386
|
+
recordId: existing.id,
|
|
10387
|
+
tenantId,
|
|
10388
|
+
record: existing,
|
|
10389
|
+
oldValues: oldValuesSnapshot,
|
|
10390
|
+
newValues: mergedData,
|
|
10391
|
+
changedAttributes,
|
|
10392
|
+
getChange: createGetChange(oldValuesSnapshot, mergedData, changedAttributes),
|
|
10393
|
+
metadata: metadata ?? {},
|
|
10394
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
10395
|
+
};
|
|
10396
|
+
}
|
|
10397
|
+
function createContextForDelete(schema, tenantId, record, metadata) {
|
|
10398
|
+
const attributeNames = Object.keys(record.values);
|
|
10399
|
+
return {
|
|
10400
|
+
objectId: schema.id,
|
|
10401
|
+
objectName: schema.name,
|
|
10402
|
+
recordId: record.id,
|
|
10403
|
+
tenantId,
|
|
10404
|
+
record,
|
|
10405
|
+
oldValues: record.values,
|
|
10406
|
+
newValues: {},
|
|
10407
|
+
changedAttributes: attributeNames,
|
|
10408
|
+
// All attributes are being "removed"
|
|
10409
|
+
getChange: createGetChange(record.values, {}, attributeNames),
|
|
10410
|
+
metadata: metadata ?? {},
|
|
10411
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
10412
|
+
};
|
|
10413
|
+
}
|
|
10414
|
+
function createContextForRestore(schema, tenantId, record, metadata) {
|
|
10415
|
+
return {
|
|
10416
|
+
objectId: schema.id,
|
|
10417
|
+
objectName: schema.name,
|
|
10418
|
+
recordId: record.id,
|
|
10419
|
+
tenantId,
|
|
10420
|
+
record,
|
|
10421
|
+
oldValues: record.values,
|
|
10422
|
+
newValues: record.values,
|
|
10423
|
+
changedAttributes: [],
|
|
10424
|
+
getChange: createGetChange(record.values, record.values, []),
|
|
10425
|
+
metadata: metadata ?? {},
|
|
10426
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
10427
|
+
};
|
|
10428
|
+
}
|
|
10429
|
+
|
|
10430
|
+
// src/runtime/services/record/rollup-cascade.ts
|
|
10431
|
+
async function recalculateParentRollups(record, schema, ctx) {
|
|
10432
|
+
const { rollupService, schemaService, findRecordsByIds } = ctx;
|
|
10433
|
+
const ownRollupAttrs = schema.attributes.filter((a) => a.type === "rollup");
|
|
10434
|
+
if (ownRollupAttrs.length > 0) {
|
|
10435
|
+
await rollupService.recalculateAndUpdate(record, schema);
|
|
10436
|
+
}
|
|
10437
|
+
const affectedParentIds = await rollupService.findAffectedParentRecords(record, schema);
|
|
10438
|
+
if (affectedParentIds.length > 0) {
|
|
10439
|
+
const parentRecords = await findRecordsByIds(affectedParentIds);
|
|
10200
10440
|
await Promise.all(
|
|
10201
|
-
|
|
10202
|
-
const
|
|
10203
|
-
|
|
10204
|
-
|
|
10441
|
+
parentRecords.map(async (parentRecord) => {
|
|
10442
|
+
const parentSchema = await schemaService.getObjectSchema(parentRecord.objectId);
|
|
10443
|
+
const rollupAttrs = parentSchema.attributes.filter(
|
|
10444
|
+
(a) => a.type === "rollup"
|
|
10445
|
+
);
|
|
10446
|
+
if (rollupAttrs.length > 0) {
|
|
10447
|
+
await rollupService.recalculateAndUpdate(parentRecord, parentSchema);
|
|
10205
10448
|
}
|
|
10206
10449
|
})
|
|
10207
10450
|
);
|
|
10208
|
-
if (idsWithoutAttrId.length > 0) {
|
|
10209
|
-
const uniqueIds = [...new Set(idsWithoutAttrId)].filter((id) => !resolvedMap.has(id));
|
|
10210
|
-
if (uniqueIds.length > 0) {
|
|
10211
|
-
const records = await this.adapter.objectRecords.findByIds(uniqueIds);
|
|
10212
|
-
for (const record of records) {
|
|
10213
|
-
if (record.label) {
|
|
10214
|
-
resolvedMap.set(record.id, record.label);
|
|
10215
|
-
}
|
|
10216
|
-
}
|
|
10217
|
-
}
|
|
10218
|
-
}
|
|
10219
|
-
return resolvedMap;
|
|
10220
|
-
}
|
|
10221
|
-
/**
|
|
10222
|
-
* Extract relation IDs from a value (string or array)
|
|
10223
|
-
* @internal
|
|
10224
|
-
*/
|
|
10225
|
-
extractRelationIds(val) {
|
|
10226
|
-
if (typeof val === "string") return [val];
|
|
10227
|
-
if (Array.isArray(val) && typeof val[0] === "string") return [val[0]];
|
|
10228
|
-
return [];
|
|
10229
10451
|
}
|
|
10230
|
-
|
|
10231
|
-
|
|
10232
|
-
|
|
10233
|
-
|
|
10234
|
-
|
|
10235
|
-
|
|
10236
|
-
|
|
10237
|
-
|
|
10238
|
-
|
|
10239
|
-
|
|
10240
|
-
|
|
10241
|
-
|
|
10242
|
-
|
|
10243
|
-
|
|
10244
|
-
|
|
10245
|
-
|
|
10246
|
-
|
|
10247
|
-
|
|
10248
|
-
|
|
10249
|
-
|
|
10250
|
-
|
|
10251
|
-
|
|
10252
|
-
|
|
10253
|
-
|
|
10254
|
-
|
|
10255
|
-
|
|
10256
|
-
|
|
10257
|
-
|
|
10452
|
+
const affectedForwardRecords = await rollupService.findRecordsWithForwardRollup(record, schema);
|
|
10453
|
+
await Promise.all(
|
|
10454
|
+
affectedForwardRecords.map(async (forwardRecord) => {
|
|
10455
|
+
const forwardSchema = await schemaService.getObjectSchema(forwardRecord.objectId);
|
|
10456
|
+
await rollupService.recalculateAndUpdate(forwardRecord, forwardSchema);
|
|
10457
|
+
})
|
|
10458
|
+
);
|
|
10459
|
+
}
|
|
10460
|
+
|
|
10461
|
+
// src/runtime/services/record.service.ts
|
|
10462
|
+
var RecordService = class extends TenantAwareService {
|
|
10463
|
+
constructor(adapter, options) {
|
|
10464
|
+
super();
|
|
10465
|
+
this.adapter = adapter;
|
|
10466
|
+
this.schemaService = new ObjectSchemaService(adapter, registry, {
|
|
10467
|
+
auditService: options?.auditService
|
|
10468
|
+
});
|
|
10469
|
+
this.relationService = new RelationService(adapter, registry);
|
|
10470
|
+
this.userService = new UserService(adapter);
|
|
10471
|
+
this.rollupService = new RollupService(adapter);
|
|
10472
|
+
this.hookRegistry = options?.hookRegistry ?? new NoopHookRegistry();
|
|
10473
|
+
this.permissionService = options?.permissionService;
|
|
10474
|
+
this.auditService = options?.auditService ?? (adapter.audit ? new AuditService(adapter) : void 0);
|
|
10475
|
+
this.policyRegistry = options?.policyRegistry === null ? null : options?.policyRegistry ?? defaultPolicyRegistry;
|
|
10476
|
+
this.labelResolver = {
|
|
10477
|
+
resolveRelationIds: (ids, attrId) => this.relationService.resolveIds(ids, attrId),
|
|
10478
|
+
findRecordLabels: (ids) => this.adapter.objectRecords.findByIds(ids)
|
|
10479
|
+
};
|
|
10480
|
+
this.rollupContext = {
|
|
10481
|
+
rollupService: this.rollupService,
|
|
10482
|
+
schemaService: this.schemaService,
|
|
10483
|
+
findRecordsByIds: (ids) => this.adapter.objectRecords.findByIds(ids)
|
|
10484
|
+
};
|
|
10258
10485
|
}
|
|
10486
|
+
// ============================================================================
|
|
10487
|
+
// CREATE
|
|
10488
|
+
// ============================================================================
|
|
10259
10489
|
/**
|
|
10260
10490
|
* Create a new record with validation
|
|
10261
10491
|
*
|
|
@@ -10265,31 +10495,17 @@ var RecordService = class extends TenantAwareService {
|
|
|
10265
10495
|
* @param data - Record data (attribute values)
|
|
10266
10496
|
* @param options - Creation options
|
|
10267
10497
|
* @returns Created record with computed completionStatus
|
|
10268
|
-
*
|
|
10269
|
-
* @example
|
|
10270
|
-
* ```typescript
|
|
10271
|
-
* const service = new RecordService(adapter, "tenant-123");
|
|
10272
|
-
*
|
|
10273
|
-
* // Create a complete record (strict validation)
|
|
10274
|
-
* const product = await service.createRecord("obj-product", {
|
|
10275
|
-
* name: "Nike Air Max",
|
|
10276
|
-
* price: 129.99,
|
|
10277
|
-
* status: "active"
|
|
10278
|
-
* });
|
|
10279
|
-
* // → product.completionStatus = "complete"
|
|
10280
|
-
*
|
|
10281
|
-
* // Create a draft record (allows missing required fields)
|
|
10282
|
-
* const draft = await service.createRecord("obj-product", {
|
|
10283
|
-
* name: "Draft Product"
|
|
10284
|
-
* }, { allowDraft: true });
|
|
10285
|
-
* // → draft.completionStatus = "draft"
|
|
10286
|
-
* ```
|
|
10287
10498
|
*/
|
|
10288
10499
|
async createRecord(objectId, data, options) {
|
|
10289
10500
|
const schema = await this.schemaService.getObjectSchema(objectId);
|
|
10290
10501
|
const dataWithDefaults = applyDefaultValues(schema, data);
|
|
10291
|
-
await this.
|
|
10292
|
-
const hookCtx =
|
|
10502
|
+
await checkPermission(this.permissionService, this.userId, schema.name, "create");
|
|
10503
|
+
const hookCtx = createContextForCreate(
|
|
10504
|
+
schema,
|
|
10505
|
+
this.tenantId,
|
|
10506
|
+
dataWithDefaults,
|
|
10507
|
+
options?.hookMetadata
|
|
10508
|
+
);
|
|
10293
10509
|
if (!options?.skipHooks) {
|
|
10294
10510
|
await this.hookRegistry.execute("beforeCreate", schema.name, hookCtx);
|
|
10295
10511
|
}
|
|
@@ -10307,7 +10523,7 @@ var RecordService = class extends TenantAwareService {
|
|
|
10307
10523
|
}
|
|
10308
10524
|
}
|
|
10309
10525
|
const completionStatus = computeRecordStatus(schema, dataWithDefaults);
|
|
10310
|
-
const label = await
|
|
10526
|
+
const label = await computeLabel(schema, dataWithDefaults, this.labelResolver);
|
|
10311
10527
|
const record = await this.adapter.objectRecords.create({
|
|
10312
10528
|
objectId,
|
|
10313
10529
|
data: dataWithDefaults,
|
|
@@ -10324,7 +10540,7 @@ var RecordService = class extends TenantAwareService {
|
|
|
10324
10540
|
};
|
|
10325
10541
|
await this.hookRegistry.execute("afterCreate", schema.name, afterCtx);
|
|
10326
10542
|
}
|
|
10327
|
-
await
|
|
10543
|
+
await recalculateParentRollups(record, schema, this.rollupContext);
|
|
10328
10544
|
if (this.auditService && this.userId) {
|
|
10329
10545
|
await this.auditService.logRecordAction({
|
|
10330
10546
|
action: "record.created",
|
|
@@ -10338,12 +10554,11 @@ var RecordService = class extends TenantAwareService {
|
|
|
10338
10554
|
}
|
|
10339
10555
|
return record;
|
|
10340
10556
|
}
|
|
10557
|
+
// ============================================================================
|
|
10558
|
+
// READ
|
|
10559
|
+
// ============================================================================
|
|
10341
10560
|
/**
|
|
10342
10561
|
* Get a record by ID
|
|
10343
|
-
*
|
|
10344
|
-
* @param recordId - Record UUID
|
|
10345
|
-
* @param options - Query options
|
|
10346
|
-
* @returns Record or null if not found
|
|
10347
10562
|
*/
|
|
10348
10563
|
async getRecord(recordId, options) {
|
|
10349
10564
|
const record = await this.adapter.objectRecords.findById(recordId);
|
|
@@ -10352,14 +10567,17 @@ var RecordService = class extends TenantAwareService {
|
|
|
10352
10567
|
}
|
|
10353
10568
|
const schema = await this.schemaService.getObjectSchema(record.objectId);
|
|
10354
10569
|
if (!options?.skipPolicyCheck) {
|
|
10355
|
-
const policy = this.
|
|
10356
|
-
if (policy
|
|
10357
|
-
|
|
10570
|
+
const policy = getPolicy(this.policyRegistry, this.userId, schema.name);
|
|
10571
|
+
if (policy) {
|
|
10572
|
+
const ctx = buildPolicyContext(schema.name, this.userId, this.tenantId);
|
|
10573
|
+
if (!checkRecordAccess(policy, record, ctx)) {
|
|
10574
|
+
return null;
|
|
10575
|
+
}
|
|
10358
10576
|
}
|
|
10359
10577
|
}
|
|
10360
10578
|
let enrichedRecord = record;
|
|
10361
10579
|
if (!options?.skipFormulas) {
|
|
10362
|
-
enrichedRecord =
|
|
10580
|
+
enrichedRecord = enrichWithFormulas(record, schema);
|
|
10363
10581
|
}
|
|
10364
10582
|
if (options?.includeSchema) {
|
|
10365
10583
|
const recordWithSchema = enrichedRecord;
|
|
@@ -10378,28 +10596,11 @@ var RecordService = class extends TenantAwareService {
|
|
|
10378
10596
|
}
|
|
10379
10597
|
return record;
|
|
10380
10598
|
}
|
|
10599
|
+
// ============================================================================
|
|
10600
|
+
// UPDATE
|
|
10601
|
+
// ============================================================================
|
|
10381
10602
|
/**
|
|
10382
10603
|
* Update a record with validation
|
|
10383
|
-
*
|
|
10384
|
-
* The completion status is automatically recalculated after each update.
|
|
10385
|
-
* A draft record becomes complete when all required fields are filled.
|
|
10386
|
-
*
|
|
10387
|
-
* Triggers beforeUpdate and afterUpdate hooks if a HookRegistry is configured.
|
|
10388
|
-
*
|
|
10389
|
-
* @param recordId - Record UUID
|
|
10390
|
-
* @param data - Partial data to update
|
|
10391
|
-
* @param options - Update options
|
|
10392
|
-
* @returns Updated record with recalculated completionStatus
|
|
10393
|
-
*
|
|
10394
|
-
* @example
|
|
10395
|
-
* ```typescript
|
|
10396
|
-
* // Update a draft record to make it complete
|
|
10397
|
-
* const updated = await service.updateRecord(draftId, {
|
|
10398
|
-
* price: 99.99,
|
|
10399
|
-
* status: "active"
|
|
10400
|
-
* });
|
|
10401
|
-
* // → updated.completionStatus = "complete" if all required fields now present
|
|
10402
|
-
* ```
|
|
10403
10604
|
*/
|
|
10404
10605
|
async updateRecord(recordId, data, options) {
|
|
10405
10606
|
const existing = await this.adapter.objectRecords.findById(recordId);
|
|
@@ -10407,17 +10608,18 @@ var RecordService = class extends TenantAwareService {
|
|
|
10407
10608
|
throw new RecordNotFoundError(recordId);
|
|
10408
10609
|
}
|
|
10409
10610
|
const schema = await this.schemaService.getObjectSchema(existing.objectId);
|
|
10410
|
-
await this.
|
|
10411
|
-
const policy = this.
|
|
10412
|
-
if (policy) {
|
|
10413
|
-
this.
|
|
10611
|
+
await checkPermission(this.permissionService, this.userId, schema.name, "update");
|
|
10612
|
+
const policy = getPolicy(this.policyRegistry, this.userId, schema.name);
|
|
10613
|
+
if (policy && this.userId) {
|
|
10614
|
+
const ctx = buildPolicyContext(schema.name, this.userId, this.tenantId);
|
|
10615
|
+
checkRecordModifyOrThrow(policy, existing, ctx);
|
|
10414
10616
|
}
|
|
10415
10617
|
const mergedData = { ...existing.values, ...data };
|
|
10416
10618
|
const changedAttributes = Object.keys(data).filter((key) => existing.values[key] !== data[key]);
|
|
10417
|
-
const hookCtx =
|
|
10619
|
+
const hookCtx = createContextForUpdate(
|
|
10418
10620
|
schema,
|
|
10621
|
+
this.tenantId,
|
|
10419
10622
|
existing,
|
|
10420
|
-
data,
|
|
10421
10623
|
mergedData,
|
|
10422
10624
|
changedAttributes,
|
|
10423
10625
|
options?.hookMetadata
|
|
@@ -10451,7 +10653,7 @@ var RecordService = class extends TenantAwareService {
|
|
|
10451
10653
|
}
|
|
10452
10654
|
}
|
|
10453
10655
|
const completionStatus = computeRecordStatus(schema, mergedData);
|
|
10454
|
-
const label = await
|
|
10656
|
+
const label = await computeLabel(schema, mergedData, this.labelResolver);
|
|
10455
10657
|
const updatePayload = {
|
|
10456
10658
|
...data,
|
|
10457
10659
|
...hookModifiedValues,
|
|
@@ -10475,7 +10677,7 @@ var RecordService = class extends TenantAwareService {
|
|
|
10475
10677
|
};
|
|
10476
10678
|
await this.hookRegistry.execute("afterUpdate", schema.name, afterCtx);
|
|
10477
10679
|
}
|
|
10478
|
-
await
|
|
10680
|
+
await recalculateParentRollups(updated, schema, this.rollupContext);
|
|
10479
10681
|
const allChangedAttributes = [
|
|
10480
10682
|
...changedAttributes,
|
|
10481
10683
|
...Object.keys(hookModifiedValues).filter((k) => !changedAttributes.includes(k))
|
|
@@ -10499,89 +10701,11 @@ var RecordService = class extends TenantAwareService {
|
|
|
10499
10701
|
}
|
|
10500
10702
|
return updated;
|
|
10501
10703
|
}
|
|
10704
|
+
// ============================================================================
|
|
10705
|
+
// DELETE
|
|
10706
|
+
// ============================================================================
|
|
10502
10707
|
/**
|
|
10503
|
-
*
|
|
10504
|
-
* @internal
|
|
10505
|
-
*/
|
|
10506
|
-
buildHookContext(schema, existing, newData, mergedData, changedAttributes, metadata) {
|
|
10507
|
-
const oldValuesSnapshot = { ...existing.values };
|
|
10508
|
-
return {
|
|
10509
|
-
objectId: schema.id,
|
|
10510
|
-
objectName: schema.name,
|
|
10511
|
-
recordId: existing.id,
|
|
10512
|
-
tenantId: this.tenantId,
|
|
10513
|
-
record: existing,
|
|
10514
|
-
oldValues: oldValuesSnapshot,
|
|
10515
|
-
newValues: mergedData,
|
|
10516
|
-
changedAttributes,
|
|
10517
|
-
getChange: (attr) => ({
|
|
10518
|
-
oldValue: oldValuesSnapshot[attr],
|
|
10519
|
-
newValue: newData[attr] ?? oldValuesSnapshot[attr],
|
|
10520
|
-
changed: changedAttributes.includes(attr)
|
|
10521
|
-
}),
|
|
10522
|
-
metadata: metadata ?? {},
|
|
10523
|
-
timestamp: /* @__PURE__ */ new Date()
|
|
10524
|
-
};
|
|
10525
|
-
}
|
|
10526
|
-
/**
|
|
10527
|
-
* Build hook context for create operations (no existing record)
|
|
10528
|
-
* @internal
|
|
10529
|
-
*/
|
|
10530
|
-
buildCreateHookContext(schema, data, metadata) {
|
|
10531
|
-
const attributeNames = Object.keys(data);
|
|
10532
|
-
return {
|
|
10533
|
-
objectId: schema.id,
|
|
10534
|
-
objectName: schema.name,
|
|
10535
|
-
recordId: "",
|
|
10536
|
-
// Will be set after creation
|
|
10537
|
-
tenantId: this.tenantId,
|
|
10538
|
-
record: null,
|
|
10539
|
-
// Will be set after creation
|
|
10540
|
-
oldValues: {},
|
|
10541
|
-
newValues: data,
|
|
10542
|
-
changedAttributes: attributeNames,
|
|
10543
|
-
// All attributes are "new"
|
|
10544
|
-
getChange: (attr) => ({
|
|
10545
|
-
oldValue: void 0,
|
|
10546
|
-
newValue: data[attr],
|
|
10547
|
-
changed: attributeNames.includes(attr)
|
|
10548
|
-
}),
|
|
10549
|
-
metadata: metadata ?? {},
|
|
10550
|
-
timestamp: /* @__PURE__ */ new Date()
|
|
10551
|
-
};
|
|
10552
|
-
}
|
|
10553
|
-
/**
|
|
10554
|
-
* Build hook context for delete operations
|
|
10555
|
-
* @internal
|
|
10556
|
-
*/
|
|
10557
|
-
buildDeleteHookContext(schema, record, metadata) {
|
|
10558
|
-
const attributeNames = Object.keys(record.values);
|
|
10559
|
-
return {
|
|
10560
|
-
objectId: schema.id,
|
|
10561
|
-
objectName: schema.name,
|
|
10562
|
-
recordId: record.id,
|
|
10563
|
-
tenantId: this.tenantId,
|
|
10564
|
-
record,
|
|
10565
|
-
oldValues: record.values,
|
|
10566
|
-
newValues: {},
|
|
10567
|
-
changedAttributes: attributeNames,
|
|
10568
|
-
// All attributes are being "removed"
|
|
10569
|
-
getChange: (attr) => ({
|
|
10570
|
-
oldValue: record.values[attr],
|
|
10571
|
-
newValue: void 0,
|
|
10572
|
-
changed: attributeNames.includes(attr)
|
|
10573
|
-
}),
|
|
10574
|
-
metadata: metadata ?? {},
|
|
10575
|
-
timestamp: /* @__PURE__ */ new Date()
|
|
10576
|
-
};
|
|
10577
|
-
}
|
|
10578
|
-
/**
|
|
10579
|
-
* Delete a record
|
|
10580
|
-
*
|
|
10581
|
-
* Triggers beforeDelete and afterDelete hooks if a HookRegistry is configured.
|
|
10582
|
-
*
|
|
10583
|
-
* @param recordId - Record UUID
|
|
10584
|
-
* @param options - Delete options
|
|
10708
|
+
* Delete a record (soft delete)
|
|
10585
10709
|
*/
|
|
10586
10710
|
async deleteRecord(recordId, options) {
|
|
10587
10711
|
const record = await this.adapter.objectRecords.findById(recordId);
|
|
@@ -10589,15 +10713,14 @@ var RecordService = class extends TenantAwareService {
|
|
|
10589
10713
|
throw new RecordNotFoundError(recordId);
|
|
10590
10714
|
}
|
|
10591
10715
|
const schema = await this.schemaService.getObjectSchema(record.objectId);
|
|
10592
|
-
await this.
|
|
10593
|
-
const policy = this.
|
|
10594
|
-
if (policy) {
|
|
10595
|
-
this.
|
|
10716
|
+
await checkPermission(this.permissionService, this.userId, schema.name, "delete");
|
|
10717
|
+
const policy = getPolicy(this.policyRegistry, this.userId, schema.name);
|
|
10718
|
+
if (policy && this.userId) {
|
|
10719
|
+
const ctx = buildPolicyContext(schema.name, this.userId, this.tenantId);
|
|
10720
|
+
checkRecordDeleteOrThrow(policy, record, ctx);
|
|
10596
10721
|
}
|
|
10597
|
-
if (options?.checkSystem) {
|
|
10598
|
-
|
|
10599
|
-
throw new ProtectedResourceError("object", schema.name, "delete");
|
|
10600
|
-
}
|
|
10722
|
+
if (options?.checkSystem && schema.system) {
|
|
10723
|
+
throw new ProtectedResourceError("object", schema.name, "delete");
|
|
10601
10724
|
}
|
|
10602
10725
|
if (!options?.skipReferenceCheck) {
|
|
10603
10726
|
const references = await this.adapter.objectRecords.countRecordsReferencingId(recordId);
|
|
@@ -10605,7 +10728,7 @@ var RecordService = class extends TenantAwareService {
|
|
|
10605
10728
|
throw new RecordReferencedError(recordId, references);
|
|
10606
10729
|
}
|
|
10607
10730
|
}
|
|
10608
|
-
const hookCtx =
|
|
10731
|
+
const hookCtx = createContextForDelete(schema, this.tenantId, record, options?.hookMetadata);
|
|
10609
10732
|
if (!options?.skipHooks) {
|
|
10610
10733
|
await this.hookRegistry.execute("beforeDelete", schema.name, hookCtx);
|
|
10611
10734
|
}
|
|
@@ -10613,7 +10736,7 @@ var RecordService = class extends TenantAwareService {
|
|
|
10613
10736
|
if (!options?.skipHooks) {
|
|
10614
10737
|
await this.hookRegistry.execute("afterDelete", schema.name, hookCtx);
|
|
10615
10738
|
}
|
|
10616
|
-
await
|
|
10739
|
+
await recalculateParentRollups(record, schema, this.rollupContext);
|
|
10617
10740
|
if (this.auditService && this.userId) {
|
|
10618
10741
|
await this.auditService.logRecordAction({
|
|
10619
10742
|
action: "record.deleted",
|
|
@@ -10626,21 +10749,20 @@ var RecordService = class extends TenantAwareService {
|
|
|
10626
10749
|
});
|
|
10627
10750
|
}
|
|
10628
10751
|
}
|
|
10752
|
+
/**
|
|
10753
|
+
* Permanently delete a record (hard delete)
|
|
10754
|
+
*/
|
|
10755
|
+
async hardDeleteRecord(recordId) {
|
|
10756
|
+
const record = await this.getRecordOrThrow(recordId);
|
|
10757
|
+
const schema = await this.schemaService.getObjectSchema(record.objectId);
|
|
10758
|
+
await checkPermission(this.permissionService, this.userId, schema.name, "delete");
|
|
10759
|
+
await this.adapter.objectRecords.hardDelete(recordId);
|
|
10760
|
+
}
|
|
10761
|
+
// ============================================================================
|
|
10762
|
+
// RESTORE
|
|
10763
|
+
// ============================================================================
|
|
10629
10764
|
/**
|
|
10630
10765
|
* Restore a soft-deleted record
|
|
10631
|
-
*
|
|
10632
|
-
* Triggers beforeRestore and afterRestore hooks if a HookRegistry is configured.
|
|
10633
|
-
*
|
|
10634
|
-
* @param recordId - Record UUID
|
|
10635
|
-
* @param options - Restore options
|
|
10636
|
-
* @returns Restored record
|
|
10637
|
-
*
|
|
10638
|
-
* @example
|
|
10639
|
-
* ```typescript
|
|
10640
|
-
* // Restore a deleted record
|
|
10641
|
-
* const restored = await service.restoreRecord("rec-123");
|
|
10642
|
-
* console.log(restored.deletedAt); // null
|
|
10643
|
-
* ```
|
|
10644
10766
|
*/
|
|
10645
10767
|
async restoreRecord(recordId, options) {
|
|
10646
10768
|
const record = await this.getRecordOrThrow(recordId);
|
|
@@ -10652,8 +10774,8 @@ var RecordService = class extends TenantAwareService {
|
|
|
10652
10774
|
);
|
|
10653
10775
|
}
|
|
10654
10776
|
const schema = await this.schemaService.getObjectSchema(record.objectId);
|
|
10655
|
-
await this.
|
|
10656
|
-
const hookCtx =
|
|
10777
|
+
await checkPermission(this.permissionService, this.userId, schema.name, "update");
|
|
10778
|
+
const hookCtx = createContextForRestore(schema, this.tenantId, record, options?.hookMetadata);
|
|
10657
10779
|
if (!options?.skipHooks) {
|
|
10658
10780
|
await this.hookRegistry.execute("beforeRestore", schema.name, hookCtx);
|
|
10659
10781
|
}
|
|
@@ -10678,162 +10800,37 @@ var RecordService = class extends TenantAwareService {
|
|
|
10678
10800
|
}
|
|
10679
10801
|
return restored;
|
|
10680
10802
|
}
|
|
10681
|
-
|
|
10682
|
-
|
|
10683
|
-
|
|
10684
|
-
*/
|
|
10685
|
-
buildRestoreHookContext(schema, record, metadata) {
|
|
10686
|
-
return {
|
|
10687
|
-
objectId: schema.id,
|
|
10688
|
-
objectName: schema.name,
|
|
10689
|
-
recordId: record.id,
|
|
10690
|
-
tenantId: this.tenantId,
|
|
10691
|
-
record,
|
|
10692
|
-
oldValues: record.values,
|
|
10693
|
-
newValues: record.values,
|
|
10694
|
-
changedAttributes: [],
|
|
10695
|
-
getChange: (attr) => ({
|
|
10696
|
-
oldValue: record.values[attr],
|
|
10697
|
-
newValue: record.values[attr],
|
|
10698
|
-
changed: false
|
|
10699
|
-
}),
|
|
10700
|
-
metadata: metadata ?? {},
|
|
10701
|
-
timestamp: /* @__PURE__ */ new Date()
|
|
10702
|
-
};
|
|
10703
|
-
}
|
|
10704
|
-
/**
|
|
10705
|
-
* Enrich a record with computed formula values
|
|
10706
|
-
*
|
|
10707
|
-
* Formula attributes are calculated at read-time from the record's values.
|
|
10708
|
-
* This method adds the computed values to the record's values object.
|
|
10709
|
-
*
|
|
10710
|
-
* @param record - The record to enrich
|
|
10711
|
-
* @param schema - The object schema containing attribute definitions
|
|
10712
|
-
* @returns Record with formula values computed
|
|
10713
|
-
* @internal
|
|
10714
|
-
*/
|
|
10715
|
-
enrichWithFormulas(record, schema) {
|
|
10716
|
-
const formulaAttrs = schema.attributes.filter(
|
|
10717
|
-
(a) => a.type === "formula"
|
|
10718
|
-
);
|
|
10719
|
-
if (formulaAttrs.length === 0) {
|
|
10720
|
-
return record;
|
|
10721
|
-
}
|
|
10722
|
-
const enrichedValues = { ...record.values };
|
|
10723
|
-
for (const attr of formulaAttrs) {
|
|
10724
|
-
enrichedValues[attr.name] = evaluateFormulaAttribute(attr, record.values);
|
|
10725
|
-
}
|
|
10726
|
-
return {
|
|
10727
|
-
...record,
|
|
10728
|
-
values: enrichedValues
|
|
10729
|
-
};
|
|
10730
|
-
}
|
|
10731
|
-
/**
|
|
10732
|
-
* Enrich multiple records with computed formula values
|
|
10733
|
-
* @internal
|
|
10734
|
-
*/
|
|
10735
|
-
enrichRecordsWithFormulas(records, schema) {
|
|
10736
|
-
const formulaAttrs = schema.attributes.filter(
|
|
10737
|
-
(a) => a.type === "formula"
|
|
10738
|
-
);
|
|
10739
|
-
if (formulaAttrs.length === 0) {
|
|
10740
|
-
return records;
|
|
10741
|
-
}
|
|
10742
|
-
return records.map((record) => this.enrichWithFormulas(record, schema));
|
|
10743
|
-
}
|
|
10744
|
-
/**
|
|
10745
|
-
* Recalculate rollups after a record changes
|
|
10746
|
-
*
|
|
10747
|
-
* This handles three cases:
|
|
10748
|
-
* 1. The record itself has rollups (e.g., aggregating from related records it points to)
|
|
10749
|
-
* 2. Parent records have rollups that aggregate from this record (reverse pattern)
|
|
10750
|
-
* 3. Records that have forward rollups pointing to this record (forward pattern)
|
|
10751
|
-
*
|
|
10752
|
-
* @param record - The record that was modified
|
|
10753
|
-
* @param schema - Schema of the record's object
|
|
10754
|
-
* @internal
|
|
10755
|
-
*/
|
|
10756
|
-
async recalculateParentRollups(record, schema) {
|
|
10757
|
-
const ownRollupAttrs = schema.attributes.filter(
|
|
10758
|
-
(a) => a.type === "rollup"
|
|
10759
|
-
);
|
|
10760
|
-
if (ownRollupAttrs.length > 0) {
|
|
10761
|
-
await this.rollupService.recalculateAndUpdate(record, schema);
|
|
10762
|
-
}
|
|
10763
|
-
const affectedParentIds = await this.rollupService.findAffectedParentRecords(record, schema);
|
|
10764
|
-
if (affectedParentIds.length > 0) {
|
|
10765
|
-
const parentRecords = await this.adapter.objectRecords.findByIds(affectedParentIds);
|
|
10766
|
-
await Promise.all(
|
|
10767
|
-
parentRecords.map(async (parentRecord) => {
|
|
10768
|
-
const parentSchema = await this.schemaService.getObjectSchema(parentRecord.objectId);
|
|
10769
|
-
const rollupAttrs = parentSchema.attributes.filter(
|
|
10770
|
-
(a) => a.type === "rollup"
|
|
10771
|
-
);
|
|
10772
|
-
if (rollupAttrs.length > 0) {
|
|
10773
|
-
await this.rollupService.recalculateAndUpdate(parentRecord, parentSchema);
|
|
10774
|
-
}
|
|
10775
|
-
})
|
|
10776
|
-
);
|
|
10777
|
-
}
|
|
10778
|
-
const affectedForwardRecords = await this.rollupService.findRecordsWithForwardRollup(
|
|
10779
|
-
record,
|
|
10780
|
-
schema
|
|
10781
|
-
);
|
|
10782
|
-
await Promise.all(
|
|
10783
|
-
affectedForwardRecords.map(async (forwardRecord) => {
|
|
10784
|
-
const forwardSchema = await this.schemaService.getObjectSchema(forwardRecord.objectId);
|
|
10785
|
-
await this.rollupService.recalculateAndUpdate(forwardRecord, forwardSchema);
|
|
10786
|
-
})
|
|
10787
|
-
);
|
|
10788
|
-
}
|
|
10789
|
-
/**
|
|
10790
|
-
* Permanently delete a record (hard delete)
|
|
10791
|
-
*
|
|
10792
|
-
* This cannot be undone. Use with caution - prefer soft delete for data safety.
|
|
10793
|
-
* Does NOT trigger delete hooks (already triggered on soft delete).
|
|
10794
|
-
*
|
|
10795
|
-
* @param recordId - Record UUID
|
|
10796
|
-
*
|
|
10797
|
-
* @example
|
|
10798
|
-
* ```typescript
|
|
10799
|
-
* // Permanently delete a record
|
|
10800
|
-
* await service.hardDeleteRecord("rec-123");
|
|
10801
|
-
* ```
|
|
10802
|
-
*/
|
|
10803
|
-
async hardDeleteRecord(recordId) {
|
|
10804
|
-
const record = await this.getRecordOrThrow(recordId);
|
|
10805
|
-
const schema = await this.schemaService.getObjectSchema(record.objectId);
|
|
10806
|
-
await this.checkPermission(schema.name, "delete");
|
|
10807
|
-
await this.adapter.objectRecords.hardDelete(recordId);
|
|
10808
|
-
}
|
|
10803
|
+
// ============================================================================
|
|
10804
|
+
// LIST & SEARCH
|
|
10805
|
+
// ============================================================================
|
|
10809
10806
|
/**
|
|
10810
10807
|
* List records for an object with pagination
|
|
10811
|
-
*
|
|
10812
|
-
* @param objectId - Object UUID
|
|
10813
|
-
* @param options - List options
|
|
10814
|
-
* @returns Records and total count
|
|
10815
10808
|
*/
|
|
10816
10809
|
async listRecords(objectId, options) {
|
|
10817
10810
|
const schema = await this.schemaService.getObjectSchema(objectId);
|
|
10818
10811
|
if (this.permissionService && this.userId) {
|
|
10819
|
-
await this.
|
|
10812
|
+
await checkPermission(this.permissionService, this.userId, schema.name, "read");
|
|
10820
10813
|
}
|
|
10821
|
-
const policy = options?.skipPolicyFilter ? void 0 : this.
|
|
10814
|
+
const policy = options?.skipPolicyFilter ? void 0 : getPolicy(this.policyRegistry, this.userId, schema.name);
|
|
10822
10815
|
let effectiveOptions = options;
|
|
10823
|
-
if (policy?.applyListFilter) {
|
|
10824
|
-
|
|
10816
|
+
if (policy?.applyListFilter && this.userId) {
|
|
10817
|
+
const ctx = buildPolicyContext(schema.name, this.userId, this.tenantId);
|
|
10818
|
+
effectiveOptions = policy.applyListFilter(ctx, options);
|
|
10825
10819
|
}
|
|
10826
|
-
const result = await
|
|
10820
|
+
const result = await runWithSchemaContext(
|
|
10821
|
+
[schema],
|
|
10822
|
+
() => this.adapter.objectRecords.list(objectId, effectiveOptions)
|
|
10823
|
+
);
|
|
10827
10824
|
let filteredRecords = result.records;
|
|
10828
10825
|
let effectiveTotal = result.total;
|
|
10829
|
-
if (policy?.canAccessRecord) {
|
|
10830
|
-
const ctx =
|
|
10826
|
+
if (policy?.canAccessRecord && this.userId) {
|
|
10827
|
+
const ctx = buildPolicyContext(schema.name, this.userId, this.tenantId);
|
|
10831
10828
|
filteredRecords = result.records.filter((record) => policy.canAccessRecord?.(ctx, record));
|
|
10832
10829
|
effectiveTotal = filteredRecords.length;
|
|
10833
10830
|
}
|
|
10834
10831
|
if (!options?.skipFormulas) {
|
|
10835
10832
|
return {
|
|
10836
|
-
records:
|
|
10833
|
+
records: enrichRecordsWithFormulas(filteredRecords, schema),
|
|
10837
10834
|
total: effectiveTotal
|
|
10838
10835
|
};
|
|
10839
10836
|
}
|
|
@@ -10844,55 +10841,43 @@ var RecordService = class extends TenantAwareService {
|
|
|
10844
10841
|
}
|
|
10845
10842
|
/**
|
|
10846
10843
|
* Search records using full-text search
|
|
10847
|
-
*
|
|
10848
|
-
* @param objectId - Object UUID
|
|
10849
|
-
* @param query - Search query
|
|
10850
|
-
* @param options - Search options
|
|
10851
|
-
* @returns Matching records and total count
|
|
10852
10844
|
*/
|
|
10853
10845
|
async searchRecords(objectId, query, options) {
|
|
10854
10846
|
const schema = await this.schemaService.getObjectSchema(objectId);
|
|
10855
10847
|
if (this.permissionService && this.userId) {
|
|
10856
|
-
await this.
|
|
10848
|
+
await checkPermission(this.permissionService, this.userId, schema.name, "read");
|
|
10857
10849
|
}
|
|
10858
|
-
const result = await
|
|
10850
|
+
const result = await runWithSchemaContext(
|
|
10851
|
+
[schema],
|
|
10852
|
+
() => this.adapter.objectRecords.search(objectId, query, options)
|
|
10853
|
+
);
|
|
10859
10854
|
if (!options?.skipFormulas) {
|
|
10860
10855
|
return {
|
|
10861
|
-
records:
|
|
10856
|
+
records: enrichRecordsWithFormulas(result.records, schema),
|
|
10862
10857
|
total: result.total
|
|
10863
10858
|
};
|
|
10864
10859
|
}
|
|
10865
10860
|
return result;
|
|
10866
10861
|
}
|
|
10862
|
+
// ============================================================================
|
|
10863
|
+
// VALIDATION & STATUS
|
|
10864
|
+
// ============================================================================
|
|
10867
10865
|
/**
|
|
10868
10866
|
* Validate data against object schema without saving
|
|
10869
|
-
*
|
|
10870
|
-
* @param objectId - Object UUID
|
|
10871
|
-
* @param data - Data to validate
|
|
10872
|
-
* @returns Validation result
|
|
10873
10867
|
*/
|
|
10874
10868
|
async validateData(objectId, data) {
|
|
10875
10869
|
const schema = await this.schemaService.getObjectSchema(objectId);
|
|
10876
10870
|
return validateObject(schema, data);
|
|
10877
10871
|
}
|
|
10878
10872
|
/**
|
|
10879
|
-
* Compute the completion status for given data without saving
|
|
10880
|
-
* Useful for UI to show draft/complete status before submitting.
|
|
10881
|
-
*
|
|
10882
|
-
* @param objectId - Object UUID
|
|
10883
|
-
* @param data - Data to check
|
|
10884
|
-
* @returns Computed completion status
|
|
10873
|
+
* Compute the completion status for given data without saving
|
|
10885
10874
|
*/
|
|
10886
10875
|
async computeStatus(objectId, data) {
|
|
10887
10876
|
const schema = await this.schemaService.getObjectSchema(objectId);
|
|
10888
10877
|
return computeRecordStatus(schema, data);
|
|
10889
10878
|
}
|
|
10890
10879
|
/**
|
|
10891
|
-
* Refresh the completion status of an existing record
|
|
10892
|
-
* Useful when schema changes and you need to recompute statuses.
|
|
10893
|
-
*
|
|
10894
|
-
* @param recordId - Record UUID
|
|
10895
|
-
* @returns Updated completion status
|
|
10880
|
+
* Refresh the completion status of an existing record
|
|
10896
10881
|
*/
|
|
10897
10882
|
async refreshRecordStatus(recordId) {
|
|
10898
10883
|
const record = await this.getRecordOrThrow(recordId);
|
|
@@ -13362,6 +13347,13 @@ export {
|
|
|
13362
13347
|
QueryNoResultError,
|
|
13363
13348
|
QueryMultipleResultsError,
|
|
13364
13349
|
TenantContextError,
|
|
13350
|
+
getSchemaFromContext,
|
|
13351
|
+
getSchemaByNameFromContext,
|
|
13352
|
+
hasSchemaContext,
|
|
13353
|
+
getSchemaContext,
|
|
13354
|
+
addSchemaToContext,
|
|
13355
|
+
runWithSchemaContext,
|
|
13356
|
+
runWithMergedSchemaContext,
|
|
13365
13357
|
getContext,
|
|
13366
13358
|
getTenantId,
|
|
13367
13359
|
getUserId,
|
|
@@ -13423,6 +13415,7 @@ export {
|
|
|
13423
13415
|
buildAuditChanges,
|
|
13424
13416
|
TenantAwareService,
|
|
13425
13417
|
TenantAwareRepository,
|
|
13418
|
+
SchemaContextAwareRepository,
|
|
13426
13419
|
AuditService,
|
|
13427
13420
|
FileService,
|
|
13428
13421
|
GeocodingService,
|