diff --git a/src/app/child-dev-project/attendance/model/event-attendance.ts b/src/app/child-dev-project/attendance/model/event-attendance.ts index 56b7ada164..022e16fb9c 100644 --- a/src/app/child-dev-project/attendance/model/event-attendance.ts +++ b/src/app/child-dev-project/attendance/model/event-attendance.ts @@ -13,7 +13,7 @@ export class EventAttendance { private _status: AttendanceStatusType; @DatabaseField({ dataType: "configurable-enum", - innerDataType: ATTENDANCE_STATUS_CONFIG_ID, + additional: ATTENDANCE_STATUS_CONFIG_ID, }) get status(): AttendanceStatusType { return this._status; diff --git a/src/app/child-dev-project/attendance/model/recurring-activity.ts b/src/app/child-dev-project/attendance/model/recurring-activity.ts index 32e3c0d169..8dcdff8f22 100644 --- a/src/app/child-dev-project/attendance/model/recurring-activity.ts +++ b/src/app/child-dev-project/attendance/model/recurring-activity.ts @@ -66,7 +66,7 @@ export class RecurringActivity extends Entity { @DatabaseField({ label: $localize`:Label for the interaction type of a recurring activity:Type`, dataType: "configurable-enum", - innerDataType: INTERACTION_TYPE_CONFIG_ID, + additional: INTERACTION_TYPE_CONFIG_ID, }) type: InteractionType; diff --git a/src/app/child-dev-project/children/aser/model/aser.ts b/src/app/child-dev-project/children/aser/model/aser.ts index 19ecb55a87..fd10ce74c3 100644 --- a/src/app/child-dev-project/children/aser/model/aser.ts +++ b/src/app/child-dev-project/children/aser/model/aser.ts @@ -45,25 +45,25 @@ export class Aser extends Entity { @DatabaseField({ label: $localize`:Label of the Hindi ASER result:Hindi`, dataType: "configurable-enum", - innerDataType: "reading-levels", + additional: "reading-levels", }) hindi: SkillLevel; @DatabaseField({ label: $localize`:Label of the Bengali ASER result:Bengali`, dataType: "configurable-enum", - innerDataType: "reading-levels", + additional: "reading-levels", }) bengali: SkillLevel; @DatabaseField({ label: $localize`:Label of the English ASER result:English`, dataType: "configurable-enum", - innerDataType: "reading-levels", + additional: "reading-levels", }) english: SkillLevel; @DatabaseField({ label: $localize`:Label of the Math ASER result:Math`, dataType: "configurable-enum", - innerDataType: "math-levels", + additional: "math-levels", }) math: SkillLevel; diff --git a/src/app/child-dev-project/children/educational-material/model/educational-material.ts b/src/app/child-dev-project/children/educational-material/model/educational-material.ts index 66d0991cfb..ffdc8e7510 100644 --- a/src/app/child-dev-project/children/educational-material/model/educational-material.ts +++ b/src/app/child-dev-project/children/educational-material/model/educational-material.ts @@ -34,7 +34,7 @@ export class EducationalMaterial extends Entity { @DatabaseField({ label: $localize`:The material which has been borrowed:Material`, dataType: "configurable-enum", - innerDataType: "materials", + additional: "materials", validators: { required: true, }, diff --git a/src/app/child-dev-project/children/model/child.ts b/src/app/child-dev-project/children/model/child.ts index 4cd743f56c..36bede16c1 100644 --- a/src/app/child-dev-project/children/model/child.ts +++ b/src/app/child-dev-project/children/model/child.ts @@ -69,14 +69,14 @@ export class Child extends Entity { @DatabaseField({ dataType: "configurable-enum", label: $localize`:Label for the gender of a child:Gender`, - innerDataType: "genders", + additional: "genders", anonymize: "retain", }) gender: ConfigurableEnumValue; @DatabaseField({ dataType: "configurable-enum", - innerDataType: "center", + additional: "center", label: $localize`:Label for the center of a child:Center`, anonymize: "retain", }) diff --git a/src/app/child-dev-project/notes/model/note.ts b/src/app/child-dev-project/notes/model/note.ts index 378815bb0d..20ded07d3d 100644 --- a/src/app/child-dev-project/notes/model/note.ts +++ b/src/app/child-dev-project/notes/model/note.ts @@ -125,7 +125,7 @@ export class Note extends Entity { @DatabaseField({ label: $localize`:Label for the category of a note:Category`, dataType: "configurable-enum", - innerDataType: INTERACTION_TYPE_CONFIG_ID, + additional: INTERACTION_TYPE_CONFIG_ID, anonymize: "retain", }) category: InteractionType; @@ -175,7 +175,7 @@ export class Note extends Entity { @DatabaseField({ label: $localize`:Status of a note:Status`, dataType: "configurable-enum", - innerDataType: "warning-levels", + additional: "warning-levels", anonymize: "retain", }) warningLevel: Ordering.EnumValue; diff --git a/src/app/core/basic-datatypes/entity/entity.datatype.spec.ts b/src/app/core/basic-datatypes/entity/entity.datatype.spec.ts index 99b11747c0..9c842ff024 100644 --- a/src/app/core/basic-datatypes/entity/entity.datatype.spec.ts +++ b/src/app/core/basic-datatypes/entity/entity.datatype.spec.ts @@ -21,6 +21,7 @@ import { mockEntityMapper } from "../../entity/entity-mapper/mock-entity-mapper- import { Child } from "../../../child-dev-project/children/model/child"; import { ChildSchoolRelation } from "../../../child-dev-project/children/model/childSchoolRelation"; import { EntityActionsService } from "../../entity/entity-actions/entity-actions.service"; +import { EntitySchemaField } from "../../entity/schema/entity-schema-field"; describe("Schema data type: entity", () => { testDatatype(new EntityDatatype(null, null), "1", "1", "User"); @@ -55,7 +56,10 @@ describe("Schema data type: entity", () => { const dataType = new EntityDatatype(entityMapper, mockRemoveService); const testValue = referencedEntity.getId(); - const testSchemaField = { additional: "Child", dataType: "entity" }; + const testSchemaField: EntitySchemaField = { + additional: "Child", + dataType: "entity", + }; const anonymizedValue = await dataType.anonymize( testValue, diff --git a/src/app/core/config/config-fix.ts b/src/app/core/config/config-fix.ts index a7db8c1390..ea2c665e6c 100644 --- a/src/app/core/config/config-fix.ts +++ b/src/app/core/config/config-fix.ts @@ -944,164 +944,108 @@ export const defaultJsonConfig = { "entity:Child": { "label": $localize`:Label for child:Child`, "labelPlural": $localize`:Plural label for child:Children`, - "attributes": [ - { - "name": "address", - "schema": { - "dataType": "location", - "label": $localize`:Label for the address of a child:Address` - } + + "attributes": { + "address": { + "dataType": "location", + "label": $localize`:Label for the address of a child:Address` }, - { - "name": "health_bloodGroup", - "schema": { - "dataType": "string", - "label": $localize`:Label for a child attribute:Blood Group` - } + "health_bloodGroup": { + "dataType": "string", + "label": $localize`:Label for a child attribute:Blood Group` }, - { - "name": "religion", - "schema": { - "dataType": "string", - "label": $localize`:Label for the religion of a child:Religion` - } + "religion": { + "dataType": "string", + "label": $localize`:Label for the religion of a child:Religion` }, - { - "name": "motherTongue", - "schema": { - "dataType": "string", - "label": $localize`:Label for the mother tongue of a child:Mother Tongue`, - description: $localize`:Tooltip description for the mother tongue of a child:The primary language spoken at home`, - } + "motherTongue": { + "dataType": "string", + "label": $localize`:Label for the mother tongue of a child:Mother Tongue`, + description: $localize`:Tooltip description for the mother tongue of a child:The primary language spoken at home`, }, - { - "name": "health_lastDentalCheckup", - "schema": { - "dataType": "date", - "label": $localize`:Label for a child attribute:Last Dental Check-Up` - } + "health_lastDentalCheckup": { + "dataType": "date", + "label": $localize`:Label for a child attribute:Last Dental Check-Up` }, - { - "name": "birth_certificate", - "schema": { - "dataType": "file", - "label": $localize`:Label for a child attribute:Birth certificate` - } + "birth_certificate": { + "dataType": "file", + "label": $localize`:Label for a child attribute:Birth certificate` } - ] + }, }, "entity:School": { - "attributes": [ - { - "name": "name", - "schema": { - "dataType": "string", - "label": $localize`:Label for the name of a school:Name` - } + "attributes": { + "name": { + "dataType": "string", + "label": $localize`:Label for the name of a school:Name` }, - { - "name": "privateSchool", - "schema": { - "dataType": "boolean", - "label": $localize`:Label for if a school is a private school:Private School` - } + "privateSchool": { + "dataType": "boolean", + "label": $localize`:Label for if a school is a private school:Private School` }, - { - "name": "language", - "schema": { - "dataType": "string", - "label": $localize`:Label for the language of a school:Language` - } + "language": { + "dataType": "string", + "label": $localize`:Label for the language of a school:Language` }, - { - "name": "address", - "schema": { - "dataType": "location", - "label": $localize`:Label for the address of a school:Address` - } + "address": { + "dataType": "location", + "label": $localize`:Label for the address of a school:Address` }, - { - "name": "phone", - "schema": { - "dataType": "string", - "label": $localize`:Label for the phone number of a school:Phone Number` - } + "phone": { + "dataType": "string", + "label": $localize`:Label for the phone number of a school:Phone Number` }, - { - "name": "timing", - "schema": { - "dataType": "string", - "label": $localize`:Label for the timing of a school:School Timing` - } + "timing": { + "dataType": "string", + "label": $localize`:Label for the timing of a school:School Timing` }, - { - "name": "remarks", - "schema": { - "dataType": "string", - "label": $localize`:Label for the remarks for a school:Remarks` - } + "remarks": { + "dataType": "string", + "label": $localize`:Label for the remarks for a school:Remarks` } - ] + }, }, "entity:HistoricalEntityData": { - "attributes": [ - { - "name": "isMotivatedDuringClass", - "schema": { - "dataType": "configurable-enum", - "innerDataType": "rating-answer", - "label": $localize`:Label for a child attribute:Motivated`, - description: $localize`:Description for a child attribute:The child is motivated during the class.` - } + "attributes": { + "isMotivatedDuringClass": { + "dataType": "configurable-enum", + "additional": "rating-answer", + "label": $localize`:Label for a child attribute:Motivated`, + description: $localize`:Description for a child attribute:The child is motivated during the class.` }, - { - "name": "isParticipatingInClass", - "schema": { - "dataType": "configurable-enum", - "innerDataType": "rating-answer", - "label": $localize`:Label for a child attribute:Participating`, - description: $localize`:Description for a child attribute:The child is actively participating in the class.` - } + "isParticipatingInClass": { + "dataType": "configurable-enum", + "additional": "rating-answer", + "label": $localize`:Label for a child attribute:Participating`, + description: $localize`:Description for a child attribute:The child is actively participating in the class.` }, - { - "name": "isInteractingWithOthers", - "schema": { - "dataType": "configurable-enum", - "innerDataType": "rating-answer", - "label": $localize`:Label for a child attribute:Interacting`, - description: $localize`:Description for a child attribute:The child interacts with other students during the class.` - } + "isInteractingWithOthers": { + "dataType": "configurable-enum", + "additional": "rating-answer", + "label": $localize`:Label for a child attribute:Interacting`, + description: $localize`:Description for a child attribute:The child interacts with other students during the class.` }, - { - "name": "doesHomework", - "schema": { - "dataType": "configurable-enum", - "innerDataType": "rating-answer", - "label": $localize`:Label for a child attribute:Homework`, - description: $localize`:Description for a child attribute:The child does its homework.` - } + "doesHomework": { + "dataType": "configurable-enum", + "additional": "rating-answer", + "label": $localize`:Label for a child attribute:Homework`, + description: $localize`:Description for a child attribute:The child does its homework.` }, - { - "name": "asksQuestions", - "schema": { - "dataType": "configurable-enum", - "innerDataType": "rating-answer", - "label": $localize`:Label for a child attribute:Asking Questions`, - description: $localize`:Description for a child attribute:The child is asking questions during the class.` - } + "asksQuestions": { + "dataType": "configurable-enum", + "additional": "rating-answer", + "label": $localize`:Label for a child attribute:Asking Questions`, + description: $localize`:Description for a child attribute:The child is asking questions during the class.` }, - ] + } }, "entity:User": { - "attributes": [ - { - "name": "phone", - "schema": { - "dataType": "string", - "label": $localize`:Label of user phone:Contact` - } - }, - ] + "attributes": { + "phone": { + "dataType": "string", + "label": $localize`:Label of user phone:Contact` + } + }, }, "view:matching": { "component": "MatchingEntities", @@ -1130,7 +1074,7 @@ export const defaultJsonConfig = { }, "entity:Todo": { - "attributes": [] + "attributes": {} }, "view:todo": { "component": "TodoList", diff --git a/src/app/core/config/config.service.spec.ts b/src/app/core/config/config.service.spec.ts index 1c7d3384e8..c3163d4301 100644 --- a/src/app/core/config/config.service.spec.ts +++ b/src/app/core/config/config.service.spec.ts @@ -4,6 +4,7 @@ import { EntityMapperService } from "../entity/entity-mapper/entity-mapper.servi import { Config } from "./config"; import { firstValueFrom, Subject } from "rxjs"; import { UpdatedEntity } from "../entity/model/entity-update"; +import { EntityConfig } from "../entity/entity-config"; describe("ConfigService", () => { let service: ConfigService; @@ -98,4 +99,39 @@ describe("ConfigService", () => { const result = service.exportConfig(); expect(result).toEqual(expected); })); + + it("should migrate entity attributes config to flattened object format with id", fakeAsync(() => { + const config = new Config(); + config.data = { + "entity:old-format": { + attributes: [ + { + name: "count", + schema: { + dataType: "number", + }, + }, + ], + }, + "entity:new-format": { + attributes: { + count: { + dataType: "number", + }, + }, + }, + }; + updateSubject.next({ entity: config, type: "update" }); + tick(); + + const expectedEntityAttributes = { + count: { dataType: "number" }, + }; + + const actualFromOld = service.getConfig("entity:old-format"); + expect(actualFromOld.attributes).toEqual(expectedEntityAttributes); + + const actualFromNew = service.getConfig("entity:new-format"); + expect(actualFromNew.attributes).toEqual(expectedEntityAttributes); + })); }); diff --git a/src/app/core/config/config.service.ts b/src/app/core/config/config.service.ts index c30c1b88fc..6c0fa46f1e 100644 --- a/src/app/core/config/config.service.ts +++ b/src/app/core/config/config.service.ts @@ -4,6 +4,7 @@ import { Config } from "./config"; import { LoggingService } from "../logging/logging.service"; import { LatestEntityLoader } from "../entity/latest-entity-loader"; import { shareReplay } from "rxjs/operators"; +import { EntitySchemaField } from "../entity/schema/entity-schema-field"; /** * Access dynamic app configuration retrieved from the database @@ -22,7 +23,7 @@ export class ConfigService extends LatestEntityLoader { super(Config, Config.CONFIG_KEY, entityMapper, logger); super.startLoading(); this.entityUpdated.subscribe(async (config) => { - this.currentConfig = config; + this.currentConfig = this.applyMigrations(config); }); } @@ -48,4 +49,37 @@ export class ConfigService extends LatestEntityLoader { } return matchingConfigs; } + + private applyMigrations(config: Config): Config { + for (let [key, conf] of Object.entries(config.data)) { + // LIST ALL MIGRATION FUNCTIONS HERE + conf = migrateEntityAttributesWithId(key, conf); + + config.data[key] = conf; + } + return config; + } +} + +/** + * Transform legacy "entity:" config format into the flattened structure containing id directly. + */ +function migrateEntityAttributesWithId(idOrPrefix: string, configData: any) { + if ( + !idOrPrefix.startsWith("entity") || + !Array.isArray(configData.attributes) + ) { + return configData; + } + + configData.attributes = configData.attributes.reduce( + (acc, attr: { name: string; schema: EntitySchemaField }) => ({ + ...acc, + [attr.name]: attr.schema, + // id inside the field schema config (FieldConfig) is added by EntityConfigService and does not need migration + }), + {}, + ); + + return configData; } diff --git a/src/app/core/entity/database-field.decorator.spec.ts b/src/app/core/entity/database-field.decorator.spec.ts index 004532357d..c2058a9834 100644 --- a/src/app/core/entity/database-field.decorator.spec.ts +++ b/src/app/core/entity/database-field.decorator.spec.ts @@ -17,6 +17,7 @@ import { Entity } from "./model/entity"; import { DatabaseField } from "./database-field.decorator"; +import { EntitySchemaField } from "./schema/entity-schema-field"; class TestClass extends Entity { @DatabaseField() @@ -39,7 +40,7 @@ describe("@DatabaseField Decorator", () => { it("results in full schema", async () => { expect(TestClass.schema).toEqual( - new Map([ + new Map([ ["fieldUndefined", { dataType: "string" }], ["fieldWithDefault", { dataType: "string" }], ["fieldDate", { dataType: "date", generateIndex: true }], diff --git a/src/app/core/entity/entity-config.service.spec.ts b/src/app/core/entity/entity-config.service.spec.ts index bdaded7390..d4d6a4b572 100644 --- a/src/app/core/entity/entity-config.service.spec.ts +++ b/src/app/core/entity/entity-config.service.spec.ts @@ -1,6 +1,6 @@ import { TestBed } from "@angular/core/testing"; -import { EntityConfig, EntityConfigService } from "./entity-config.service"; +import { EntityConfigService } from "./entity-config.service"; import { DatabaseEntity, EntityRegistry, @@ -12,12 +12,14 @@ import { ConfigService } from "../config/config.service"; import { EntitySchemaService } from "./schema/entity-schema.service"; import { EntityMapperService } from "./entity-mapper/entity-mapper.service"; import { mockEntityMapper } from "./entity-mapper/mock-entity-mapper-service"; +import { EntityConfig } from "./entity-config"; +import { EntitySchemaField } from "./schema/entity-schema-field"; describe("EntityConfigService", () => { let service: EntityConfigService; let mockConfigService: jasmine.SpyObj; const testConfig: EntityConfig = { - attributes: [{ name: "testAttribute", schema: { dataType: "string" } }], + attributes: { testAttribute: { dataType: "string" } }, }; beforeEach(() => { @@ -54,7 +56,7 @@ describe("EntityConfigService", () => { }); it("should load a given EntityType", () => { - const config: EntityConfig = { attributes: [] }; + const config: EntityConfig = {}; mockConfigService.getConfig.and.returnValue(config); const result = service.getEntityConfig(Test); expect(mockConfigService.getConfig).toHaveBeenCalledWith("entity:Test"); @@ -67,25 +69,11 @@ describe("EntityConfigService", () => { const mockEntityConfigs: (EntityConfig & { _id: string })[] = [ { _id: "entity:Test", - attributes: [ - { - name: ATTRIBUTE_1_NAME, - schema: { - dataType: "string", - }, - }, - ], + attributes: { [ATTRIBUTE_1_NAME]: { dataType: "string" } }, }, { _id: "entity:Test2", - attributes: [ - { - name: ATTRIBUTE_2_NAME, - schema: { - dataType: "number", - }, - }, - ], + attributes: { [ATTRIBUTE_2_NAME]: { dataType: "number" } }, }, ]; mockConfigService.getAllConfigs.and.returnValue(mockEntityConfigs); @@ -124,14 +112,17 @@ describe("EntityConfigService", () => { }); it("should create a new subclass with the schema of the extended", () => { - const schema = { dataType: "string", label: "Dynamic Property" }; + const schema: EntitySchemaField = { + dataType: "string", + label: "Dynamic Property", + }; mockConfigService.getAllConfigs.and.returnValue([ { _id: "entity:DynamicTest", label: "Dynamic Test Entity", extends: "Test", - attributes: [{ name: "dynamicProperty", schema }], - }, + attributes: { dynamicProperty: schema }, + } as EntityConfig, ]); service.setupEntitiesFromConfig(); @@ -141,7 +132,7 @@ describe("EntityConfigService", () => { expect([...dynamicEntity.schema.entries()]).toEqual( jasmine.arrayContaining([...Test.schema.entries()]), ); - expect(dynamicEntity.schema.get("dynamicProperty")).toBe(schema); + expect(dynamicEntity.schema.get("dynamicProperty")).toEqual(schema); const dynamicInstance = new dynamicEntity("someId"); expect(dynamicInstance instanceof Test).toBeTrue(); expect(dynamicInstance.getId(true)).toBe("DynamicTest:someId"); diff --git a/src/app/core/entity/entity-config.service.ts b/src/app/core/entity/entity-config.service.ts index b33bbd307d..0f79e82100 100644 --- a/src/app/core/entity/entity-config.service.ts +++ b/src/app/core/entity/entity-config.service.ts @@ -1,10 +1,10 @@ import { Injectable } from "@angular/core"; import { Entity, EntityConstructor } from "./model/entity"; import { ConfigService } from "../config/config.service"; -import { EntitySchemaField } from "./schema/entity-schema-field"; -import { addPropertySchema } from "./database-field.decorator"; import { EntityRegistry } from "./database-entity.decorator"; import { IconName } from "@fortawesome/fontawesome-svg-core"; +import { EntityConfig } from "./entity-config"; +import { addPropertySchema } from "./database-field.decorator"; /** * A service that allows to work with configuration-objects @@ -15,8 +15,11 @@ import { IconName } from "@fortawesome/fontawesome-svg-core"; providedIn: "root", }) export class EntityConfigService { + /** @deprecated will become private, use the service to access the data */ static readonly PREFIX_ENTITY_CONFIG = "entity:"; + // TODO: merge with EntityRegistry? + constructor( private configService: ConfigService, private entities: EntityRegistry, @@ -67,15 +70,10 @@ export class EntityConfigService { configAttributes?: EntityConfig, ) { const entityConfig = configAttributes || this.getEntityConfig(entityType); - if (entityConfig?.attributes) { - entityConfig.attributes.forEach((attribute) => - addPropertySchema( - entityType.prototype, - attribute.name, - attribute.schema, - ), - ); + for (const [key, value] of Object.entries(entityConfig?.attributes ?? {})) { + addPropertySchema(entityType.prototype, key, value); } + // TODO: shall we just assign all properties that are present in the config object? entityType.toStringAttributes = entityConfig.toStringAttributes ?? entityType.toStringAttributes; @@ -100,67 +98,3 @@ export class EntityConfigService { return this.configService.getConfig(configName); } } - -/** - * Dynamic configuration for a entity. - * This allows to change entity metadata based on the configuration. - */ -export interface EntityConfig { - /** - * A list of attributes that will be dynamically added/overwritten to the entity. - */ - attributes?: { - /** - * The name of the attribute (class variable) to be added/overwritten. - */ - name: string; - - /** - * The (new) schema configuration for this attribute. - */ - schema: EntitySchemaField; - }[]; - - /** - * A list of attributes which should be shown when calling the `.toString()` method of this entity. - * E.g. showing the first and last name of a child. - * - * (optional) the default is the ID of the entity (`.entityId`) - */ - toStringAttributes?: string[]; - - /** - * human-readable name/label of the entity in the UI - */ - label?: string; - - /** - * human-readable name/label of the entity in the UI when referring to multiple - */ - labelPlural?: string; - - /** - * icon used to visualize the entity type - */ - icon?: string; - - /** - * color used for to highlight this entity type across the app - */ - color?: string; - - /** - * base route of views for this entity type - */ - route?: string; - - /** - * when a new entity is created, all properties from this class will also be available - */ - extends?: string; - - /** - * whether the type can contain personally identifiable information (PII) - */ - hasPII?: boolean; -} diff --git a/src/app/core/entity/entity-config.ts b/src/app/core/entity/entity-config.ts new file mode 100644 index 0000000000..e9529cb05e --- /dev/null +++ b/src/app/core/entity/entity-config.ts @@ -0,0 +1,55 @@ +import { EntitySchemaField } from "./schema/entity-schema-field"; + +/** + * Dynamic configuration for a entity. + * This allows to change entity metadata based on the configuration. + */ +export interface EntityConfig { + /** + * A list of attributes that will be dynamically added/overwritten to the entity. + */ + attributes?: { [key: string]: EntitySchemaField }; + + /** + * A list of attributes which should be shown when calling the `.toString()` method of this entity. + * E.g. showing the first and last name of a child. + * + * (optional) the default is the ID of the entity (`.entityId`) + */ + toStringAttributes?: string[]; + + /** + * human-readable name/label of the entity in the UI + */ + label?: string; + + /** + * human-readable name/label of the entity in the UI when referring to multiple + */ + labelPlural?: string; + + /** + * icon used to visualize the entity type + */ + icon?: string; + + /** + * color used for to highlight this entity type across the app + */ + color?: string; + + /** + * base route of views for this entity type + */ + route?: string; + + /** + * when a new entity is created, all properties from this class will also be available + */ + extends?: string; + + /** + * whether the type can contain personally identifiable information (PII) + */ + hasPII?: boolean; +} diff --git a/src/app/core/entity/schema/entity-schema.service.spec.ts b/src/app/core/entity/schema/entity-schema.service.spec.ts index 05bede894e..f4d7ec9ff0 100644 --- a/src/app/core/entity/schema/entity-schema.service.spec.ts +++ b/src/app/core/entity/schema/entity-schema.service.spec.ts @@ -129,7 +129,9 @@ describe("EntitySchemaService", () => { ConfigurableEnumDatatype, ); - const entityArraySchema: EntitySchemaField = { dataType: "entity-array" }; + const entityArraySchema: EntitySchemaField = { + dataType: "entity-array", + }; expect(service.getInnermostDatatype(entityArraySchema)).toBeInstanceOf( EntityDatatype, ); diff --git a/src/app/core/filter/filter-generator/filter-generator.service.spec.ts b/src/app/core/filter/filter-generator/filter-generator.service.spec.ts index 7619317d40..e39895f970 100644 --- a/src/app/core/filter/filter-generator/filter-generator.service.spec.ts +++ b/src/app/core/filter/filter-generator/filter-generator.service.spec.ts @@ -91,8 +91,9 @@ describe("FilterGeneratorService", () => { // enum name in additional field const schemaAdditional = { + id: "otherEnum", dataType: schema.dataType, - additional: schema.innerDataType, + additional: schema.additional, }; Note.schema.set("otherEnum", schemaAdditional); @@ -109,9 +110,10 @@ describe("FilterGeneratorService", () => { // enum as array const schemaArray = { + id: "otherEnum", dataType: "array", innerDataType: schema.dataType, - additional: schema.innerDataType, + additional: schema.additional, }; Note.schema.set("otherEnum", schemaArray); diff --git a/src/app/core/filter/filter.service.ts b/src/app/core/filter/filter.service.ts index 2c90c2db40..178652f229 100644 --- a/src/app/core/filter/filter.service.ts +++ b/src/app/core/filter/filter.service.ts @@ -83,7 +83,9 @@ export class FilterService { } private parseConfigurableEnumValue(property: EntitySchemaField, value) { - const enumValues = this.enumService.getEnumValues(property.innerDataType); + const enumValues = this.enumService.getEnumValues( + property.additional ?? property.innerDataType, + ); return enumValues.find(({ id }) => id === value["id"]); } diff --git a/src/app/core/site-settings/site-settings.ts b/src/app/core/site-settings/site-settings.ts index d5f2fc51a9..f328eb8aa7 100644 --- a/src/app/core/site-settings/site-settings.ts +++ b/src/app/core/site-settings/site-settings.ts @@ -24,7 +24,7 @@ export class SiteSettings extends Entity { label: $localize`Default language`, description: $localize`This will only be applied once the app is reloaded`, dataType: "configurable-enum", - innerDataType: LOCALE_ENUM_ID, + additional: LOCALE_ENUM_ID, }) defaultLanguage: ConfigurableEnumValue = availableLocales.values.find( ({ id }) => id === "en-US", diff --git a/src/app/features/config-setup/config-import-parser.service.spec.ts b/src/app/features/config-setup/config-import-parser.service.spec.ts index 4054b3949b..71ea8cfdee 100644 --- a/src/app/features/config-setup/config-import-parser.service.spec.ts +++ b/src/app/features/config-setup/config-import-parser.service.spec.ts @@ -1,13 +1,13 @@ import { TestBed } from "@angular/core/testing"; import { ConfigImportParserService } from "./config-import-parser.service"; -import { EntityConfig } from "../../core/entity/entity-config.service"; -import { EntitySchemaField } from "../../core/entity/schema/entity-schema-field"; +import { EntityConfig } from "../../core/entity/entity-config"; import { ConfigService } from "../../core/config/config.service"; import { ConfigFieldRaw } from "./config-field.raw"; import { EntityListConfig } from "../../core/entity-list/EntityListConfig"; import { EntityDetailsConfig } from "../../core/entity-details/EntityDetailsConfig"; import { ViewConfig } from "../../core/config/dynamic-routing/view-config.interface"; +import { EntitySchemaField } from "../../core/entity/schema/entity-schema-field"; describe("ConfigImportParserService", () => { let service: ConfigImportParserService; @@ -26,7 +26,7 @@ describe("ConfigImportParserService", () => { function expectToBeParsedIntoEntityConfig( inputs: ConfigFieldRaw[], - expectedOutputs: { name: string; schema: EntitySchemaField }[], + expectedOutputs: { [key: string]: EntitySchemaField }, ) { const entityName = "test"; const result = service.parseImportDefinition(inputs, entityName, false); @@ -77,26 +77,17 @@ describe("ConfigImportParserService", () => { description: "some extra explanation", }; - expectToBeParsedIntoEntityConfig( - [configImport_name, configImport_dob], - [ - { - name: configImport_name.id, - schema: { - dataType: configImport_name.dataType, - label: configImport_name.label, - }, - }, - { - name: configImport_dob.id, - schema: { - dataType: configImport_dob.dataType, - label: configImport_dob.label, - description: configImport_dob.description, - }, - }, - ], - ); + expectToBeParsedIntoEntityConfig([configImport_name, configImport_dob], { + [configImport_name.id]: { + dataType: configImport_name.dataType, + label: configImport_name.label, + }, + [configImport_dob.id]: { + dataType: configImport_dob.dataType, + label: configImport_dob.label, + description: configImport_dob.description, + }, + }); }); it("should skip fields where dataType is not defined", () => { @@ -113,15 +104,12 @@ describe("ConfigImportParserService", () => { dataType: "string", }, ], - [ - { - name: "name", - schema: { - dataType: "string", - label: "name", - }, + { + name: { + dataType: "string", + label: "name", }, - ], + }, ); }); @@ -134,15 +122,12 @@ describe("ConfigImportParserService", () => { dataType: "string", }, ], - [ - { - name: "dateOfBirth", - schema: { - dataType: "string", - label: "date of birth", - }, + { + dateOfBirth: { + dataType: "string", + label: "date of birth", }, - ], + }, ); }); @@ -187,32 +172,23 @@ describe("ConfigImportParserService", () => { additional_type_details: null, }, ], - [ - { - name: "hometown", - schema: { - dataType: "configurable-enum", - label: "hometown", - innerDataType: "hometown", - }, + { + hometown: { + dataType: "configurable-enum", + label: "hometown", + innerDataType: "hometown", }, - { - name: "city", - schema: { - dataType: "configurable-enum", - label: "city", - innerDataType: "hometown", // reuse the previous enum! - }, + city: { + dataType: "configurable-enum", + label: "city", + innerDataType: "hometown", // reuse the previous enum! }, - { - name: "missingEnum", - schema: { - dataType: "configurable-enum", - label: "missing", - innerDataType: ConfigImportParserService.NOT_CONFIGURED_KEY, // reuse the previous enum! - }, + missingEnum: { + dataType: "configurable-enum", + label: "missing", + innerDataType: ConfigImportParserService.NOT_CONFIGURED_KEY, // reuse the previous enum! }, - ], + }, ); expect(parsedConfig["enum:hometown"]).toEqual([ diff --git a/src/app/features/config-setup/config-import-parser.service.ts b/src/app/features/config-setup/config-import-parser.service.ts index 77c63eff0a..3069ffe0b1 100644 --- a/src/app/features/config-setup/config-import-parser.service.ts +++ b/src/app/features/config-setup/config-import-parser.service.ts @@ -1,5 +1,4 @@ import { Injectable } from "@angular/core"; -import { EntityConfig } from "../../core/entity/entity-config.service"; import { EntityListConfig, GroupConfig, @@ -14,6 +13,8 @@ import { EntitySchemaField } from "../../core/entity/schema/entity-schema-field" import { ConfigFieldRaw } from "./config-field.raw"; import { ViewConfig } from "../../core/config/dynamic-routing/view-config.interface"; import { defaultJsonConfig } from "../../core/config/config-fix"; +import { EntityConfig } from "../../core/entity/entity-config"; +import { EntityConfigService } from "../../core/entity/entity-config.service"; @Injectable({ providedIn: "root", @@ -65,11 +66,14 @@ export class ConfigImportParserService { ): GeneratedConfig { this.reset(); - const entity: EntityConfig = { - attributes: configRaw - .filter((field) => !!field.dataType) - .map((field) => this.parseFieldDefinition(field, entityName)), - }; + const entity: EntityConfig = { attributes: {} }; + for (const f of configRaw) { + if (!f?.dataType) { + continue; + } + const parsedField = this.parseFieldDefinition(f, entityName); + entity.attributes[parsedField.id] = parsedField.schema; + } const generatedConfig: GeneratedConfig = {}; @@ -77,7 +81,8 @@ export class ConfigImportParserService { this.initializeDefaultValues(generatedConfig); } - generatedConfig["entity:" + entityName] = entity; + generatedConfig[EntityConfigService.PREFIX_ENTITY_CONFIG + entityName] = + entity; // add enum configs for (const [key, enumConfig] of this.enumsAvailable) { @@ -92,7 +97,10 @@ export class ConfigImportParserService { return generatedConfig; } - private parseFieldDefinition(fieldDef: ConfigFieldRaw, entityType: string) { + private parseFieldDefinition( + fieldDef: ConfigFieldRaw, + entityType: string, + ): { id: string; schema: EntitySchemaField } { const fieldId = fieldDef.id ?? ConfigImportParserService.generateIdFromLabel(fieldDef.label); @@ -138,7 +146,7 @@ export class ConfigImportParserService { this.generateOrUpdateDetailsViewConfig(fieldDef, entityType, fieldId); deleteEmptyProperties(schema); - return { name: fieldId, schema: schema }; + return { id: fieldId, schema: schema }; } /** diff --git a/src/app/features/file/couchdb-file.service.spec.ts b/src/app/features/file/couchdb-file.service.spec.ts index 8eac2a28d1..09fa1f4c43 100644 --- a/src/app/features/file/couchdb-file.service.spec.ts +++ b/src/app/features/file/couchdb-file.service.spec.ts @@ -47,7 +47,9 @@ describe("CouchdbFileService", () => { mockSnackbar = jasmine.createSpyObj(["openFromComponent"]); dismiss = jasmine.createSpy(); mockSnackbar.openFromComponent.and.returnValue({ dismiss } as any); - Entity.schema.set("testProp", { dataType: FileDatatype.dataType }); + Entity.schema.set("testProp", { + dataType: FileDatatype.dataType, + }); TestBed.configureTestingModule({ providers: [ diff --git a/src/app/features/historical-data/demo-historical-data-generator.ts b/src/app/features/historical-data/demo-historical-data-generator.ts index 38fb1341a9..b22fa1f01f 100644 --- a/src/app/features/historical-data/demo-historical-data-generator.ts +++ b/src/app/features/historical-data/demo-historical-data-generator.ts @@ -3,9 +3,10 @@ import { HistoricalEntityData } from "./model/historical-entity-data"; import { Injectable } from "@angular/core"; import { DemoChildGenerator } from "../../child-dev-project/children/demo-data-generators/demo-child-generator.service"; import { faker } from "../../core/demo-data/faker"; -import { DemoConfigGeneratorService } from "../../core/config/demo-config-generator.service"; import { ratingAnswers } from "./model/rating-answers"; import { EntityConfigService } from "../../core/entity/entity-config.service"; +import { DemoConfigGeneratorService } from "../../core/config/demo-config-generator.service"; +import { EntityConfig } from "../../core/entity/entity-config"; export class DemoHistoricalDataConfig { minCountAttributes: number; @@ -26,18 +27,23 @@ export class DemoHistoricalDataGenerator extends DemoDataGenerator attr.name); + const attributes: any[] = Object.keys( + ( + config.data[ + EntityConfigService.PREFIX_ENTITY_CONFIG + + HistoricalEntityData.ENTITY_TYPE + ] as EntityConfig + ).attributes, + ); + const entities: HistoricalEntityData[] = []; for (const child of this.childrenGenerator.entities) { const countOfData = diff --git a/src/app/features/location/map/map-properties-popup/map-properties-popup.component.spec.ts b/src/app/features/location/map/map-properties-popup/map-properties-popup.component.spec.ts index 62fe7a3e59..2aeaf6ba23 100644 --- a/src/app/features/location/map/map-properties-popup/map-properties-popup.component.spec.ts +++ b/src/app/features/location/map/map-properties-popup/map-properties-popup.component.spec.ts @@ -21,7 +21,10 @@ describe("MapPropertiesPopupComponent", () => { let mockDialogRef: jasmine.SpyObj>; beforeEach(async () => { - Child.schema.set("address", { label: "Address", dataType: "location" }); + Child.schema.set("address", { + label: "Address", + dataType: "location", + }); Child.schema.set("otherAddress", { label: "Other address", dataType: "location",