217 lines
7.8 KiB
TypeScript
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,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|