@stndrds/schema 0.1.0-alpha.47 → 0.1.0-alpha.49

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.
@@ -323,7 +323,14 @@ function initializePinCodeService(salt) {
323
323
  }
324
324
 
325
325
  // src/runtime/cache.ts
326
- import { createHash } from "crypto";
326
+ function fnv1aHash(str) {
327
+ let hash = 2166136261;
328
+ for (let i = 0; i < str.length; i++) {
329
+ hash ^= str.charCodeAt(i);
330
+ hash = hash * 16777619 >>> 0;
331
+ }
332
+ return hash.toString(16).padStart(8, "0");
333
+ }
327
334
  function hashOptions(options) {
328
335
  if (options === null || options === void 0 || typeof options === "object" && Object.keys(options).length === 0) {
329
336
  return "default";
@@ -340,7 +347,7 @@ function hashOptions(options) {
340
347
  }
341
348
  return value;
342
349
  });
343
- return createHash("md5").update(sortedJson).digest("hex").slice(0, 8);
350
+ return fnv1aHash(sortedJson);
344
351
  }
345
352
  var cacheKeys = {
346
353
  // -------------------------------------------------------------------------
@@ -369,6 +376,12 @@ var cacheKeys = {
369
376
  // -------------------------------------------------------------------------
370
377
  /** Relation options for an attribute */
371
378
  relationOptions: (tenantId, attrId, hash) => `rel:${tenantId}:${attrId}:${hash}`,
379
+ /**
380
+ * Resolved relation by composite ID.
381
+ * Composite ID format: `${attributeId}:${recordId}`
382
+ * Used by cachedByMany in RelationService.resolveIds()
383
+ */
384
+ resolvedRelation: (tenantId, compositeId) => `relres:${tenantId}:${compositeId}`,
372
385
  // -------------------------------------------------------------------------
373
386
  // Rollups - TTL: 2 minutes (high volatility)
374
387
  // -------------------------------------------------------------------------
@@ -408,6 +421,19 @@ var cacheKeys = {
408
421
  allPermissions: (tenantId) => `perms:${tenantId}:*`,
409
422
  /** All relation cache for a tenant */
410
423
  allRelations: (tenantId) => `rel:${tenantId}:*`,
424
+ /** All resolved relations cache for a tenant */
425
+ allResolvedRelations: (tenantId) => `relres:${tenantId}:*`,
426
+ /**
427
+ * Resolved relations for a specific record (all attributeId variants).
428
+ * Pattern matches `relres:${tenantId}:*:${recordId}` to invalidate
429
+ * all cached labels for a record regardless of which attribute resolved it.
430
+ */
431
+ resolvedRelationsByRecord: (tenantId, recordId) => `relres:${tenantId}:*:${recordId}`,
432
+ /**
433
+ * Resolved relations for a specific attribute.
434
+ * Invalidates when a relation's displayTemplate changes.
435
+ */
436
+ resolvedRelationsByAttr: (tenantId, attributeId) => `relres:${tenantId}:${attributeId}:*`,
411
437
  /** All rollup cache for a tenant */
412
438
  allRollups: (tenantId) => `rollup:${tenantId}:*`,
413
439
  /** All rollups for a specific record */
@@ -460,6 +486,8 @@ var cacheTtl = {
460
486
  permissions: 15 * 60 * 1e3,
461
487
  /** Relation options - medium volatility (5 minutes) */
462
488
  relations: 5 * 60 * 1e3,
489
+ /** Resolved relations - medium volatility (5 minutes) */
490
+ resolvedRelations: 5 * 60 * 1e3,
463
491
  /** Rollup values - high volatility (2 minutes) */
464
492
  rollup: 2 * 60 * 1e3,
465
493
  /** Individual records - high volatility (1 minute) */
@@ -492,6 +520,7 @@ var defaultTtl = {
492
520
  workflowById: cacheTtl.workflows,
493
521
  workflowList: cacheTtl.workflows,
494
522
  relationOptions: cacheTtl.relations,
523
+ resolvedRelation: cacheTtl.resolvedRelations,
495
524
  rollupValue: cacheTtl.rollup,
496
525
  userPermissions: cacheTtl.permissions,
497
526
  recordList: cacheTtl.recordList,
@@ -4408,6 +4437,72 @@ var BaseService = class {
4408
4437
  const pattern = patternFn(this.tenantId, id);
4409
4438
  await this.cache.deletePattern(pattern);
4410
4439
  }
4440
+ /**
4441
+ * Cache multiple resources by ID, fetching only missing ones.
4442
+ * Useful for batch operations with individual caching (e.g., resolveIds).
4443
+ *
4444
+ * Unlike `cachedBy` which caches a single resource, this method:
4445
+ * 1. Checks the cache for each ID in parallel
4446
+ * 2. Collects cache misses
4447
+ * 3. Fetches only missing items via the fetcher
4448
+ * 4. Caches new results individually
4449
+ * 5. Returns all results (cached + freshly fetched)
4450
+ *
4451
+ * @param keyType - Type of cache key (e.g., "resolvedRelation")
4452
+ * @param ids - Resource identifiers to fetch
4453
+ * @param fetcher - Function to fetch missing items (receives only cache-miss IDs)
4454
+ * @param getId - Function to extract ID from a fetched item
4455
+ * @param ttlMs - Optional TTL override
4456
+ *
4457
+ * @example
4458
+ * ```typescript
4459
+ * // Cache relations by composite ID (attributeId:recordId)
4460
+ * return this.cachedByMany(
4461
+ * "resolvedRelation",
4462
+ * ids.map(id => `${attributeId}:${id}`),
4463
+ * async (missingCompositeIds) => {
4464
+ * const missingRecordIds = missingCompositeIds.map(c => c.split(":")[1]);
4465
+ * return this.fetchRelations(missingRecordIds, attributeId);
4466
+ * },
4467
+ * (item) => `${attributeId}:${item.id}`
4468
+ * );
4469
+ * ```
4470
+ */
4471
+ async cachedByMany(keyType, ids, fetcher, getId, ttlMs) {
4472
+ const cache = this.cache;
4473
+ if (!cache || ids.length === 0) {
4474
+ return fetcher(ids);
4475
+ }
4476
+ const keyFn = cacheKeys[keyType];
4477
+ const ttl = ttlMs ?? defaultTtl[keyType] ?? 6e4;
4478
+ const cacheChecks = await Promise.all(
4479
+ ids.map(async (id) => {
4480
+ const key = keyFn(this.tenantId, id);
4481
+ const cached = await cache.get(key);
4482
+ return { id, cached };
4483
+ })
4484
+ );
4485
+ const results = [];
4486
+ const missingIds = [];
4487
+ for (const { id, cached } of cacheChecks) {
4488
+ if (cached !== null) {
4489
+ results.push(cached);
4490
+ } else {
4491
+ missingIds.push(id);
4492
+ }
4493
+ }
4494
+ if (missingIds.length > 0) {
4495
+ const fetched = await fetcher(missingIds);
4496
+ await Promise.all(
4497
+ fetched.map(async (item) => {
4498
+ const key = keyFn(this.tenantId, getId(item));
4499
+ await cache.set(key, item, ttl);
4500
+ })
4501
+ );
4502
+ results.push(...fetched);
4503
+ }
4504
+ return results;
4505
+ }
4411
4506
  };
4412
4507
  var BaseRepository = class {
4413
4508
  /**
@@ -7717,6 +7812,7 @@ var ObjectSchemaService = class extends BaseService {
7717
7812
  return new Map(records.map((r) => [r.id, r.label]));
7718
7813
  });
7719
7814
  });
7815
+ await this.invalidateCachePattern(cacheKeys.allResolvedRelations(this.tenantId));
7720
7816
  }
7721
7817
  return this.convertDBObjectToDefinition(updatedDbObject, dbAttributes);
7722
7818
  }
@@ -9445,8 +9541,8 @@ var RelationService = class extends BaseService {
9445
9541
  * Resolve record IDs to their display labels.
9446
9542
  * Useful for displaying current values in the UI.
9447
9543
  *
9448
- * Uses batch fetching for performance - fetches all records in one query,
9449
- * then groups by objectId to minimize schema lookups.
9544
+ * Uses caching per individual record ID for optimal performance.
9545
+ * Cache key format: `${attributeId}:${recordId}` to handle different displayTemplates.
9450
9546
  *
9451
9547
  * @param ids - Record IDs to resolve
9452
9548
  * @param attributeId - Relation attribute ID to use its displayTemplate for label rendering
@@ -9461,6 +9557,153 @@ var RelationService = class extends BaseService {
9461
9557
  if (!ids || ids.length === 0) {
9462
9558
  return [];
9463
9559
  }
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
+ );
9571
+ }
9572
+ /**
9573
+ * Resolve multiple attribute/IDs batches in a single operation.
9574
+ * Optimized for DataGrid scenarios with multiple relation columns.
9575
+ *
9576
+ * Benefits over multiple resolveIds() calls:
9577
+ * - Single DB query for all records across all attributes
9578
+ * - Deduplication of records referenced by multiple attributes
9579
+ * - Single schema lookup per objectId
9580
+ *
9581
+ * Uses caching per individual record ID for optimal performance.
9582
+ *
9583
+ * @param requests - Array of { attributeId, ids } to resolve
9584
+ * @returns Map of attributeId to resolved options
9585
+ *
9586
+ * @example
9587
+ * ```typescript
9588
+ * const results = await relationService.resolveIdsBatch([
9589
+ * { attributeId: "attr-company", ids: ["rec-1", "rec-2"] },
9590
+ * { attributeId: "attr-contact", ids: ["rec-3", "rec-4"] },
9591
+ * ]);
9592
+ * // { "attr-company": [...], "attr-contact": [...] }
9593
+ * ```
9594
+ */
9595
+ async resolveIdsBatch(requests) {
9596
+ if (!requests || requests.length === 0) {
9597
+ return {};
9598
+ }
9599
+ const validRequests = requests.filter((r) => r.ids && r.ids.length > 0);
9600
+ if (validRequests.length === 0) {
9601
+ return {};
9602
+ }
9603
+ const allCompositeIds = [];
9604
+ const compositeIdsByAttr = /* @__PURE__ */ new Map();
9605
+ for (const req of validRequests) {
9606
+ const composites = req.ids.map((id) => `${req.attributeId}:${id}`);
9607
+ allCompositeIds.push(...composites);
9608
+ compositeIdsByAttr.set(req.attributeId, composites);
9609
+ }
9610
+ const allResolved = await this.cachedByMany(
9611
+ "resolvedRelation",
9612
+ allCompositeIds,
9613
+ (missingCompositeIds) => this.fetchResolveIdsBatch(missingCompositeIds),
9614
+ (item) => item._compositeId,
9615
+ cacheTtl.resolvedRelations
9616
+ );
9617
+ const response = {};
9618
+ const resolvedMap = new Map(allResolved.map((item) => [item._compositeId, item]));
9619
+ for (const req of validRequests) {
9620
+ const composites = compositeIdsByAttr.get(req.attributeId) ?? [];
9621
+ const options = [];
9622
+ for (const compositeId of composites) {
9623
+ const resolved = resolvedMap.get(compositeId);
9624
+ if (resolved) {
9625
+ const { _compositeId, ...option } = resolved;
9626
+ options.push(option);
9627
+ }
9628
+ }
9629
+ response[req.attributeId] = options;
9630
+ }
9631
+ return response;
9632
+ }
9633
+ /**
9634
+ * Internal method to fetch and resolve multiple composite IDs at once.
9635
+ * Optimized for batch operations - single DB query for all records.
9636
+ */
9637
+ async fetchResolveIdsBatch(compositeIds) {
9638
+ if (compositeIds.length === 0) {
9639
+ return [];
9640
+ }
9641
+ const parsed = compositeIds.map((c) => {
9642
+ const [attributeId, recordId] = c.split(":");
9643
+ return { compositeId: c, attributeId, recordId };
9644
+ });
9645
+ const uniqueRecordIds = [...new Set(parsed.map((p) => p.recordId))];
9646
+ const uniqueAttributeIds = [...new Set(parsed.map((p) => p.attributeId))];
9647
+ const records = await this.adapter.objectRecords.findByIds(uniqueRecordIds);
9648
+ if (records.length === 0) {
9649
+ return [];
9650
+ }
9651
+ const recordMap = new Map(records.map((r) => [r.id, r]));
9652
+ const attributePromises = uniqueAttributeIds.map((id) => this.findAttributeById(id));
9653
+ const attributes = await Promise.all(attributePromises);
9654
+ const attributeMap = new Map(uniqueAttributeIds.map((id, i) => [id, attributes[i]]));
9655
+ const uniqueObjectIds = [...new Set(records.map((r) => r.objectId))];
9656
+ const schemaPromises = uniqueObjectIds.map((id) => this.schemaService.getObjectSchema(id));
9657
+ const schemas = await Promise.all(schemaPromises);
9658
+ const schemaMap = new Map(uniqueObjectIds.map((id, i) => [id, schemas[i]]));
9659
+ const resolved = [];
9660
+ for (const { compositeId, attributeId, recordId } of parsed) {
9661
+ const record = recordMap.get(recordId);
9662
+ if (!record) {
9663
+ continue;
9664
+ }
9665
+ const objectSchema = schemaMap.get(record.objectId);
9666
+ if (!objectSchema) {
9667
+ continue;
9668
+ }
9669
+ const attribute = attributeMap.get(attributeId);
9670
+ const targetConfig = attribute?.targets?.find((t) => t.object === objectSchema.name);
9671
+ 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
+ }
9686
+ resolved.push({
9687
+ _compositeId: compositeId,
9688
+ id: record.id,
9689
+ objectId: record.objectId,
9690
+ objectName: objectSchema.name,
9691
+ objectLabel: objectSchema.label,
9692
+ objectIcon: objectSchema.icon,
9693
+ label
9694
+ });
9695
+ }
9696
+ return resolved;
9697
+ }
9698
+ /**
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.
9702
+ */
9703
+ async fetchResolveIds(ids, attributeId) {
9704
+ if (ids.length === 0) {
9705
+ return [];
9706
+ }
9464
9707
  const attribute = await this.findAttributeById(attributeId);
9465
9708
  const records = await this.adapter.objectRecords.findByIds(ids);
9466
9709
  if (records.length === 0) {
@@ -9487,8 +9730,8 @@ var RelationService = class extends BaseService {
9487
9730
  customTemplate,
9488
9731
  record.values,
9489
9732
  objectSchema.attributes,
9490
- async (ids2) => {
9491
- const linkedRecords = await this.adapter.objectRecords.findByIds(ids2);
9733
+ async (nestedIds) => {
9734
+ const linkedRecords = await this.adapter.objectRecords.findByIds(nestedIds);
9492
9735
  return new Map(linkedRecords.map((r) => [r.id, r.label]));
9493
9736
  }
9494
9737
  );
@@ -10140,6 +10383,7 @@ var RecordService = class extends BaseService {
10140
10383
  await this.invalidateCache(cacheKeys.record(this.tenantId, recordId));
10141
10384
  await this.invalidateLists("allRecordLists", existing.objectId);
10142
10385
  await this.invalidateLists("allSearchResults", existing.objectId);
10386
+ await this.invalidateCachePattern(cacheKeys.resolvedRelationsByRecord(this.tenantId, recordId));
10143
10387
  if (!options?.skipHooks) {
10144
10388
  const afterCtx = {
10145
10389
  ...hookCtx,
@@ -10207,6 +10451,7 @@ var RecordService = class extends BaseService {
10207
10451
  await this.invalidateLists("allRecordLists", record.objectId);
10208
10452
  await this.invalidateLists("allSearchResults", record.objectId);
10209
10453
  await this.invalidateCachePattern(cacheKeys.allGlobalSearch(this.tenantId));
10454
+ await this.invalidateCachePattern(cacheKeys.resolvedRelationsByRecord(this.tenantId, recordId));
10210
10455
  if (!options?.skipHooks) {
10211
10456
  await this.hookRegistry.execute("afterDelete", schema.name, hookCtx);
10212
10457
  }
@@ -10235,6 +10480,7 @@ var RecordService = class extends BaseService {
10235
10480
  await this.invalidateLists("allRecordLists", record.objectId);
10236
10481
  await this.invalidateLists("allSearchResults", record.objectId);
10237
10482
  await this.invalidateCachePattern(cacheKeys.allGlobalSearch(this.tenantId));
10483
+ await this.invalidateCachePattern(cacheKeys.resolvedRelationsByRecord(this.tenantId, recordId));
10238
10484
  }
10239
10485
  // ============================================================================
10240
10486
  // RESTORE
@@ -10262,6 +10508,7 @@ var RecordService = class extends BaseService {
10262
10508
  await this.invalidateLists("allRecordLists", record.objectId);
10263
10509
  await this.invalidateLists("allSearchResults", record.objectId);
10264
10510
  await this.invalidateCachePattern(cacheKeys.allGlobalSearch(this.tenantId));
10511
+ await this.invalidateCachePattern(cacheKeys.resolvedRelationsByRecord(this.tenantId, recordId));
10265
10512
  if (!options?.skipHooks) {
10266
10513
  const afterCtx = {
10267
10514
  ...hookCtx,