@stndrds/schema 1.0.0-alpha.83 → 1.0.0-alpha.85

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.
@@ -29,6 +29,26 @@ function generateTemplateName(label) {
29
29
  function indexBy(items, keyFn) {
30
30
  return new Map(items.map((item) => [keyFn(item), item]));
31
31
  }
32
+ function deepEqual(a, b) {
33
+ if (a === b) return true;
34
+ if (a == null || b == null) return false;
35
+ if (typeof a !== typeof b) return false;
36
+ if (Array.isArray(a)) {
37
+ if (!Array.isArray(b)) return false;
38
+ if (a.length !== b.length) return false;
39
+ return a.every((item, index) => deepEqual(item, b[index]));
40
+ }
41
+ if (typeof a === "object" && typeof b === "object") {
42
+ const aObj = a;
43
+ const bObj = b;
44
+ const aKeys = Object.keys(aObj);
45
+ const bKeys = Object.keys(bObj);
46
+ if (aKeys.length !== bKeys.length) return false;
47
+ return aKeys.every((key) => key in bObj && deepEqual(aObj[key], bObj[key]));
48
+ }
49
+ return false;
50
+ }
51
+
32
52
 
33
53
 
34
54
 
@@ -38,4 +58,4 @@ function indexBy(items, keyFn) {
38
58
 
39
59
 
40
60
 
41
- exports.asTenantId = asTenantId; exports.asUserId = asUserId; exports.generateId = generateId; exports.generatePrefixedId = generatePrefixedId; exports.slugify = slugify; exports.generateTemplateName = generateTemplateName; exports.indexBy = indexBy;
61
+ exports.asTenantId = asTenantId; exports.asUserId = asUserId; exports.generateId = generateId; exports.generatePrefixedId = generatePrefixedId; exports.slugify = slugify; exports.generateTemplateName = generateTemplateName; exports.indexBy = indexBy; exports.deepEqual = deepEqual;
@@ -1,8 +1,9 @@
1
1
  import {
2
2
  asTenantId,
3
3
  asUserId,
4
+ deepEqual,
4
5
  generateId
5
- } from "./chunk-V2RPPE2Y.mjs";
6
+ } from "./chunk-XAORXFAX.mjs";
6
7
  import {
7
8
  computeRecordStatus,
8
9
  createFormAttributeValidator,
@@ -12,7 +13,7 @@ import {
12
13
  validateDraftOrThrow,
13
14
  validateObject,
14
15
  validateObjectOrThrow
15
- } from "./chunk-7VNLLASJ.mjs";
16
+ } from "./chunk-JUVFK6XD.mjs";
16
17
  import {
17
18
  __require
18
19
  } from "./chunk-Y6FXYEAI.mjs";
@@ -3458,96 +3459,6 @@ function createMockAIConversationsRepository(stores) {
3458
3459
  }
3459
3460
  };
3460
3461
  }
