@stndrds/schema 1.0.0-alpha.84 → 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";
@@ -5822,6 +5823,9 @@ var FORBIDDEN_PROPERTY_TYPES = [
5822
5823
  "document",
5823
5824
  "richtext"
5824
5825
  ];
5826
+ function hasOptions2(attr) {
5827
+ return attr.type === "select" || attr.type === "status" || attr.type === "multiselect";
5828
+ }
5825
5829
 
5826
5830
  // src/builders/attribute-validators.ts
5827
5831
  import { ICONS } from "@stndrds/constants";
@@ -5926,6 +5930,20 @@ function validateOptions(options, attributeName) {
5926
5930
  validateIconName(option.icon, `option "${option.id}" of "${attributeName}"`);
5927
5931
  }
5928
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
+ }
5929
5947
  }
5930
5948
 
5931
5949
  // src/builders/attribute-builders.ts
@@ -10210,6 +10228,7 @@ var BilateralSyncService = class extends BaseService {
10210
10228
  const addedIds = newData.ids.filter((id) => !oldData.ids.includes(id));
10211
10229
  const removedIds = oldData.ids.filter((id) => !newData.ids.includes(id));
10212
10230
  const commonIds = newData.ids.filter((id) => oldData.ids.includes(id));
10231
+ const inverseMappings = this.buildInverseMappings(sourceAttr);
10213
10232
  await Promise.all([
10214
10233
  // Add new relations
10215
10234
  ...addedIds.map(
@@ -10219,7 +10238,9 @@ var BilateralSyncService = class extends BaseService {
10219
10238
  sourceRecordId,
10220
10239
  sourceSchema.name,
10221
10240
  sourceAttr.name,
10222
- newData.properties.get(targetId)
10241
+ newData.properties.get(targetId),
10242
+ inverseMappings,
10243
+ bilateral
10223
10244
  )
10224
10245
  ),
10225
10246
  // Remove deleted relations
@@ -10235,7 +10256,9 @@ var BilateralSyncService = class extends BaseService {
10235
10256
  sourceSchema.name,
10236
10257
  sourceAttr.name,
10237
10258
  newData.properties.get(targetId),
10238
- oldData.properties.get(targetId)
10259
+ oldData.properties.get(targetId),
10260
+ inverseMappings,
10261
+ bilateral
10239
10262
  )
10240
10263
  )
10241
10264
  ]);
@@ -10279,7 +10302,7 @@ var BilateralSyncService = class extends BaseService {
10279
10302
  * Add an ID to an inverse relation (with properties).
10280
10303
  * @private
10281
10304
  */
10282
- async addInverseRelation(targetRecordId, inverseAttr, sourceRecordId, sourceObject, sourceAttribute, properties) {
10305
+ async addInverseRelation(targetRecordId, inverseAttr, sourceRecordId, sourceObject, sourceAttribute, properties, inverseMappings, bilateral) {
10283
10306
  const targetRecord = await this.adapter.objectRecords.findById(targetRecordId);
10284
10307
  if (!targetRecord) {
10285
10308
  return;
@@ -10310,14 +10333,27 @@ var BilateralSyncService = class extends BaseService {
10310
10333
  this.adapter
10311
10334
  );
10312
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
+ }
10313
10349
  }
10314
10350
  }
10315
10351
  /**
10316
10352
  * Update properties of an existing inverse relation.
10317
10353
  * @private
10318
10354
  */
10319
- async updateInverseRelationProperties(targetRecordId, _inverseAttr, sourceRecordId, sourceObject, sourceAttribute, newProperties, oldProperties) {
10320
- if (JSON.stringify(newProperties) === JSON.stringify(oldProperties)) {
10355
+ async updateInverseRelationProperties(targetRecordId, _inverseAttr, sourceRecordId, sourceObject, sourceAttribute, newProperties, oldProperties, inverseMappings, bilateral) {
10356
+ if (deepEqual(newProperties, oldProperties)) {
10321
10357
  return;
10322
10358
  }
10323
10359
  if (!this.adapter.relationAttributes) {
@@ -10335,12 +10371,32 @@ var BilateralSyncService = class extends BaseService {
10335
10371
  [{ id: targetRecordId, props: newProperties }],
10336
10372
  this.adapter
10337
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
+ }
10338
10387
  } else {
10339
10388
  await this.adapter.relationAttributes.deleteBySource(
10340
10389
  sourceObject,
10341
10390
  sourceRecordId,
10342
10391
  sourceAttribute
10343
10392
  );
10393
+ if (bilateral) {
10394
+ await this.adapter.relationAttributes.deleteBySource(
10395
+ bilateral.object,
10396
+ targetRecordId,
10397
+ bilateral.attribute
10398
+ );
10399
+ }
10344
10400
  }
10345
10401
  }
10346
10402
  /**
@@ -10372,6 +10428,54 @@ var BilateralSyncService = class extends BaseService {
10372
10428
  });
10373
10429
  await this.invalidateTargetRecordCaches(targetRecordId, targetRecord.objectId);
10374
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
+ }
10375
10479
  /**
10376
10480
  * Normalize a relation value to an array of IDs.
10377
10481
  * @private
@@ -12454,7 +12558,7 @@ var RecordService = class extends BaseService {
12454
12558
  }
12455
12559
  }
12456
12560
  }
12457
- for (const [attrName, value] of Object.entries(normalizedData)) {
12561
+ for (const [attrName, value] of Object.entries(dataWithDefaults)) {
12458
12562
  const attr = schema.attributes.find((a) => a.name === attrName);
12459
12563
  if (attr?.type === "relation" && isBilateralRelation(attr)) {
12460
12564
  await this.bilateralSyncService.syncBilateralRelation(
@@ -12666,7 +12770,7 @@ var RecordService = class extends BaseService {
12666
12770
  }
12667
12771
  }
12668
12772
  }
12669
- for (const [attrName, value] of Object.entries(normalizedUpdate)) {
12773
+ for (const [attrName, value] of Object.entries(dataToUpdate)) {
12670
12774
  const attr = schema.attributes.find((a) => a.name === attrName);
12671
12775
  if (attr?.type === "relation" && isBilateralRelation(attr)) {
12672
12776
  const oldValue = bilateralOldValues[attrName];
@@ -18015,6 +18119,7 @@ export {
18015
18119
  RESERVED_ATTRIBUTE_NAMES,
18016
18120
  PolicyViolationError,
18017
18121
  FORBIDDEN_PROPERTY_TYPES,
18122
+ hasOptions2 as hasOptions,
18018
18123
  SYSTEM_ATTRIBUTES,
18019
18124
  getSystemAttributeList,
18020
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) => {