@twin.org/entity 0.9.3-next.9 → 0.10.1-next.1

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.
@@ -3,14 +3,39 @@
3
3
  /* eslint-disable @typescript-eslint/no-explicit-any */
4
4
  import "reflect-metadata";
5
5
  import "tslib";
6
+ import { GeneralError, Is } from "@twin.org/core";
7
+ import { EntitySchemaPropertyType } from "../models/entitySchemaPropertyType.js";
6
8
  import { DecoratorHelper } from "../utils/decoratorHelper.js";
7
9
  /**
8
10
  * Decorator to produce schema property data for entities.
9
11
  * @param options The options for the property.
10
12
  * @returns The property decorator.
13
+ * @throws GeneralError if an index group is declared on an object or array property.
14
+ * @throws GeneralError if the same index group name is declared more than once for the property.
11
15
  */
12
16
  export function property(options) {
13
17
  return (target, propertyKey) => {
18
+ if (Is.arrayValue(options.indexGroup)) {
19
+ if (options.type === EntitySchemaPropertyType.Object ||
20
+ options.type === EntitySchemaPropertyType.Array) {
21
+ throw new GeneralError("propertyDecorator", "indexGroupTypeNotSupported", {
22
+ property: propertyKey,
23
+ type: options.type
24
+ });
25
+ }
26
+ const seenGroups = new Set();
27
+ for (const propertyIndex of options.indexGroup) {
28
+ if (Is.stringValue(propertyIndex?.name)) {
29
+ if (seenGroups.has(propertyIndex.name)) {
30
+ throw new GeneralError("propertyDecorator", "duplicateIndexGroup", {
31
+ property: propertyKey,
32
+ group: propertyIndex.name
33
+ });
34
+ }
35
+ seenGroups.add(propertyIndex.name);
36
+ }
37
+ }
38
+ }
14
39
  const entitySchema = DecoratorHelper.getSchema(target);
15
40
  entitySchema.properties ??= [];
16
41
  const idx = entitySchema.properties.findIndex(p => p.property === propertyKey);
@@ -1 +1 @@
1
- {"version":3,"file":"propertyDecorator.js","sourceRoot":"","sources":["../../../src/decorators/propertyDecorator.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,uDAAuD;AACvD,OAAO,kBAAkB,CAAC;AAC1B,OAAO,OAAO,CAAC;AAEf,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAE9D;;;;GAIG;AACH,MAAM,UAAU,QAAQ,CAAC,OAAgD;IACxE,OAAO,CAAC,MAAW,EAAE,WAAmB,EAAE,EAAE;QAC3C,MAAM,YAAY,GAAG,eAAe,CAAC,SAAS,CAAM,MAAM,CAAC,CAAC;QAC5D,YAAY,CAAC,UAAU,KAAK,EAAE,CAAC;QAC/B,MAAM,GAAG,GAAG,YAAY,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,WAAW,CAAC,CAAC;QAC/E,IAAI,GAAG,KAAK,CAAC,CAAC,EAAE,CAAC;YAChB,YAAY,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG;gBAC9B,GAAG,OAAO;gBACV,QAAQ,EAAE,WAAW;aACrB,CAAC;QACH,CAAC;aAAM,CAAC;YACP,YAAY,CAAC,UAAU,CAAC,IAAI,CAAC;gBAC5B,GAAG,OAAO;gBACV,QAAQ,EAAE,WAAW;aACrB,CAAC,CAAC;QACJ,CAAC;QACD,eAAe,CAAC,SAAS,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACjD,CAAC,CAAC;AACH,CAAC","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\n/* eslint-disable @typescript-eslint/no-explicit-any */\nimport \"reflect-metadata\";\nimport \"tslib\";\nimport type { IEntitySchemaProperty } from \"../models/IEntitySchemaProperty.js\";\nimport { DecoratorHelper } from \"../utils/decoratorHelper.js\";\n\n/**\n * Decorator to produce schema property data for entities.\n * @param options The options for the property.\n * @returns The property decorator.\n */\nexport function property(options: Omit<IEntitySchemaProperty, \"property\">): any {\n\treturn (target: any, propertyKey: string) => {\n\t\tconst entitySchema = DecoratorHelper.getSchema<any>(target);\n\t\tentitySchema.properties ??= [];\n\t\tconst idx = entitySchema.properties.findIndex(p => p.property === propertyKey);\n\t\tif (idx !== -1) {\n\t\t\tentitySchema.properties[idx] = {\n\t\t\t\t...options,\n\t\t\t\tproperty: propertyKey\n\t\t\t};\n\t\t} else {\n\t\t\tentitySchema.properties.push({\n\t\t\t\t...options,\n\t\t\t\tproperty: propertyKey\n\t\t\t});\n\t\t}\n\t\tDecoratorHelper.setSchema(target, entitySchema);\n\t};\n}\n"]}
1
+ {"version":3,"file":"propertyDecorator.js","sourceRoot":"","sources":["../../../src/decorators/propertyDecorator.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,uDAAuD;AACvD,OAAO,kBAAkB,CAAC;AAC1B,OAAO,OAAO,CAAC;AACf,OAAO,EAAE,YAAY,EAAE,EAAE,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,wBAAwB,EAAE,MAAM,uCAAuC,CAAC;AAEjF,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAE9D;;;;;;GAMG;AACH,MAAM,UAAU,QAAQ,CAAC,OAAgD;IACxE,OAAO,CAAC,MAAW,EAAE,WAAmB,EAAE,EAAE;QAC3C,IAAI,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;YACvC,IACC,OAAO,CAAC,IAAI,KAAK,wBAAwB,CAAC,MAAM;gBAChD,OAAO,CAAC,IAAI,KAAK,wBAAwB,CAAC,KAAK,EAC9C,CAAC;gBACF,MAAM,IAAI,YAAY,CAAC,mBAAmB,EAAE,4BAA4B,EAAE;oBACzE,QAAQ,EAAE,WAAW;oBACrB,IAAI,EAAE,OAAO,CAAC,IAAI;iBAClB,CAAC,CAAC;YACJ,CAAC;YAED,MAAM,UAAU,GAAG,IAAI,GAAG,EAAU,CAAC;YACrC,KAAK,MAAM,aAAa,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;gBAChD,IAAI,EAAE,CAAC,WAAW,CAAC,aAAa,EAAE,IAAI,CAAC,EAAE,CAAC;oBACzC,IAAI,UAAU,CAAC,GAAG,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC;wBACxC,MAAM,IAAI,YAAY,CAAC,mBAAmB,EAAE,qBAAqB,EAAE;4BAClE,QAAQ,EAAE,WAAW;4BACrB,KAAK,EAAE,aAAa,CAAC,IAAI;yBACzB,CAAC,CAAC;oBACJ,CAAC;oBACD,UAAU,CAAC,GAAG,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;gBACpC,CAAC;YACF,CAAC;QACF,CAAC;QAED,MAAM,YAAY,GAAG,eAAe,CAAC,SAAS,CAAM,MAAM,CAAC,CAAC;QAC5D,YAAY,CAAC,UAAU,KAAK,EAAE,CAAC;QAC/B,MAAM,GAAG,GAAG,YAAY,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,WAAW,CAAC,CAAC;QAC/E,IAAI,GAAG,KAAK,CAAC,CAAC,EAAE,CAAC;YAChB,YAAY,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG;gBAC9B,GAAG,OAAO;gBACV,QAAQ,EAAE,WAAW;aACrB,CAAC;QACH,CAAC;aAAM,CAAC;YACP,YAAY,CAAC,UAAU,CAAC,IAAI,CAAC;gBAC5B,GAAG,OAAO;gBACV,QAAQ,EAAE,WAAW;aACrB,CAAC,CAAC;QACJ,CAAC;QACD,eAAe,CAAC,SAAS,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACjD,CAAC,CAAC;AACH,CAAC","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\n/* eslint-disable @typescript-eslint/no-explicit-any */\nimport \"reflect-metadata\";\nimport \"tslib\";\nimport { GeneralError, Is } from \"@twin.org/core\";\nimport { EntitySchemaPropertyType } from \"../models/entitySchemaPropertyType.js\";\nimport type { IEntitySchemaProperty } from \"../models/IEntitySchemaProperty.js\";\nimport { DecoratorHelper } from \"../utils/decoratorHelper.js\";\n\n/**\n * Decorator to produce schema property data for entities.\n * @param options The options for the property.\n * @returns The property decorator.\n * @throws GeneralError if an index group is declared on an object or array property.\n * @throws GeneralError if the same index group name is declared more than once for the property.\n */\nexport function property(options: Omit<IEntitySchemaProperty, \"property\">): any {\n\treturn (target: any, propertyKey: string) => {\n\t\tif (Is.arrayValue(options.indexGroup)) {\n\t\t\tif (\n\t\t\t\toptions.type === EntitySchemaPropertyType.Object ||\n\t\t\t\toptions.type === EntitySchemaPropertyType.Array\n\t\t\t) {\n\t\t\t\tthrow new GeneralError(\"propertyDecorator\", \"indexGroupTypeNotSupported\", {\n\t\t\t\t\tproperty: propertyKey,\n\t\t\t\t\ttype: options.type\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tconst seenGroups = new Set<string>();\n\t\t\tfor (const propertyIndex of options.indexGroup) {\n\t\t\t\tif (Is.stringValue(propertyIndex?.name)) {\n\t\t\t\t\tif (seenGroups.has(propertyIndex.name)) {\n\t\t\t\t\t\tthrow new GeneralError(\"propertyDecorator\", \"duplicateIndexGroup\", {\n\t\t\t\t\t\t\tproperty: propertyKey,\n\t\t\t\t\t\t\tgroup: propertyIndex.name\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tseenGroups.add(propertyIndex.name);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst entitySchema = DecoratorHelper.getSchema<any>(target);\n\t\tentitySchema.properties ??= [];\n\t\tconst idx = entitySchema.properties.findIndex(p => p.property === propertyKey);\n\t\tif (idx !== -1) {\n\t\t\tentitySchema.properties[idx] = {\n\t\t\t\t...options,\n\t\t\t\tproperty: propertyKey\n\t\t\t};\n\t\t} else {\n\t\t\tentitySchema.properties.push({\n\t\t\t\t...options,\n\t\t\t\tproperty: propertyKey\n\t\t\t});\n\t\t}\n\t\tDecoratorHelper.setSchema(target, entitySchema);\n\t};\n}\n"]}
package/dist/es/index.js CHANGED
@@ -12,6 +12,7 @@ export * from "./models/IComparatorGroup.js";
12
12
  export * from "./models/IEntitySchema.js";
13
13
  export * from "./models/IEntitySchemaOptions.js";
14
14
  export * from "./models/IEntitySchemaProperty.js";
15
+ export * from "./models/IEntitySchemaPropertyIndex.js";
15
16
  export * from "./models/IEntitySort.js";
16
17
  export * from "./models/logicalOperator.js";
17
18
  export * from "./models/sortDirection.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,cAAc,iCAAiC,CAAC;AAChD,cAAc,mCAAmC,CAAC;AAClD,cAAc,oCAAoC,CAAC;AACnD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wCAAwC,CAAC;AACvD,cAAc,sCAAsC,CAAC;AACrD,cAAc,yBAAyB,CAAC;AACxC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,kCAAkC,CAAC;AACjD,cAAc,mCAAmC,CAAC;AAClD,cAAc,yBAAyB,CAAC;AACxC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,mCAAmC,CAAC;AAClD,cAAc,yBAAyB,CAAC;AACxC,cAAc,+BAA+B,CAAC","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nexport * from \"./decorators/entityDecorator.js\";\nexport * from \"./decorators/propertyDecorator.js\";\nexport * from \"./factories/entitySchemaFactory.js\";\nexport * from \"./models/comparisonOperator.js\";\nexport * from \"./models/entityCondition.js\";\nexport * from \"./models/entitySchemaPropertyFormat.js\";\nexport * from \"./models/entitySchemaPropertyType.js\";\nexport * from \"./models/IComparator.js\";\nexport * from \"./models/IComparatorGroup.js\";\nexport * from \"./models/IEntitySchema.js\";\nexport * from \"./models/IEntitySchemaOptions.js\";\nexport * from \"./models/IEntitySchemaProperty.js\";\nexport * from \"./models/IEntitySort.js\";\nexport * from \"./models/logicalOperator.js\";\nexport * from \"./models/sortDirection.js\";\nexport * from \"./utils/decoratorHelper.js\";\nexport * from \"./utils/entityConditions.js\";\nexport * from \"./utils/entitySchemaHelper.js\";\nexport * from \"./utils/entitySchemaDiffHelper.js\";\nexport * from \"./utils/entitySorter.js\";\nexport * from \"./models/IEntitySchemaDiff.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,cAAc,iCAAiC,CAAC;AAChD,cAAc,mCAAmC,CAAC;AAClD,cAAc,oCAAoC,CAAC;AACnD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wCAAwC,CAAC;AACvD,cAAc,sCAAsC,CAAC;AACrD,cAAc,yBAAyB,CAAC;AACxC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,kCAAkC,CAAC;AACjD,cAAc,mCAAmC,CAAC;AAClD,cAAc,wCAAwC,CAAC;AACvD,cAAc,yBAAyB,CAAC;AACxC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,mCAAmC,CAAC;AAClD,cAAc,yBAAyB,CAAC;AACxC,cAAc,+BAA+B,CAAC","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nexport * from \"./decorators/entityDecorator.js\";\nexport * from \"./decorators/propertyDecorator.js\";\nexport * from \"./factories/entitySchemaFactory.js\";\nexport * from \"./models/comparisonOperator.js\";\nexport * from \"./models/entityCondition.js\";\nexport * from \"./models/entitySchemaPropertyFormat.js\";\nexport * from \"./models/entitySchemaPropertyType.js\";\nexport * from \"./models/IComparator.js\";\nexport * from \"./models/IComparatorGroup.js\";\nexport * from \"./models/IEntitySchema.js\";\nexport * from \"./models/IEntitySchemaOptions.js\";\nexport * from \"./models/IEntitySchemaProperty.js\";\nexport * from \"./models/IEntitySchemaPropertyIndex.js\";\nexport * from \"./models/IEntitySort.js\";\nexport * from \"./models/logicalOperator.js\";\nexport * from \"./models/sortDirection.js\";\nexport * from \"./utils/decoratorHelper.js\";\nexport * from \"./utils/entityConditions.js\";\nexport * from \"./utils/entitySchemaHelper.js\";\nexport * from \"./utils/entitySchemaDiffHelper.js\";\nexport * from \"./utils/entitySorter.js\";\nexport * from \"./models/IEntitySchemaDiff.js\";\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"IEntitySchemaProperty.js","sourceRoot":"","sources":["../../../src/models/IEntitySchemaProperty.ts"],"names":[],"mappings":"","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type { EntitySchemaPropertyFormat } from \"./entitySchemaPropertyFormat.js\";\nimport type { EntitySchemaPropertyType } from \"./entitySchemaPropertyType.js\";\nimport type { SortDirection } from \"./sortDirection.js\";\n\n/**\n * Definition for an entity schema property.\n */\nexport interface IEntitySchemaProperty<T = unknown> {\n\t/**\n\t * The property name from the entity.\n\t */\n\tproperty: keyof T;\n\n\t/**\n\t * The type of the property.\n\t */\n\ttype: EntitySchemaPropertyType;\n\n\t/**\n\t * The format of the property.\n\t */\n\tformat?: EntitySchemaPropertyFormat;\n\n\t/**\n\t * Is this the primary index property.\n\t */\n\tisPrimary?: boolean;\n\n\t/**\n\t * Is this a secondary index property.\n\t */\n\tisSecondary?: boolean;\n\n\t/**\n\t * Is this property used as the optimistic-lock version token.\n\t * When true, connectors automatically manage the field: the value is\n\t * incremented on every successful write, and a write is rejected with a\n\t * ConflictError when the submitted value does not match the stored value.\n\t * Must be an integer property.\n\t */\n\tisVersion?: boolean;\n\n\t/**\n\t * Default sort direction for this field, leave empty if not sortable.\n\t */\n\tsortDirection?: SortDirection;\n\n\t/**\n\t * Is the property optional.\n\t */\n\toptional?: boolean;\n\n\t/**\n\t * The type of the item (only applies when type is `array`).\n\t */\n\titemType?: EntitySchemaPropertyType;\n\n\t/**\n\t * The type ref of the item (only applies when type is either `array` or `object`).\n\t */\n\titemTypeRef?: string;\n\n\t/**\n\t * Description of the object.\n\t */\n\tdescription?: string;\n\n\t/**\n\t * Examples of the property values.\n\t */\n\texamples?: unknown[];\n\n\t/**\n\t * A default value which can be used in migrations when the property value is not provided.\n\t */\n\tdefaultValue?: unknown;\n}\n"]}
