RenaiApp/src/renderer/store/repositories/editable-entity-repository.ts

217 lines
7.8 KiB
TypeScript

import { EntityRepository, EntityRepositoryInterface } from './entity-repository';
type EntityRepositoryUpdater<T> = (entity: T) => T;
interface UpdatePartialFillerFunction<T, F extends keyof T> {
/**
* fills the updatePartial based on the original and updated values of a field,
* presumably when those 2 values differ
*
* @param updatePartial - the object to fill
* @param key - the key of the field
* @param updated - the updated object, or new object
* @param original - the original object, or deleted object
*/
(updatePartial: Partial<T>, key: F, updated: T, original: T): void;
}
interface RelatedRepositoryUpdaterFunction<T, F extends keyof T> {
/**
* updates related repositories based on the updatePartial;
* and optionally the updated and original values when there was an original value
*
* @param updatePartial - the partial object which signifies which fields have been updated via API
* @param key - the key of the field
* @param updated - the updated object, or new object
* @param original - the original object, or deleted object
*/
(updatePartial: Partial<T>, key: F, updated: T | undefined, original: T | undefined): Promise<void>;
}
/**
* for keys in T, define functions to fill the update partial object
* based on the previous and current field values
*/
export type UpdatePartialFillers<T> = {
[F in keyof T]?: UpdatePartialFillerFunction<T, F>;
};
/**
* for key in T, define function to update related repositories
* based on the update partial object and optionally
* the previous and current field values
*/
export type RelatedRepositoryUpdaters<T> = {
[F in keyof T]?: RelatedRepositoryUpdaterFunction<T, F>;
};
export interface EditableEntityRepositoryInterface<
Serialized extends IdentifiableInterface<Id>,
Id extends Identifier = number,
> extends EntityRepositoryInterface<Serialized, Id> {
/**
* create an entity based on partial data via API
*
* adds the entity to the state
*
* @param partial - the partial entity data
*/
create(partial: Partial<Serialized>): Promise<Serialized>;
/**
* call this method to update the entity with the specified identifier based on the previous entity
*
* the updater callback receives the current entity as parameter
*
* this method calls all subscribers after updating the entity
*
* @param identifier - identifies the entity
* @param updater - is called with the current entity
*/
update(identifier: Id, updater: EntityRepositoryUpdater<Serialized>): Promise<void>;
/**
* deletes the entity from the state and all its subscribers, and via API
*
* does not notify subscribers in any way
*
* @param identifier - identifies the entity
*/
delete(identifier: Id): Promise<void>;
}
export class EditableEntityRepository<Serialized extends IdentifiableInterface<Id>, Id extends Identifier = number>
extends EntityRepository<Serialized, Id>
implements EditableEntityRepositoryInterface<Serialized, Id>
{
protected readonly apiCreate: (partial: Partial<Serialized>) => Promise<Serialized>;
protected readonly apiUpdate: (identifier: Id, partial: Partial<Serialized>) => Promise<Serialized>;
protected readonly apiDelete: (identifier: Id) => Promise<void>;
protected readonly updatePartialFillers: UpdatePartialFillers<Serialized>;
protected readonly relatedRepositoryUpdaters: RelatedRepositoryUpdaters<Serialized>;
/**
* @param apiRead - get the fresh serialized entity from the API
* @param apiCreate - create a new entity via the API
* @param apiUpdate - update an existing entity via the API
* @param apiDelete - delete an existing entity via the API
* @param updatePartialFillers - logic on how to handle an updated entity, resulting in the object being sent to the API
* @param relatedRepositoryUpdaters - logic on which related repositories to refresh and how to do that
*/
public constructor(
apiRead: (identifier: Id) => Promise<Serialized>,
apiCreate: (partial: Partial<Serialized>) => Promise<Serialized>,
apiUpdate: (identifier: Id, partial: Partial<Serialized>) => Promise<Serialized>,
apiDelete: (identifier: Id) => Promise<void>,
updatePartialFillers: UpdatePartialFillers<Serialized>,
relatedRepositoryUpdaters: RelatedRepositoryUpdaters<Serialized>,
) {
super(apiRead);
this.apiCreate = apiCreate;
this.apiUpdate = apiUpdate;
this.apiDelete = apiDelete;
this.updatePartialFillers = updatePartialFillers;
this.relatedRepositoryUpdaters = relatedRepositoryUpdaters;
}
public async create(partial: Partial<Serialized>): Promise<Serialized> {
// create entity via API
const serialized = await this.apiCreate(partial);
// set serialized entity in state
this.state[serialized.id] = serialized;
// update related repositories (all fields)
await this.updateRelatedRepositories(serialized, serialized, undefined);
return serialized;
}
public async update(identifier: Id, updater: EntityRepositoryUpdater<Serialized>): Promise<void> {
// get original entity
const original = this.state[identifier];
if (original) {
// run updater function, and get update partial
const copy = { ...original };
const updatePartial = await this.fillUpdatePartial(updater(copy as Serialized), original as Serialized);
if (Object.keys(updatePartial).length) {
// update entity via API
const updated = await this.apiUpdate(identifier, updatePartial);
// set updated entity in state
this.state[identifier] = updated;
// run subscribers of entity
this.run(identifier);
// update related repositories (changed fields)
await this.updateRelatedRepositories(updatePartial, updated, original as Serialized);
}
}
}
public async delete(identifier: Id): Promise<void> {
// delete entity via API
await this.apiDelete(identifier);
// update related repositories, this should update the DOM in such a way that no subscribers are left
const serialized = this.state[identifier];
if (serialized) {
await this.updateRelatedRepositories(serialized as Serialized, undefined, serialized);
}
// delete entity in state and run remaining subscribers with undefined, then delete them
delete this.state[identifier];
this.run(identifier);
delete this.subscribers[identifier];
}
/**
* runs the update partial filler functions
*
* @param updated - the updated entity
* @param original - the original entity
*
* @return a promise of the updatePartial
*/
private async fillUpdatePartial(updated: Serialized, original: Serialized): Promise<Partial<Serialized>> {
const updatePartial: Partial<Serialized> = {};
await Promise.all(
Object.keys(this.updatePartialFillers).map((field) =>
this.updatePartialFillers[field as keyof Serialized]?.(
updatePartial,
field as keyof Serialized,
updated,
original,
),
),
);
return updatePartial;
}
/**
* update all related repositories
*
* @param updatePartial - the update partial to base the update on (whole object on creation and deletion
* @param updated - the updated object, or new object on creation
* @param original - the original object, or old object on deletion
*
* @return a promise to update all related repositories
*/
private updateRelatedRepositories(
updatePartial: Partial<Serialized>,
updated: Serialized | undefined,
original: Serialized | undefined,
): Promise<unknown> {
return Promise.all(
Object.keys(this.relatedRepositoryUpdaters).map((field) =>
this.relatedRepositoryUpdaters[field as keyof Serialized]?.(
updatePartial,
field as keyof Serialized,
updated,
original,
),
),
);
}
}