diff --git a/src/app/core/entity/entity-mapper.service.spec.ts b/src/app/core/entity/entity-mapper.service.spec.ts index ed85004509..a125ecd866 100644 --- a/src/app/core/entity/entity-mapper.service.spec.ts +++ b/src/app/core/entity/entity-mapper.service.spec.ts @@ -22,10 +22,13 @@ import { waitForAsync } from "@angular/core/testing"; import { PouchDatabase } from "../database/pouch-database"; import { DatabaseEntity, entityRegistry } from "./database-entity.decorator"; import { Child } from "../../child-dev-project/children/model/child"; +import { TEST_USER } from "../../utils/mocked-testing.module"; +import { SessionService } from "../session/session-service/session.service"; describe("EntityMapperService", () => { let entityMapper: EntityMapperService; let testDatabase: PouchDatabase; + let mockSessionService: jasmine.SpyObj; const existingEntity = { _id: "Entity:existing-entity", @@ -41,9 +44,11 @@ describe("EntityMapperService", () => { beforeEach(waitForAsync(() => { testDatabase = PouchDatabase.create(); + mockSessionService = jasmine.createSpyObj(["getCurrentUser"]); entityMapper = new EntityMapperService( testDatabase, new EntitySchemaService(), + mockSessionService, entityRegistry ); @@ -273,6 +278,36 @@ describe("EntityMapperService", () => { }); }); + it("sets the entityCreated property on save if it is a new entity & entityUpdated on subsequent saves", async () => { + jasmine.clock().install(); + mockSessionService.getCurrentUser.and.returnValue({ + name: TEST_USER, + roles: [], + }); + const id = "test_created"; + const entity = new Entity(id); + + const mockTime1 = 1; + jasmine.clock().mockDate(new Date(mockTime1)); + await entityMapper.save(entity); + const createdEntity = await entityMapper.load(Entity, id); + + expect(createdEntity.created?.at.getTime()).toEqual(mockTime1); + expect(createdEntity.created?.by).toEqual(TEST_USER); + expect(createdEntity.updated?.at.getTime()).toEqual(mockTime1); + expect(createdEntity.updated?.by).toEqual(TEST_USER); + + const mockTime2 = mockTime1 + 1; + jasmine.clock().mockDate(new Date(mockTime2)); + await entityMapper.save(createdEntity); + const updatedEntity = await entityMapper.load(Entity, id); + + expect(updatedEntity.created?.at.getTime()).toEqual(mockTime1); + expect(updatedEntity.updated?.at.getTime()).toEqual(mockTime2); + + jasmine.clock().uninstall(); + }); + function receiveUpdatesAndTestTypeAndId(type?: string, entityId?: string) { return new Promise((resolve) => { entityMapper.receiveUpdates(Entity).subscribe((e) => { diff --git a/src/app/core/entity/entity-mapper.service.ts b/src/app/core/entity/entity-mapper.service.ts index f6109ffd72..1a9ed4db83 100644 --- a/src/app/core/entity/entity-mapper.service.ts +++ b/src/app/core/entity/entity-mapper.service.ts @@ -23,6 +23,8 @@ import { Observable } from "rxjs"; import { UpdatedEntity } from "./model/entity-update"; import { EntityRegistry } from "./database-entity.decorator"; import { map } from "rxjs/operators"; +import { UpdateMetadata } from "./model/update-metadata"; +import { SessionService } from "../session/session-service/session.service"; /** * Handles loading and saving of data for any higher-level feature module. @@ -39,6 +41,7 @@ export class EntityMapperService { constructor( private _db: Database, private entitySchemaService: EntitySchemaService, + private sessionService: SessionService, private registry: EntityRegistry ) {} @@ -136,6 +139,7 @@ export class EntityMapperService { entity: T, forceUpdate: boolean = false ): Promise { + this.setEntityMetadata(entity); const rawData = this.entitySchemaService.transformEntityToDatabaseFormat(entity); const result = await this._db.put(rawData, forceUpdate); @@ -153,6 +157,7 @@ export class EntityMapperService { * @param entities The entities to save */ public async saveAll(entities: Entity[]): Promise { + entities.forEach((e) => this.setEntityMetadata(e)); const rawData = entities.map((e) => this.entitySchemaService.transformEntityToDatabaseFormat(e) ); @@ -183,4 +188,14 @@ export class EntityMapperService { return constructible; } } + + private setEntityMetadata(entity: Entity) { + const newMetadata = new UpdateMetadata( + this.sessionService.getCurrentUser()?.name + ); + if (entity.isNew) { + entity.created = newMetadata; + } + entity.updated = newMetadata; + } } diff --git a/src/app/core/entity/mock-entity-mapper-service.ts b/src/app/core/entity/mock-entity-mapper-service.ts index 923067ea94..bac2100c1a 100644 --- a/src/app/core/entity/mock-entity-mapper-service.ts +++ b/src/app/core/entity/mock-entity-mapper-service.ts @@ -22,7 +22,7 @@ export class MockEntityMapperService extends EntityMapperService { private observables: Map>> = new Map(); constructor() { - super(null, null, entityRegistry); + super(null, null, null, entityRegistry); } private publishUpdates(type: string, update: UpdatedEntity) { @@ -45,9 +45,7 @@ export class MockEntityMapperService extends EntityMapperService { this.data.get(type).set(entity.getId(), entity); this.publishUpdates( entity.getType(), - alreadyExists - ? { type: "update", entity } - : { type: "new", entity } + alreadyExists ? { type: "update", entity } : { type: "new", entity } ); } diff --git a/src/app/core/entity/model/entity.ts b/src/app/core/entity/model/entity.ts index 2f21c09b42..8cbfdc03a2 100644 --- a/src/app/core/entity/model/entity.ts +++ b/src/app/core/entity/model/entity.ts @@ -20,6 +20,7 @@ import { EntitySchema } from "../schema/entity-schema"; import { DatabaseField } from "../database-field.decorator"; import { getWarningLevelColor, WarningLevel } from "./warning-level"; import { IconName } from "@fortawesome/fontawesome-svg-core"; +import { UpdateMetadata } from "./update-metadata"; /** * This represents a static class of type . @@ -169,6 +170,18 @@ export class Entity { /** internal database doc revision, used to detect conflicts by PouchDB/CouchDB */ @DatabaseField() _rev: string; + @DatabaseField({ + dataType: "schema-embed", + additional: UpdateMetadata, + }) + created: UpdateMetadata; + + @DatabaseField({ + dataType: "schema-embed", + additional: UpdateMetadata, + }) + updated: UpdateMetadata; + @DatabaseField({ label: $localize`:Label of checkbox:Inactive`, description: $localize`:Description of checkbox:Ticking this box will archive the record. No data will be lost but the record will be hidden.`, diff --git a/src/app/core/entity/model/update-metadata.ts b/src/app/core/entity/model/update-metadata.ts new file mode 100644 index 0000000000..4f9a58eade --- /dev/null +++ b/src/app/core/entity/model/update-metadata.ts @@ -0,0 +1,17 @@ +import { DatabaseField } from "../database-field.decorator"; + +/** + * Object to store metadata about a "revision" of a document including date and author of the change. + */ +export class UpdateMetadata { + /** when the update was saved to db */ + @DatabaseField() at: Date; + + /** username who saved the update */ + @DatabaseField() by: string; + + constructor(by: string, at: Date = new Date()) { + this.by = by; + this.at = at; + } +}