1
+ {"version":3,"file":"IEntitySchemaProperty.js","sourceRoot":"","sources":["../../../src/models/IEntitySchemaProperty.ts"],"names":[],"mappings":"","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type { EntitySchemaPropertyFormat } from \"./entitySchemaPropertyFormat.js\";\nimport type { EntitySchemaPropertyType } from \"./entitySchemaPropertyType.js\";\nimport type { IEntitySchemaPropertyIndex } from \"./IEntitySchemaPropertyIndex.js\";\nimport type { SortDirection } from \"./sortDirection.js\";\n\n/**\n * Definition for an entity schema property.\n */\nexport interface IEntitySchemaProperty<T = unknown> {\n\t/**\n\t * The property name from the entity.\n\t */\n\tproperty: keyof T;\n\n\t/**\n\t * The type of the property.\n\t */\n\ttype: EntitySchemaPropertyType;\n\n\t/**\n\t * The format of the property.\n\t */\n\tformat?: EntitySchemaPropertyFormat;\n\n\t/**\n\t * The maximum length of the property value i.e. for text fields.\n\t */\n\tmaxLength?: number;\n\n\t/**\n\t * Is this the primary index property.\n\t */\n\tisPrimary?: boolean;\n\n\t/**\n\t * Is this a secondary index property.\n\t */\n\tisSecondary?: boolean;\n\n\t/**\n\t * The composite indexes this property is part of.\n\t * Connectors can use these to build a composite index for each group name,\n\t * combining all the properties which share that name, ordered by their index.\n\t */\n\tindexGroup?: IEntitySchemaPropertyIndex[];\n\n\t/**\n\t * Is this property used as the optimistic-lock version token.\n\t * When true, connectors automatically manage the field: the value is\n\t * incremented on every successful write, and a write is rejected with a\n\t * ConflictError when the submitted value does not match the stored value.\n\t * Must be an integer property.\n\t */\n\tisVersion?: boolean;\n\n\t/**\n\t * Default sort direction for this field, leave empty if not sortable.\n\t */\n\tsortDirection?: SortDirection;\n\n\t/**\n\t * Is the property optional.\n\t */\n\toptional?: boolean;\n\n\t/**\n\t * The type of the item (only applies when type is `array`).\n\t */\n\titemType?: EntitySchemaPropertyType;\n\n\t/**\n\t * The type ref of the item (only applies when type is either `array` or `object`).\n\t */\n\titemTypeRef?: string;\n\n\t/**\n\t * Description of the object.\n\t */\n\tdescription?: string;\n\n\t/**\n\t * Examples of the property values.\n\t */\n\texamples?: unknown[];\n\n\t/**\n\t * A default value which can be used in migrations when the property value is not provided.\n\t */\n\tdefaultValue?: unknown;\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=IEntitySchemaPropertyIndex.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"IEntitySchemaPropertyIndex.js","sourceRoot":"","sources":["../../../src/models/IEntitySchemaPropertyIndex.ts"],"names":[],"mappings":"","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type { SortDirection } from \"./sortDirection.js\";\n\n/**\n * Definition of a composite index that a property is part of.\n */\nexport interface IEntitySchemaPropertyIndex {\n\t/**\n\t * The name of the composite index group.\n\t */\n\tname: string;\n\n\t/**\n\t * The sort direction for the property within the index.\n\t */\n\tdirection: SortDirection;\n\n\t/**\n\t * The position of the property within the index, ordered ascending.\n\t */\n\tindex: number;\n}\n"]}
@@ -12,7 +12,7 @@ export class EntitySchemaDiffHelper {
12
12
  /**
13
13
  * Compare two arrays of entity schema properties and return a structured diff.
14
14
  *
15
- * Properties are matched by their `property` key name. A property is considered modified when any structural field differs: `type`, `format`, `isPrimary`, `isSecondary`, `isVersion`, `sortDirection`, `optional`, `itemType`, or `itemTypeRef`.
15
+ * Properties are matched by their `property` key name. A property is considered modified when any structural field differs: `type`, `format`, `isPrimary`, `isSecondary`, `indexGroup`, `isVersion`, `sortDirection`, `optional`, `itemType`, or `itemTypeRef`.
16
16
  * Documentation-only fields (`description`, `examples`) are intentionally excluded from the comparison to avoid spurious diffs.
17
17
  *
18
18
  * Because a pure name change cannot be detected automatically, callers may supply a `renames` list mapping old names to new names. Renamed properties appear in `modified` (never in `added` or `removed`) even when no other fields changed. Rename lookups take priority over direct same-name matches, which allows swap renames to work correctly and prevents a renamed source from silently disappearing when the target name already existed in the old schema. Self-renames (`from === to`) are ignored and the property is classified normally.
@@ -128,13 +128,40 @@ export class EntitySchemaDiffHelper {
128
128
  Guards.object(EntitySchemaDiffHelper.CLASS_NAME, "schema2", schema2);
129
129
  return (schema1.type === schema2.type &&
130
130
  schema1.format === schema2.format &&
131
+ schema1.maxLength === schema2.maxLength &&
131
132
  schema1.isPrimary === schema2.isPrimary &&
132
133
  schema1.isSecondary === schema2.isSecondary &&
134
+ EntitySchemaDiffHelper.indexGroupsEqual(schema1.indexGroup, schema2.indexGroup) &&
133
135
  schema1.isVersion === schema2.isVersion &&
134
136
  schema1.sortDirection === schema2.sortDirection &&
135
137
  schema1.optional === schema2.optional &&
136
138
  schema1.itemType === schema2.itemType &&
137
139
  schema1.itemTypeRef === schema2.itemTypeRef);
138
140
  }
141
+ /**
142
+ * Compare two index group lists, treating them as unordered sets of indexes.
143
+ * The order a property declares its indexes in has no effect on the indexes a connector builds.
144
+ * @param indexGroup1 The first index group list.
145
+ * @param indexGroup2 The second index group list.
146
+ * @returns True if both lists contain the same name, direction and index values.
147
+ * @internal
148
+ */
149
+ static indexGroupsEqual(indexGroup1, indexGroup2) {
150
+ const groups1 = indexGroup1 ?? [];
151
+ const unmatchedGroups2 = [...(indexGroup2 ?? [])];
152
+ if (groups1.length !== unmatchedGroups2.length) {
153
+ return false;
154
+ }
155
+ for (const group1 of groups1) {
156
+ const matchIndex = unmatchedGroups2.findIndex(group2 => group2?.name === group1?.name &&
157
+ group2?.direction === group1?.direction &&
158
+ group2?.index === group1?.index);
159
+ if (matchIndex === -1) {
160
+ return false;
161
+ }
162
+ unmatchedGroups2.splice(matchIndex, 1);
163
+ }
164
+ return unmatchedGroups2.length === 0;
165
+ }
139
166
  }
140
167
  //# sourceMappingURL=entitySchemaDiffHelper.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"entitySchemaDiffHelper.js","sourceRoot":"","sources":["../../../src/utils/entitySchemaDiffHelper.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { GeneralError, Guards } from \"@twin.org/core\";\nimport { nameof } from \"@twin.org/nameof\";\nimport type { IEntitySchemaDiff } from \"../models/IEntitySchemaDiff.js\";\nimport type { IEntitySchemaProperty } from \"../models/IEntitySchemaProperty.js\";\n\n/**\n * Helper class for comparing entity schemas and generating diffs.\n */\nexport class EntitySchemaDiffHelper {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<EntitySchemaDiffHelper>();\n\n\t/**\n\t * Compare two arrays of entity schema properties and return a structured diff.\n\t *\n\t * Properties are matched by their `property` key name. A property is considered modified when any structural field differs: `type`, `format`, `isPrimary`, `isSecondary`, `isVersion`, `sortDirection`, `optional`, `itemType`, or `itemTypeRef`.\n\t * Documentation-only fields (`description`, `examples`) are intentionally excluded from the comparison to avoid spurious diffs.\n\t *\n\t * Because a pure name change cannot be detected automatically, callers may supply a `renames` list mapping old names to new names. Renamed properties appear in `modified` (never in `added` or `removed`) even when no other fields changed. Rename lookups take priority over direct same-name matches, which allows swap renames to work correctly and prevents a renamed source from silently disappearing when the target name already existed in the old schema. Self-renames (`from === to`) are ignored and the property is classified normally.\n\t *\n\t * When `renames` contains duplicate entries: if two entries share the same target, the last definition wins and the first source is treated as removed; if two entries share the same source, the first target wins and the second target is treated as added. Both cases are deterministic but callers should avoid them.\n\t * @param oldProperties The property descriptors from the current (live) schema.\n\t * @param newProperties The property descriptors from the target (new) schema.\n\t * @param renames Optional list of property renames `{ from, to }` where `from` is the old name and `to` is the new name.\n\t * @returns A diff object with `added`, `removed`, `modified`, and `unchanged` arrays, each containing full `IEntitySchemaProperty` descriptors.\n\t * @throws `GeneralError` if either input array contains duplicate property keys.\n\t */\n\tpublic static diff<T, U = T>(\n\t\toldProperties: IEntitySchemaProperty<T>[],\n\t\tnewProperties: IEntitySchemaProperty<U>[],\n\t\trenames?: { from: string; to: string }[]\n\t): IEntitySchemaDiff<T, U> {\n\t\tGuards.array(EntitySchemaDiffHelper.CLASS_NAME, nameof(oldProperties), oldProperties);\n\t\tGuards.array(EntitySchemaDiffHelper.CLASS_NAME, nameof(newProperties), newProperties);\n\n\t\tconst added: IEntitySchemaProperty<U>[] = [];\n\t\tconst removed: IEntitySchemaProperty<T>[] = [];\n\t\tconst modified: IEntitySchemaDiff<T, U>[\"modified\"] = [];\n\t\tconst unchanged: IEntitySchemaProperty<T | U>[] = [];\n\n\t\tconst oldMap = new Map<string, IEntitySchemaProperty<T>>();\n\t\tfor (const prop of oldProperties) {\n\t\t\tconst propKey = prop.property as string;\n\t\t\tif (oldMap.has(propKey)) {\n\t\t\t\tthrow new GeneralError(EntitySchemaDiffHelper.CLASS_NAME, \"duplicateOldProperty\", {\n\t\t\t\t\tproperty: propKey\n\t\t\t\t});\n\t\t\t}\n\t\t\toldMap.set(propKey, prop);\n\t\t}\n\n\t\tconst newMap = new Map<string, IEntitySchemaProperty<U>>();\n\t\tfor (const prop of newProperties) {\n\t\t\tconst propKey = prop.property as string;\n\t\t\tif (newMap.has(propKey)) {\n\t\t\t\tthrow new GeneralError(EntitySchemaDiffHelper.CLASS_NAME, \"duplicateNewProperty\", {\n\t\t\t\t\tproperty: propKey\n\t\t\t\t});\n\t\t\t}\n\t\t\tnewMap.set(propKey, prop);\n\t\t}\n\n\t\t// new-name → old-name, used when iterating newProperties\n\t\tconst renameToFrom = new Map<string, string>();\n\t\tif (renames) {\n\t\t\tfor (const rename of renames) {\n\t\t\t\trenameToFrom.set(rename.to, rename.from);\n\t\t\t}\n\t\t}\n\n\t\t// Old names that were consumed by a rename (prevents double-use of the same source).\n\t\tconst consumedByRename = new Set<string>();\n\t\t// New names matched via rename path (their same-named old prop is not their direct match).\n\t\tconst newKeyMatchedViaRename = new Set<string>();\n\n\t\tfor (const newProp of newProperties) {\n\t\t\tconst key = newProp.property as string;\n\n\t\t\t// Rename lookup takes priority over a direct same-name match so that:\n\t\t\t// - swap renames work (both names exist in old and new)\n\t\t\t// - a renamed source does not vanish when the target name already existed in old\n\t\t\t// Self-renames (fromKey === key) are skipped so the property is classified normally.\n\t\t\tconst fromKey = renameToFrom.get(key);\n\t\t\tconst renamedSource = fromKey !== undefined ? oldMap.get(fromKey) : undefined;\n\n\t\t\tif (\n\t\t\t\tfromKey !== undefined &&\n\t\t\t\tfromKey !== key &&\n\t\t\t\trenamedSource !== undefined &&\n\t\t\t\t!consumedByRename.has(fromKey)\n\t\t\t) {\n\t\t\t\tmodified.push({ from: renamedSource, to: newProp });\n\t\t\t\tconsumedByRename.add(fromKey);\n\t\t\t\tnewKeyMatchedViaRename.add(key);\n\t\t\t} else {\n\t\t\t\tconst oldProp = oldMap.get(key);\n\t\t\t\tif (oldProp !== undefined) {\n\t\t\t\t\tif (!EntitySchemaDiffHelper.schemaPropertiesEqual(oldProp, newProp)) {\n\t\t\t\t\t\tmodified.push({ from: oldProp, to: newProp });\n\t\t\t\t\t} else {\n\t\t\t\t\t\tunchanged.push(newProp as IEntitySchemaProperty<T | U>);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tadded.push(newProp);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfor (const oldProp of oldProperties) {\n\t\t\tconst key = oldProp.property as string;\n\t\t\t// Removed when absent from new, or when its same-named new prop was claimed by a rename\n\t\t\t// (meaning this old prop was not the match for that new prop).\n\t\t\t// Exception: skip if this old prop was itself consumed as a rename source.\n\t\t\tif ((!newMap.has(key) || newKeyMatchedViaRename.has(key)) && !consumedByRename.has(key)) {\n\t\t\t\tremoved.push(oldProp);\n\t\t\t}\n\t\t}\n\n\t\treturn { added, removed, modified, unchanged };\n\t}\n\n\t/**\n\t * Returns true when the diff contains at least one added, removed, or modified property.\n\t * @param diff The diff to check.\n\t * @returns True if the diff has any structural changes.\n\t */\n\tpublic static hasChanges<T, U>(diff: IEntitySchemaDiff<T, U>): boolean {\n\t\tGuards.object(EntitySchemaDiffHelper.CLASS_NAME, nameof(diff), diff);\n\t\tGuards.array(EntitySchemaDiffHelper.CLASS_NAME, nameof(diff.added), diff.added);\n\t\tGuards.array(EntitySchemaDiffHelper.CLASS_NAME, nameof(diff.removed), diff.removed);\n\t\tGuards.array(EntitySchemaDiffHelper.CLASS_NAME, nameof(diff.modified), diff.modified);\n\t\treturn diff.added.length > 0 || diff.removed.length > 0 || diff.modified.length > 0;\n\t}\n\n\t/**\n\t * Compare two property descriptors for structural equality.\n\t * The `property` name field and documentation fields (`description`, `examples`) are intentionally excluded - callers match by name before invoking this method.\n\t * @param schema1 The first property descriptor.\n\t * @param schema2 The second property descriptor.\n\t * @returns True if all structural fields are equal.\n\t */\n\tpublic static schemaPropertiesEqual<T, U>(\n\t\tschema1: IEntitySchemaProperty<T>,\n\t\tschema2: IEntitySchemaProperty<U>\n\t): boolean {\n\t\tGuards.object(EntitySchemaDiffHelper.CLASS_NAME, nameof(schema1), schema1);\n\t\tGuards.object(EntitySchemaDiffHelper.CLASS_NAME, nameof(schema2), schema2);\n\t\treturn (\n\t\t\tschema1.type === schema2.type &&\n\t\t\tschema1.format === schema2.format &&\n\t\t\tschema1.isPrimary === schema2.isPrimary &&\n\t\t\tschema1.isSecondary === schema2.isSecondary &&\n\t\t\tschema1.isVersion === schema2.isVersion &&\n\t\t\tschema1.sortDirection === schema2.sortDirection &&\n\t\t\tschema1.optional === schema2.optional &&\n\t\t\tschema1.itemType === schema2.itemType &&\n\t\t\tschema1.itemTypeRef === schema2.itemTypeRef\n\t\t);\n\t}\n}\n"]}
