@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.
@@ -482,7 +482,7 @@ var TenantContextError = class _TenantContextError extends Error {
482
482
  }
483
483
  };
484
484
 
485
- // src/runtime/context/tenant-context.ts
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 getContext() {
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 getStorage().getStore() !== void 0;
631
+ return getStorage2().getStore() !== void 0;
538
632
  }
539
633
  function runWithContext(context, fn) {
540
634
  const frozenContext = Object.freeze({ ...context });
541
- return getStorage().run(frozenContext, fn);
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 === "percentage") {
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, computeLabel) {
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 computeLabel(record.values);
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 = await this.adapter.objectRecords.findById(id);
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 sourceObject = await this.adapter.objects.findByName(sourceObjectName);
9685
- if (!sourceObject) {
9686
- return { value: null, recordCount: 0 };
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
- const sourceAttributes = await this.adapter.attributes.findByObjectId(sourceObject.id);
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
- sourceObject.id,
9699
- reverseRelationAttr.name,
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.service.ts
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
- var RecordService = class extends TenantAwareService {
10096
- constructor(adapter, options) {
10097
- super();
10098
- this.adapter = adapter;
10099
- this.schemaService = new ObjectSchemaService(adapter, registry, {
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
- * Check permission for an action on an object.
10112
- * Only checks if permissionService and userId are configured.
10113
- * @internal
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
- * Get policy for an object if one exists and userId is configured.
10122
- * @internal
10123
- */
10124
- getPolicy(objectName) {
10125
- if (!(this.policyRegistry && this.userId)) {
10126
- return void 0;
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
- * Build policy context for the current request.
10132
- * @internal
10133
- */
10134
- buildPolicyContext(objectName) {
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
- * Check if user can access a record based on policy.
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
- * Check if user can modify a record based on policy.
10155
- * Throws PolicyViolationError if denied.
10156
- * @internal
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
- * Check if user can delete a record based on policy.
10169
- * Throws PolicyViolationError if denied.
10170
- * @internal
10171
- */
10172
- checkRecordDelete(policy, record) {
10173
- if (!policy.canDeleteRecord) {
10174
- return;
10175
- }
10176
- const canDelete = policy.canDeleteRecord(this.buildPolicyContext(policy.objectName), record);
10177
- if (!canDelete) {
10178
- throw new PolicyViolationError(policy.objectName, "delete", record.id);
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
- * Resolve relation IDs to their display labels
10183
- * @internal
10184
- */
10185
- async resolveRelationLabels(relationAttrs, values) {
10186
- const resolvedMap = /* @__PURE__ */ new Map();
10187
- const idsWithAttrId = [];
10188
- const idsWithoutAttrId = [];
10189
- for (const attr of relationAttrs) {
10190
- const val = values[attr.name];
10191
- const ids = this.extractRelationIds(val);
10192
- if (ids.length > 0) {
10193
- if (attr.id) {
10194
- idsWithAttrId.push({ attrId: attr.id, ids });
10195
- } else {
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
- idsWithAttrId.map(async ({ attrId, ids }) => {
10202
- const resolved = await this.relationService.resolveIds(ids, attrId);
10203
- for (const r of resolved) {
10204
- resolvedMap.set(r.id, r.label);
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
- * Compute display label from schema expression
10232
- * Automatically resolves relation attribute values to their labels
10233
- * and select/multiselect values to their option labels
10234
- * @internal
10235
- */
10236
- async computeLabel(schema, values) {
10237
- const attrNames = extractAttributeNames(schema.labelExpression);
10238
- let enrichedValues = enrichValuesForDisplay(values, schema.attributes);
10239
- const relationAttrs = schema.attributes.filter(
10240
- (attr) => attr.type === "relation" && attrNames.includes(attr.name)
10241
- );
10242
- if (relationAttrs.length === 0) {
10243
- return renderLabelExpression(schema.labelExpression, enrichedValues);
10244
- }
10245
- const resolvedMap = await this.resolveRelationLabels(relationAttrs, values);
10246
- if (resolvedMap.size === 0) {
10247
- return renderLabelExpression(schema.labelExpression, enrichedValues);
10248
- }
10249
- enrichedValues = { ...enrichedValues };
10250
- for (const attr of relationAttrs) {
10251
- const val = values[attr.name];
10252
- const ids = this.extractRelationIds(val);
10253
- if (ids.length > 0 && resolvedMap.has(ids[0])) {
10254
- enrichedValues[attr.name] = resolvedMap.get(ids[0]);
10255
- }
10256
- }
10257
- return renderLabelExpression(schema.labelExpression, enrichedValues);
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.checkPermission(schema.name, "create");
10292
- const hookCtx = this.buildCreateHookContext(schema, dataWithDefaults, options?.hookMetadata);
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 this.computeLabel(schema, dataWithDefaults);
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 this.recalculateParentRollups(record, schema);
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.getPolicy(schema.name);
10356
- if (policy && !this.checkRecordAccess(policy, record)) {
10357
- return null;
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 = this.enrichWithFormulas(record, schema);
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.checkPermission(schema.name, "update");
10411
- const policy = this.getPolicy(schema.name);
10412
- if (policy) {
10413
- this.checkRecordModify(policy, existing);
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 = this.buildHookContext(
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 this.computeLabel(schema, mergedData);
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 this.recalculateParentRollups(updated, schema);
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
- * Build hook context for update operations
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.checkPermission(schema.name, "delete");
10593
- const policy = this.getPolicy(schema.name);
10594
- if (policy) {
10595
- this.checkRecordDelete(policy, record);
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
- if (schema.system) {
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 = this.buildDeleteHookContext(schema, record, options?.hookMetadata);
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 this.recalculateParentRollups(record, schema);
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.checkPermission(schema.name, "update");
10656
- const hookCtx = this.buildRestoreHookContext(schema, record, options?.hookMetadata);
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
- * Build hook context for restore operations
10683
- * @internal
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.checkPermission(schema.name, "read");
10812
+ await checkPermission(this.permissionService, this.userId, schema.name, "read");
10820
10813
  }
10821
- const policy = options?.skipPolicyFilter ? void 0 : this.getPolicy(schema.name);
10814
+ const policy = options?.skipPolicyFilter ? void 0 : getPolicy(this.policyRegistry, this.userId, schema.name);
10822
10815
  let effectiveOptions = options;
10823
- if (policy?.applyListFilter) {
10824
- effectiveOptions = policy.applyListFilter(this.buildPolicyContext(schema.name), options);
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 this.adapter.objectRecords.list(objectId, effectiveOptions);
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 = this.buildPolicyContext(schema.name);
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: this.enrichRecordsWithFormulas(filteredRecords, schema),
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.checkPermission(schema.name, "read");
10848
+ await checkPermission(this.permissionService, this.userId, schema.name, "read");
10857
10849
  }
10858
- const result = await this.adapter.objectRecords.search(objectId, query, options);
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: this.enrichRecordsWithFormulas(result.records, schema),
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,