refactored mutator to listen for returned data from event listeners

This commit is contained in:
dswbx
2025-01-16 10:10:47 +01:00
parent 438e36f185
commit 6c9707d12c
7 changed files with 96 additions and 28 deletions

View File

@@ -5,8 +5,13 @@ import type { Generated } from "kysely";
export type PrimaryFieldType = number | Generated<number>; export type PrimaryFieldType = number | Generated<number>;
// biome-ignore lint/suspicious/noEmptyInterface: <explanation> export interface DB {
export interface DB {} // make sure to make unknown as "any"
[key: string]: {
id: PrimaryFieldType;
[key: string]: any;
};
}
export const config = { export const config = {
server: { server: {

View File

@@ -1,3 +1,8 @@
export type EventClass = {
new (params: any): Event<any, any>;
slug: string;
};
export abstract class Event<Params = any, Returning = void> { export abstract class Event<Params = any, Returning = void> {
_returning!: Returning; _returning!: Returning;
@@ -9,7 +14,9 @@ export abstract class Event<Params = any, Returning = void> {
params: Params; params: Params;
returned: boolean = false; returned: boolean = false;
validate(value: Returning): Event<Params, Returning> | void {} validate(value: Returning): Event<Params, Returning> | void {
throw new EventReturnedWithoutValidation(this as any, value);
}
protected clone<This extends Event<Params, Returning> = Event<Params, Returning>>( protected clone<This extends Event<Params, Returning> = Event<Params, Returning>>(
this: This, this: This,
@@ -39,3 +46,13 @@ export class InvalidEventReturn extends Error {
super(`Expected "${expected}", got "${given}"`); super(`Expected "${expected}", got "${given}"`);
} }
} }
export class EventReturnedWithoutValidation extends Error {
constructor(
event: EventClass,
public data: any
) {
// @ts-expect-error slug is static
super(`Event "${event.constructor.slug}" returned without validation`);
}
}

View File

@@ -1,4 +1,4 @@
import { type Event, InvalidEventReturn } from "./Event"; import { type Event, type EventClass, InvalidEventReturn } from "./Event";
import { EventListener, type ListenerHandler, type ListenerMode } from "./EventListener"; import { EventListener, type ListenerHandler, type ListenerMode } from "./EventListener";
export type RegisterListenerConfig = export type RegisterListenerConfig =
@@ -12,10 +12,8 @@ export interface EmitsEvents {
emgr: EventManager; emgr: EventManager;
} }
export type EventClass = { // for compatibility, moved it to Event.ts
new (params: any): Event<any, any>; export type { EventClass };
slug: string;
};
export class EventManager< export class EventManager<
RegisteredEvents extends Record<string, EventClass> = Record<string, EventClass> RegisteredEvents extends Record<string, EventClass> = Record<string, EventClass>

View File

@@ -192,10 +192,26 @@ export class Entity<
this.data = data; this.data = data;
} }
// @todo: add tests
isValidData(data: EntityData, context: TActionContext, explain?: boolean): boolean { isValidData(data: EntityData, context: TActionContext, explain?: boolean): boolean {
if (typeof data !== "object") {
if (explain) {
throw new Error(`Entity "${this.name}" data must be an object`);
}
}
const fields = this.getFillableFields(context, false); const fields = this.getFillableFields(context, false);
//const fields = this.fields; const field_names = fields.map((f) => f.name);
//console.log("data", data); const given_keys = Object.keys(data);
if (given_keys.some((key) => !field_names.includes(key))) {
if (explain) {
throw new Error(
`Entity "${this.name}" data must only contain known keys, got: "${given_keys}"`
);
}
}
for (const field of fields) { for (const field of fields) {
if (!field.isValid(data[field.name], context)) { if (!field.isValid(data[field.name], context)) {
console.log("Entity.isValidData:invalid", context, field.name, data[field.name]); console.log("Entity.isValidData:invalid", context, field.name, data[field.name]);

View File

@@ -132,14 +132,17 @@ export class Mutator<
throw new Error(`Creation of system entity "${entity.name}" is disabled`); throw new Error(`Creation of system entity "${entity.name}" is disabled`);
} }
// @todo: establish the original order from "data" const result = await this.emgr.emit(
new Mutator.Events.MutatorInsertBefore({ entity, data: data as any })
);
// if listener returned, take what's returned
const _data = result.returned ? result.params.data : data;
const validatedData = { const validatedData = {
...entity.getDefaultObject(), ...entity.getDefaultObject(),
...(await this.getValidatedData(data, "create")) ...(await this.getValidatedData(_data, "create"))
}; };
await this.emgr.emit(new Mutator.Events.MutatorInsertBefore({ entity, data: validatedData }));
// check if required fields are present // check if required fields are present
const required = entity.getRequiredFields(); const required = entity.getRequiredFields();
for (const field of required) { for (const field of required) {
@@ -169,16 +172,17 @@ export class Mutator<
throw new Error("ID must be provided for update"); throw new Error("ID must be provided for update");
} }
const validatedData = await this.getValidatedData(data, "update"); const result = await this.emgr.emit(
await this.emgr.emit(
new Mutator.Events.MutatorUpdateBefore({ new Mutator.Events.MutatorUpdateBefore({
entity, entity,
entityId: id, entityId: id,
data: validatedData as any data
}) })
); );
const _data = result.returned ? result.params.data : data;
const validatedData = await this.getValidatedData(_data, "update");
const query = this.conn const query = this.conn
.updateTable(entity.name) .updateTable(entity.name)
.set(validatedData as any) .set(validatedData as any)

View File

@@ -1,20 +1,48 @@
import type { PrimaryFieldType } from "core"; import type { PrimaryFieldType } from "core";
import { Event } from "core/events"; import { Event, InvalidEventReturn } from "core/events";
import type { Entity, EntityData } from "../entities"; import type { Entity, EntityData } from "../entities";
import type { RepoQuery } from "../server/data-query-impl"; import type { RepoQuery } from "../server/data-query-impl";
export class MutatorInsertBefore extends Event<{ entity: Entity; data: EntityData }> { export class MutatorInsertBefore extends Event<{ entity: Entity; data: EntityData }, EntityData> {
static override slug = "mutator-insert-before"; static override slug = "mutator-insert-before";
override validate(data: EntityData) {
const { entity } = this.params;
if (!entity.isValidData(data, "create")) {
throw new InvalidEventReturn("EntityData", "invalid");
}
return this.clone({
entity,
data
});
}
} }
export class MutatorInsertAfter extends Event<{ entity: Entity; data: EntityData }> { export class MutatorInsertAfter extends Event<{ entity: Entity; data: EntityData }> {
static override slug = "mutator-insert-after"; static override slug = "mutator-insert-after";
} }
export class MutatorUpdateBefore extends Event<{ export class MutatorUpdateBefore extends Event<
{
entity: Entity; entity: Entity;
entityId: PrimaryFieldType; entityId: PrimaryFieldType;
data: EntityData; data: EntityData;
}> { },
EntityData
> {
static override slug = "mutator-update-before"; static override slug = "mutator-update-before";
override validate(data: EntityData) {
const { entity, ...rest } = this.params;
if (!entity.isValidData(data, "update")) {
throw new InvalidEventReturn("EntityData", "invalid");
}
return this.clone({
...rest,
entity,
data
});
}
} }
export class MutatorUpdateAfter extends Event<{ export class MutatorUpdateAfter extends Event<{
entity: Entity; entity: Entity;

View File

@@ -109,7 +109,7 @@ export const useEntityQuery = <
options?: SWRConfiguration & { enabled?: boolean; revalidateOnMutate?: boolean } options?: SWRConfiguration & { enabled?: boolean; revalidateOnMutate?: boolean }
) => { ) => {
const api = useApi().data; const api = useApi().data;
const key = makeKey(api, entity, id, query); const key = makeKey(api, entity as string, id, query);
const { read, ...actions } = useEntity<Entity, Id>(entity, id); const { read, ...actions } = useEntity<Entity, Id>(entity, id);
const fetcher = () => read(query); const fetcher = () => read(query);
@@ -121,7 +121,7 @@ export const useEntityQuery = <
}); });
const mutateAll = async () => { const mutateAll = async () => {
const entityKey = makeKey(api, entity); const entityKey = makeKey(api, entity as string);
return mutate((key) => typeof key === "string" && key.startsWith(entityKey), undefined, { return mutate((key) => typeof key === "string" && key.startsWith(entityKey), undefined, {
revalidate: true revalidate: true
}); });
@@ -167,7 +167,7 @@ export async function mutateEntityCache<
return prev; return prev;
} }
const entityKey = makeKey(api, entity); const entityKey = makeKey(api, entity as string);
return mutate( return mutate(
(key) => typeof key === "string" && key.startsWith(entityKey), (key) => typeof key === "string" && key.startsWith(entityKey),