1
+ {"version":3,"file":"entitySchemaDiffHelper.js","sourceRoot":"","sources":["../../../src/utils/entitySchemaDiffHelper.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { GeneralError, Guards } from \"@twin.org/core\";\nimport { nameof } from \"@twin.org/nameof\";\nimport type { IEntitySchemaDiff } from \"../models/IEntitySchemaDiff.js\";\nimport type { IEntitySchemaProperty } from \"../models/IEntitySchemaProperty.js\";\nimport type { IEntitySchemaPropertyIndex } from \"../models/IEntitySchemaPropertyIndex.js\";\n\n/**\n * Helper class for comparing entity schemas and generating diffs.\n */\nexport class EntitySchemaDiffHelper {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<EntitySchemaDiffHelper>();\n\n\t/**\n\t * Compare two arrays of entity schema properties and return a structured diff.\n\t *\n\t * Properties are matched by their `property` key name. A property is considered modified when any structural field differs: `type`, `format`, `isPrimary`, `isSecondary`, `indexGroup`, `isVersion`, `sortDirection`, `optional`, `itemType`, or `itemTypeRef`.\n\t * Documentation-only fields (`description`, `examples`) are intentionally excluded from the comparison to avoid spurious diffs.\n\t *\n\t * Because a pure name change cannot be detected automatically, callers may supply a `renames` list mapping old names to new names. Renamed properties appear in `modified` (never in `added` or `removed`) even when no other fields changed. Rename lookups take priority over direct same-name matches, which allows swap renames to work correctly and prevents a renamed source from silently disappearing when the target name already existed in the old schema. Self-renames (`from === to`) are ignored and the property is classified normally.\n\t *\n\t * When `renames` contains duplicate entries: if two entries share the same target, the last definition wins and the first source is treated as removed; if two entries share the same source, the first target wins and the second target is treated as added. Both cases are deterministic but callers should avoid them.\n\t * @param oldProperties The property descriptors from the current (live) schema.\n\t * @param newProperties The property descriptors from the target (new) schema.\n\t * @param renames Optional list of property renames `{ from, to }` where `from` is the old name and `to` is the new name.\n\t * @returns A diff object with `added`, `removed`, `modified`, and `unchanged` arrays, each containing full `IEntitySchemaProperty` descriptors.\n\t * @throws `GeneralError` if either input array contains duplicate property keys.\n\t */\n\tpublic static diff<T, U = T>(\n\t\toldProperties: IEntitySchemaProperty<T>[],\n\t\tnewProperties: IEntitySchemaProperty<U>[],\n\t\trenames?: { from: string; to: string }[]\n\t): IEntitySchemaDiff<T, U> {\n\t\tGuards.array(EntitySchemaDiffHelper.CLASS_NAME, nameof(oldProperties), oldProperties);\n\t\tGuards.array(EntitySchemaDiffHelper.CLASS_NAME, nameof(newProperties), newProperties);\n\n\t\tconst added: IEntitySchemaProperty<U>[] = [];\n\t\tconst removed: IEntitySchemaProperty<T>[] = [];\n\t\tconst modified: IEntitySchemaDiff<T, U>[\"modified\"] = [];\n\t\tconst unchanged: IEntitySchemaProperty<T | U>[] = [];\n\n\t\tconst oldMap = new Map<string, IEntitySchemaProperty<T>>();\n\t\tfor (const prop of oldProperties) {\n\t\t\tconst propKey = prop.property as string;\n\t\t\tif (oldMap.has(propKey)) {\n\t\t\t\tthrow new GeneralError(EntitySchemaDiffHelper.CLASS_NAME, \"duplicateOldProperty\", {\n\t\t\t\t\tproperty: propKey\n\t\t\t\t});\n\t\t\t}\n\t\t\toldMap.set(propKey, prop);\n\t\t}\n\n\t\tconst newMap = new Map<string, IEntitySchemaProperty<U>>();\n\t\tfor (const prop of newProperties) {\n\t\t\tconst propKey = prop.property as string;\n\t\t\tif (newMap.has(propKey)) {\n\t\t\t\tthrow new GeneralError(EntitySchemaDiffHelper.CLASS_NAME, \"duplicateNewProperty\", {\n\t\t\t\t\tproperty: propKey\n\t\t\t\t});\n\t\t\t}\n\t\t\tnewMap.set(propKey, prop);\n\t\t}\n\n\t\t// new-name → old-name, used when iterating newProperties\n\t\tconst renameToFrom = new Map<string, string>();\n\t\tif (renames) {\n\t\t\tfor (const rename of renames) {\n\t\t\t\trenameToFrom.set(rename.to, rename.from);\n\t\t\t}\n\t\t}\n\n\t\t// Old names that were consumed by a rename (prevents double-use of the same source).\n\t\tconst consumedByRename = new Set<string>();\n\t\t// New names matched via rename path (their same-named old prop is not their direct match).\n\t\tconst newKeyMatchedViaRename = new Set<string>();\n\n\t\tfor (const newProp of newProperties) {\n\t\t\tconst key = newProp.property as string;\n\n\t\t\t// Rename lookup takes priority over a direct same-name match so that:\n\t\t\t// - swap renames work (both names exist in old and new)\n\t\t\t// - a renamed source does not vanish when the target name already existed in old\n\t\t\t// Self-renames (fromKey === key) are skipped so the property is classified normally.\n\t\t\tconst fromKey = renameToFrom.get(key);\n\t\t\tconst renamedSource = fromKey !== undefined ? oldMap.get(fromKey) : undefined;\n\n\t\t\tif (\n\t\t\t\tfromKey !== undefined &&\n\t\t\t\tfromKey !== key &&\n\t\t\t\trenamedSource !== undefined &&\n\t\t\t\t!consumedByRename.has(fromKey)\n\t\t\t) {\n\t\t\t\tmodified.push({ from: renamedSource, to: newProp });\n\t\t\t\tconsumedByRename.add(fromKey);\n\t\t\t\tnewKeyMatchedViaRename.add(key);\n\t\t\t} else {\n\t\t\t\tconst oldProp = oldMap.get(key);\n\t\t\t\tif (oldProp !== undefined) {\n\t\t\t\t\tif (!EntitySchemaDiffHelper.schemaPropertiesEqual(oldProp, newProp)) {\n\t\t\t\t\t\tmodified.push({ from: oldProp, to: newProp });\n\t\t\t\t\t} else {\n\t\t\t\t\t\tunchanged.push(newProp as IEntitySchemaProperty<T | U>);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tadded.push(newProp);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfor (const oldProp of oldProperties) {\n\t\t\tconst key = oldProp.property as string;\n\t\t\t// Removed when absent from new, or when its same-named new prop was claimed by a rename\n\t\t\t// (meaning this old prop was not the match for that new prop).\n\t\t\t// Exception: skip if this old prop was itself consumed as a rename source.\n\t\t\tif ((!newMap.has(key) || newKeyMatchedViaRename.has(key)) && !consumedByRename.has(key)) {\n\t\t\t\tremoved.push(oldProp);\n\t\t\t}\n\t\t}\n\n\t\treturn { added, removed, modified, unchanged };\n\t}\n\n\t/**\n\t * Returns true when the diff contains at least one added, removed, or modified property.\n\t * @param diff The diff to check.\n\t * @returns True if the diff has any structural changes.\n\t */\n\tpublic static hasChanges<T, U>(diff: IEntitySchemaDiff<T, U>): boolean {\n\t\tGuards.object(EntitySchemaDiffHelper.CLASS_NAME, nameof(diff), diff);\n\t\tGuards.array(EntitySchemaDiffHelper.CLASS_NAME, nameof(diff.added), diff.added);\n\t\tGuards.array(EntitySchemaDiffHelper.CLASS_NAME, nameof(diff.removed), diff.removed);\n\t\tGuards.array(EntitySchemaDiffHelper.CLASS_NAME, nameof(diff.modified), diff.modified);\n\t\treturn diff.added.length > 0 || diff.removed.length > 0 || diff.modified.length > 0;\n\t}\n\n\t/**\n\t * Compare two property descriptors for structural equality.\n\t * The `property` name field and documentation fields (`description`, `examples`) are intentionally excluded - callers match by name before invoking this method.\n\t * @param schema1 The first property descriptor.\n\t * @param schema2 The second property descriptor.\n\t * @returns True if all structural fields are equal.\n\t */\n\tpublic static schemaPropertiesEqual<T, U>(\n\t\tschema1: IEntitySchemaProperty<T>,\n\t\tschema2: IEntitySchemaProperty<U>\n\t): boolean {\n\t\tGuards.object(EntitySchemaDiffHelper.CLASS_NAME, nameof(schema1), schema1);\n\t\tGuards.object(EntitySchemaDiffHelper.CLASS_NAME, nameof(schema2), schema2);\n\t\treturn (\n\t\t\tschema1.type === schema2.type &&\n\t\t\tschema1.format === schema2.format &&\n\t\t\tschema1.maxLength === schema2.maxLength &&\n\t\t\tschema1.isPrimary === schema2.isPrimary &&\n\t\t\tschema1.isSecondary === schema2.isSecondary &&\n\t\t\tEntitySchemaDiffHelper.indexGroupsEqual(schema1.indexGroup, schema2.indexGroup) &&\n\t\t\tschema1.isVersion === schema2.isVersion &&\n\t\t\tschema1.sortDirection === schema2.sortDirection &&\n\t\t\tschema1.optional === schema2.optional &&\n\t\t\tschema1.itemType === schema2.itemType &&\n\t\t\tschema1.itemTypeRef === schema2.itemTypeRef\n\t\t);\n\t}\n\n\t/**\n\t * Compare two index group lists, treating them as unordered sets of indexes.\n\t * The order a property declares its indexes in has no effect on the indexes a connector builds.\n\t * @param indexGroup1 The first index group list.\n\t * @param indexGroup2 The second index group list.\n\t * @returns True if both lists contain the same name, direction and index values.\n\t * @internal\n\t */\n\tprivate static indexGroupsEqual(\n\t\tindexGroup1?: IEntitySchemaPropertyIndex[],\n\t\tindexGroup2?: IEntitySchemaPropertyIndex[]\n\t): boolean {\n\t\tconst groups1 = indexGroup1 ?? [];\n\t\tconst unmatchedGroups2 = [...(indexGroup2 ?? [])];\n\n\t\tif (groups1.length !== unmatchedGroups2.length) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfor (const group1 of groups1) {\n\t\t\tconst matchIndex = unmatchedGroups2.findIndex(\n\t\t\t\tgroup2 =>\n\t\t\t\t\tgroup2?.name === group1?.name &&\n\t\t\t\t\tgroup2?.direction === group1?.direction &&\n\t\t\t\t\tgroup2?.index === group1?.index\n\t\t\t);\n\t\t\tif (matchIndex === -1) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tunmatchedGroups2.splice(matchIndex, 1);\n\t\t}\n\t\treturn unmatchedGroups2.length === 0;\n\t}\n}\n"]}
@@ -2,7 +2,9 @@
2
2
  // SPDX-License-Identifier: Apache-2.0.
3
3
  import { GeneralError, Guards, Is } from "@twin.org/core";
4
4
  import { DecoratorHelper } from "./decoratorHelper.js";
5
+ import { EntitySchemaPropertyFormat } from "../models/entitySchemaPropertyFormat.js";
5
6
  import { EntitySchemaPropertyType } from "../models/entitySchemaPropertyType.js";
7
+ import { SortDirection } from "../models/sortDirection.js";
6
8
  /**
7
9
  * Class to help with entity schema operations.
8
10
  */
@@ -11,6 +13,17 @@ export class EntitySchemaHelper {
11
13
  * Runtime name for the class.
12
14
  */
13
15
  static CLASS_NAME = "EntitySchemaHelper";
16
+ /**
17
+ * The default maximum lengths for string properties, keyed by their format.
18
+ */
19
+ static FORMAT_MAX_LENGTHS = {
20
+ [EntitySchemaPropertyFormat.Uuid]: 36,
21
+ [EntitySchemaPropertyFormat.Date]: 64,
22
+ [EntitySchemaPropertyFormat.Time]: 64,
23
+ [EntitySchemaPropertyFormat.DateTime]: 64,
24
+ [EntitySchemaPropertyFormat.Email]: 254,
25
+ [EntitySchemaPropertyFormat.Uri]: 2048
26
+ };
14
27
  /**
15
28
  * Get the schema for the specified object.
16
29
  * @param target The object to get the schema data for.
@@ -74,6 +87,70 @@ export class EntitySchemaHelper {
74
87
  }))
75
88
  : undefined;
76
89
  }
90
+ /**
91
+ * Get the composite index groups from the schema.
92
+ * Each property can be part of multiple indexes through its `indexGroup` list, so a property
93
+ * can appear in more than one group. The properties within a group are ordered by the `index`
94
+ * of their index entry, and each is returned with the sort direction it declared for that group.
95
+ * @param entitySchema The entity schema to find the index groups from.
96
+ * @returns The properties and their directions keyed by the group name, empty if there are no groups.
97
+ * @throws GeneralError if an index entry has an invalid direction or index, or if two properties
98
+ * claim the same index within the same group, or if a group contains fewer than two properties.
99
+ */
100
+ static getIndexGroups(entitySchema) {
101
+ Guards.object(EntitySchemaHelper.CLASS_NAME, "entitySchema", entitySchema);
102
+ const groupEntries = {};
103
+ const groupIndexes = {};
104
+ for (const property of entitySchema.properties ?? []) {
105
+ if (Is.arrayValue(property.indexGroup)) {
106
+ for (const propertyIndex of property.indexGroup) {
107
+ if (Is.stringValue(propertyIndex?.name)) {
108
+ if (!Object.values(SortDirection).includes(propertyIndex.direction)) {
109
+ throw new GeneralError(EntitySchemaHelper.CLASS_NAME, "invalidIndexGroupDirection", {
110
+ group: propertyIndex.name,
111
+ direction: propertyIndex.direction,
112
+ property: property.property
113
+ });
114
+ }
115
+ if (!Is.integer(propertyIndex.index) || propertyIndex.index < 0) {
116
+ throw new GeneralError(EntitySchemaHelper.CLASS_NAME, "invalidIndexGroupIndex", {
117
+ group: propertyIndex.name,
118
+ index: propertyIndex.index,
119
+ property: property.property
120
+ });
121
+ }
122
+ groupIndexes[propertyIndex.name] ??= new Set();
123
+ if (groupIndexes[propertyIndex.name].has(propertyIndex.index)) {
124
+ throw new GeneralError(EntitySchemaHelper.CLASS_NAME, "duplicateIndexGroupIndex", {
125
+ group: propertyIndex.name,
126
+ index: propertyIndex.index,
127
+ property: property.property
128
+ });
129
+ }
130
+ groupIndexes[propertyIndex.name].add(propertyIndex.index);
131
+ groupEntries[propertyIndex.name] ??= [];
132
+ groupEntries[propertyIndex.name].push({
133
+ entry: { property, direction: propertyIndex.direction },
134
+ index: propertyIndex.index
135
+ });
136
+ }
137
+ }
138
+ }
139
+ }
140
+ const indexGroups = {};
141
+ for (const group of Object.keys(groupEntries)) {
142
+ if (groupEntries[group].length < 2) {
143
+ throw new GeneralError(EntitySchemaHelper.CLASS_NAME, "indexGroupMustHaveAtLeastTwoProperties", {
144
+ group,
145
+ count: groupEntries[group].length
146
+ });
147
+ }
148
+ indexGroups[group] = groupEntries[group]
149
+ .sort((a, b) => a.index - b.index)
150
+ .map(groupEntry => groupEntry.entry);
151
+ }
152
+ return indexGroups;
153
+ }
77
154
  /**
78
155
  * Build sort properties from the schema and override if necessary.
79
156
  * @param entitySchema The entity schema to retrieve the default sort keys.
@@ -105,7 +182,7 @@ export class EntitySchemaHelper {
105
182
  * Validate the entity against the schema.
106
183
  * @param entity The entity to validate.
107
184
  * @param entitySchema The schema to validate against.
108
- * @throws If the entity is invalid.
185
+ * @throws If the entity is invalid, or a string value exceeds its maxLength.
109
186
  */
110
187
  static validateEntity(entity, entitySchema) {
111
188
  Guards.object(EntitySchemaHelper.CLASS_NAME, "entity", entity);
@@ -154,6 +231,21 @@ export class EntitySchemaHelper {
154
231
  type: prop.type
155
232
  });
156
233
  }
