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>;
// 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 = {
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> {
_returning!: Returning;
@@ -9,7 +14,9 @@ export abstract class Event<Params = any, Returning = void> {
params: Params;
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>>(
this: This,
@@ -39,3 +46,13 @@ export class InvalidEventReturn extends Error {
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";
export type RegisterListenerConfig =
@@ -12,10 +12,8 @@ export interface EmitsEvents {
emgr: EventManager;
}
export type EventClass = {
new (params: any): Event<any, any>;
slug: string;
};
// for compatibility, moved it to Event.ts
export type { EventClass };
export class EventManager<
RegisteredEvents extends Record<string, EventClass> = Record<string, EventClass>

View File

@@ -192,10 +192,26 @@ export class Entity<
this.data = data;
}
// @todo: add tests
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.fields;
//console.log("data", data);
const field_names = fields.map((f) => f.name);
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) {
if (!field.isValid(data[field.name], context)) {
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`);
}
// @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 = {
...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
const required = entity.getRequiredFields();
for (const field of required) {
@@ -169,16 +172,17 @@ export class Mutator<
throw new Error("ID must be provided for update");
}
const validatedData = await this.getValidatedData(data, "update");
await this.emgr.emit(
const result = await this.emgr.emit(
new Mutator.Events.MutatorUpdateBefore({
entity,
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
.updateTable(entity.name)
.set(validatedData as any)

View File

@@ -1,20 +1,48 @@
import type { PrimaryFieldType } from "core";
import { Event } from "core/events";
import { Event, InvalidEventReturn } from "core/events";
import type { Entity, EntityData } from "../entities";
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";
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 }> {
static override slug = "mutator-insert-after";
}
export class MutatorUpdateBefore extends Event<{
export class MutatorUpdateBefore extends Event<
{
entity: Entity;
entityId: PrimaryFieldType;
data: EntityData;
}> {
},
EntityData
> {
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<{
entity: Entity;

View File

@@ -109,7 +109,7 @@ export const useEntityQuery = <
options?: SWRConfiguration & { enabled?: boolean; revalidateOnMutate?: boolean }
) => {
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 fetcher = () => read(query);
@@ -121,7 +121,7 @@ export const useEntityQuery = <
});
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, {
revalidate: true
});
@@ -167,7 +167,7 @@ export async function mutateEntityCache<
return prev;
}
const entityKey = makeKey(api, entity);
const entityKey = makeKey(api, entity as string);
return mutate(
(key) => typeof key === "string" && key.startsWith(entityKey),