@stndrds/schema 0.1.0-alpha.41 → 0.1.0-alpha.43

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.
@@ -3300,11 +3300,11 @@ function createMockObjectRecordsRepository(stores) {
3300
3300
  }
3301
3301
  return Promise.resolve(updated);
3302
3302
  },
3303
- async batchRefreshLabels(objectId, computeLabel) {
3303
+ async batchRefreshLabels(objectId, computeLabel2) {
3304
3304
  let updated = 0;
3305
3305
  for (const record of stores.objectRecords.values()) {
3306
3306
  if (record.objectId === objectId) {
3307
- const newLabel = await computeLabel(record.values);
3307
+ const newLabel = await computeLabel2(record.values);
3308
3308
  if (newLabel !== record.label) {
3309
3309
  record.label = newLabel;
3310
3310
  record.updatedAt = /* @__PURE__ */ new Date();
@@ -10212,7 +10212,7 @@ var UserService = class extends TenantAwareService {
10212
10212
  }
10213
10213
  };
10214
10214
 
10215
- // src/runtime/services/record.service.ts
10215
+ // src/runtime/services/record/defaults.ts
10216
10216
  function applyDefaultValues(schema, data) {
10217
10217
  const result = { ...data };
10218
10218
  for (const attr of schema.attributes) {
@@ -10225,170 +10225,267 @@ function applyDefaultValues(schema, data) {
10225
10225
  }
10226
10226
  return result;
10227
10227
  }
10228
- var RecordService = class extends TenantAwareService {
10229
- constructor(adapter, options) {
10230
- super();
10231
- this.adapter = adapter;
10232
- this.schemaService = new ObjectSchemaService(adapter, registry, {
10233
- auditService: options?.auditService
10234
- });
10235
- this.relationService = new RelationService(adapter, registry);
10236
- this.userService = new UserService(adapter);
10237
- this.rollupService = new RollupService(adapter);
10238
- this.hookRegistry = options?.hookRegistry ?? new NoopHookRegistry();
10239
- this.permissionService = options?.permissionService;
10240
- this.auditService = options?.auditService ?? (adapter.audit ? new AuditService(adapter) : void 0);
10241
- 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);
10242
10233
  }
10243
- /**
10244
- * Check permission for an action on an object.
10245
- * Only checks if permissionService and userId are configured.
10246
- * @internal
10247
- */
10248
- async checkPermission(objectName, action) {
10249
- if (this.permissionService && this.userId) {
10250
- await this.permissionService.checkObjectAccess(this.userId, objectName, action);
10251
- }
10234
+ }
10235
+ function getPolicy(policyRegistry, userId, objectName) {
10236
+ if (!(policyRegistry && userId)) {
10237
+ return void 0;
10252
10238
  }
10253
- /**
10254
- * Get policy for an object if one exists and userId is configured.
10255
- * @internal
10256
- */
10257
- getPolicy(objectName) {
10258
- if (!(this.policyRegistry && this.userId)) {
10259
- return void 0;
10260
- }
10261
- 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;
10262
10247
  }
10263
- /**
10264
- * Build policy context for the current request.
10265
- * @internal
10266
- */
10267
- buildPolicyContext(objectName) {
10268
- return {
10269
- // biome-ignore lint/style/noNonNullAssertion: userId is guaranteed to be set
10270
- userId: this.userId,
10271
- tenantId: this.tenantId,
10272
- objectName
10273
- };
10248
+ return policy.canAccessRecord(context, record);
10249
+ }
10250
+ function checkRecordModifyOrThrow(policy, record, context) {
10251
+ if (!policy.canModifyRecord) {
10252
+ return;
10274
10253
  }
10275
- /**
10276
- * Check if user can access a record based on policy.
10277
- * Returns true if no policy exists or user can access.
10278
- * @internal
10279
- */
10280
- checkRecordAccess(policy, record) {
10281
- if (!policy.canAccessRecord) {
10282
- return true;
10283
- }
10284
- return policy.canAccessRecord(this.buildPolicyContext(policy.objectName), record);
10254
+ if (!policy.canModifyRecord(context, record)) {
10255
+ throw new PolicyViolationError(policy.objectName, "update", record.id);
10285
10256
  }
10286
- /**
10287
- * Check if user can modify a record based on policy.
10288
- * Throws PolicyViolationError if denied.
10289
- * @internal
10290
- */
10291
- checkRecordModify(policy, record) {
10292
- if (!policy.canModifyRecord) {
10293
- return;
10294
- }
10295
- const canModify = policy.canModifyRecord(this.buildPolicyContext(policy.objectName), record);
10296
- if (!canModify) {
10297
- throw new PolicyViolationError(policy.objectName, "update", record.id);
10298
- }
10257
+ }
10258
+ function checkRecordDeleteOrThrow(policy, record, context) {
10259
+ if (!policy.canDeleteRecord) {
10260
+ return;
10299
10261
  }
10300
- /**
10301
- * Check if user can delete a record based on policy.
10302
- * Throws PolicyViolationError if denied.
10303
- * @internal
10304
- */
10305
- checkRecordDelete(policy, record) {
10306
- if (!policy.canDeleteRecord) {
10307
- return;
10308
- }
10309
- const canDelete = policy.canDeleteRecord(this.buildPolicyContext(policy.objectName), record);
10310
- if (!canDelete) {
10311
- 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
+ }
10312
10285
  }
10313
10286
  }
10314
- /**
10315
- * Resolve relation IDs to their display labels
10316
- * @internal
10317
- */
10318
- async resolveRelationLabels(relationAttrs, values) {
10319
- const resolvedMap = /* @__PURE__ */ new Map();
10320
- const idsWithAttrId = [];
10321
- const idsWithoutAttrId = [];
10322
- for (const attr of relationAttrs) {
10323
- const val = values[attr.name];
10324
- const ids = this.extractRelationIds(val);
10325
- if (ids.length > 0) {
10326
- if (attr.id) {
10327
- idsWithAttrId.push({ attrId: attr.id, ids });
10328
- } else {
10329
- 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);
10330
10302
  }
10331
10303
  }
10332
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);
10333
10440
  await Promise.all(
10334
- idsWithAttrId.map(async ({ attrId, ids }) => {
10335
- const resolved = await this.relationService.resolveIds(ids, attrId);
10336
- for (const r of resolved) {
10337
- 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);
10338
10448
  }
10339
10449
  })
10340
10450
  );
10341
- if (idsWithoutAttrId.length > 0) {
10342
- const uniqueIds = [...new Set(idsWithoutAttrId)].filter((id) => !resolvedMap.has(id));
10343
- if (uniqueIds.length > 0) {
10344
- const records = await this.adapter.objectRecords.findByIds(uniqueIds);
10345
- for (const record of records) {
10346
- if (record.label) {
10347
- resolvedMap.set(record.id, record.label);
10348
- }
10349
- }
10350
- }
10351
- }
10352
- return resolvedMap;
10353
10451
  }
10354
- /**
10355
- * Extract relation IDs from a value (string or array)
10356
- * @internal
10357
- */
10358
- extractRelationIds(val) {
10359
- if (typeof val === "string") return [val];
10360
- if (Array.isArray(val) && typeof val[0] === "string") return [val[0]];
10361
- return [];
10362
- }
10363
- /**
10364
- * Compute display label from schema expression
10365
- * Automatically resolves relation attribute values to their labels
10366
- * and select/multiselect values to their option labels
10367
- * @internal
10368
- */
10369
- async computeLabel(schema, values) {
10370
- const attrNames = extractAttributeNames(schema.labelExpression);
10371
- let enrichedValues = enrichValuesForDisplay(values, schema.attributes);
10372
- const relationAttrs = schema.attributes.filter(
10373
- (attr) => attr.type === "relation" && attrNames.includes(attr.name)
10374
- );
10375
- if (relationAttrs.length === 0) {
10376
- return renderLabelExpression(schema.labelExpression, enrichedValues);
10377
- }
10378
- const resolvedMap = await this.resolveRelationLabels(relationAttrs, values);
10379
- if (resolvedMap.size === 0) {
10380
- return renderLabelExpression(schema.labelExpression, enrichedValues);
10381
- }
10382
- enrichedValues = { ...enrichedValues };
10383
- for (const attr of relationAttrs) {
10384
- const val = values[attr.name];
10385
- const ids = this.extractRelationIds(val);
10386
- if (ids.length > 0 && resolvedMap.has(ids[0])) {
10387
- enrichedValues[attr.name] = resolvedMap.get(ids[0]);
10388
- }
10389
- }
10390
- 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
+ };
10391
10485
  }
10486
+ // ============================================================================
10487
+ // CREATE
10488
+ // ============================================================================
10392
10489
  /**
10393
10490
  * Create a new record with validation
10394
10491
  *
@@ -10398,31 +10495,17 @@ var RecordService = class extends TenantAwareService {
10398
10495
  * @param data - Record data (attribute values)
10399
10496
  * @param options - Creation options
10400
10497
  * @returns Created record with computed completionStatus
10401
- *
10402
- * @example
10403
- * ```typescript
10404
- * const service = new RecordService(adapter, "tenant-123");
10405
- *
10406
- * // Create a complete record (strict validation)
10407
- * const product = await service.createRecord("obj-product", {
10408
- * name: "Nike Air Max",
10409
- * price: 129.99,
10410
- * status: "active"
10411
- * });
10412
- * // → product.completionStatus = "complete"
10413
- *
10414
- * // Create a draft record (allows missing required fields)
10415
- * const draft = await service.createRecord("obj-product", {
10416
- * name: "Draft Product"
10417
- * }, { allowDraft: true });
10418
- * // → draft.completionStatus = "draft"
10419
- * ```
10420
10498
  */
10421
10499
  async createRecord(objectId, data, options) {
10422
10500
  const schema = await this.schemaService.getObjectSchema(objectId);
10423
10501
  const dataWithDefaults = applyDefaultValues(schema, data);
10424
- await this.checkPermission(schema.name, "create");
10425
- 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
+ );
10426
10509
  if (!options?.skipHooks) {
10427
10510
  await this.hookRegistry.execute("beforeCreate", schema.name, hookCtx);
10428
10511
  }
@@ -10440,7 +10523,7 @@ var RecordService = class extends TenantAwareService {
10440
10523
  }
10441
10524
  }
10442
10525
  const completionStatus = computeRecordStatus(schema, dataWithDefaults);
10443
- const label = await this.computeLabel(schema, dataWithDefaults);
10526
+ const label = await computeLabel(schema, dataWithDefaults, this.labelResolver);
10444
10527
  const record = await this.adapter.objectRecords.create({
10445
10528
  objectId,
10446
10529
  data: dataWithDefaults,
@@ -10457,7 +10540,7 @@ var RecordService = class extends TenantAwareService {
10457
10540
  };
10458
10541
  await this.hookRegistry.execute("afterCreate", schema.name, afterCtx);
10459
10542
  }
10460
- await this.recalculateParentRollups(record, schema);
10543
+ await recalculateParentRollups(record, schema, this.rollupContext);
10461
10544
  if (this.auditService && this.userId) {
10462
10545
  await this.auditService.logRecordAction({
10463
10546
  action: "record.created",
@@ -10471,12 +10554,11 @@ var RecordService = class extends TenantAwareService {
10471
10554
  }
10472
10555
  return record;
10473
10556
  }
10557
+ // ============================================================================
10558
+ // READ
10559
+ // ============================================================================
10474
10560
  /**
10475
10561
  * Get a record by ID
10476
- *
10477
- * @param recordId - Record UUID
10478
- * @param options - Query options
10479
- * @returns Record or null if not found
10480
10562
  */
10481
10563
  async getRecord(recordId, options) {
10482
10564
  const record = await this.adapter.objectRecords.findById(recordId);
@@ -10485,14 +10567,17 @@ var RecordService = class extends TenantAwareService {
10485
10567
  }
10486
10568
  const schema = await this.schemaService.getObjectSchema(record.objectId);
10487
10569
  if (!options?.skipPolicyCheck) {
10488
- const policy = this.getPolicy(schema.name);
10489
- if (policy && !this.checkRecordAccess(policy, record)) {
10490
- 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
+ }
10491
10576
  }
10492
10577
  }
10493
10578
  let enrichedRecord = record;
10494
10579
  if (!options?.skipFormulas) {
10495
- enrichedRecord = this.enrichWithFormulas(record, schema);
10580
+ enrichedRecord = enrichWithFormulas(record, schema);
10496
10581
  }
10497
10582
  if (options?.includeSchema) {
10498
10583
  const recordWithSchema = enrichedRecord;
@@ -10511,28 +10596,11 @@ var RecordService = class extends TenantAwareService {
10511
10596
  }
10512
10597
  return record;
10513
10598
  }
10599
+ // ============================================================================
10600
+ // UPDATE
10601
+ // ============================================================================
10514
10602
  /**
10515
10603
  * Update a record with validation
10516
- *
10517
- * The completion status is automatically recalculated after each update.
10518
- * A draft record becomes complete when all required fields are filled.
10519
- *
10520
- * Triggers beforeUpdate and afterUpdate hooks if a HookRegistry is configured.
10521
- *
10522
- * @param recordId - Record UUID
10523
- * @param data - Partial data to update
10524
- * @param options - Update options
10525
- * @returns Updated record with recalculated completionStatus
10526
- *
10527
- * @example
10528
- * ```typescript
10529
- * // Update a draft record to make it complete
10530
- * const updated = await service.updateRecord(draftId, {
10531
- * price: 99.99,
10532
- * status: "active"
10533
- * });
10534
- * // → updated.completionStatus = "complete" if all required fields now present
10535
- * ```
10536
10604
  */
10537
10605
  async updateRecord(recordId, data, options) {
10538
10606
  const existing = await this.adapter.objectRecords.findById(recordId);
@@ -10540,17 +10608,18 @@ var RecordService = class extends TenantAwareService {
10540
10608
  throw new RecordNotFoundError(recordId);
10541
10609
  }
10542
10610
  const schema = await this.schemaService.getObjectSchema(existing.objectId);
10543
- await this.checkPermission(schema.name, "update");
10544
- const policy = this.getPolicy(schema.name);
10545
- if (policy) {
10546
- 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);
10547
10616
  }
10548
10617
  const mergedData = { ...existing.values, ...data };
10549
10618
  const changedAttributes = Object.keys(data).filter((key) => existing.values[key] !== data[key]);
10550
- const hookCtx = this.buildHookContext(
10619
+ const hookCtx = createContextForUpdate(
10551
10620
  schema,
10621
+ this.tenantId,
10552
10622
  existing,
10553
- data,
10554
10623
  mergedData,
10555
10624
  changedAttributes,
10556
10625
  options?.hookMetadata
@@ -10584,7 +10653,7 @@ var RecordService = class extends TenantAwareService {
10584
10653
  }
10585
10654
  }
10586
10655
  const completionStatus = computeRecordStatus(schema, mergedData);
10587
- const label = await this.computeLabel(schema, mergedData);
10656
+ const label = await computeLabel(schema, mergedData, this.labelResolver);
10588
10657
  const updatePayload = {
10589
10658
  ...data,
10590
10659
  ...hookModifiedValues,
@@ -10608,7 +10677,7 @@ var RecordService = class extends TenantAwareService {
10608
10677
  };
10609
10678
  await this.hookRegistry.execute("afterUpdate", schema.name, afterCtx);
10610
10679
  }
10611
- await this.recalculateParentRollups(updated, schema);
10680
+ await recalculateParentRollups(updated, schema, this.rollupContext);
10612
10681
  const allChangedAttributes = [
10613
10682
  ...changedAttributes,
10614
10683
  ...Object.keys(hookModifiedValues).filter((k) => !changedAttributes.includes(k))
@@ -10632,89 +10701,11 @@ var RecordService = class extends TenantAwareService {
10632
10701
  }
10633
10702
  return updated;
10634
10703
  }
10704
+ // ============================================================================
10705
+ // DELETE
10706
+ // ============================================================================
10635
10707
  /**
10636
- * Build hook context for update operations
10637
- * @internal
10638
- */
10639
- buildHookContext(schema, existing, newData, mergedData, changedAttributes, metadata) {
10640
- const oldValuesSnapshot = { ...existing.values };
10641
- return {
10642
- objectId: schema.id,
10643
- objectName: schema.name,
10644
- recordId: existing.id,
10645
- tenantId: this.tenantId,
10646
- record: existing,
10647
- oldValues: oldValuesSnapshot,
10648
- newValues: mergedData,
10649
- changedAttributes,
10650
- getChange: (attr) => ({
10651
- oldValue: oldValuesSnapshot[attr],
10652
- newValue: newData[attr] ?? oldValuesSnapshot[attr],
10653
- changed: changedAttributes.includes(attr)
10654
- }),
10655
- metadata: metadata ?? {},
10656
- timestamp: /* @__PURE__ */ new Date()
10657
- };
10658
- }
10659
- /**
10660
- * Build hook context for create operations (no existing record)
10661
- * @internal
10662
- */
10663
- buildCreateHookContext(schema, data, metadata) {
10664
- const attributeNames = Object.keys(data);
10665
- return {
10666
- objectId: schema.id,
10667
- objectName: schema.name,
10668
- recordId: "",
10669
- // Will be set after creation
10670
- tenantId: this.tenantId,
10671
- record: null,
10672
- // Will be set after creation
10673
- oldValues: {},
10674
- newValues: data,
10675
- changedAttributes: attributeNames,
10676
- // All attributes are "new"
10677
- getChange: (attr) => ({
10678
- oldValue: void 0,
10679
- newValue: data[attr],
10680
- changed: attributeNames.includes(attr)
10681
- }),
10682
- metadata: metadata ?? {},
10683
- timestamp: /* @__PURE__ */ new Date()
10684
- };
10685
- }
10686
- /**
10687
- * Build hook context for delete operations
10688
- * @internal
10689
- */
10690
- buildDeleteHookContext(schema, record, metadata) {
10691
- const attributeNames = Object.keys(record.values);
10692
- return {
10693
- objectId: schema.id,
10694
- objectName: schema.name,
10695
- recordId: record.id,
10696
- tenantId: this.tenantId,
10697
- record,
10698
- oldValues: record.values,
10699
- newValues: {},
10700
- changedAttributes: attributeNames,
10701
- // All attributes are being "removed"
10702
- getChange: (attr) => ({
10703
- oldValue: record.values[attr],
10704
- newValue: void 0,
10705
- changed: attributeNames.includes(attr)
10706
- }),
10707
- metadata: metadata ?? {},
10708
- timestamp: /* @__PURE__ */ new Date()
10709
- };
10710
- }
10711
- /**
10712
- * Delete a record
10713
- *
10714
- * Triggers beforeDelete and afterDelete hooks if a HookRegistry is configured.
10715
- *
10716
- * @param recordId - Record UUID
10717
- * @param options - Delete options
10708
+ * Delete a record (soft delete)
10718
10709
  */
10719
10710
  async deleteRecord(recordId, options) {
10720
10711
  const record = await this.adapter.objectRecords.findById(recordId);
@@ -10722,15 +10713,14 @@ var RecordService = class extends TenantAwareService {
10722
10713
  throw new RecordNotFoundError(recordId);
10723
10714
  }
10724
10715
  const schema = await this.schemaService.getObjectSchema(record.objectId);
10725
- await this.checkPermission(schema.name, "delete");
10726
- const policy = this.getPolicy(schema.name);
10727
- if (policy) {
10728
- 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);
10729
10721
  }
10730
- if (options?.checkSystem) {
10731
- if (schema.system) {
10732
- throw new ProtectedResourceError("object", schema.name, "delete");
10733
- }
10722
+ if (options?.checkSystem && schema.system) {
10723
+ throw new ProtectedResourceError("object", schema.name, "delete");
10734
10724
  }
10735
10725
  if (!options?.skipReferenceCheck) {
10736
10726
  const references = await this.adapter.objectRecords.countRecordsReferencingId(recordId);
@@ -10738,7 +10728,7 @@ var RecordService = class extends TenantAwareService {
10738
10728
  throw new RecordReferencedError(recordId, references);
10739
10729
  }
10740
10730
  }
10741
- const hookCtx = this.buildDeleteHookContext(schema, record, options?.hookMetadata);
10731
+ const hookCtx = createContextForDelete(schema, this.tenantId, record, options?.hookMetadata);
10742
10732
  if (!options?.skipHooks) {
10743
10733
  await this.hookRegistry.execute("beforeDelete", schema.name, hookCtx);
10744
10734
  }
@@ -10746,7 +10736,7 @@ var RecordService = class extends TenantAwareService {
10746
10736
  if (!options?.skipHooks) {
10747
10737
  await this.hookRegistry.execute("afterDelete", schema.name, hookCtx);
10748
10738
  }
10749
- await this.recalculateParentRollups(record, schema);
10739
+ await recalculateParentRollups(record, schema, this.rollupContext);
10750
10740
  if (this.auditService && this.userId) {
10751
10741
  await this.auditService.logRecordAction({
10752
10742
  action: "record.deleted",
@@ -10759,21 +10749,20 @@ var RecordService = class extends TenantAwareService {
10759
10749
  });
10760
10750
  }
10761
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
+ // ============================================================================
10762
10764
  /**
10763
10765
  * Restore a soft-deleted record
10764
- *
10765
- * Triggers beforeRestore and afterRestore hooks if a HookRegistry is configured.
10766
- *
10767
- * @param recordId - Record UUID
10768
- * @param options - Restore options
10769
- * @returns Restored record
10770
- *
10771
- * @example
10772
- * ```typescript
10773
- * // Restore a deleted record
10774
- * const restored = await service.restoreRecord("rec-123");
10775
- * console.log(restored.deletedAt); // null
10776
- * ```
10777
10766
  */
10778
10767
  async restoreRecord(recordId, options) {
10779
10768
  const record = await this.getRecordOrThrow(recordId);
@@ -10785,8 +10774,8 @@ var RecordService = class extends TenantAwareService {
10785
10774
  );
10786
10775
  }
10787
10776
  const schema = await this.schemaService.getObjectSchema(record.objectId);
10788
- await this.checkPermission(schema.name, "update");
10789
- 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);
10790
10779
  if (!options?.skipHooks) {
10791
10780
  await this.hookRegistry.execute("beforeRestore", schema.name, hookCtx);
10792
10781
  }
@@ -10811,150 +10800,22 @@ var RecordService = class extends TenantAwareService {
10811
10800
  }
10812
10801
  return restored;
10813
10802
  }
10814
- /**
10815
- * Build hook context for restore operations
10816
- * @internal
10817
- */
10818
- buildRestoreHookContext(schema, record, metadata) {
10819
- return {
10820
- objectId: schema.id,
10821
- objectName: schema.name,
10822
- recordId: record.id,
10823
- tenantId: this.tenantId,
10824
- record,
10825
- oldValues: record.values,
10826
- newValues: record.values,
10827
- changedAttributes: [],
10828
- getChange: (attr) => ({
10829
- oldValue: record.values[attr],
10830
- newValue: record.values[attr],
10831
- changed: false
10832
- }),
10833
- metadata: metadata ?? {},
10834
- timestamp: /* @__PURE__ */ new Date()
10835
- };
10836
- }
10837
- /**
10838
- * Enrich a record with computed formula values
10839
- *
10840
- * Formula attributes are calculated at read-time from the record's values.
10841
- * This method adds the computed values to the record's values object.
10842
- *
10843
- * @param record - The record to enrich
10844
- * @param schema - The object schema containing attribute definitions
10845
- * @returns Record with formula values computed
10846
- * @internal
10847
- */
10848
- enrichWithFormulas(record, schema) {
10849
- const formulaAttrs = schema.attributes.filter(
10850
- (a) => a.type === "formula"
10851
- );
10852
- if (formulaAttrs.length === 0) {
10853
- return record;
10854
- }
10855
- const enrichedValues = { ...record.values };
10856
- for (const attr of formulaAttrs) {
10857
- enrichedValues[attr.name] = evaluateFormulaAttribute(attr, record.values);
10858
- }
10859
- return {
10860
- ...record,
10861
- values: enrichedValues
10862
- };
10863
- }
10864
- /**
10865
- * Enrich multiple records with computed formula values
10866
- * @internal
10867
- */
10868
- enrichRecordsWithFormulas(records, schema) {
10869
- const formulaAttrs = schema.attributes.filter(
10870
- (a) => a.type === "formula"
10871
- );
10872
- if (formulaAttrs.length === 0) {
10873
- return records;
10874
- }
10875
- return records.map((record) => this.enrichWithFormulas(record, schema));
10876
- }
10877
- /**
10878
- * Recalculate rollups after a record changes
10879
- *
10880
- * This handles three cases:
10881
- * 1. The record itself has rollups (e.g., aggregating from related records it points to)
10882
- * 2. Parent records have rollups that aggregate from this record (reverse pattern)
10883
- * 3. Records that have forward rollups pointing to this record (forward pattern)
10884
- *
10885
- * @param record - The record that was modified
10886
- * @param schema - Schema of the record's object
10887
- * @internal
10888
- */
10889
- async recalculateParentRollups(record, schema) {
10890
- const ownRollupAttrs = schema.attributes.filter(
10891
- (a) => a.type === "rollup"
10892
- );
10893
- if (ownRollupAttrs.length > 0) {
10894
- await this.rollupService.recalculateAndUpdate(record, schema);
10895
- }
10896
- const affectedParentIds = await this.rollupService.findAffectedParentRecords(record, schema);
10897
- if (affectedParentIds.length > 0) {
10898
- const parentRecords = await this.adapter.objectRecords.findByIds(affectedParentIds);
10899
- await Promise.all(
10900
- parentRecords.map(async (parentRecord) => {
10901
- const parentSchema = await this.schemaService.getObjectSchema(parentRecord.objectId);
10902
- const rollupAttrs = parentSchema.attributes.filter(
10903
- (a) => a.type === "rollup"
10904
- );
10905
- if (rollupAttrs.length > 0) {
10906
- await this.rollupService.recalculateAndUpdate(parentRecord, parentSchema);
10907
- }
10908
- })
10909
- );
10910
- }
10911
- const affectedForwardRecords = await this.rollupService.findRecordsWithForwardRollup(
10912
- record,
10913
- schema
10914
- );
10915
- await Promise.all(
10916
- affectedForwardRecords.map(async (forwardRecord) => {
10917
- const forwardSchema = await this.schemaService.getObjectSchema(forwardRecord.objectId);
10918
- await this.rollupService.recalculateAndUpdate(forwardRecord, forwardSchema);
10919
- })
10920
- );
10921
- }
10922
- /**
10923
- * Permanently delete a record (hard delete)
10924
- *
10925
- * This cannot be undone. Use with caution - prefer soft delete for data safety.
10926
- * Does NOT trigger delete hooks (already triggered on soft delete).
10927
- *
10928
- * @param recordId - Record UUID
10929
- *
10930
- * @example
10931
- * ```typescript
10932
- * // Permanently delete a record
10933
- * await service.hardDeleteRecord("rec-123");
10934
- * ```
10935
- */
10936
- async hardDeleteRecord(recordId) {
10937
- const record = await this.getRecordOrThrow(recordId);
10938
- const schema = await this.schemaService.getObjectSchema(record.objectId);
10939
- await this.checkPermission(schema.name, "delete");
10940
- await this.adapter.objectRecords.hardDelete(recordId);
10941
- }
10803
+ // ============================================================================
10804
+ // LIST & SEARCH
10805
+ // ============================================================================
10942
10806
  /**
10943
10807
  * List records for an object with pagination
10944
- *
10945
- * @param objectId - Object UUID
10946
- * @param options - List options
10947
- * @returns Records and total count
10948
10808
  */
10949
10809
  async listRecords(objectId, options) {
10950
10810
  const schema = await this.schemaService.getObjectSchema(objectId);
10951
10811
  if (this.permissionService && this.userId) {
10952
- await this.checkPermission(schema.name, "read");
10812
+ await checkPermission(this.permissionService, this.userId, schema.name, "read");
10953
10813
  }
10954
- const policy = options?.skipPolicyFilter ? void 0 : this.getPolicy(schema.name);
10814
+ const policy = options?.skipPolicyFilter ? void 0 : getPolicy(this.policyRegistry, this.userId, schema.name);
10955
10815
  let effectiveOptions = options;
10956
- if (policy?.applyListFilter) {
10957
- 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);
10958
10819
  }
10959
10820
  const result = await runWithSchemaContext(
10960
10821
  [schema],
@@ -10962,14 +10823,14 @@ var RecordService = class extends TenantAwareService {
10962
10823
  );
10963
10824
  let filteredRecords = result.records;
10964
10825
  let effectiveTotal = result.total;
10965
- if (policy?.canAccessRecord) {
10966
- const ctx = this.buildPolicyContext(schema.name);
10826
+ if (policy?.canAccessRecord && this.userId) {
10827
+ const ctx = buildPolicyContext(schema.name, this.userId, this.tenantId);
10967
10828
  filteredRecords = result.records.filter((record) => policy.canAccessRecord?.(ctx, record));
10968
10829
  effectiveTotal = filteredRecords.length;
10969
10830
  }
10970
10831
  if (!options?.skipFormulas) {
10971
10832
  return {
10972
- records: this.enrichRecordsWithFormulas(filteredRecords, schema),
10833
+ records: enrichRecordsWithFormulas(filteredRecords, schema),
10973
10834
  total: effectiveTotal
10974
10835
  };
10975
10836
  }
@@ -10980,16 +10841,11 @@ var RecordService = class extends TenantAwareService {
10980
10841
  }
10981
10842
  /**
10982
10843
  * Search records using full-text search
10983
- *
10984
- * @param objectId - Object UUID
10985
- * @param query - Search query
10986
- * @param options - Search options
10987
- * @returns Matching records and total count
10988
10844
  */
10989
10845
  async searchRecords(objectId, query, options) {
10990
10846
  const schema = await this.schemaService.getObjectSchema(objectId);
10991
10847
  if (this.permissionService && this.userId) {
10992
- await this.checkPermission(schema.name, "read");
10848
+ await checkPermission(this.permissionService, this.userId, schema.name, "read");
10993
10849
  }
10994
10850
  const result = await runWithSchemaContext(
10995
10851
  [schema],
@@ -10997,41 +10853,31 @@ var RecordService = class extends TenantAwareService {
10997
10853
  );
10998
10854
  if (!options?.skipFormulas) {
10999
10855
  return {
11000
- records: this.enrichRecordsWithFormulas(result.records, schema),
10856
+ records: enrichRecordsWithFormulas(result.records, schema),
11001
10857
  total: result.total
11002
10858
  };
11003
10859
  }
11004
10860
  return result;
11005
10861
  }
10862
+ // ============================================================================
10863
+ // VALIDATION & STATUS
10864
+ // ============================================================================
11006
10865
  /**
11007
10866
  * Validate data against object schema without saving
11008
- *
11009
- * @param objectId - Object UUID
11010
- * @param data - Data to validate
11011
- * @returns Validation result
11012
10867
  */
11013
10868
  async validateData(objectId, data) {
11014
10869
  const schema = await this.schemaService.getObjectSchema(objectId);
11015
10870
  return validateObject(schema, data);
11016
10871
  }
11017
10872
  /**
11018
- * Compute the completion status for given data without saving.
11019
- * Useful for UI to show draft/complete status before submitting.
11020
- *
11021
- * @param objectId - Object UUID
11022
- * @param data - Data to check
11023
- * @returns Computed completion status
10873
+ * Compute the completion status for given data without saving
11024
10874
  */
11025
10875
  async computeStatus(objectId, data) {
11026
10876
  const schema = await this.schemaService.getObjectSchema(objectId);
11027
10877
  return computeRecordStatus(schema, data);
11028
10878
  }
11029
10879
  /**
11030
- * Refresh the completion status of an existing record.
11031
- * Useful when schema changes and you need to recompute statuses.
11032
- *
11033
- * @param recordId - Record UUID
11034
- * @returns Updated completion status
10880
+ * Refresh the completion status of an existing record
11035
10881
  */
11036
10882
  async refreshRecordStatus(recordId) {
11037
10883
  const record = await this.getRecordOrThrow(recordId);