234
+ if (prop.type === EntitySchemaPropertyType.String && Is.string(value)) {
235
+ // Formats with a known length default to it when no explicit maxLength is set
236
+ const maxLength = prop.maxLength ??
237
+ (Is.stringValue(prop.format)
238
+ ? EntitySchemaHelper.FORMAT_MAX_LENGTHS[prop.format]
239
+ : undefined);
240
+ if (Is.number(maxLength) && maxLength > 0 && value.length > maxLength) {
241
+ // The value is longer than the maximum length defined in the schema
242
+ throw new GeneralError(EntitySchemaHelper.CLASS_NAME, "maxLengthExceeded", {
243
+ property: prop.property,
244
+ maxLength,
245
+ length: value.length
246
+ });
247
+ }
248
+ }
157
249
  }
158
250
  if (allKeys.length > 0) {
159
251
  // There are keys in the entity that are not in the schema
@@ -1 +1 @@
1
- {"version":3,"file":"entitySchemaHelper.js","sourceRoot":"","sources":["../../../src/utils/entitySchemaHelper.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,gBAAgB,CAAC;AAE1D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,wBAAwB,EAAE,MAAM,uCAAuC,CAAC;AAMjF;;GAEG;AACH,MAAM,OAAO,kBAAkB;IAC9B;;OAEG;IACI,MAAM,CAAU,UAAU,wBAAwC;IAEzE;;;;OAIG;IACH,8DAA8D;IACvD,MAAM,CAAC,SAAS,CAAc,MAAW;QAC/C,OAAO,eAAe,CAAC,SAAS,CAAI,MAAM,CAAC,CAAC;IAC7C,CAAC;IAED;;;;;;;;OAQG;IACI,MAAM,CAAC,UAAU,CAAC,YAA2B;QACnD,MAAM,CAAC,MAAM,CAAgB,kBAAkB,CAAC,UAAU,kBAAwB,YAAY,CAAC,CAAC;QAEhG,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,YAAY,EAAE,OAAO,CAAC,EAAE,CAAC;YACtC,MAAM,CAAC,OAAO,CACb,kBAAkB,CAAC,UAAU,0BAE7B,YAAY,EAAE,OAAO,CACrB,CAAC;YACF,IAAI,YAAY,CAAC,OAAO,GAAG,CAAC,EAAE,CAAC;gBAC9B,MAAM,IAAI,YAAY,CACrB,kBAAkB,CAAC,UAAU,EAC7B,qCAAqC,EACrC;oBACC,OAAO,EAAE,YAAY,CAAC,OAAO;iBAC7B,CACD,CAAC;YACH,CAAC;QACF,CAAC;QAED,OAAO,YAAY,CAAC,OAAO,IAAI,CAAC,CAAC;IAClC,CAAC;IAED;;;;;OAKG;IACI,MAAM,CAAC,aAAa,CAAI,YAA8B;QAC5D,MAAM,CAAC,MAAM,CACZ,kBAAkB,CAAC,UAAU,kBAE7B,YAAY,CACZ,CAAC;QAEF,MAAM,WAAW,GAAG,CAAC,YAAY,CAAC,UAAU,IAAI,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QAC9E,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC9B,MAAM,IAAI,YAAY,CAAC,kBAAkB,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;QACtE,CAAC;QACD,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC5B,MAAM,IAAI,YAAY,CAAC,kBAAkB,CAAC,UAAU,EAAE,mBAAmB,CAAC,CAAC;QAC5E,CAAC;QACD,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC;IACvB,CAAC;IAED;;;;OAIG;IACI,MAAM,CAAC,iBAAiB,CAAI,YAA8B;QAChE,MAAM,CAAC,MAAM,CACZ,kBAAkB,CAAC,UAAU,kBAE7B,YAAY,CACZ,CAAC;QAEF,MAAM,UAAU,GAAG,CAAC,YAAY,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC;QAE/F,OAAO,UAAU,CAAC,MAAM,GAAG,CAAC;YAC3B,CAAC,CAAC,UAAU,CAAC,GAAG,CACd,CAAC,CAAC,EAAE,CACH,CAAC;gBACA,QAAQ,EAAE,CAAC,CAAC,QAAQ;gBACpB,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,aAAa,EAAE,CAAC,CAAC,aAAa;aAC9B,CAAmB,CACrB;YACF,CAAC,CAAC,SAAS,CAAC;IACd,CAAC;IAED;;;;;OAKG;IACI,MAAM,CAAC,mBAAmB,CAChC,YAA8B,EAC9B,gBAGG;QAEH,MAAM,CAAC,MAAM,CAAC,kBAAkB,CAAC,UAAU,kBAAwB,YAAY,CAAC,CAAC;QAEjF,IAAI,aAA2C,CAAC;QAEhD,IAAI,EAAE,CAAC,UAAU,CAAC,gBAAgB,CAAC,EAAE,CAAC;YACrC,aAAa,GAAG,EAAE,CAAC;YAEnB,KAAK,MAAM,OAAO,IAAI,gBAAgB,EAAE,CAAC;gBACxC,MAAM,QAAQ,GAAG,CAAC,YAAY,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;gBAC5F,IAAI,QAAQ,EAAE,CAAC;oBACd,aAAa,CAAC,IAAI,CAAC;wBAClB,QAAQ,EAAE,OAAO,CAAC,QAAQ;wBAC1B,aAAa,EAAE,OAAO,CAAC,aAAa;wBACpC,IAAI,EAAE,QAAQ,CAAC,IAAI;qBACnB,CAAC,CAAC;gBACJ,CAAC;YACF,CAAC;QACF,CAAC;aAAM,CAAC;YACP,aAAa,GAAG,kBAAkB,CAAC,iBAAiB,CAAC,YAAY,CAAC,CAAC;QACpE,CAAC;QAED,OAAO,aAAa,CAAC;IACtB,CAAC;IAED;;;;;OAKG;IACI,MAAM,CAAC,cAAc,CAAI,MAAS,EAAE,YAA8B;QACxE,MAAM,CAAC,MAAM,CAAC,kBAAkB,CAAC,UAAU,YAAkB,MAAM,CAAC,CAAC;QACrE,MAAM,CAAC,MAAM,CACZ,kBAAkB,CAAC,UAAU,kBAE7B,YAAY,CACZ,CAAC;QAEF,MAAM,UAAU,GAAG,YAAY,CAAC,UAAU,IAAI,EAAE,CAAC;QACjD,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC;YACvD,MAAM,IAAI,YAAY,CAAC,kBAAkB,CAAC,UAAU,EAAE,yBAAyB,CAAC,CAAC;QAClF,CAAC;QAED,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAEpC,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;YAC/B,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,QAAkB,CAAC,CAAC;YACrD,IAAI,GAAG,KAAK,CAAC,CAAC,EAAE,CAAC;gBAChB,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;YACxB,CAAC;YAED,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACpC,MAAM,SAAS,GAAG,OAAO,KAAK,CAAC;YAE/B,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;gBACrB,4EAA4E;gBAC5E,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;oBACpB,MAAM,IAAI,YAAY,CAAC,kBAAkB,CAAC,UAAU,EAAE,iBAAiB,EAAE;wBACxE,QAAQ,EAAE,IAAI,CAAC,QAAQ;wBACvB,IAAI,EAAE,IAAI,CAAC,IAAI;qBACf,CAAC,CAAC;gBACJ,CAAC;YACF,CAAC;iBAAM,IAAI,IAAI,CAAC,IAAI,KAAK,wBAAwB,CAAC,OAAO,IAAI,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;gBAChF,gFAAgF;YACjF,CAAC;iBAAM,IACN,IAAI,CAAC,IAAI,KAAK,wBAAwB,CAAC,MAAM;gBAC7C,CAAC,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC;oBAChB,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC;oBACf,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC;oBAChB,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC;oBAChB,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;oBACjB,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EACf,CAAC;gBACF,yGAAyG;YAC1G,CAAC;iBAAM,IAAI,IAAI,CAAC,IAAI,KAAK,wBAAwB,CAAC,KAAK,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC5E,4EAA4E;YAC7E,CAAC;iBAAM,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;gBACpC,gDAAgD;gBAChD,MAAM,IAAI,YAAY,CAAC,kBAAkB,CAAC,UAAU,EAAE,uBAAuB,EAAE;oBAC9E,KAAK;oBACL,QAAQ,EAAE,IAAI,CAAC,QAAQ;oBACvB,IAAI,EAAE,IAAI,CAAC,IAAI;iBACf,CAAC,CAAC;YACJ,CAAC;QACF,CAAC;QAED,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxB,0DAA0D;YAC1D,MAAM,IAAI,YAAY,CAAC,kBAAkB,CAAC,UAAU,EAAE,mBAAmB,EAAE;gBAC1E,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC;aACxB,CAAC,CAAC;QACJ,CAAC;IACF,CAAC;IAED;;;;;;OAMG;IACI,MAAM,CAAC,mBAAmB,CAAI,MAAwB;QAC5D,MAAM,CAAC,MAAM,CAAC,kBAAkB,CAAC,UAAU,YAAkB,MAAM,CAAC,CAAC;QAErE,MAAM,iBAAiB,GAAG,CAAC,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,IAAI,CAAC,CAAC;QACtF,IAAI,iBAAiB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAClC,MAAM,IAAI,YAAY,CAAC,kBAAkB,CAAC,UAAU,EAAE,2BAA2B,CAAC,CAAC;QACpF,CAAC;QACD,IAAI,iBAAiB,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACpC,IAAI,iBAAiB,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,wBAAwB,CAAC,OAAO,EAAE,CAAC;gBACpE,MAAM,IAAI,YAAY,CAAC,kBAAkB,CAAC,UAAU,EAAE,8BAA8B,EAAE;oBACrF,QAAQ,EAAE,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;oBAC/C,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,CAAC,IAAI;iBAC/B,CAAC,CAAC;YACJ,CAAC;YACD,OAAO,iBAAiB,CAAC,CAAC,CAAC,CAAC,QAA8B,CAAC;QAC5D,CAAC;QACD,OAAO,SAAS,CAAC;IAClB,CAAC","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { GeneralError, Guards, Is } from \"@twin.org/core\";\nimport { nameof } from \"@twin.org/nameof\";\nimport { DecoratorHelper } from \"./decoratorHelper.js\";\nimport { EntitySchemaPropertyType } from \"../models/entitySchemaPropertyType.js\";\nimport type { IEntitySchema } from \"../models/IEntitySchema.js\";\nimport type { IEntitySchemaProperty } from \"../models/IEntitySchemaProperty.js\";\nimport type { IEntitySort } from \"../models/IEntitySort.js\";\nimport type { SortDirection } from \"../models/sortDirection.js\";\n\n/**\n * Class to help with entity schema operations.\n */\nexport class EntitySchemaHelper {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<EntitySchemaHelper>();\n\n\t/**\n\t * Get the schema for the specified object.\n\t * @param target The object to get the schema data for.\n\t * @returns The schema for the object if it can be found.\n\t */\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tpublic static getSchema<T = unknown>(target: any): IEntitySchema<T> {\n\t\treturn DecoratorHelper.getSchema<T>(target);\n\t}\n\n\t/**\n\t * Get the version of the entity schema, defaulting to 0 when absent.\n\t * This is the single source of truth for the \"absent version = v0\" convention.\n\t * When a version is present it must be a non-negative integer >= 0.\n\t * @param entitySchema The entity schema to read the version from.\n\t * @returns The declared version, or 0 if no version was set.\n\t * @throws GuardError if entitySchema is undefined or version is not an integer.\n\t * @throws GeneralError if version is present but less than 0.\n\t */\n\tpublic static getVersion(entitySchema: IEntitySchema): number {\n\t\tGuards.object<IEntitySchema>(EntitySchemaHelper.CLASS_NAME, nameof(entitySchema), entitySchema);\n\n\t\tif (!Is.empty(entitySchema?.version)) {\n\t\t\tGuards.integer(\n\t\t\t\tEntitySchemaHelper.CLASS_NAME,\n\t\t\t\tnameof(entitySchema?.version),\n\t\t\t\tentitySchema?.version\n\t\t\t);\n\t\t\tif (entitySchema.version < 0) {\n\t\t\t\tthrow new GeneralError(\n\t\t\t\t\tEntitySchemaHelper.CLASS_NAME,\n\t\t\t\t\t\"versionMustBeGreaterThanOrEqualZero\",\n\t\t\t\t\t{\n\t\t\t\t\t\tversion: entitySchema.version\n\t\t\t\t\t}\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\treturn entitySchema.version ?? 0;\n\t}\n\n\t/**\n\t * Get the primary key from the entity schema.\n\t * @param entitySchema The entity schema to find the primary key from.\n\t * @returns The key if only one was found.\n\t * @throws If no primary key was found, or more than one.\n\t */\n\tpublic static getPrimaryKey<T>(entitySchema: IEntitySchema<T>): IEntitySchemaProperty<T> {\n\t\tGuards.object<IEntitySchema<T>>(\n\t\t\tEntitySchemaHelper.CLASS_NAME,\n\t\t\tnameof(entitySchema),\n\t\t\tentitySchema\n\t\t);\n\n\t\tconst primaryKeys = (entitySchema.properties ?? [])?.filter(p => p.isPrimary);\n\t\tif (primaryKeys.length === 0) {\n\t\t\tthrow new GeneralError(EntitySchemaHelper.CLASS_NAME, \"noIsPrimary\");\n\t\t}\n\t\tif (primaryKeys.length > 1) {\n\t\t\tthrow new GeneralError(EntitySchemaHelper.CLASS_NAME, \"multipleIsPrimary\");\n\t\t}\n\t\treturn primaryKeys[0];\n\t}\n\n\t/**\n\t * Get the sort properties from the schema.\n\t * @param entitySchema The entity schema to find the primary key from.\n\t * @returns The sort keys from the schema or undefined if there are none.\n\t */\n\tpublic static getSortProperties<T>(entitySchema: IEntitySchema<T>): IEntitySort<T>[] | undefined {\n\t\tGuards.object<IEntitySchema<T>>(\n\t\t\tEntitySchemaHelper.CLASS_NAME,\n\t\t\tnameof(entitySchema),\n\t\t\tentitySchema\n\t\t);\n\n\t\tconst sortFields = (entitySchema.properties ?? []).filter(p => !Is.undefined(p.sortDirection));\n\n\t\treturn sortFields.length > 0\n\t\t\t? sortFields.map(\n\t\t\t\t\tp =>\n\t\t\t\t\t\t({\n\t\t\t\t\t\t\tproperty: p.property,\n\t\t\t\t\t\t\ttype: p.type,\n\t\t\t\t\t\t\tsortDirection: p.sortDirection\n\t\t\t\t\t\t}) as IEntitySort<T>\n\t\t\t\t)\n\t\t\t: undefined;\n\t}\n\n\t/**\n\t * Build sort properties from the schema and override if necessary.\n\t * @param entitySchema The entity schema to retrieve the default sort keys.\n\t * @param overrideSortKeys The override sort keys.\n\t * @returns The finalised sort keys.\n\t */\n\tpublic static buildSortProperties<T>(\n\t\tentitySchema: IEntitySchema<T>,\n\t\toverrideSortKeys?: {\n\t\t\tproperty: keyof T;\n\t\t\tsortDirection: SortDirection;\n\t\t}[]\n\t): IEntitySort<T>[] | undefined {\n\t\tGuards.object(EntitySchemaHelper.CLASS_NAME, nameof(entitySchema), entitySchema);\n\n\t\tlet finalSortKeys: IEntitySort<T>[] | undefined;\n\n\t\tif (Is.arrayValue(overrideSortKeys)) {\n\t\t\tfinalSortKeys = [];\n\n\t\t\tfor (const sortKey of overrideSortKeys) {\n\t\t\t\tconst property = (entitySchema.properties ?? []).find(p => p.property === sortKey.property);\n\t\t\t\tif (property) {\n\t\t\t\t\tfinalSortKeys.push({\n\t\t\t\t\t\tproperty: sortKey.property,\n\t\t\t\t\t\tsortDirection: sortKey.sortDirection,\n\t\t\t\t\t\ttype: property.type\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tfinalSortKeys = EntitySchemaHelper.getSortProperties(entitySchema);\n\t\t}\n\n\t\treturn finalSortKeys;\n\t}\n\n\t/**\n\t * Validate the entity against the schema.\n\t * @param entity The entity to validate.\n\t * @param entitySchema The schema to validate against.\n\t * @throws If the entity is invalid.\n\t */\n\tpublic static validateEntity<T>(entity: T, entitySchema: IEntitySchema<T>): void {\n\t\tGuards.object(EntitySchemaHelper.CLASS_NAME, nameof(entity), entity);\n\t\tGuards.object<IEntitySchema<T>>(\n\t\t\tEntitySchemaHelper.CLASS_NAME,\n\t\t\tnameof(entitySchema),\n\t\t\tentitySchema\n\t\t);\n\n\t\tconst properties = entitySchema.properties ?? [];\n\t\tif (properties.length === 0 && Is.objectValue(entity)) {\n\t\t\tthrow new GeneralError(EntitySchemaHelper.CLASS_NAME, \"invalidEntityProperties\");\n\t\t}\n\n\t\tconst allKeys = Object.keys(entity);\n\n\t\tfor (const prop of properties) {\n\t\t\tconst idx = allKeys.indexOf(prop.property as string);\n\t\t\tif (idx !== -1) {\n\t\t\t\tallKeys.splice(idx, 1);\n\t\t\t}\n\n\t\t\tconst value = entity[prop.property];\n\t\t\tconst valueType = typeof value;\n\n\t\t\tif (Is.empty(value)) {\n\t\t\t\t// If the value is empty but the property is not optional, then it's invalid\n\t\t\t\tif (!prop.optional) {\n\t\t\t\t\tthrow new GeneralError(EntitySchemaHelper.CLASS_NAME, \"invalidOptional\", {\n\t\t\t\t\t\tproperty: prop.property,\n\t\t\t\t\t\ttype: prop.type\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} else if (prop.type === EntitySchemaPropertyType.Integer && Is.integer(value)) {\n\t\t\t\t// If the schema expects an integer and the value is an integer, then it's valid\n\t\t\t} else if (\n\t\t\t\tprop.type === EntitySchemaPropertyType.Object &&\n\t\t\t\t(Is.object(value) ||\n\t\t\t\t\tIs.array(value) ||\n\t\t\t\t\tIs.string(value) ||\n\t\t\t\t\tIs.number(value) ||\n\t\t\t\t\tIs.boolean(value) ||\n\t\t\t\t\tIs.null(value))\n\t\t\t) {\n\t\t\t\t// If the schema expects an object and the value is anything that can be JSON serialised, then it's valid\n\t\t\t} else if (prop.type === EntitySchemaPropertyType.Array && Is.array(value)) {\n\t\t\t\t// If the schema expects an array and the value is an array, then it's valid\n\t\t\t} else if (prop.type !== valueType) {\n\t\t\t\t// The schema type does not match the value type\n\t\t\t\tthrow new GeneralError(EntitySchemaHelper.CLASS_NAME, \"invalidEntityProperty\", {\n\t\t\t\t\tvalue,\n\t\t\t\t\tproperty: prop.property,\n\t\t\t\t\ttype: prop.type\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tif (allKeys.length > 0) {\n\t\t\t// There are keys in the entity that are not in the schema\n\t\t\tthrow new GeneralError(EntitySchemaHelper.CLASS_NAME, \"invalidEntityKeys\", {\n\t\t\t\tkeys: allKeys.join(\", \")\n\t\t\t});\n\t\t}\n\t}\n\n\t/**\n\t * Find the property in the schema that is marked as the optimistic-lock version token.\n\t * @param schema The entity schema to search.\n\t * @returns The name of the version property, or undefined if none is declared.\n\t * @throws GeneralError if more than one property has isVersion set.\n\t * @throws GeneralError if the version property type is not integer.\n\t */\n\tpublic static findVersionProperty<T>(schema: IEntitySchema<T>): string | undefined {\n\t\tGuards.object(EntitySchemaHelper.CLASS_NAME, nameof(schema), schema);\n\n\t\tconst versionProperties = (schema.properties ?? []).filter(p => p.isVersion === true);\n\t\tif (versionProperties.length > 1) {\n\t\t\tthrow new GeneralError(EntitySchemaHelper.CLASS_NAME, \"multipleVersionProperties\");\n\t\t}\n\t\tif (versionProperties.length === 1) {\n\t\t\tif (versionProperties[0].type !== EntitySchemaPropertyType.Integer) {\n\t\t\t\tthrow new GeneralError(EntitySchemaHelper.CLASS_NAME, \"versionPropertyMustBeInteger\", {\n\t\t\t\t\tproperty: String(versionProperties[0].property),\n\t\t\t\t\ttype: versionProperties[0].type\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn versionProperties[0].property as string | undefined;\n\t\t}\n\t\treturn undefined;\n\t}\n}\n"]}
1
+ {"version":3,"file":"entitySchemaHelper.js","sourceRoot":"","sources":["../../../src/utils/entitySchemaHelper.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,gBAAgB,CAAC;AAE1D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,0BAA0B,EAAE,MAAM,yCAAyC,CAAC;AACrF,OAAO,EAAE,wBAAwB,EAAE,MAAM,uCAAuC,CAAC;AAIjF,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAE3D;;GAEG;AACH,MAAM,OAAO,kBAAkB;IAC9B;;OAEG;IACI,MAAM,CAAU,UAAU,wBAAwC;IAEzE;;OAEG;IACI,MAAM,CAAU,kBAAkB,GAAiC;QACzE,CAAC,0BAA0B,CAAC,IAAI,CAAC,EAAE,EAAE;QACrC,CAAC,0BAA0B,CAAC,IAAI,CAAC,EAAE,EAAE;QACrC,CAAC,0BAA0B,CAAC,IAAI,CAAC,EAAE,EAAE;QACrC,CAAC,0BAA0B,CAAC,QAAQ,CAAC,EAAE,EAAE;QACzC,CAAC,0BAA0B,CAAC,KAAK,CAAC,EAAE,GAAG;QACvC,CAAC,0BAA0B,CAAC,GAAG,CAAC,EAAE,IAAI;KACtC,CAAC;IAEF;;;;OAIG;IACH,8DAA8D;IACvD,MAAM,CAAC,SAAS,CAAc,MAAW;QAC/C,OAAO,eAAe,CAAC,SAAS,CAAI,MAAM,CAAC,CAAC;IAC7C,CAAC;IAED;;;;;;;;OAQG;IACI,MAAM,CAAC,UAAU,CAAC,YAA2B;QACnD,MAAM,CAAC,MAAM,CAAgB,kBAAkB,CAAC,UAAU,kBAAwB,YAAY,CAAC,CAAC;QAEhG,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,YAAY,EAAE,OAAO,CAAC,EAAE,CAAC;YACtC,MAAM,CAAC,OAAO,CACb,kBAAkB,CAAC,UAAU,0BAE7B,YAAY,EAAE,OAAO,CACrB,CAAC;YACF,IAAI,YAAY,CAAC,OAAO,GAAG,CAAC,EAAE,CAAC;gBAC9B,MAAM,IAAI,YAAY,CACrB,kBAAkB,CAAC,UAAU,EAC7B,qCAAqC,EACrC;oBACC,OAAO,EAAE,YAAY,CAAC,OAAO;iBAC7B,CACD,CAAC;YACH,CAAC;QACF,CAAC;QAED,OAAO,YAAY,CAAC,OAAO,IAAI,CAAC,CAAC;IAClC,CAAC;IAED;;;;;OAKG;IACI,MAAM,CAAC,aAAa,CAAI,YAA8B;QAC5D,MAAM,CAAC,MAAM,CACZ,kBAAkB,CAAC,UAAU,kBAE7B,YAAY,CACZ,CAAC;QAEF,MAAM,WAAW,GAAG,CAAC,YAAY,CAAC,UAAU,IAAI,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QAC9E,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC9B,MAAM,IAAI,YAAY,CAAC,kBAAkB,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;QACtE,CAAC;QACD,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC5B,MAAM,IAAI,YAAY,CAAC,kBAAkB,CAAC,UAAU,EAAE,mBAAmB,CAAC,CAAC;QAC5E,CAAC;QACD,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC;IACvB,CAAC;IAED;;;;OAIG;IACI,MAAM,CAAC,iBAAiB,CAAI,YAA8B;QAChE,MAAM,CAAC,MAAM,CACZ,kBAAkB,CAAC,UAAU,kBAE7B,YAAY,CACZ,CAAC;QAEF,MAAM,UAAU,GAAG,CAAC,YAAY,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC;QAE/F,OAAO,UAAU,CAAC,MAAM,GAAG,CAAC;YAC3B,CAAC,CAAC,UAAU,CAAC,GAAG,CACd,CAAC,CAAC,EAAE,CACH,CAAC;gBACA,QAAQ,EAAE,CAAC,CAAC,QAAQ;gBACpB,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,aAAa,EAAE,CAAC,CAAC,aAAa;aAC9B,CAAmB,CACrB;YACF,CAAC,CAAC,SAAS,CAAC;IACd,CAAC;IAED;;;;;;;;;OASG;IACI,MAAM,CAAC,cAAc,CAAI,YAA8B;QAG7D,MAAM,CAAC,MAAM,CACZ,kBAAkB,CAAC,UAAU,kBAE7B,YAAY,CACZ,CAAC;QAEF,MAAM,YAAY,GAKd,EAAE,CAAC;QACP,MAAM,YAAY,GAAqC,EAAE,CAAC;QAE1D,KAAK,MAAM,QAAQ,IAAI,YAAY,CAAC,UAAU,IAAI,EAAE,EAAE,CAAC;YACtD,IAAI,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;gBACxC,KAAK,MAAM,aAAa,IAAI,QAAQ,CAAC,UAAU,EAAE,CAAC;oBACjD,IAAI,EAAE,CAAC,WAAW,CAAC,aAAa,EAAE,IAAI,CAAC,EAAE,CAAC;wBACzC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,QAAQ,CAAC,aAAa,CAAC,SAAS,CAAC,EAAE,CAAC;4BACrE,MAAM,IAAI,YAAY,CAAC,kBAAkB,CAAC,UAAU,EAAE,4BAA4B,EAAE;gCACnF,KAAK,EAAE,aAAa,CAAC,IAAI;gCACzB,SAAS,EAAE,aAAa,CAAC,SAAS;gCAClC,QAAQ,EAAE,QAAQ,CAAC,QAAQ;6BAC3B,CAAC,CAAC;wBACJ,CAAC;wBAED,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,aAAa,CAAC,KAAK,GAAG,CAAC,EAAE,CAAC;4BACjE,MAAM,IAAI,YAAY,CAAC,kBAAkB,CAAC,UAAU,EAAE,wBAAwB,EAAE;gCAC/E,KAAK,EAAE,aAAa,CAAC,IAAI;gCACzB,KAAK,EAAE,aAAa,CAAC,KAAK;gCAC1B,QAAQ,EAAE,QAAQ,CAAC,QAAQ;6BAC3B,CAAC,CAAC;wBACJ,CAAC;wBAED,YAAY,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,IAAI,GAAG,EAAE,CAAC;wBAC/C,IAAI,YAAY,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;4BAC/D,MAAM,IAAI,YAAY,CAAC,kBAAkB,CAAC,UAAU,EAAE,0BAA0B,EAAE;gCACjF,KAAK,EAAE,aAAa,CAAC,IAAI;gCACzB,KAAK,EAAE,aAAa,CAAC,KAAK;gCAC1B,QAAQ,EAAE,QAAQ,CAAC,QAAQ;6BAC3B,CAAC,CAAC;wBACJ,CAAC;wBACD,YAAY,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;wBAE1D,YAAY,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;wBACxC,YAAY,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC;4BACrC,KAAK,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,aAAa,CAAC,SAAS,EAAE;4BACvD,KAAK,EAAE,aAAa,CAAC,KAAK;yBAC1B,CAAC,CAAC;oBACJ,CAAC;gBACF,CAAC;YACF,CAAC;QACF,CAAC;QAED,MAAM,WAAW,GAEb,EAAE,CAAC;QAEP,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC;YAC/C,IAAI,YAAY,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACpC,MAAM,IAAI,YAAY,CACrB,kBAAkB,CAAC,UAAU,EAC7B,wCAAwC,EACxC;oBACC,KAAK;oBACL,KAAK,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC,MAAM;iBACjC,CACD,CAAC;YACH,CAAC;YAED,WAAW,CAAC,KAAK,CAAC,GAAG,YAAY,CAAC,KAAK,CAAC;iBACtC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC;iBACjC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QACvC,CAAC;QAED,OAAO,WAAW,CAAC;IACpB,CAAC;IAED;;;;;OAKG;IACI,MAAM,CAAC,mBAAmB,CAChC,YAA8B,EAC9B,gBAGG;QAEH,MAAM,CAAC,MAAM,CAAC,kBAAkB,CAAC,UAAU,kBAAwB,YAAY,CAAC,CAAC;QAEjF,IAAI,aAA2C,CAAC;QAEhD,IAAI,EAAE,CAAC,UAAU,CAAC,gBAAgB,CAAC,EAAE,CAAC;YACrC,aAAa,GAAG,EAAE,CAAC;YAEnB,KAAK,MAAM,OAAO,IAAI,gBAAgB,EAAE,CAAC;gBACxC,MAAM,QAAQ,GAAG,CAAC,YAAY,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;gBAC5F,IAAI,QAAQ,EAAE,CAAC;oBACd,aAAa,CAAC,IAAI,CAAC;wBAClB,QAAQ,EAAE,OAAO,CAAC,QAAQ;wBAC1B,aAAa,EAAE,OAAO,CAAC,aAAa;wBACpC,IAAI,EAAE,QAAQ,CAAC,IAAI;qBACnB,CAAC,CAAC;gBACJ,CAAC;YACF,CAAC;QACF,CAAC;aAAM,CAAC;YACP,aAAa,GAAG,kBAAkB,CAAC,iBAAiB,CAAC,YAAY,CAAC,CAAC;QACpE,CAAC;QAED,OAAO,aAAa,CAAC;IACtB,CAAC;IAED;;;;;OAKG;IACI,MAAM,CAAC,cAAc,CAAI,MAAS,EAAE,YAA8B;QACxE,MAAM,CAAC,MAAM,CAAC,kBAAkB,CAAC,UAAU,YAAkB,MAAM,CAAC,CAAC;QACrE,MAAM,CAAC,MAAM,CACZ,kBAAkB,CAAC,UAAU,kBAE7B,YAAY,CACZ,CAAC;QAEF,MAAM,UAAU,GAAG,YAAY,CAAC,UAAU,IAAI,EAAE,CAAC;QACjD,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC;YACvD,MAAM,IAAI,YAAY,CAAC,kBAAkB,CAAC,UAAU,EAAE,yBAAyB,CAAC,CAAC;QAClF,CAAC;QAED,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAEpC,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;YAC/B,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,QAAkB,CAAC,CAAC;YACrD,IAAI,GAAG,KAAK,CAAC,CAAC,EAAE,CAAC;gBAChB,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;YACxB,CAAC;YAED,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACpC,MAAM,SAAS,GAAG,OAAO,KAAK,CAAC;YAE/B,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;gBACrB,4EAA4E;gBAC5E,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;oBACpB,MAAM,IAAI,YAAY,CAAC,kBAAkB,CAAC,UAAU,EAAE,iBAAiB,EAAE;wBACxE,QAAQ,EAAE,IAAI,CAAC,QAAQ;wBACvB,IAAI,EAAE,IAAI,CAAC,IAAI;qBACf,CAAC,CAAC;gBACJ,CAAC;YACF,CAAC;iBAAM,IAAI,IAAI,CAAC,IAAI,KAAK,wBAAwB,CAAC,OAAO,IAAI,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;gBAChF,gFAAgF;YACjF,CAAC;iBAAM,IACN,IAAI,CAAC,IAAI,KAAK,wBAAwB,CAAC,MAAM;gBAC7C,CAAC,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC;oBAChB,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC;oBACf,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC;oBAChB,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC;oBAChB,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;oBACjB,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EACf,CAAC;gBACF,yGAAyG;YAC1G,CAAC;iBAAM,IAAI,IAAI,CAAC,IAAI,KAAK,wBAAwB,CAAC,KAAK,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC5E,4EAA4E;YAC7E,CAAC;iBAAM,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;gBACpC,gDAAgD;gBAChD,MAAM,IAAI,YAAY,CAAC,kBAAkB,CAAC,UAAU,EAAE,uBAAuB,EAAE;oBAC9E,KAAK;oBACL,QAAQ,EAAE,IAAI,CAAC,QAAQ;oBACvB,IAAI,EAAE,IAAI,CAAC,IAAI;iBACf,CAAC,CAAC;YACJ,CAAC;YAED,IAAI,IAAI,CAAC,IAAI,KAAK,wBAAwB,CAAC,MAAM,IAAI,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;gBACvE,8EAA8E;gBAC9E,MAAM,SAAS,GACd,IAAI,CAAC,SAAS;oBACd,CAAC,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC;wBAC3B,CAAC,CAAC,kBAAkB,CAAC,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC;wBACpD,CAAC,CAAC,SAAS,CAAC,CAAC;gBAEf,IAAI,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,SAAS,GAAG,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,SAAS,EAAE,CAAC;oBACvE,oEAAoE;oBACpE,MAAM,IAAI,YAAY,CAAC,kBAAkB,CAAC,UAAU,EAAE,mBAAmB,EAAE;wBAC1E,QAAQ,EAAE,IAAI,CAAC,QAAQ;wBACvB,SAAS;wBACT,MAAM,EAAE,KAAK,CAAC,MAAM;qBACpB,CAAC,CAAC;gBACJ,CAAC;YACF,CAAC;QACF,CAAC;QAED,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxB,0DAA0D;YAC1D,MAAM,IAAI,YAAY,CAAC,kBAAkB,CAAC,UAAU,EAAE,mBAAmB,EAAE;gBAC1E,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC;aACxB,CAAC,CAAC;QACJ,CAAC;IACF,CAAC;IAED;;;;;;OAMG;IACI,MAAM,CAAC,mBAAmB,CAAI,MAAwB;QAC5D,MAAM,CAAC,MAAM,CAAC,kBAAkB,CAAC,UAAU,YAAkB,MAAM,CAAC,CAAC;QAErE,MAAM,iBAAiB,GAAG,CAAC,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,IAAI,CAAC,CAAC;QACtF,IAAI,iBAAiB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAClC,MAAM,IAAI,YAAY,CAAC,kBAAkB,CAAC,UAAU,EAAE,2BAA2B,CAAC,CAAC;QACpF,CAAC;QACD,IAAI,iBAAiB,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACpC,IAAI,iBAAiB,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,wBAAwB,CAAC,OAAO,EAAE,CAAC;gBACpE,MAAM,IAAI,YAAY,CAAC,kBAAkB,CAAC,UAAU,EAAE,8BAA8B,EAAE;oBACrF,QAAQ,EAAE,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;oBAC/C,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,CAAC,IAAI;iBAC/B,CAAC,CAAC;YACJ,CAAC;YACD,OAAO,iBAAiB,CAAC,CAAC,CAAC,CAAC,QAA8B,CAAC;QAC5D,CAAC;QACD,OAAO,SAAS,CAAC;IAClB,CAAC","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { GeneralError, Guards, Is } from \"@twin.org/core\";\nimport { nameof } from \"@twin.org/nameof\";\nimport { DecoratorHelper } from \"./decoratorHelper.js\";\nimport { EntitySchemaPropertyFormat } from \"../models/entitySchemaPropertyFormat.js\";\nimport { EntitySchemaPropertyType } from \"../models/entitySchemaPropertyType.js\";\nimport type { IEntitySchema } from \"../models/IEntitySchema.js\";\nimport type { IEntitySchemaProperty } from \"../models/IEntitySchemaProperty.js\";\nimport type { IEntitySort } from \"../models/IEntitySort.js\";\nimport { SortDirection } from \"../models/sortDirection.js\";\n\n/**\n * Class to help with entity schema operations.\n */\nexport class EntitySchemaHelper {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<EntitySchemaHelper>();\n\n\t/**\n\t * The default maximum lengths for string properties, keyed by their format.\n\t */\n\tpublic static readonly FORMAT_MAX_LENGTHS: { [format: string]: number } = {\n\t\t[EntitySchemaPropertyFormat.Uuid]: 36,\n\t\t[EntitySchemaPropertyFormat.Date]: 64,\n\t\t[EntitySchemaPropertyFormat.Time]: 64,\n\t\t[EntitySchemaPropertyFormat.DateTime]: 64,\n\t\t[EntitySchemaPropertyFormat.Email]: 254,\n\t\t[EntitySchemaPropertyFormat.Uri]: 2048\n\t};\n\n\t/**\n\t * Get the schema for the specified object.\n\t * @param target The object to get the schema data for.\n\t * @returns The schema for the object if it can be found.\n\t */\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tpublic static getSchema<T = unknown>(target: any): IEntitySchema<T> {\n\t\treturn DecoratorHelper.getSchema<T>(target);\n\t}\n\n\t/**\n\t * Get the version of the entity schema, defaulting to 0 when absent.\n\t * This is the single source of truth for the \"absent version = v0\" convention.\n\t * When a version is present it must be a non-negative integer >= 0.\n\t * @param entitySchema The entity schema to read the version from.\n\t * @returns The declared version, or 0 if no version was set.\n\t * @throws GuardError if entitySchema is undefined or version is not an integer.\n\t * @throws GeneralError if version is present but less than 0.\n\t */\n\tpublic static getVersion(entitySchema: IEntitySchema): number {\n\t\tGuards.object<IEntitySchema>(EntitySchemaHelper.CLASS_NAME, nameof(entitySchema), entitySchema);\n\n\t\tif (!Is.empty(entitySchema?.version)) {\n\t\t\tGuards.integer(\n\t\t\t\tEntitySchemaHelper.CLASS_NAME,\n\t\t\t\tnameof(entitySchema?.version),\n\t\t\t\tentitySchema?.version\n\t\t\t);\n\t\t\tif (entitySchema.version < 0) {\n\t\t\t\tthrow new GeneralError(\n\t\t\t\t\tEntitySchemaHelper.CLASS_NAME,\n\t\t\t\t\t\"versionMustBeGreaterThanOrEqualZero\",\n\t\t\t\t\t{\n\t\t\t\t\t\tversion: entitySchema.version\n\t\t\t\t\t}\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\treturn entitySchema.version ?? 0;\n\t}\n\n\t/**\n\t * Get the primary key from the entity schema.\n\t * @param entitySchema The entity schema to find the primary key from.\n\t * @returns The key if only one was found.\n\t * @throws If no primary key was found, or more than one.\n\t */\n\tpublic static getPrimaryKey<T>(entitySchema: IEntitySchema<T>): IEntitySchemaProperty<T> {\n\t\tGuards.object<IEntitySchema<T>>(\n\t\t\tEntitySchemaHelper.CLASS_NAME,\n\t\t\tnameof(entitySchema),\n\t\t\tentitySchema\n\t\t);\n\n\t\tconst primaryKeys = (entitySchema.properties ?? [])?.filter(p => p.isPrimary);\n\t\tif (primaryKeys.length === 0) {\n\t\t\tthrow new GeneralError(EntitySchemaHelper.CLASS_NAME, \"noIsPrimary\");\n\t\t}\n\t\tif (primaryKeys.length > 1) {\n\t\t\tthrow new GeneralError(EntitySchemaHelper.CLASS_NAME, \"multipleIsPrimary\");\n\t\t}\n\t\treturn primaryKeys[0];\n\t}\n\n\t/**\n\t * Get the sort properties from the schema.\n\t * @param entitySchema The entity schema to find the primary key from.\n\t * @returns The sort keys from the schema or undefined if there are none.\n\t */\n\tpublic static getSortProperties<T>(entitySchema: IEntitySchema<T>): IEntitySort<T>[] | undefined {\n\t\tGuards.object<IEntitySchema<T>>(\n\t\t\tEntitySchemaHelper.CLASS_NAME,\n\t\t\tnameof(entitySchema),\n\t\t\tentitySchema\n\t\t);\n\n\t\tconst sortFields = (entitySchema.properties ?? []).filter(p => !Is.undefined(p.sortDirection));\n\n\t\treturn sortFields.length > 0\n\t\t\t? sortFields.map(\n\t\t\t\t\tp =>\n\t\t\t\t\t\t({\n\t\t\t\t\t\t\tproperty: p.property,\n\t\t\t\t\t\t\ttype: p.type,\n\t\t\t\t\t\t\tsortDirection: p.sortDirection\n\t\t\t\t\t\t}) as IEntitySort<T>\n\t\t\t\t)\n\t\t\t: undefined;\n\t}\n\n\t/**\n\t * Get the composite index groups from the schema.\n\t * Each property can be part of multiple indexes through its `indexGroup` list, so a property\n\t * can appear in more than one group. The properties within a group are ordered by the `index`\n\t * of their index entry, and each is returned with the sort direction it declared for that group.\n\t * @param entitySchema The entity schema to find the index groups from.\n\t * @returns The properties and their directions keyed by the group name, empty if there are no groups.\n\t * @throws GeneralError if an index entry has an invalid direction or index, or if two properties\n\t * claim the same index within the same group, or if a group contains fewer than two properties.\n\t */\n\tpublic static getIndexGroups<T>(entitySchema: IEntitySchema<T>): {\n\t\t[group: string]: { property: IEntitySchemaProperty<T>; direction: SortDirection }[];\n\t} {\n\t\tGuards.object<IEntitySchema<T>>(\n\t\t\tEntitySchemaHelper.CLASS_NAME,\n\t\t\tnameof(entitySchema),\n\t\t\tentitySchema\n\t\t);\n\n\t\tconst groupEntries: {\n\t\t\t[group: string]: {\n\t\t\t\tentry: { property: IEntitySchemaProperty<T>; direction: SortDirection };\n\t\t\t\tindex: number;\n\t\t\t}[];\n\t\t} = {};\n\t\tconst groupIndexes: { [group: string]: Set<number> } = {};\n\n\t\tfor (const property of entitySchema.properties ?? []) {\n\t\t\tif (Is.arrayValue(property.indexGroup)) {\n\t\t\t\tfor (const propertyIndex of property.indexGroup) {\n\t\t\t\t\tif (Is.stringValue(propertyIndex?.name)) {\n\t\t\t\t\t\tif (!Object.values(SortDirection).includes(propertyIndex.direction)) {\n\t\t\t\t\t\t\tthrow new GeneralError(EntitySchemaHelper.CLASS_NAME, \"invalidIndexGroupDirection\", {\n\t\t\t\t\t\t\t\tgroup: propertyIndex.name,\n\t\t\t\t\t\t\t\tdirection: propertyIndex.direction,\n\t\t\t\t\t\t\t\tproperty: property.property\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (!Is.integer(propertyIndex.index) || propertyIndex.index < 0) {\n\t\t\t\t\t\t\tthrow new GeneralError(EntitySchemaHelper.CLASS_NAME, \"invalidIndexGroupIndex\", {\n\t\t\t\t\t\t\t\tgroup: propertyIndex.name,\n\t\t\t\t\t\t\t\tindex: propertyIndex.index,\n\t\t\t\t\t\t\t\tproperty: property.property\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tgroupIndexes[propertyIndex.name] ??= new Set();\n\t\t\t\t\t\tif (groupIndexes[propertyIndex.name].has(propertyIndex.index)) {\n\t\t\t\t\t\t\tthrow new GeneralError(EntitySchemaHelper.CLASS_NAME, \"duplicateIndexGroupIndex\", {\n\t\t\t\t\t\t\t\tgroup: propertyIndex.name,\n\t\t\t\t\t\t\t\tindex: propertyIndex.index,\n\t\t\t\t\t\t\t\tproperty: property.property\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t\tgroupIndexes[propertyIndex.name].add(propertyIndex.index);\n\n\t\t\t\t\t\tgroupEntries[propertyIndex.name] ??= [];\n\t\t\t\t\t\tgroupEntries[propertyIndex.name].push({\n\t\t\t\t\t\t\tentry: { property, direction: propertyIndex.direction },\n\t\t\t\t\t\t\tindex: propertyIndex.index\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst indexGroups: {\n\t\t\t[group: string]: { property: IEntitySchemaProperty<T>; direction: SortDirection }[];\n\t\t} = {};\n\n\t\tfor (const group of Object.keys(groupEntries)) {\n\t\t\tif (groupEntries[group].length < 2) {\n\t\t\t\tthrow new GeneralError(\n\t\t\t\t\tEntitySchemaHelper.CLASS_NAME,\n\t\t\t\t\t\"indexGroupMustHaveAtLeastTwoProperties\",\n\t\t\t\t\t{\n\t\t\t\t\t\tgroup,\n\t\t\t\t\t\tcount: groupEntries[group].length\n\t\t\t\t\t}\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tindexGroups[group] = groupEntries[group]\n\t\t\t\t.sort((a, b) => a.index - b.index)\n\t\t\t\t.map(groupEntry => groupEntry.entry);\n\t\t}\n\n\t\treturn indexGroups;\n\t}\n\n\t/**\n\t * Build sort properties from the schema and override if necessary.\n\t * @param entitySchema The entity schema to retrieve the default sort keys.\n\t * @param overrideSortKeys The override sort keys.\n\t * @returns The finalised sort keys.\n\t */\n\tpublic static buildSortProperties<T>(\n\t\tentitySchema: IEntitySchema<T>,\n\t\toverrideSortKeys?: {\n\t\t\tproperty: keyof T;\n\t\t\tsortDirection: SortDirection;\n\t\t}[]\n\t): IEntitySort<T>[] | undefined {\n\t\tGuards.object(EntitySchemaHelper.CLASS_NAME, nameof(entitySchema), entitySchema);\n\n\t\tlet finalSortKeys: IEntitySort<T>[] | undefined;\n\n\t\tif (Is.arrayValue(overrideSortKeys)) {\n\t\t\tfinalSortKeys = [];\n\n\t\t\tfor (const sortKey of overrideSortKeys) {\n\t\t\t\tconst property = (entitySchema.properties ?? []).find(p => p.property === sortKey.property);\n\t\t\t\tif (property) {\n\t\t\t\t\tfinalSortKeys.push({\n\t\t\t\t\t\tproperty: sortKey.property,\n\t\t\t\t\t\tsortDirection: sortKey.sortDirection,\n\t\t\t\t\t\ttype: property.type\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tfinalSortKeys = EntitySchemaHelper.getSortProperties(entitySchema);\n\t\t}\n\n\t\treturn finalSortKeys;\n\t}\n\n\t/**\n\t * Validate the entity against the schema.\n\t * @param entity The entity to validate.\n\t * @param entitySchema The schema to validate against.\n\t * @throws If the entity is invalid, or a string value exceeds its maxLength.\n\t */\n\tpublic static validateEntity<T>(entity: T, entitySchema: IEntitySchema<T>): void {\n\t\tGuards.object(EntitySchemaHelper.CLASS_NAME, nameof(entity), entity);\n\t\tGuards.object<IEntitySchema<T>>(\n\t\t\tEntitySchemaHelper.CLASS_NAME,\n\t\t\tnameof(entitySchema),\n\t\t\tentitySchema\n\t\t);\n\n\t\tconst properties = entitySchema.properties ?? [];\n\t\tif (properties.length === 0 && Is.objectValue(entity)) {\n\t\t\tthrow new GeneralError(EntitySchemaHelper.CLASS_NAME, \"invalidEntityProperties\");\n\t\t}\n\n\t\tconst allKeys = Object.keys(entity);\n\n\t\tfor (const prop of properties) {\n\t\t\tconst idx = allKeys.indexOf(prop.property as string);\n\t\t\tif (idx !== -1) {\n\t\t\t\tallKeys.splice(idx, 1);\n\t\t\t}\n\n\t\t\tconst value = entity[prop.property];\n\t\t\tconst valueType = typeof value;\n\n\t\t\tif (Is.empty(value)) {\n\t\t\t\t// If the value is empty but the property is not optional, then it's invalid\n\t\t\t\tif (!prop.optional) {\n\t\t\t\t\tthrow new GeneralError(EntitySchemaHelper.CLASS_NAME, \"invalidOptional\", {\n\t\t\t\t\t\tproperty: prop.property,\n\t\t\t\t\t\ttype: prop.type\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} else if (prop.type === EntitySchemaPropertyType.Integer && Is.integer(value)) {\n\t\t\t\t// If the schema expects an integer and the value is an integer, then it's valid\n\t\t\t} else if (\n\t\t\t\tprop.type === EntitySchemaPropertyType.Object &&\n\t\t\t\t(Is.object(value) ||\n\t\t\t\t\tIs.array(value) ||\n\t\t\t\t\tIs.string(value) ||\n\t\t\t\t\tIs.number(value) ||\n\t\t\t\t\tIs.boolean(value) ||\n\t\t\t\t\tIs.null(value))\n\t\t\t) {\n\t\t\t\t// If the schema expects an object and the value is anything that can be JSON serialised, then it's valid\n\t\t\t} else if (prop.type === EntitySchemaPropertyType.Array && Is.array(value)) {\n\t\t\t\t// If the schema expects an array and the value is an array, then it's valid\n\t\t\t} else if (prop.type !== valueType) {\n\t\t\t\t// The schema type does not match the value type\n\t\t\t\tthrow new GeneralError(EntitySchemaHelper.CLASS_NAME, \"invalidEntityProperty\", {\n\t\t\t\t\tvalue,\n\t\t\t\t\tproperty: prop.property,\n\t\t\t\t\ttype: prop.type\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (prop.type === EntitySchemaPropertyType.String && Is.string(value)) {\n\t\t\t\t// Formats with a known length default to it when no explicit maxLength is set\n\t\t\t\tconst maxLength =\n\t\t\t\t\tprop.maxLength ??\n\t\t\t\t\t(Is.stringValue(prop.format)\n\t\t\t\t\t\t? EntitySchemaHelper.FORMAT_MAX_LENGTHS[prop.format]\n\t\t\t\t\t\t: undefined);\n\n\t\t\t\tif (Is.number(maxLength) && maxLength > 0 && value.length > maxLength) {\n\t\t\t\t\t// The value is longer than the maximum length defined in the schema\n\t\t\t\t\tthrow new GeneralError(EntitySchemaHelper.CLASS_NAME, \"maxLengthExceeded\", {\n\t\t\t\t\t\tproperty: prop.property,\n\t\t\t\t\t\tmaxLength,\n\t\t\t\t\t\tlength: value.length\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (allKeys.length > 0) {\n\t\t\t// There are keys in the entity that are not in the schema\n\t\t\tthrow new GeneralError(EntitySchemaHelper.CLASS_NAME, \"invalidEntityKeys\", {\n\t\t\t\tkeys: allKeys.join(\", \")\n\t\t\t});\n\t\t}\n\t}\n\n\t/**\n\t * Find the property in the schema that is marked as the optimistic-lock version token.\n\t * @param schema The entity schema to search.\n\t * @returns The name of the version property, or undefined if none is declared.\n\t * @throws GeneralError if more than one property has isVersion set.\n\t * @throws GeneralError if the version property type is not integer.\n\t */\n\tpublic static findVersionProperty<T>(schema: IEntitySchema<T>): string | undefined {\n\t\tGuards.object(EntitySchemaHelper.CLASS_NAME, nameof(schema), schema);\n\n\t\tconst versionProperties = (schema.properties ?? []).filter(p => p.isVersion === true);\n\t\tif (versionProperties.length > 1) {\n\t\t\tthrow new GeneralError(EntitySchemaHelper.CLASS_NAME, \"multipleVersionProperties\");\n\t\t}\n\t\tif (versionProperties.length === 1) {\n\t\t\tif (versionProperties[0].type !== EntitySchemaPropertyType.Integer) {\n\t\t\t\tthrow new GeneralError(EntitySchemaHelper.CLASS_NAME, \"versionPropertyMustBeInteger\", {\n\t\t\t\t\tproperty: String(versionProperties[0].property),\n\t\t\t\t\ttype: versionProperties[0].type\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn versionProperties[0].property as string | undefined;\n\t\t}\n\t\treturn undefined;\n\t}\n}\n"]}
@@ -5,5 +5,7 @@ import type { IEntitySchemaProperty } from "../models/IEntitySchemaProperty.js";
5
5
  * Decorator to produce schema property data for entities.
6
6
  * @param options The options for the property.
7
7
  * @returns The property decorator.
8
+ * @throws GeneralError if an index group is declared on an object or array property.
9
+ * @throws GeneralError if the same index group name is declared more than once for the property.
8
10
  */
9
11
  export declare function property(options: Omit<IEntitySchemaProperty, "property">): any;
@@ -10,6 +10,7 @@ export * from "./models/IComparatorGroup.js";
10
10
  export * from "./models/IEntitySchema.js";
11
11
  export * from "./models/IEntitySchemaOptions.js";
12
12
  export * from "./models/IEntitySchemaProperty.js";
13
+ export * from "./models/IEntitySchemaPropertyIndex.js";
13
14
  export * from "./models/IEntitySort.js";
14
15
  export * from "./models/logicalOperator.js";
15
16
  export * from "./models/sortDirection.js";
@@ -1,5 +1,6 @@
1
1
  import type { EntitySchemaPropertyFormat } from "./entitySchemaPropertyFormat.js";
2
2
  import type { EntitySchemaPropertyType } from "./entitySchemaPropertyType.js";
3
+ import type { IEntitySchemaPropertyIndex } from "./IEntitySchemaPropertyIndex.js";
3
4
  import type { SortDirection } from "./sortDirection.js";
4
5
  /**
5
6
  * Definition for an entity schema property.
@@ -17,6 +18,10 @@ export interface IEntitySchemaProperty<T = unknown> {
17
18
  * The format of the property.
18
19
  */
19
20
  format?: EntitySchemaPropertyFormat;
21
+ /**
22
+ * The maximum length of the property value i.e. for text fields.
23
+ */
24
+ maxLength?: number;
20
25
  /**
21
26
  * Is this the primary index property.
22
27
  */
@@ -25,6 +30,12 @@ export interface IEntitySchemaProperty<T = unknown> {
25
30
  * Is this a secondary index property.
26
31
  */
27
32
  isSecondary?: boolean;
33
+ /**
34
+ * The composite indexes this property is part of.
35
+ * Connectors can use these to build a composite index for each group name,
36
+ * combining all the properties which share that name, ordered by their index.
37
+ */
38
+ indexGroup?: IEntitySchemaPropertyIndex[];
28
39
  /**
29
40
  * Is this property used as the optimistic-lock version token.
30
41
  * When true, connectors automatically manage the field: the value is
@@ -0,0 +1,18 @@
1
+ import type { SortDirection } from "./sortDirection.js";
2
+ /**
3
+ * Definition of a composite index that a property is part of.
4
+ */
5
+ export interface IEntitySchemaPropertyIndex {
6
+ /**
7
+ * The name of the composite index group.
8
+ */
9
+ name: string;
10
+ /**
11
+ * The sort direction for the property within the index.
12
+ */
13
+ direction: SortDirection;
14
+ /**
15
+ * The position of the property within the index, ordered ascending.
16
+ */
17
+ index: number;
18
+ }
@@ -11,7 +11,7 @@ export declare class EntitySchemaDiffHelper {
11
11
  /**
12
12
  * Compare two arrays of entity schema properties and return a structured diff.
13
13
  *
14
- * Properties are matched by their `property` key name. A property is considered modified when any structural field differs: `type`, `format`, `isPrimary`, `isSecondary`, `isVersion`, `sortDirection`, `optional`, `itemType`, or `itemTypeRef`.
14
+ * Properties are matched by their `property` key name. A property is considered modified when any structural field differs: `type`, `format`, `isPrimary`, `isSecondary`, `indexGroup`, `isVersion`, `sortDirection`, `optional`, `itemType`, or `itemTypeRef`.
15
15
  * Documentation-only fields (`description`, `examples`) are intentionally excluded from the comparison to avoid spurious diffs.
16
16
  *
17
17
  * Because a pure name change cannot be detected automatically, callers may supply a `renames` list mapping old names to new names. Renamed properties appear in `modified` (never in `added` or `removed`) even when no other fields changed. Rename lookups take priority over direct same-name matches, which allows swap renames to work correctly and prevents a renamed source from silently disappearing when the target name already existed in the old schema. Self-renames (`from === to`) are ignored and the property is classified normally.
@@ -1,7 +1,7 @@
1
1
  import type { IEntitySchema } from "../models/IEntitySchema.js";
2
2
  import type { IEntitySchemaProperty } from "../models/IEntitySchemaProperty.js";
3
3
  import type { IEntitySort } from "../models/IEntitySort.js";
4
- import type { SortDirection } from "../models/sortDirection.js";
4
+ import { SortDirection } from "../models/sortDirection.js";
5
5
  /**
6
6
  * Class to help with entity schema operations.
7
7
  */
@@ -10,6 +10,12 @@ export declare class EntitySchemaHelper {
10
10
  * Runtime name for the class.
11
11
  */
12
12
  static readonly CLASS_NAME: string;
13
+ /**
14
+ * The default maximum lengths for string properties, keyed by their format.
15
+ */
16
+ static readonly FORMAT_MAX_LENGTHS: {
17
+ [format: string]: number;
18
+ };
13
19
  /**
14
20
  * Get the schema for the specified object.
15
21
  * @param target The object to get the schema data for.
@@ -39,6 +45,22 @@ export declare class EntitySchemaHelper {
39
45
  * @returns The sort keys from the schema or undefined if there are none.
40
46
  */
41
47
  static getSortProperties<T>(entitySchema: IEntitySchema<T>): IEntitySort<T>[] | undefined;
48
+ /**
49
+ * Get the composite index groups from the schema.
50
+ * Each property can be part of multiple indexes through its `indexGroup` list, so a property
51
+ * can appear in more than one group. The properties within a group are ordered by the `index`
52
+ * of their index entry, and each is returned with the sort direction it declared for that group.
53
+ * @param entitySchema The entity schema to find the index groups from.
54
+ * @returns The properties and their directions keyed by the group name, empty if there are no groups.
55
+ * @throws GeneralError if an index entry has an invalid direction or index, or if two properties
56
+ * claim the same index within the same group, or if a group contains fewer than two properties.
57
+ */
58
+ static getIndexGroups<T>(entitySchema: IEntitySchema<T>): {
59
+ [group: string]: {
60
+ property: IEntitySchemaProperty<T>;
61
+ direction: SortDirection;
62
+ }[];
63
+ };
42
64
  /**
43
65
  * Build sort properties from the schema and override if necessary.
44
66
  * @param entitySchema The entity schema to retrieve the default sort keys.
@@ -53,7 +75,7 @@ export declare class EntitySchemaHelper {
53
75
  * Validate the entity against the schema.
54
76
  * @param entity The entity to validate.
55
77
  * @param entitySchema The schema to validate against.
56
- * @throws If the entity is invalid.
78
+ * @throws If the entity is invalid, or a string value exceeds its maxLength.
57
79
  */
58
80
  static validateEntity<T>(entity: T, entitySchema: IEntitySchema<T>): void;
59
81
  /**
package/docs/changelog.md CHANGED
@@ -1,5 +1,125 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.10.1-next.1](https://github.com/iotaledger/twin-framework/compare/entity-v0.10.1-next.0...entity-v0.10.1-next.1) (2026-09-17)
4
+
5
+
6
+ ### Features
7
+
8
+ * add IEntitySchemaDiff and entitySchemaDiff utility ([#282](https://github.com/iotaledger/twin-framework/issues/282)) ([9d63e94](https://github.com/iotaledger/twin-framework/commit/9d63e94021ee2ffc138004ee68cf53d08a6b17f9))
9
+ * add support for null in EntityConditions.compare ([922c4ba](https://github.com/iotaledger/twin-framework/commit/922c4ba8af578b4e7eaaf21b3c37a9d788941487))
10
+ * add version field to IEntitySchema and EntitySchemaHelper.getVersion for migration support ([#346](https://github.com/iotaledger/twin-framework/issues/346)) ([e74557e](https://github.com/iotaledger/twin-framework/commit/e74557e4ccbda5b9971f4cfcd0852ba5957cead0))
11
+ * composite indexes ([b506d95](https://github.com/iotaledger/twin-framework/commit/b506d957a5eff6ff6012be773e98af36e750be4a))
12
+ * composite indexes ([#509](https://github.com/iotaledger/twin-framework/issues/509)) ([44b3327](https://github.com/iotaledger/twin-framework/commit/44b33270e6c8dabcc81cc4aa8d27a69255a5e7d4))
13
+ * entity max string lengths ([#497](https://github.com/iotaledger/twin-framework/issues/497)) ([7fc91e2](https://github.com/iotaledger/twin-framework/commit/7fc91e2a49b9f1e2113bfe10983e083298ef2539))
14
+ * entity schema decorators default value ([33397c2](https://github.com/iotaledger/twin-framework/commit/33397c2e24978a91257371a4c63ce7f6a7125d0c))
15
+ * entity schema diff updates ([#294](https://github.com/iotaledger/twin-framework/issues/294)) ([7a7a94d](https://github.com/iotaledger/twin-framework/commit/7a7a94d14ea5e785dd68fd6de1c5a84941721d28))
16
+ * linting and dependency update ([676b4e9](https://github.com/iotaledger/twin-framework/commit/676b4e9d9bce158065200bbf875bb31da81d166d))
17
+ * optimistic locking version property for schemas ([#443](https://github.com/iotaledger/twin-framework/issues/443)) ([1b90987](https://github.com/iotaledger/twin-framework/commit/1b909875dc915a0ec133d8424d013ae7e4ddfb48))
18
+ * support for object comparisons in entity conditions ([edae91d](https://github.com/iotaledger/twin-framework/commit/edae91d3205524080188a35e0ab04da036fa4f39))
19
+ * typescript 6 update ([1d10f31](https://github.com/iotaledger/twin-framework/commit/1d10f31e6516ec622773f45e88af82fe749b384a))
20
+ * update dependencies ([4da77ab](https://github.com/iotaledger/twin-framework/commit/4da77ab30f499e52825ac5a76f51436ceb59c26e))
21
+
22
+
23
+ ### Bug Fixes
24
+
25
+ * allow vacuous truth to match SQL semantics ([5bbc5e5](https://github.com/iotaledger/twin-framework/commit/5bbc5e53069f90bd39485a74a284eda9e5864e66))
26
+ * coerce and includes ([#429](https://github.com/iotaledger/twin-framework/issues/429)) ([98c9132](https://github.com/iotaledger/twin-framework/commit/98c9132316c0393ede9d6e634bbae9e5ece2d6b9))
27
+ * ensure __decorate is defined for decorators ([103a563](https://github.com/iotaledger/twin-framework/commit/103a563ce01ebdef6240d2e590e7b026e8692684))
28
+ * update copyright year ([#260](https://github.com/iotaledger/twin-framework/issues/260)) ([c4ad930](https://github.com/iotaledger/twin-framework/commit/c4ad930fcc84ba6b5447a8074574329870b4c3f5))
29
+ * vacuous entity condition ([#385](https://github.com/iotaledger/twin-framework/issues/385)) ([e1082bf](https://github.com/iotaledger/twin-framework/commit/e1082bff4a66ac270efc9c6654d974c19f889d2d))
30
+
31
+
32
+ ### Dependencies
33
+
34
+ * The following workspace dependencies were updated
35
+ * dependencies
36
+ * @twin.org/core bumped from 0.10.1-next.0 to 0.10.1-next.1
37
+ * @twin.org/nameof bumped from 0.10.1-next.0 to 0.10.1-next.1
38
+ * devDependencies
39
+ * @twin.org/nameof-transformer bumped from 0.10.1-next.0 to 0.10.1-next.1
40
+ * @twin.org/nameof-vitest-plugin bumped from 0.10.1-next.0 to 0.10.1-next.1
41
+ * @twin.org/validate-locales bumped from 0.10.1-next.0 to 0.10.1-next.1
42
+
43
+ ## [0.10.0](https://github.com/iotaledger/twin-framework/compare/entity-v0.10.0...entity-v0.10.0) (2026-09-16)
44
+
45
+
46
+ ### Features
47
+
48
+ * release to production ([b24cba1](https://github.com/iotaledger/twin-framework/commit/b24cba1b6a969278d638e632590602ec881e49fb))
49
+ * release to production ([787287d](https://github.com/iotaledger/twin-framework/commit/787287d06ea8319657401589d61fff369310c422))
50
+ * release to production ([53f4843](https://github.com/iotaledger/twin-framework/commit/53f484326b2851d7a506d2620db24c4a65cee7b3))
51
+ * release to production ([56cda4d](https://github.com/iotaledger/twin-framework/commit/56cda4da93e978c5be19ec7cfd421ae2a7fe4147))
52
+ * release to production ([f7c6586](https://github.com/iotaledger/twin-framework/commit/f7c6586f6976b903b647b4c5ac5ad9421e0c9051))
53
+ * release to production ([829d53d](https://github.com/iotaledger/twin-framework/commit/829d53d3953b1e1b40b0243c04cfdfd3842aac7b))
54
+ * release to production ([5cf3a76](https://github.com/iotaledger/twin-framework/commit/5cf3a76a09eff2e6414d0cba846c7c37400a11d6))
55
+ * release to production ([#330](https://github.com/iotaledger/twin-framework/issues/330)) ([d73f565](https://github.com/iotaledger/twin-framework/commit/d73f565588d156d23ef49b2a5718973756f7a696))
56
+ * release to production ([#382](https://github.com/iotaledger/twin-framework/issues/382)) ([bbed01a](https://github.com/iotaledger/twin-framework/commit/bbed01a605ee9724bda77a0f7feab249118c2d90))
57
+ * release to production ([#417](https://github.com/iotaledger/twin-framework/issues/417)) ([59727e7](https://github.com/iotaledger/twin-framework/commit/59727e73903a137310ca48fe469189cf29879cb9))
58
+ * release to production ([#459](https://github.com/iotaledger/twin-framework/issues/459)) ([e26e2d9](https://github.com/iotaledger/twin-framework/commit/e26e2d9a88767364c32494c45232033447b26e22))
59
+ * release to production ([#504](https://github.com/iotaledger/twin-framework/issues/504)) ([cfde07f](https://github.com/iotaledger/twin-framework/commit/cfde07f7790c62269e71144b95243ec441df38ba))
60
+
61
+
62
+ ### Miscellaneous Chores
63
+
64
+ * release to production ([63cae24](https://github.com/iotaledger/twin-framework/commit/63cae2401f6c11f93b2a01260b665064e8bd28e0))
65
+
66
+ ## [0.9.3-next.12](https://github.com/iotaledger/twin-framework/compare/entity-v0.9.3-next.11...entity-v0.9.3-next.12) (2026-09-14)
67
+
68
+
69
+ ### Features
70
+
71
+ * entity max string lengths ([#497](https://github.com/iotaledger/twin-framework/issues/497)) ([7fc91e2](https://github.com/iotaledger/twin-framework/commit/7fc91e2a49b9f1e2113bfe10983e083298ef2539))
72
+
73
+
74
+ ### Dependencies
75
+
76
+ * The following workspace dependencies were updated
77
+ * dependencies
78
+ * @twin.org/core bumped from 0.9.3-next.11 to 0.9.3-next.12
79
+ * @twin.org/nameof bumped from 0.9.3-next.11 to 0.9.3-next.12
80
+ * devDependencies
81
+ * @twin.org/nameof-transformer bumped from 0.9.3-next.11 to 0.9.3-next.12
82
+ * @twin.org/nameof-vitest-plugin bumped from 0.9.3-next.11 to 0.9.3-next.12
83
+ * @twin.org/validate-locales bumped from 0.9.3-next.11 to 0.9.3-next.12
84
+
85
+ ## [0.9.3-next.11](https://github.com/iotaledger/twin-framework/compare/entity-v0.9.3-next.10...entity-v0.9.3-next.11) (2026-09-10)
86
+
87
+
88
+ ### Miscellaneous Chores
89
+
90
+ * **entity:** Synchronize repo versions
91
+
92
+
93
+ ### Dependencies
94
+
95
+ * The following workspace dependencies were updated
96
+ * dependencies
97
+ * @twin.org/core bumped from 0.9.3-next.10 to 0.9.3-next.11
98
+ * @twin.org/nameof bumped from 0.9.3-next.10 to 0.9.3-next.11
99
+ * devDependencies
100
+ * @twin.org/nameof-transformer bumped from 0.9.3-next.10 to 0.9.3-next.11
101
+ * @twin.org/nameof-vitest-plugin bumped from 0.9.3-next.10 to 0.9.3-next.11
102
+ * @twin.org/validate-locales bumped from 0.9.3-next.10 to 0.9.3-next.11
103
+
104
+ ## [0.9.3-next.10](https://github.com/iotaledger/twin-framework/compare/entity-v0.9.3-next.9...entity-v0.9.3-next.10) (2026-09-10)
105
+
106
+
107
+ ### Miscellaneous Chores
108
+
109
+ * **entity:** Synchronize repo versions
110
+
111
+
112
+ ### Dependencies
113
+
114
+ * The following workspace dependencies were updated
115
+ * dependencies
116
+ * @twin.org/core bumped from 0.9.3-next.9 to 0.9.3-next.10
117
+ * @twin.org/nameof bumped from 0.9.3-next.9 to 0.9.3-next.10
118
+ * devDependencies
119
+ * @twin.org/nameof-transformer bumped from 0.9.3-next.9 to 0.9.3-next.10
120
+ * @twin.org/nameof-vitest-plugin bumped from 0.9.3-next.9 to 0.9.3-next.10
121
+ * @twin.org/validate-locales bumped from 0.9.3-next.9 to 0.9.3-next.10
122
+
3
123
  ## [0.9.3-next.9](https://github.com/iotaledger/twin-framework/compare/entity-v0.9.3-next.8...entity-v0.9.3-next.9) (2026-09-10)
4
124
 
5
125
 
@@ -28,7 +28,7 @@ Runtime name for the class.
28
28
 
29
29
  Compare two arrays of entity schema properties and return a structured diff.
30
30
 
31
- Properties are matched by their `property` key name. A property is considered modified when any structural field differs: `type`, `format`, `isPrimary`, `isSecondary`, `isVersion`, `sortDirection`, `optional`, `itemType`, or `itemTypeRef`.
31
+ Properties are matched by their `property` key name. A property is considered modified when any structural field differs: `type`, `format`, `isPrimary`, `isSecondary`, `indexGroup`, `isVersion`, `sortDirection`, `optional`, `itemType`, or `itemTypeRef`.
32
32
  Documentation-only fields (`description`, `examples`) are intentionally excluded from the comparison to avoid spurious diffs.
33
33
 
34
34
  Because a pure name change cannot be detected automatically, callers may supply a `renames` list mapping old names to new names. Renamed properties appear in `modified` (never in `added` or `removed`) even when no other fields changed. Rename lookups take priority over direct same-name matches, which allows swap renames to work correctly and prevents a renamed source from silently disappearing when the target name already existed in the old schema. Self-renames (`from === to`) are ignored and the property is classified normally.
@@ -20,6 +20,18 @@ Class to help with entity schema operations.
20
20
 
21
21
  Runtime name for the class.
22
22
 
23
+ ***
24
+
25
+ ### FORMAT\_MAX\_LENGTHS {#format_max_lengths}
26
+
27
+ > `readonly` `static` **FORMAT\_MAX\_LENGTHS**: `object`
28
+
29
+ The default maximum lengths for string properties, keyed by their format.
30
+
31
+ #### Index Signature
32
+
33
+ \[`format`: `string`\]: `number`
34
+
23
35
  ## Methods
24
36
 
25
37
  ### getSchema() {#getschema}
@@ -142,6 +154,42 @@ The sort keys from the schema or undefined if there are none.
142
154
 
143
155
  ***
144
156
 
157
+ ### getIndexGroups() {#getindexgroups}
158
+
159
+ > `static` **getIndexGroups**\<`T`\>(`entitySchema`): `object`
160
+
161
+ Get the composite index groups from the schema.
162
+ Each property can be part of multiple indexes through its `indexGroup` list, so a property
163
+ can appear in more than one group. The properties within a group are ordered by the `index`
164
+ of their index entry, and each is returned with the sort direction it declared for that group.
165
+
166
+ #### Type Parameters
167
+
168
+ ##### T
169
+
170
+ `T`
171
+
172
+ #### Parameters
173
+
174
+ ##### entitySchema
175
+
176
+ [`IEntitySchema`](../interfaces/IEntitySchema.md)\<`T`\>
177
+
178
+ The entity schema to find the index groups from.
179
+
180
+ #### Returns
181
+
182
+ `object`
183
+
184
+ The properties and their directions keyed by the group name, empty if there are no groups.
185
+
186
+ #### Throws
187
+
188
+ GeneralError if an index entry has an invalid direction or index, or if two properties
189
+ claim the same index within the same group, or if a group contains fewer than two properties.
190
+
191
+ ***
192
+
145
193
  ### buildSortProperties() {#buildsortproperties}
146
194
 
147
195
  > `static` **buildSortProperties**\<`T`\>(`entitySchema`, `overrideSortKeys?`): [`IEntitySort`](../interfaces/IEntitySort.md)\<`T`\>[] \| `undefined`
@@ -208,7 +256,7 @@ The schema to validate against.
208
256
 
209
257
  #### Throws
210
258
 
211
- If the entity is invalid.
259
+ If the entity is invalid, or a string value exceeds its maxLength.
212
260
 
213
261
  ***
214
262
 
@@ -17,3 +17,11 @@ The options for the property.
17
17
  `any`
18
18
 
19
19
  The property decorator.
20
+
21
+ ## Throws
22
+
23
+ GeneralError if an index group is declared on an object or array property.
24
+
25
+ ## Throws
26
+
27
+ GeneralError if the same index group name is declared more than once for the property.
@@ -16,6 +16,7 @@
16
16
  - [IEntitySchemaDiff](interfaces/IEntitySchemaDiff.md)
17
17
  - [IEntitySchemaOptions](interfaces/IEntitySchemaOptions.md)
18
18
  - [IEntitySchemaProperty](interfaces/IEntitySchemaProperty.md)
19
+ - [IEntitySchemaPropertyIndex](interfaces/IEntitySchemaPropertyIndex.md)
19
20
  - [IEntitySort](interfaces/IEntitySort.md)
20
21
 
21
22
  ## Type Aliases
@@ -34,6 +34,14 @@ The format of the property.
34
34
 
35
35
  ***
36
36
 
37
+ ### maxLength? {#maxlength}
38
+
39
+ > `optional` **maxLength?**: `number`
40
+
41
+ The maximum length of the property value i.e. for text fields.
42
+
43
+ ***
44
+
37
45
  ### isPrimary? {#isprimary}
38
46
 
39
47
  > `optional` **isPrimary?**: `boolean`
@@ -50,6 +58,16 @@ Is this a secondary index property.
50
58
 
51
59
  ***
52
60
 
61
+ ### indexGroup? {#indexgroup}
62
+
63
+ > `optional` **indexGroup?**: [`IEntitySchemaPropertyIndex`](IEntitySchemaPropertyIndex.md)[]
64
+
65
+ The composite indexes this property is part of.
66
+ Connectors can use these to build a composite index for each group name,
67
+ combining all the properties which share that name, ordered by their index.
68
+
69
+ ***
70
+
53
71
  ### isVersion? {#isversion}
54
72
 
55
73
  > `optional` **isVersion?**: `boolean`
@@ -0,0 +1,27 @@
1
+ # Interface: IEntitySchemaPropertyIndex
2
+
3
+ Definition of a composite index that a property is part of.
4
+
5
+ ## Properties
6
+
7
+ ### name {#name}
8
+
9
+ > **name**: `string`
10
+
11
+ The name of the composite index group.
12
+
13
+ ***
14
+
15
+ ### direction {#direction}
16
+
17
+ > **direction**: [`SortDirection`](../type-aliases/SortDirection.md)
18
+
19
+ The sort direction for the property within the index.
20
+
21
+ ***
22
+
23
+ ### index {#index}
24
+
25
+ > **index**: `number`
26
+
27
+ The position of the property within the index, ordered ascending.
package/locales/en.json CHANGED
@@ -4,6 +4,10 @@
4
4
  "duplicateOldProperty": "The oldProperties array contains a duplicate property key \"{property}\"",
5
5
  "duplicateNewProperty": "The newProperties array contains a duplicate property key \"{property}\""
6
6
  },
7
+ "propertyDecorator": {
8
+ "indexGroupTypeNotSupported": "The property \"{property}\" of type \"{type}\" cannot be part of an index group",
9
+ "duplicateIndexGroup": "The property \"{property}\" declares the index group \"{group}\" more than once"
10
+ },
7
11
  "entitySchemaHelper": {
8
12
  "noIsPrimary": "Property \"entitySchema.properties\" must contain a value with isPrimary set",
9
13
  "multipleIsPrimary": "Property \"entitySchema.properties\" contains more than one property with isPrimary set",
@@ -11,8 +15,13 @@
11
15
  "invalidEntityProperty": "The entity value of \"{value}\" does not match the type \"{type}\" for property \"{property}\"",
12
16
  "invalidOptional": "The entity property \"{property}\" of type \"{type}\" is not optional, but no value has been provided",
13
17
  "invalidEntityKeys": "The entity had additional properties that are not in the schema, \"{keys}\"",
18
+ "maxLengthExceeded": "The entity property \"{property}\" has a length of {length} which exceeds the maximum length of {maxLength}",
14
19
  "versionMustBeGreaterThanOrEqualZero": "Property \"entitySchema.version\" must be an integer >= 0, but got {version}",
15
20
  "multipleVersionProperties": "The schema has more than one property with isVersion set, only one is allowed",
21
+ "invalidIndexGroupDirection": "The index group \"{group}\" on the property \"{property}\" has the direction \"{direction}\" which is not a valid sort direction",
22
+ "invalidIndexGroupIndex": "The index group \"{group}\" on the property \"{property}\" has the index {index} which must be an integer >= 0",
23
+ "duplicateIndexGroupIndex": "The index group \"{group}\" has more than one property using the index {index}, found again on the property \"{property}\"",
24
+ "indexGroupMustHaveAtLeastTwoProperties": "The index group \"{group}\" must contain at least two properties, but contains {count}",
16
25
  "versionPropertyMustBeInteger": "The version property \"{property}\" has type \"{type}\" but must be of type integer"
17
26
  }
18
27
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twin.org/entity",
3
- "version": "0.9.3-next.9",
3
+ "version": "0.10.1-next.1",
4
4
  "description": "Helpers for defining and working with entities",
5
5
  "repository": {
6
6
  "type": "git",
@@ -14,8 +14,8 @@
14
14
  "node": ">=24.0.0"
15
15
  },
16
16
  "dependencies": {
17
- "@twin.org/core": "0.9.3-next.9",
18
- "@twin.org/nameof": "0.9.3-next.9",
17
+ "@twin.org/core": "0.10.1-next.1",
18
+ "@twin.org/nameof": "0.10.1-next.1",
19
19
  "reflect-metadata": "0.2.2",
20
20
  "tslib": "2.8.1"
21
21
  },