@stndrds/schema 0.1.0-alpha.50 → 0.1.0-alpha.52

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.
@@ -3152,6 +3152,10 @@ function createMockFilesRepository(stores) {
3152
3152
  if (file2?.deletedAt) return Promise.resolve(null);
3153
3153
  return Promise.resolve(file2 ?? null);
3154
3154
  },
3155
+ findByIds(ids) {
3156
+ const files = ids.map((id) => stores.files.get(id)).filter((f) => f != null && !f.deletedAt);
3157
+ return Promise.resolve(files);
3158
+ },
3155
3159
  create(data) {
3156
3160
  const tenantId = getTenantId();
3157
3161
  const file2 = {
@@ -4123,6 +4127,284 @@ function createMockWorkflowParticipationsRepository(stores) {
4123
4127
  }
4124
4128
  };
4125
4129
  }
4130
+ function requireUserId() {
4131
+ const userId = getUserId();
4132
+ if (!userId) {
4133
+ throw new Error("User context required for AI operations");
4134
+ }
4135
+ return userId;
4136
+ }
4137
+ function createMockAIConversationsRepository(stores) {
4138
+ return {
4139
+ findById(id) {
4140
+ const conversation = stores.aiConversations.get(id);
4141
+ if (!conversation || conversation.deletedAt) return Promise.resolve(null);
4142
+ const tenantId = getTenantId();
4143
+ if (conversation.tenantId !== tenantId) return Promise.resolve(null);
4144
+ return Promise.resolve(conversation);
4145
+ },
4146
+ list(options) {
4147
+ const tenantId = getTenantId();
4148
+ const userId = requireUserId();
4149
+ let results = Array.from(stores.aiConversations.values()).filter((c) => {
4150
+ if (c.tenantId !== tenantId || c.userId !== userId) return false;
4151
+ if (!options?.includeDeleted && c.deletedAt) return false;
4152
+ return true;
4153
+ });
4154
+ results.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
4155
+ const total = results.length;
4156
+ if (options?.limit) {
4157
+ results = results.slice(options.offset ?? 0, (options.offset ?? 0) + options.limit);
4158
+ }
4159
+ return Promise.resolve({ conversations: results, total });
4160
+ },
4161
+ create(data) {
4162
+ const tenantId = getTenantId();
4163
+ const userId = requireUserId();
4164
+ const now = /* @__PURE__ */ new Date();
4165
+ const conversation = {
4166
+ id: generateId(),
4167
+ tenantId,
4168
+ userId,
4169
+ title: data.title ?? null,
4170
+ messageCount: 0,
4171
+ totalTokens: 0,
4172
+ totalCost: 0,
4173
+ createdAt: now,
4174
+ updatedAt: now,
4175
+ deletedAt: null
4176
+ };
4177
+ stores.aiConversations.set(conversation.id, conversation);
4178
+ return Promise.resolve(conversation);
4179
+ },
4180
+ updateTitle(id, title) {
4181
+ const conversation = stores.aiConversations.get(id);
4182
+ if (!conversation || conversation.deletedAt) return Promise.resolve(null);
4183
+ const tenantId = getTenantId();
4184
+ if (conversation.tenantId !== tenantId) return Promise.resolve(null);
4185
+ conversation.title = title;
4186
+ conversation.updatedAt = /* @__PURE__ */ new Date();
4187
+ stores.aiConversations.set(id, conversation);
4188
+ return Promise.resolve(conversation);
4189
+ },
4190
+ delete(id) {
4191
+ const conversation = stores.aiConversations.get(id);
4192
+ if (!conversation) return Promise.resolve(false);
4193
+ const tenantId = getTenantId();
4194
+ if (conversation.tenantId !== tenantId) return Promise.resolve(false);
4195
+ conversation.deletedAt = /* @__PURE__ */ new Date();
4196
+ stores.aiConversations.set(id, conversation);
4197
+ return Promise.resolve(true);
4198
+ },
4199
+ addMessage(input) {
4200
+ const now = /* @__PURE__ */ new Date();
4201
+ const message = {
4202
+ id: generateId(),
4203
+ conversationId: input.conversationId,
4204
+ role: input.role,
4205
+ content: input.content,
4206
+ thinkingLevel: input.thinkingLevel ?? null,
4207
+ thinkingSummary: input.thinkingSummary ?? null,
4208
+ toolCalls: input.toolCalls ?? null,
4209
+ inputTokens: input.inputTokens ?? null,
4210
+ outputTokens: input.outputTokens ?? null,
4211
+ cost: input.cost ?? null,
4212
+ provider: input.provider ?? null,
4213
+ model: input.model ?? null,
4214
+ attachmentIds: input.attachmentIds ?? null,
4215
+ createdAt: now
4216
+ };
4217
+ stores.aiMessages.set(message.id, message);
4218
+ const conversation = stores.aiConversations.get(input.conversationId);
4219
+ if (conversation) {
4220
+ conversation.messageCount++;
4221
+ conversation.totalTokens += (input.inputTokens ?? 0) + (input.outputTokens ?? 0);
4222
+ conversation.totalCost += input.cost ?? 0;
4223
+ conversation.updatedAt = now;
4224
+ stores.aiConversations.set(input.conversationId, conversation);
4225
+ }
4226
+ return Promise.resolve(message);
4227
+ },
4228
+ listMessages(conversationId, options) {
4229
+ let results = Array.from(stores.aiMessages.values()).filter((m) => m.conversationId === conversationId).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
4230
+ const total = results.length;
4231
+ if (options?.limit) {
4232
+ results = results.slice(options.offset ?? 0, (options.offset ?? 0) + options.limit);
4233
+ }
4234
+ return Promise.resolve({ messages: results, total });
4235
+ },
4236
+ getRecentMessages(conversationId, count = 20) {
4237
+ const results = Array.from(stores.aiMessages.values()).filter((m) => m.conversationId === conversationId).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()).slice(-count);
4238
+ return Promise.resolve(results);
4239
+ }
4240
+ };
4241
+ }
4242
+ function createMockAIUserMemoryRepository(stores) {
4243
+ const getKey = () => {
4244
+ const tenantId = getTenantId();
4245
+ const userId = requireUserId();
4246
+ return `${tenantId}:${userId}`;
4247
+ };
4248
+ return {
4249
+ get() {
4250
+ const key = getKey();
4251
+ return Promise.resolve(stores.aiUserMemory.get(key) ?? null);
4252
+ },
4253
+ upsert(data) {
4254
+ const key = getKey();
4255
+ const tenantId = getTenantId();
4256
+ const userId = requireUserId();
4257
+ const now = /* @__PURE__ */ new Date();
4258
+ const existing = stores.aiUserMemory.get(key);
4259
+ const memory = {
4260
+ id: existing?.id ?? generateId(),
4261
+ tenantId,
4262
+ userId,
4263
+ preferences: data.preferences ?? existing?.preferences ?? {},
4264
+ facts: data.facts ?? existing?.facts ?? [],
4265
+ createdAt: existing?.createdAt ?? now,
4266
+ updatedAt: now
4267
+ };
4268
+ stores.aiUserMemory.set(key, memory);
4269
+ return Promise.resolve(memory);
4270
+ },
4271
+ addFact(fact) {
4272
+ const key = getKey();
4273
+ const tenantId = getTenantId();
4274
+ const userId = requireUserId();
4275
+ const now = /* @__PURE__ */ new Date();
4276
+ const existing = stores.aiUserMemory.get(key);
4277
+ const memory = {
4278
+ id: existing?.id ?? generateId(),
4279
+ tenantId,
4280
+ userId,
4281
+ preferences: existing?.preferences ?? {},
4282
+ facts: [...existing?.facts ?? [], fact],
4283
+ createdAt: existing?.createdAt ?? now,
4284
+ updatedAt: now
4285
+ };
4286
+ stores.aiUserMemory.set(key, memory);
4287
+ return Promise.resolve(memory);
4288
+ },
4289
+ removeFact(fact) {
4290
+ const key = getKey();
4291
+ const tenantId = getTenantId();
4292
+ const userId = requireUserId();
4293
+ const now = /* @__PURE__ */ new Date();
4294
+ const existing = stores.aiUserMemory.get(key);
4295
+ const memory = {
4296
+ id: existing?.id ?? generateId(),
4297
+ tenantId,
4298
+ userId,
4299
+ preferences: existing?.preferences ?? {},
4300
+ facts: (existing?.facts ?? []).filter((f) => f !== fact),
4301
+ createdAt: existing?.createdAt ?? now,
4302
+ updatedAt: now
4303
+ };
4304
+ stores.aiUserMemory.set(key, memory);
4305
+ return Promise.resolve(memory);
4306
+ },
4307
+ setPreference(prefKey, value) {
4308
+ const memoryKey = getKey();
4309
+ const tenantId = getTenantId();
4310
+ const userId = requireUserId();
4311
+ const now = /* @__PURE__ */ new Date();
4312
+ const existing = stores.aiUserMemory.get(memoryKey);
4313
+ const memory = {
4314
+ id: existing?.id ?? generateId(),
4315
+ tenantId,
4316
+ userId,
4317
+ preferences: { ...existing?.preferences ?? {}, [prefKey]: value },
4318
+ facts: existing?.facts ?? [],
4319
+ createdAt: existing?.createdAt ?? now,
4320
+ updatedAt: now
4321
+ };
4322
+ stores.aiUserMemory.set(memoryKey, memory);
4323
+ return Promise.resolve(memory);
4324
+ },
4325
+ clear() {
4326
+ const key = getKey();
4327
+ stores.aiUserMemory.delete(key);
4328
+ return Promise.resolve();
4329
+ }
4330
+ };
4331
+ }
4332
+ function createMockAIUsageMetricsRepository(stores) {
4333
+ const getDateKey = (date2) => {
4334
+ const tenantId = getTenantId();
4335
+ const dateStr = date2.toISOString().split("T")[0];
4336
+ return `${tenantId}:${dateStr}`;
4337
+ };
4338
+ return {
4339
+ recordUsage(data) {
4340
+ const tenantId = getTenantId();
4341
+ const now = /* @__PURE__ */ new Date();
4342
+ const key = getDateKey(now);
4343
+ const existing = stores.aiUsageMetrics.get(key);
4344
+ const providerBreakdown = existing?.providerBreakdown ?? {};
4345
+ if (!providerBreakdown[data.provider]) {
4346
+ providerBreakdown[data.provider] = { requests: 0, tokens: 0, cost: 0 };
4347
+ }
4348
+ providerBreakdown[data.provider].requests++;
4349
+ providerBreakdown[data.provider].tokens += data.tokens;
4350
+ providerBreakdown[data.provider].cost += data.cost;
4351
+ const toolUsage = existing?.toolUsage ?? {};
4352
+ if (data.toolName) {
4353
+ toolUsage[data.toolName] = (toolUsage[data.toolName] ?? 0) + 1;
4354
+ }
4355
+ const metrics = {
4356
+ id: existing?.id ?? generateId(),
4357
+ tenantId,
4358
+ date: new Date(now.toISOString().split("T")[0] ?? now.toISOString()),
4359
+ requestCount: (existing?.requestCount ?? 0) + 1,
4360
+ totalTokens: (existing?.totalTokens ?? 0) + data.tokens,
4361
+ totalCost: (existing?.totalCost ?? 0) + data.cost,
4362
+ providerBreakdown,
4363
+ toolUsage
4364
+ };
4365
+ stores.aiUsageMetrics.set(key, metrics);
4366
+ return Promise.resolve();
4367
+ },
4368
+ getByDateRange(startDate, endDate) {
4369
+ const tenantId = getTenantId();
4370
+ const results = Array.from(stores.aiUsageMetrics.values()).filter((m) => {
4371
+ if (m.tenantId !== tenantId) return false;
4372
+ return m.date >= startDate && m.date <= endDate;
4373
+ });
4374
+ results.sort((a, b) => a.date.getTime() - b.date.getTime());
4375
+ return Promise.resolve(results);
4376
+ },
4377
+ getCurrentMonthUsage() {
4378
+ const tenantId = getTenantId();
4379
+ const now = /* @__PURE__ */ new Date();
4380
+ const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
4381
+ const monthMetrics = Array.from(stores.aiUsageMetrics.values()).filter((m) => {
4382
+ if (m.tenantId !== tenantId) return false;
4383
+ return m.date >= startOfMonth;
4384
+ });
4385
+ const aggregated = {
4386
+ requestCount: 0,
4387
+ totalTokens: 0,
4388
+ totalCost: 0,
4389
+ providerBreakdown: {}
4390
+ };
4391
+ for (const m of monthMetrics) {
4392
+ aggregated.requestCount += m.requestCount;
4393
+ aggregated.totalTokens += m.totalTokens;
4394
+ aggregated.totalCost += m.totalCost;
4395
+ for (const [provider, stats] of Object.entries(m.providerBreakdown)) {
4396
+ if (!aggregated.providerBreakdown[provider]) {
4397
+ aggregated.providerBreakdown[provider] = { requests: 0, tokens: 0, cost: 0 };
4398
+ }
4399
+ aggregated.providerBreakdown[provider].requests += stats.requests;
4400
+ aggregated.providerBreakdown[provider].tokens += stats.tokens;
4401
+ aggregated.providerBreakdown[provider].cost += stats.cost;
4402
+ }
4403
+ }
4404
+ return Promise.resolve(aggregated);
4405
+ }
4406
+ };
4407
+ }
4126
4408
  function createMockAdapter() {
4127
4409
  const stores = {
4128
4410
  objects: /* @__PURE__ */ new Map(),
@@ -4136,7 +4418,12 @@ function createMockAdapter() {
4136
4418
  userRoles: /* @__PURE__ */ new Map(),
4137
4419
  workflows: /* @__PURE__ */ new Map(),
4138
4420
  workflowInstances: /* @__PURE__ */ new Map(),
4139
- workflowParticipations: /* @__PURE__ */ new Map()
4421
+ workflowParticipations: /* @__PURE__ */ new Map(),
4422
+ // AI stores
4423
+ aiConversations: /* @__PURE__ */ new Map(),
4424
+ aiMessages: /* @__PURE__ */ new Map(),
4425
+ aiUserMemory: /* @__PURE__ */ new Map(),
4426
+ aiUsageMetrics: /* @__PURE__ */ new Map()
4140
4427
  };
4141
4428
  const adapter = {
4142
4429
  objects: createMockObjectsRepository(stores),
@@ -4149,6 +4436,10 @@ function createMockAdapter() {
4149
4436
  workflows: createMockWorkflowsRepository(stores),
4150
4437
  workflowInstances: createMockWorkflowInstancesRepository(stores),
4151
4438
  workflowParticipations: createMockWorkflowParticipationsRepository(stores),
4439
+ // AI repositories
4440
+ aiConversations: createMockAIConversationsRepository(stores),
4441
+ aiUserMemory: createMockAIUserMemoryRepository(stores),
4442
+ aiUsageMetrics: createMockAIUsageMetricsRepository(stores),
4152
4443
  async transaction(callback) {
4153
4444
  return await callback(adapter);
4154
4445
  },
@@ -4168,6 +4459,10 @@ function createMockAdapter() {
4168
4459
  stores.workflows.clear();
4169
4460
  stores.workflowInstances.clear();
4170
4461
  stores.workflowParticipations.clear();
4462
+ stores.aiConversations.clear();
4463
+ stores.aiMessages.clear();
4464
+ stores.aiUserMemory.clear();
4465
+ stores.aiUsageMetrics.clear();
4171
4466
  }
4172
4467
  };
4173
4468
  return adapter;
@@ -9301,12 +9596,105 @@ var RecordQueryService = class extends BaseService {
9301
9596
  }
9302
9597
  };
9303
9598
 
9599
+ // src/runtime/services/record/record-resolver.service.ts
9600
+ var RecordResolverService = class extends BaseService {
9601
+ constructor(adapter) {
9602
+ super(adapter);
9603
+ }
9604
+ // ============================================================================
9605
+ // CACHED RECORD ACCESS
9606
+ // ============================================================================
9607
+ /**
9608
+ * Find a record by ID with caching.
9609
+ *
9610
+ * Uses the shared record cache for optimal performance.
9611
+ * Delegates to findByIds for consistent cache handling.
9612
+ *
9613
+ * @param id - Record ID
9614
+ * @returns Record or null if not found
9615
+ */
9616
+ async findById(id) {
9617
+ if (!id) return null;
9618
+ const results = await this.findByIds([id]);
9619
+ return results[0] ?? null;
9620
+ }
9621
+ /**
9622
+ * Find multiple records by IDs with caching.
9623
+ *
9624
+ * Each record is cached individually for reuse across services.
9625
+ * Only fetches records not already in cache.
9626
+ *
9627
+ * @param ids - Record IDs to fetch
9628
+ * @returns Array of found records (missing IDs are not included)
9629
+ */
9630
+ async findByIds(ids) {
9631
+ if (!ids || ids.length === 0) {
9632
+ return [];
9633
+ }
9634
+ const uniqueIds = [...new Set(ids)];
9635
+ return this.cachedByMany(
9636
+ "record",
9637
+ uniqueIds,
9638
+ (missingIds) => this.adapter.objectRecords.findByIds(missingIds),
9639
+ (record) => record.id,
9640
+ cacheTtl.records
9641
+ );
9642
+ }
9643
+ // ============================================================================
9644
+ // FACTORY METHODS
9645
+ // ============================================================================
9646
+ /**
9647
+ * Create a RelationLabelResolver callback for computeLabelWithRelations.
9648
+ *
9649
+ * Used by RelationService.resolveLabel() and ObjectSchemaService.
9650
+ *
9651
+ * @returns Callback that resolves record IDs to their labels (cached)
9652
+ */
9653
+ createRelationLabelResolver() {
9654
+ return async (ids) => {
9655
+ const records = await this.findByIds(ids);
9656
+ return new Map(records.map((r) => [r.id, r.label]));
9657
+ };
9658
+ }
9659
+ /**
9660
+ * Create a LabelResolver interface for label computation helpers.
9661
+ *
9662
+ * Used by RecordService for computing record labels.
9663
+ *
9664
+ * @param relationService - RelationService for resolving relation display labels
9665
+ * @returns LabelResolver interface with cached record fetching
9666
+ */
9667
+ createLabelResolver(relationService) {
9668
+ return {
9669
+ resolveRelationIds: (ids, attrId) => relationService.resolveIds(ids, attrId),
9670
+ findRecordLabels: (ids) => this.findByIds(ids)
9671
+ };
9672
+ }
9673
+ /**
9674
+ * Create a RollupCascadeContext for rollup recalculation.
9675
+ *
9676
+ * Used by RecordService after create/update/delete operations.
9677
+ *
9678
+ * @param rollupService - RollupService for recalculating rollups
9679
+ * @param schemaService - ObjectSchemaService for fetching schemas
9680
+ * @returns Context with cached record fetching
9681
+ */
9682
+ createRollupContext(rollupService, schemaService) {
9683
+ return {
9684
+ rollupService,
9685
+ schemaService,
9686
+ findRecordsByIds: (ids) => this.findByIds(ids)
9687
+ };
9688
+ }
9689
+ };
9690
+
9304
9691
  // src/runtime/services/record/relation.service.ts
9305
9692
  var RelationService = class extends BaseService {
9306
9693
  constructor(adapter, nativeRegistry, options) {
9307
9694
  super(adapter);
9308
9695
  this.schemaService = new ObjectSchemaService(adapter, nativeRegistry);
9309
- this.queryService = options?.queryService;
9696
+ this.queryService = options.queryService;
9697
+ this.recordResolver = options.recordResolver;
9310
9698
  }
9311
9699
  /**
9312
9700
  * Set the query service after construction.
@@ -9384,7 +9772,7 @@ var RelationService = class extends BaseService {
9384
9772
  });
9385
9773
  return errors;
9386
9774
  }
9387
- const records = await this.adapter.objectRecords.findByIds(ids);
9775
+ const records = await this.recordResolver.findByIds(ids);
9388
9776
  const recordMap = new Map(records.map((r) => [r.id, r]));
9389
9777
  const invalidIds = [];
9390
9778
  for (const id of ids) {
@@ -9506,20 +9894,7 @@ var RelationService = class extends BaseService {
9506
9894
  const result = query ? await queryService.searchRecords(objectSchema.id, query, queryOptions) : await queryService.listRecords(objectSchema.id, queryOptions);
9507
9895
  totalCount += result.total;
9508
9896
  for (const record of result.records) {
9509
- let label;
9510
- if (target.displayTemplate) {
9511
- label = await computeLabelWithRelations(
9512
- target.displayTemplate,
9513
- record.values,
9514
- objectSchema.attributes,
9515
- async (ids) => {
9516
- const linkedRecords = await this.adapter.objectRecords.findByIds(ids);
9517
- return new Map(linkedRecords.map((r) => [r.id, r.label]));
9518
- }
9519
- );
9520
- } else {
9521
- label = record.label;
9522
- }
9897
+ const label = await this.resolveLabel(record, objectSchema, target.displayTemplate);
9523
9898
  allOptions.push({
9524
9899
  id: record.id,
9525
9900
  objectId: objectSchema.id,
@@ -9557,17 +9932,8 @@ var RelationService = class extends BaseService {
9557
9932
  if (!ids || ids.length === 0) {
9558
9933
  return [];
9559
9934
  }
9560
- const compositeIds = ids.map((id) => `${attributeId}:${id}`);
9561
- return this.cachedByMany(
9562
- "resolvedRelation",
9563
- compositeIds,
9564
- async (missingCompositeIds) => {
9565
- const missingRecordIds = missingCompositeIds.map((c) => c.split(":")[1]);
9566
- return this.fetchResolveIds(missingRecordIds, attributeId);
9567
- },
9568
- (item) => `${attributeId}:${item.id}`,
9569
- cacheTtl.resolvedRelations
9570
- );
9935
+ const result = await this.resolveIdsBatch([{ attributeId, ids }]);
9936
+ return result[attributeId] ?? [];
9571
9937
  }
9572
9938
  /**
9573
9939
  * Resolve multiple attribute/IDs batches in a single operation.
@@ -9610,7 +9976,7 @@ var RelationService = class extends BaseService {
9610
9976
  const allResolved = await this.cachedByMany(
9611
9977
  "resolvedRelation",
9612
9978
  allCompositeIds,
9613
- (missingCompositeIds) => this.fetchResolveIdsBatch(missingCompositeIds),
9979
+ (missingCompositeIds) => this.resolveCompositeIds(missingCompositeIds),
9614
9980
  (item) => item._compositeId,
9615
9981
  cacheTtl.resolvedRelations
9616
9982
  );
@@ -9631,10 +9997,12 @@ var RelationService = class extends BaseService {
9631
9997
  return response;
9632
9998
  }
9633
9999
  /**
9634
- * Internal method to fetch and resolve multiple composite IDs at once.
10000
+ * Internal method to resolve multiple composite IDs at once.
9635
10001
  * Optimized for batch operations - single DB query for all records.
10002
+ *
10003
+ * @param compositeIds - Array of composite IDs in format "attributeId:recordId"
9636
10004
  */
9637
- async fetchResolveIdsBatch(compositeIds) {
10005
+ async resolveCompositeIds(compositeIds) {
9638
10006
  if (compositeIds.length === 0) {
9639
10007
  return [];
9640
10008
  }
@@ -9644,7 +10012,7 @@ var RelationService = class extends BaseService {
9644
10012
  });
9645
10013
  const uniqueRecordIds = [...new Set(parsed.map((p) => p.recordId))];
9646
10014
  const uniqueAttributeIds = [...new Set(parsed.map((p) => p.attributeId))];
9647
- const records = await this.adapter.objectRecords.findByIds(uniqueRecordIds);
10015
+ const records = await this.recordResolver.findByIds(uniqueRecordIds);
9648
10016
  if (records.length === 0) {
9649
10017
  return [];
9650
10018
  }
@@ -9669,20 +10037,7 @@ var RelationService = class extends BaseService {
9669
10037
  const attribute = attributeMap.get(attributeId);
9670
10038
  const targetConfig = attribute?.targets?.find((t) => t.object === objectSchema.name);
9671
10039
  const customTemplate = targetConfig?.displayTemplate;
9672
- let label;
9673
- if (customTemplate) {
9674
- label = await computeLabelWithRelations(
9675
- customTemplate,
9676
- record.values,
9677
- objectSchema.attributes,
9678
- async (nestedIds) => {
9679
- const linkedRecords = await this.adapter.objectRecords.findByIds(nestedIds);
9680
- return new Map(linkedRecords.map((r) => [r.id, r.label]));
9681
- }
9682
- );
9683
- } else {
9684
- label = record.label;
9685
- }
10040
+ const label = await this.resolveLabel(record, objectSchema, customTemplate);
9686
10041
  resolved.push({
9687
10042
  _compositeId: compositeId,
9688
10043
  id: record.id,
@@ -9696,59 +10051,22 @@ var RelationService = class extends BaseService {
9696
10051
  return resolved;
9697
10052
  }
9698
10053
  /**
9699
- * Internal method to fetch and resolve relation IDs (no caching).
9700
- * Uses batch fetching for performance - fetches all records in one query,
9701
- * then groups by objectId to minimize schema lookups.
10054
+ * Resolve the display label for a record.
10055
+ * Uses custom template if provided, otherwise falls back to pre-computed label.
9702
10056
  */
9703
- async fetchResolveIds(ids, attributeId) {
9704
- if (ids.length === 0) {
9705
- return [];
9706
- }
9707
- const attribute = await this.findAttributeById(attributeId);
9708
- const records = await this.adapter.objectRecords.findByIds(ids);
9709
- if (records.length === 0) {
9710
- return [];
9711
- }
9712
- const recordsByObjectId = /* @__PURE__ */ new Map();
9713
- for (const record of records) {
9714
- const existing = recordsByObjectId.get(record.objectId) ?? [];
9715
- existing.push(record);
9716
- recordsByObjectId.set(record.objectId, existing);
9717
- }
9718
- const resolved = [];
9719
- for (const [objectId, objectRecords] of recordsByObjectId) {
9720
- const objectSchema = await this.schemaService.getObjectSchema(objectId);
9721
- if (!objectSchema) {
9722
- continue;
9723
- }
9724
- const targetConfig = attribute?.targets?.find((t) => t.object === objectSchema.name);
9725
- const customTemplate = targetConfig?.displayTemplate;
9726
- for (const record of objectRecords) {
9727
- let label;
9728
- if (customTemplate) {
9729
- label = await computeLabelWithRelations(
9730
- customTemplate,
9731
- record.values,
9732
- objectSchema.attributes,
9733
- async (nestedIds) => {
9734
- const linkedRecords = await this.adapter.objectRecords.findByIds(nestedIds);
9735
- return new Map(linkedRecords.map((r) => [r.id, r.label]));
9736
- }
9737
- );
9738
- } else {
9739
- label = record.label;
10057
+ async resolveLabel(record, objectSchema, customTemplate) {
10058
+ if (customTemplate) {
10059
+ return computeLabelWithRelations(
10060
+ customTemplate,
10061
+ record.values,
10062
+ objectSchema.attributes,
10063
+ async (nestedIds) => {
10064
+ const linkedRecords = await this.recordResolver.findByIds(nestedIds);
10065
+ return new Map(linkedRecords.map((r) => [r.id, r.label]));
9740
10066
  }
9741
- resolved.push({
9742
- id: record.id,
9743
- objectId: record.objectId,
9744
- objectName: objectSchema.name,
9745
- objectLabel: objectSchema.label,
9746
- objectIcon: objectSchema.icon,
9747
- label
9748
- });
9749
- }
10067
+ );
9750
10068
  }
9751
- return resolved;
10069
+ return record.label;
9752
10070
  }
9753
10071
  /**
9754
10072
  * Find a relation attribute by ID.
@@ -9779,8 +10097,9 @@ var RelationService = class extends BaseService {
9779
10097
 
9780
10098
  // src/runtime/services/record/rollup.service.ts
9781
10099
  var RollupService = class extends BaseService {
9782
- constructor(adapter) {
10100
+ constructor(adapter, options) {
9783
10101
  super(adapter);
10102
+ this.recordResolver = options.recordResolver;
9784
10103
  }
9785
10104
  /**
9786
10105
  * Calculate a rollup value for a record
@@ -9834,7 +10153,7 @@ var RollupService = class extends BaseService {
9834
10153
  * Example: entreprise222 has relation "entreprises" → companies, rollup collects from companies
9835
10154
  */
9836
10155
  async calculateForward(recordId, rollupAttr) {
9837
- const record = await this.adapter.objectRecords.findById(recordId);
10156
+ const record = await this.recordResolver.findById(recordId);
9838
10157
  if (!record) {
9839
10158
  return { value: null, recordCount: 0 };
9840
10159
  }
@@ -9848,7 +10167,7 @@ var RollupService = class extends BaseService {
9848
10167
  if (relatedIds.length === 0) {
9849
10168
  return { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 };
9850
10169
  }
9851
- const relatedRecords = await this.adapter.objectRecords.findByIds(relatedIds);
10170
+ const relatedRecords = await this.recordResolver.findByIds(relatedIds);
9852
10171
  if (relatedRecords.length === 0) {
9853
10172
  return { value: this.getEmptyValue(rollupAttr.function), recordCount: 0 };
9854
10173
  }
@@ -9859,7 +10178,7 @@ var RollupService = class extends BaseService {
9859
10178
  * Example: Company has rollup on "orders", Order has relation "company" → companies
9860
10179
  */
9861
10180
  async calculateReverse(recordId, rollupAttr, schema) {
9862
- const record = await this.adapter.objectRecords.findById(recordId);
10181
+ const record = await this.recordResolver.findById(recordId);
9863
10182
  if (!record) {
9864
10183
  return { value: null, recordCount: 0 };
9865
10184
  }
@@ -10166,25 +10485,25 @@ var RecordService = class extends BaseService {
10166
10485
  this.permissionService = options?.permissionService;
10167
10486
  this.auditService = options?.auditService ?? (adapter.audit ? new AuditService(adapter) : void 0);
10168
10487
  this.policyRegistry = options?.policyRegistry === null ? null : options?.policyRegistry ?? defaultPolicyRegistry;
10488
+ this.recordResolver = new RecordResolverService(adapter);
10169
10489
  this.queryService = new RecordQueryService(adapter, this.schemaService, {
10170
10490
  permissionService: this.permissionService,
10171
10491
  policyRegistry: this.policyRegistry
10172
10492
  });
10173
10493
  this.relationService = new RelationService(adapter, registry, {
10174
- queryService: this.queryService
10494
+ queryService: this.queryService,
10495
+ recordResolver: this.recordResolver
10496
+ });
10497
+ this.rollupService = new RollupService(adapter, {
10498
+ recordResolver: this.recordResolver
10175
10499
  });
10176
10500
  this.userService = new UserService(adapter);
10177
- this.rollupService = new RollupService(adapter);
10178
10501
  this.hookRegistry = options?.hookRegistry ?? new NoopHookRegistry();
10179
- this.labelResolver = {
10180
- resolveRelationIds: (ids, attrId) => this.relationService.resolveIds(ids, attrId),
10181
- findRecordLabels: (ids) => this.adapter.objectRecords.findByIds(ids)
10182
- };
10183
- this.rollupContext = {
10184
- rollupService: this.rollupService,
10185
- schemaService: this.schemaService,
10186
- findRecordsByIds: (ids) => this.adapter.objectRecords.findByIds(ids)
10187
- };
10502
+ this.labelResolver = this.recordResolver.createLabelResolver(this.relationService);
10503
+ this.rollupContext = this.recordResolver.createRollupContext(
10504
+ this.rollupService,
10505
+ this.schemaService
10506
+ );
10188
10507
  }
10189
10508
  // ============================================================================
10190
10509
  // CREATE
@@ -10579,15 +10898,19 @@ var RecordService = class extends BaseService {
10579
10898
  }
10580
10899
  };
10581
10900
 
10582
- // src/runtime/services/record/relation-resolver.service.ts
10583
- var RelationResolverService = class {
10584
- constructor(adapter) {
10585
- this.adapter = adapter;
10901
+ // src/runtime/services/record/formula-resolver.service.ts
10902
+ var FormulaResolverService = class extends BaseService {
10903
+ constructor(adapter, options) {
10904
+ super(adapter);
10905
+ this.recordResolver = options.recordResolver;
10586
10906
  }
10907
+ // ============================================================================
10908
+ // RESOLUTION
10909
+ // ============================================================================
10587
10910
  /**
10588
10911
  * Resolve values from related records for formula evaluation
10589
10912
  *
10590
- * Phase 2: Supports 1 level of relation traversal only
10913
+ * Supports 1 level of relation traversal only.
10591
10914
  *
10592
10915
  * @param record - The source record
10593
10916
  * @param schema - Schema of the source object
@@ -10596,7 +10919,6 @@ var RelationResolverService = class {
10596
10919
  *
10597
10920
  * @example
10598
10921
  * ```typescript
10599
- * // For an order with company relation
10600
10922
  * const resolved = await resolver.resolveRelationValues(
10601
10923
  * orderRecord,
10602
10924
  * orderSchema,
@@ -10625,7 +10947,7 @@ var RelationResolverService = class {
10625
10947
  if (idsToFetch.length === 0) {
10626
10948
  return result;
10627
10949
  }
10628
- const relatedRecords = await this.adapter.objectRecords.findByIds(idsToFetch);
10950
+ const relatedRecords = await this.recordResolver.findByIds(idsToFetch);
10629
10951
  for (const relatedRecord of relatedRecords) {
10630
10952
  const attrName = attrIdMap.get(relatedRecord.id);
10631
10953
  if (attrName) {
@@ -10671,7 +10993,7 @@ var RelationResolverService = class {
10671
10993
  if (allIdsToFetch.size === 0) {
10672
10994
  return resultMap;
10673
10995
  }
10674
- const relatedRecords = await this.adapter.objectRecords.findByIds([...allIdsToFetch]);
10996
+ const relatedRecords = await this.recordResolver.findByIds([...allIdsToFetch]);
10675
10997
  const relatedRecordMap = new Map(relatedRecords.map((r) => [r.id, r]));
10676
10998
  for (const record of records) {
10677
10999
  const result = resultMap.get(record.id);
@@ -10704,10 +11026,12 @@ var RelationResolverService = class {
10704
11026
  }
10705
11027
  return flat;
10706
11028
  }
11029
+ // ============================================================================
11030
+ // PRIVATE HELPERS
11031
+ // ============================================================================
10707
11032
  /**
10708
11033
  * Extract a single relation ID from a value
10709
11034
  * Handles both single (string) and multi (array) relations
10710
- * @internal
10711
11035
  */
10712
11036
  extractSingleId(value) {
10713
11037
  if (typeof value === "string" && value.length > 0) {
@@ -10726,9 +11050,9 @@ var RollupScheduler = class {
10726
11050
  this.adapter = adapter;
10727
11051
  this.getSchemaById = getSchemaById;
10728
11052
  this.pending = /* @__PURE__ */ new Map();
10729
- this.rollupService = new RollupService(adapter);
10730
- this.debounceMs = options?.debounceMs ?? 100;
10731
- this.maxPending = options?.maxPending ?? 100;
11053
+ this.rollupService = new RollupService(adapter, { recordResolver: options.recordResolver });
11054
+ this.debounceMs = options.debounceMs ?? 100;
11055
+ this.maxPending = options.maxPending ?? 100;
10732
11056
  }
10733
11057
  /**
10734
11058
  * Schedule a rollup recalculation for a parent record.
@@ -14163,10 +14487,11 @@ export {
14163
14487
  createContextForRestore,
14164
14488
  recalculateParentRollups,
14165
14489
  RecordQueryService,
14490
+ RecordResolverService,
14166
14491
  RelationService,
14167
14492
  RollupService,
14168
14493
  RecordService,
14169
- RelationResolverService,
14494
+ FormulaResolverService,
14170
14495
  RollupScheduler,
14171
14496
  WorkflowService,
14172
14497
  WorkflowInstanceService,