From 9eb2b4fbe2cabe56c63d109079137c02c2b8cf0f Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 30 Nov 2023 17:46:46 +0100 Subject: [PATCH] refactor(core): change EntityConfig to flattened structure with EntitySchemaField including .id (#2102) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --------- This functionality has been developed for the project “codo”. codo is developed under the projects “Landungsbrücken – Patenschaften in Hamburg stärken” and “openTransfer Patenschaften”. It is funded through the program “Menschen stärken Menschen” by the German Federal Ministry of Family Affairs, Senior Citizens, Women and Youth. More information at https://github.com/codo-mentoring “Landungsbrücken – Patenschaften in Hamburg stärken” is a project of BürgerStiftung Hamburg in cooperation with the Mentor.Ring Hamburg. With a mix of networking opportunities, capacity building and financial support the project strengthens Hamburg’s scene of mentoring projects since its founding in 2016. The “Stiftung Bürgermut” foundation since 2007 supports the digital and real exchange of experiences and connections of active citizens. Within the federal program “Menschen stärken Menschen” the foundation as part of its program “openTransfer Patenschaften” offers support services for connecting, spreading and upskilling mentoring organisations across Germany. Diese Funktion wurde entwickelt für das Projekt codo. codo wird entwickelt im Rahmen der Projekte Landungsbrücken – Patenschaften in Hamburg stärken und openTransfer Patenschaften. Er ist gefördert durch das Bundesprogramm Menschen stärken Menschen des Bundesministeriums für Familie, Senioren, Frauen und Jugend. Mehr Informationen unter https://github.com/codo-mentoring “Landungsbrücken – Patenschaften in Hamburg stärken” ist ein Projekt der BürgerStiftung Hamburg in Kooperation mit dem Mentor.Ring Hamburg. Mit einer Mischung aus Vernetzungsangeboten, Qualifizierungsmaßnahmen und finanzieller Förderung stärkt das Projekt die Hamburger Szene der Patenschaftsprojekte seit der Gründung im Jahr 2016. Die Stiftung Bürgermut fördert seit 2007 den digitalen und realen Erfahrungsaustausch und die Vernetzung von engagierten Bürger:innen. Innerhalb des Bundesprogramms „Menschen stärken Menschen” bietet die Stiftung im Rahmen ihres Programms openTransfer Patenschaften Unterstützungsleistungen zur Vernetzung, Verbreitung und Qualifizierung von Patenschafts- und Mentoringorganisationen bundesweit. Co-authored-by: codo-mentoring <117934638+codo-mentoring@users.noreply.github.com> --- .../attendance/model/event-attendance.ts | 2 +- .../attendance/model/recurring-activity.ts | 2 +- .../children/aser/model/aser.ts | 8 +- .../model/educational-material.ts | 2 +- .../child-dev-project/children/model/child.ts | 4 +- src/app/child-dev-project/notes/model/note.ts | 4 +- .../entity/entity.datatype.spec.ts | 6 +- src/app/core/config/config-fix.ts | 214 +++++++----------- src/app/core/config/config.service.spec.ts | 36 +++ src/app/core/config/config.service.ts | 36 ++- .../entity/database-field.decorator.spec.ts | 3 +- .../core/entity/entity-config.service.spec.ts | 37 ++- src/app/core/entity/entity-config.service.ts | 82 +------ src/app/core/entity/entity-config.ts | 55 +++++ .../schema/entity-schema.service.spec.ts | 4 +- .../filter-generator.service.spec.ts | 6 +- src/app/core/filter/filter.service.ts | 4 +- src/app/core/site-settings/site-settings.ts | 2 +- .../config-import-parser.service.spec.ts | 100 ++++---- .../config-import-parser.service.ts | 26 ++- .../file/couchdb-file.service.spec.ts | 4 +- .../demo-historical-data-generator.ts | 18 +- .../map-properties-popup.component.spec.ts | 5 +- 23 files changed, 330 insertions(+), 330 deletions(-) create mode 100644 src/app/core/entity/entity-config.ts 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",