3461
- function createMockAIUserMemoryRepository(stores) {
3462
- const getKey = () => {
3463
- const tenantId = getTenantId();
3464
- const userId = requireUserId();
3465
- return `${tenantId}:${userId}`;
3466
- };
3467
- return {
3468
- get() {
3469
- const key = getKey();
3470
- return Promise.resolve(stores.aiUserMemory.get(key) ?? null);
3471
- },
3472
- upsert(data) {
3473
- const key = getKey();
3474
- const tenantId = getTenantId();
3475
- const userId = requireUserId();
3476
- const now = /* @__PURE__ */ new Date();
3477
- const existing = stores.aiUserMemory.get(key);
3478
- const memory = {
3479
- id: existing?.id ?? generateId(),
3480
- tenantId,
3481
- userId,
3482
- preferences: data.preferences ?? existing?.preferences ?? {},
3483
- facts: data.facts ?? existing?.facts ?? [],
3484
- createdAt: existing?.createdAt ?? now,
3485
- updatedAt: now
3486
- };
3487
- stores.aiUserMemory.set(key, memory);
3488
- return Promise.resolve(memory);
3489
- },
3490
- addFact(fact) {
3491
- const key = getKey();
3492
- const tenantId = getTenantId();
3493
- const userId = requireUserId();
3494
- const now = /* @__PURE__ */ new Date();
3495
- const existing = stores.aiUserMemory.get(key);
3496
- const memory = {
3497
- id: existing?.id ?? generateId(),
3498
- tenantId,
3499
- userId,
3500
- preferences: existing?.preferences ?? {},
3501
- facts: [...existing?.facts ?? [], fact],
3502
- createdAt: existing?.createdAt ?? now,
3503
- updatedAt: now
3504
- };
3505
- stores.aiUserMemory.set(key, memory);
3506
- return Promise.resolve(memory);
3507
- },
3508
- removeFact(fact) {
3509
- const key = getKey();
3510
- const tenantId = getTenantId();
3511
- const userId = requireUserId();
3512
- const now = /* @__PURE__ */ new Date();
3513
- const existing = stores.aiUserMemory.get(key);
3514
- const memory = {
3515
- id: existing?.id ?? generateId(),
3516
- tenantId,
3517
- userId,
3518
- preferences: existing?.preferences ?? {},
3519
- facts: (existing?.facts ?? []).filter((f) => f !== fact),
3520
- createdAt: existing?.createdAt ?? now,
3521
- updatedAt: now
3522
- };
3523
- stores.aiUserMemory.set(key, memory);
3524
- return Promise.resolve(memory);
3525
- },
3526
- setPreference(prefKey, value) {
3527
- const memoryKey = getKey();
3528
- const tenantId = getTenantId();
3529
- const userId = requireUserId();
3530
- const now = /* @__PURE__ */ new Date();
3531
- const existing = stores.aiUserMemory.get(memoryKey);
3532
- const memory = {
3533
- id: existing?.id ?? generateId(),
3534
- tenantId,
3535
- userId,
3536
- preferences: { ...existing?.preferences ?? {}, [prefKey]: value },
3537
- facts: existing?.facts ?? [],
3538
- createdAt: existing?.createdAt ?? now,
3539
- updatedAt: now
3540
- };
3541
- stores.aiUserMemory.set(memoryKey, memory);
3542
- return Promise.resolve(memory);
3543
- },
3544
- clear() {
3545
- const key = getKey();
3546
- stores.aiUserMemory.delete(key);
3547
- return Promise.resolve();
3548
- }
3549
- };
3550
- }
3551
3462
  function createMockAIUsageMetricsRepository(stores) {
3552
3463
  const getDateKey = (date2) => {
3553
3464
  const tenantId = getTenantId();
@@ -4592,7 +4503,6 @@ function createEmptyStores() {
4592
4503
  workflowAccessGrants: /* @__PURE__ */ new Map(),
4593
4504
  aiConversations: /* @__PURE__ */ new Map(),
4594
4505
  aiMessages: /* @__PURE__ */ new Map(),
4595
- aiUserMemory: /* @__PURE__ */ new Map(),
4596
4506
  aiUsageMetrics: /* @__PURE__ */ new Map(),
4597
4507
  relationAttributes: /* @__PURE__ */ new Map()
4598
4508
  };
@@ -5556,7 +5466,6 @@ function createMockAdapter() {
5556
5466
  workflowAccessGrants: createMockWorkflowAccessGrantsRepository(stores),
5557
5467
  // AI repositories
5558
5468
  aiConversations: createMockAIConversationsRepository(stores),
5559
- aiUserMemory: createMockAIUserMemoryRepository(stores),
5560
5469
  aiUsageMetrics: createMockAIUsageMetricsRepository(stores),
5561
5470
  // Relation attributes repository
5562
5471
  relationAttributes: createMockRelationAttributesRepository(stores),
@@ -5583,7 +5492,6 @@ function createMockAdapter() {
5583
5492
  stores.workflowAccessGrants.clear();
5584
5493
  stores.aiConversations.clear();
5585
5494
  stores.aiMessages.clear();
5586
- stores.aiUserMemory.clear();
5587
5495
  stores.aiUsageMetrics.clear();
5588
5496
  stores.relationAttributes.clear();
5589
5497
  }
@@ -5915,6 +5823,9 @@ var FORBIDDEN_PROPERTY_TYPES = [
5915
5823
  "document",
5916
5824
  "richtext"
5917
5825
  ];
5826
+ function hasOptions2(attr) {
5827
+ return attr.type === "select" || attr.type === "status" || attr.type === "multiselect";
5828
+ }
5918
5829
 
5919
5830
  // src/builders/attribute-validators.ts
5920
5831
  import { ICONS } from "@stndrds/constants";
@@ -6019,6 +5930,20 @@ function validateOptions(options, attributeName) {
6019
5930
  validateIconName(option.icon, `option "${option.id}" of "${attributeName}"`);
6020
5931
  }
6021
5932
  }
5933
+ for (const option of options) {
5934
+ if (option.inverse === void 0) continue;
5935
+ const inverseOption = options.find((o) => o.value === option.inverse);
5936
+ if (!inverseOption) {
5937
+ throw new Error(
5938
+ `[AttributeBuilder] Option "${option.value}" in "${attributeName}" has inverse "${option.inverse}" which does not match any option value.`
5939
+ );
5940
+ }
5941
+ if (inverseOption.inverse !== option.value) {
5942
+ throw new Error(
5943
+ `[AttributeBuilder] Inverse mismatch in "${attributeName}": "${option.value}" points to "${option.inverse}", but "${option.inverse}" points to "${inverseOption.inverse ?? "(none)"}". Inverse pairs must be symmetric.`
5944
+ );
5945
+ }
5946
+ }
6022
5947
  }
6023
5948
 
6024
5949
  // src/builders/attribute-builders.ts
@@ -10303,6 +10228,7 @@ var BilateralSyncService = class extends BaseService {
10303
10228
  const addedIds = newData.ids.filter((id) => !oldData.ids.includes(id));
10304
10229
  const removedIds = oldData.ids.filter((id) => !newData.ids.includes(id));
10305
10230
  const commonIds = newData.ids.filter((id) => oldData.ids.includes(id));
10231
+ const inverseMappings = this.buildInverseMappings(sourceAttr);
10306
10232
  await Promise.all([
10307
10233
  // Add new relations
10308
10234
  ...addedIds.map(
@@ -10312,7 +10238,9 @@ var BilateralSyncService = class extends BaseService {
10312
10238
  sourceRecordId,
10313
10239
  sourceSchema.name,
10314
10240
  sourceAttr.name,
10315
- newData.properties.get(targetId)
10241
+ newData.properties.get(targetId),
10242
+ inverseMappings,
10243
+ bilateral
10316
10244
  )
10317
10245
  ),
10318
10246
  // Remove deleted relations
@@ -10328,7 +10256,9 @@ var BilateralSyncService = class extends BaseService {
10328
10256
  sourceSchema.name,
10329
10257
  sourceAttr.name,
10330
10258
  newData.properties.get(targetId),
10331
- oldData.properties.get(targetId)
10259
+ oldData.properties.get(targetId),
10260
+ inverseMappings,
10261
+ bilateral
10332
10262
  )
10333
10263
  )
10334
10264
  ]);
@@ -10372,7 +10302,7 @@ var BilateralSyncService = class extends BaseService {
10372
10302
  * Add an ID to an inverse relation (with properties).
10373
10303
  * @private
10374
10304
  */
10375
- async addInverseRelation(targetRecordId, inverseAttr, sourceRecordId, sourceObject, sourceAttribute, properties) {
10305
+ async addInverseRelation(targetRecordId, inverseAttr, sourceRecordId, sourceObject, sourceAttribute, properties, inverseMappings, bilateral) {
10376
10306
  const targetRecord = await this.adapter.objectRecords.findById(targetRecordId);
10377
10307
  if (!targetRecord) {
10378
10308
  return;
@@ -10403,14 +10333,27 @@ var BilateralSyncService = class extends BaseService {
10403
10333
  this.adapter
10404
10334
  );
10405
10335
  }
10336
+ if (inverseMappings && bilateral) {
10337
+ const invertedProps = this.invertProperties(properties, inverseMappings);
10338
+ const targetSchemaObj = await this.schemaService.getObjectSchemaByName(bilateral.object);
10339
+ if (targetSchemaObj) {
10340
+ await this.relationPropertiesService.syncRelationProperties(
10341
+ targetSchemaObj,
10342
+ targetRecordId,
10343
+ bilateral.attribute,
10344
+ [{ id: sourceRecordId, props: invertedProps }],
10345
+ this.adapter
10346
+ );
10347
+ }
10348
+ }
10406
10349
  }
10407
10350
  }
10408
10351
  /**
10409
10352
  * Update properties of an existing inverse relation.
10410
10353
  * @private
10411
10354
  */
10412
- async updateInverseRelationProperties(targetRecordId, _inverseAttr, sourceRecordId, sourceObject, sourceAttribute, newProperties, oldProperties) {
10413
- if (JSON.stringify(newProperties) === JSON.stringify(oldProperties)) {
10355
+ async updateInverseRelationProperties(targetRecordId, _inverseAttr, sourceRecordId, sourceObject, sourceAttribute, newProperties, oldProperties, inverseMappings, bilateral) {
10356
+ if (deepEqual(newProperties, oldProperties)) {
10414
10357
  return;
10415
10358
  }
10416
10359
  if (!this.adapter.relationAttributes) {
@@ -10428,12 +10371,32 @@ var BilateralSyncService = class extends BaseService {
10428
10371
  [{ id: targetRecordId, props: newProperties }],
10429
10372
  this.adapter
10430
10373
  );
10374
+ if (inverseMappings && bilateral) {
10375
+ const invertedProps = this.invertProperties(newProperties, inverseMappings);
10376
+ const targetSchemaObj = await this.schemaService.getObjectSchemaByName(bilateral.object);
10377
+ if (targetSchemaObj) {
10378
+ await this.relationPropertiesService.syncRelationProperties(
10379
+ targetSchemaObj,
10380
+ targetRecordId,
10381
+ bilateral.attribute,
10382
+ [{ id: sourceRecordId, props: invertedProps }],
10383
+ this.adapter
10384
+ );
10385
+ }
10386
+ }
10431
10387
  } else {
10432
10388
  await this.adapter.relationAttributes.deleteBySource(
10433
10389
  sourceObject,
10434
10390
  sourceRecordId,
10435
10391
  sourceAttribute
10436
10392
  );
10393
+ if (bilateral) {
10394
+ await this.adapter.relationAttributes.deleteBySource(
10395
+ bilateral.object,
10396
+ targetRecordId,
10397
+ bilateral.attribute
10398
+ );
10399
+ }
10437
10400
  }
10438
10401
  }
10439
10402
  /**
@@ -10465,6 +10428,54 @@ var BilateralSyncService = class extends BaseService {
10465
10428
  });
10466
10429
  await this.invalidateTargetRecordCaches(targetRecordId, targetRecord.objectId);
10467
10430
  }
10431
+ /**
10432
+ * Build inverse value mappings from property definitions.
10433
+ * Scans all select/status properties for options with `inverse` field.
10434
+ * @returns Map<propertyName, Map<sourceValue, inverseValue>>
10435
+ * @private
10436
+ */
10437
+ buildInverseMappings(sourceAttr) {
10438
+ const mappings = /* @__PURE__ */ new Map();
10439
+ const definitions = sourceAttr.properties?.definitions;
10440
+ if (!definitions) return mappings;
10441
+ for (const def of definitions) {
10442
+ if (!hasOptions2(def)) continue;
10443
+ const options = def.options;
10444
+ const valueMap = /* @__PURE__ */ new Map();
10445
+ for (const option of options) {
10446
+ if (option.inverse) {
10447
+ valueMap.set(option.value, option.inverse);
10448
+ }
10449
+ }
10450
+ if (valueMap.size > 0) {
10451
+ mappings.set(def.name, valueMap);
10452
+ }
10453
+ }
10454
+ return mappings;
10455
+ }
10456
+ /**
10457
+ * Apply inverse mappings to a set of properties.
10458
+ * Properties without a mapping are copied as-is.
10459
+ * @private
10460
+ */
10461
+ invertProperties(props, mappings) {
10462
+ const result = {};
10463
+ for (const [key, value] of Object.entries(props)) {
10464
+ const valueMap = mappings.get(key);
10465
+ if (!valueMap) {
10466
+ result[key] = value;
10467
+ } else if (typeof value === "string") {
10468
+ result[key] = valueMap.get(value) ?? value;
10469
+ } else if (Array.isArray(value)) {
10470
+ result[key] = value.map(
10471
+ (v) => typeof v === "string" ? valueMap.get(v) ?? v : v
10472
+ );
10473
+ } else {
10474
+ result[key] = value;
10475
+ }
10476
+ }
10477
+ return result;
10478
+ }
10468
10479
  /**
10469
10480
  * Normalize a relation value to an array of IDs.
10470
10481
  * @private
@@ -12547,7 +12558,7 @@ var RecordService = class extends BaseService {
12547
12558
  }
12548
12559
  }
12549
12560
  }
12550
- for (const [attrName, value] of Object.entries(normalizedData)) {
12561
+ for (const [attrName, value] of Object.entries(dataWithDefaults)) {
12551
12562
  const attr = schema.attributes.find((a) => a.name === attrName);
12552
12563
  if (attr?.type === "relation" && isBilateralRelation(attr)) {
12553
12564
  await this.bilateralSyncService.syncBilateralRelation(
@@ -12759,7 +12770,7 @@ var RecordService = class extends BaseService {
12759
12770
  }
12760
12771
  }
12761
12772
  }
12762
- for (const [attrName, value] of Object.entries(normalizedUpdate)) {
12773
+ for (const [attrName, value] of Object.entries(dataToUpdate)) {
12763
12774
  const attr = schema.attributes.find((a) => a.name === attrName);
12764
12775
  if (attr?.type === "relation" && isBilateralRelation(attr)) {
12765
12776
  const oldValue = bilateralOldValues[attrName];
@@ -18108,6 +18119,7 @@ export {
18108
18119
  RESERVED_ATTRIBUTE_NAMES,
18109
18120
  PolicyViolationError,
18110
18121
  FORBIDDEN_PROPERTY_TYPES,
18122
+ hasOptions2 as hasOptions,
18111
18123
  SYSTEM_ATTRIBUTES,
18112
18124
  getSystemAttributeList,
18113
18125
  isSystemAttribute,
@@ -134,7 +134,8 @@ var optionSchema = z.object({
134
134
  color: z.string().optional(),
135
135
  icon: z.string().optional(),
136
136
  description: z.string().optional(),
137
- group: z.enum(["idle", "in_progress", "finished"]).optional()
137
+ group: z.enum(["idle", "in_progress", "finished"]).optional(),
138
+ inverse: z.string().optional()
138
139
  });
139
140
  var optionsArraySchema = z.array(optionSchema).min(1).refine(
140
141
  (options) => {