mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-17 21:06:04 +00:00
Merge pull request #46 from bknd-io/feat/emgr-return
refactor event system to support returnable events
This commit is contained in:
@@ -1,8 +1,12 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
|
||||||
import { Event, EventManager, NoParamEvent } from "../../src/core/events";
|
import { Event, EventManager, InvalidEventReturn, NoParamEvent } from "../../src/core/events";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "../helper";
|
||||||
|
|
||||||
|
beforeAll(disableConsoleLog);
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
class SpecialEvent extends Event<{ foo: string }> {
|
class SpecialEvent extends Event<{ foo: string }> {
|
||||||
static slug = "special-event";
|
static override slug = "special-event";
|
||||||
|
|
||||||
isBar() {
|
isBar() {
|
||||||
return this.params.foo === "bar";
|
return this.params.foo === "bar";
|
||||||
@@ -10,37 +14,139 @@ class SpecialEvent extends Event<{ foo: string }> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class InformationalEvent extends NoParamEvent {
|
class InformationalEvent extends NoParamEvent {
|
||||||
static slug = "informational-event";
|
static override slug = "informational-event";
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReturnEvent extends Event<{ foo: string }, string> {
|
||||||
|
static override slug = "return-event";
|
||||||
|
|
||||||
|
override validate(value: string) {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
throw new InvalidEventReturn("string", typeof value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.clone({
|
||||||
|
foo: [this.params.foo, value].join("-")
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("EventManager", async () => {
|
describe("EventManager", async () => {
|
||||||
test("test", async () => {
|
test("executes", async () => {
|
||||||
|
const call = mock(() => null);
|
||||||
|
const delayed = mock(() => null);
|
||||||
|
|
||||||
const emgr = new EventManager();
|
const emgr = new EventManager();
|
||||||
emgr.registerEvents([SpecialEvent, InformationalEvent]);
|
emgr.registerEvents([SpecialEvent, InformationalEvent]);
|
||||||
|
|
||||||
|
expect(emgr.eventExists("special-event")).toBe(true);
|
||||||
|
expect(emgr.eventExists("informational-event")).toBe(true);
|
||||||
|
expect(emgr.eventExists("unknown-event")).toBe(false);
|
||||||
|
|
||||||
emgr.onEvent(
|
emgr.onEvent(
|
||||||
SpecialEvent,
|
SpecialEvent,
|
||||||
async (event, name) => {
|
async (event, name) => {
|
||||||
console.log("Event: ", name, event.params.foo, event.isBar());
|
expect(name).toBe("special-event");
|
||||||
console.log("wait...");
|
expect(event.isBar()).toBe(true);
|
||||||
|
call();
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
console.log("done waiting");
|
delayed();
|
||||||
},
|
},
|
||||||
"sync"
|
"sync"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// don't allow unknown
|
||||||
|
expect(() => emgr.on("unknown", () => void 0)).toThrow();
|
||||||
|
|
||||||
emgr.onEvent(InformationalEvent, async (event, name) => {
|
emgr.onEvent(InformationalEvent, async (event, name) => {
|
||||||
console.log("Event: ", name, event.params);
|
call();
|
||||||
|
expect(name).toBe("informational-event");
|
||||||
});
|
});
|
||||||
|
|
||||||
await emgr.emit(new SpecialEvent({ foo: "bar" }));
|
await emgr.emit(new SpecialEvent({ foo: "bar" }));
|
||||||
console.log("done");
|
await emgr.emit(new InformationalEvent());
|
||||||
|
|
||||||
// expect construct signatures to not cause ts errors
|
// expect construct signatures to not cause ts errors
|
||||||
new SpecialEvent({ foo: "bar" });
|
new SpecialEvent({ foo: "bar" });
|
||||||
new InformationalEvent();
|
new InformationalEvent();
|
||||||
|
|
||||||
expect(true).toBe(true);
|
expect(call).toHaveBeenCalledTimes(2);
|
||||||
|
expect(delayed).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("custom async executor", async () => {
|
||||||
|
const call = mock(() => null);
|
||||||
|
const asyncExecutor = (p: Promise<any>[]) => {
|
||||||
|
call();
|
||||||
|
return Promise.all(p);
|
||||||
|
};
|
||||||
|
const emgr = new EventManager(
|
||||||
|
{ InformationalEvent },
|
||||||
|
{
|
||||||
|
asyncExecutor
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
emgr.onEvent(InformationalEvent, async () => {});
|
||||||
|
await emgr.emit(new InformationalEvent());
|
||||||
|
expect(call).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("piping", async () => {
|
||||||
|
const onInvalidReturn = mock(() => null);
|
||||||
|
const asyncEventCallback = mock(() => null);
|
||||||
|
const emgr = new EventManager(
|
||||||
|
{ ReturnEvent, InformationalEvent },
|
||||||
|
{
|
||||||
|
onInvalidReturn
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// @ts-expect-error InformationalEvent has no return value
|
||||||
|
emgr.onEvent(InformationalEvent, async () => {
|
||||||
|
asyncEventCallback();
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
emgr.onEvent(ReturnEvent, async () => "1", "sync");
|
||||||
|
emgr.onEvent(ReturnEvent, async () => "0", "sync");
|
||||||
|
|
||||||
|
// @ts-expect-error must be string
|
||||||
|
emgr.onEvent(ReturnEvent, async () => 0, "sync");
|
||||||
|
|
||||||
|
// return is not required
|
||||||
|
emgr.onEvent(ReturnEvent, async () => {}, "sync");
|
||||||
|
|
||||||
|
// was "async", will not return
|
||||||
|
const e1 = await emgr.emit(new InformationalEvent());
|
||||||
|
expect(e1.returned).toBe(false);
|
||||||
|
|
||||||
|
const e2 = await emgr.emit(new ReturnEvent({ foo: "bar" }));
|
||||||
|
expect(e2.returned).toBe(true);
|
||||||
|
expect(e2.params.foo).toBe("bar-1-0");
|
||||||
|
expect(onInvalidReturn).toHaveBeenCalled();
|
||||||
|
expect(asyncEventCallback).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("once", async () => {
|
||||||
|
const call = mock(() => null);
|
||||||
|
const emgr = new EventManager({ InformationalEvent });
|
||||||
|
|
||||||
|
emgr.onEvent(
|
||||||
|
InformationalEvent,
|
||||||
|
async (event, slug) => {
|
||||||
|
expect(event).toBeInstanceOf(InformationalEvent);
|
||||||
|
expect(slug).toBe("informational-event");
|
||||||
|
call();
|
||||||
|
},
|
||||||
|
{ mode: "sync", once: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(emgr.getListeners().length).toBe(1);
|
||||||
|
await emgr.emit(new InformationalEvent());
|
||||||
|
expect(emgr.getListeners().length).toBe(0);
|
||||||
|
await emgr.emit(new InformationalEvent());
|
||||||
|
expect(emgr.getListeners().length).toBe(0);
|
||||||
|
expect(call).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
|
import type { EventManager } from "../../../src/core/events";
|
||||||
import {
|
import {
|
||||||
Entity,
|
Entity,
|
||||||
EntityManager,
|
EntityManager,
|
||||||
@@ -10,6 +11,7 @@ import {
|
|||||||
RelationMutator,
|
RelationMutator,
|
||||||
TextField
|
TextField
|
||||||
} from "../../../src/data";
|
} from "../../../src/data";
|
||||||
|
import * as proto from "../../../src/data/prototype";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
@@ -83,14 +85,12 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
|||||||
|
|
||||||
// persisting reference should ...
|
// persisting reference should ...
|
||||||
expect(
|
expect(
|
||||||
postRelMutator.persistReference(relations[0], "users", {
|
postRelMutator.persistReference(relations[0]!, "users", {
|
||||||
$set: { id: userData.data.id }
|
$set: { id: userData.data.id }
|
||||||
})
|
})
|
||||||
).resolves.toEqual(["users_id", userData.data.id]);
|
).resolves.toEqual(["users_id", userData.data.id]);
|
||||||
// @todo: add what methods are allowed to relation, like $create should not be allowed for post<>users
|
// @todo: add what methods are allowed to relation, like $create should not be allowed for post<>users
|
||||||
|
|
||||||
process.exit(0);
|
|
||||||
|
|
||||||
const userRelMutator = new RelationMutator(users, em);
|
const userRelMutator = new RelationMutator(users, em);
|
||||||
expect(userRelMutator.getRelationalKeys()).toEqual(["posts"]);
|
expect(userRelMutator.getRelationalKeys()).toEqual(["posts"]);
|
||||||
});
|
});
|
||||||
@@ -99,7 +99,7 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
|||||||
expect(
|
expect(
|
||||||
em.mutator(posts).insertOne({
|
em.mutator(posts).insertOne({
|
||||||
title: "post1",
|
title: "post1",
|
||||||
users_id: 1 // user does not exist yet
|
users_id: 100 // user does not exist yet
|
||||||
})
|
})
|
||||||
).rejects.toThrow();
|
).rejects.toThrow();
|
||||||
});
|
});
|
||||||
@@ -299,4 +299,71 @@ describe("[data] Mutator (Events)", async () => {
|
|||||||
expect(events.has(MutatorEvents.MutatorDeleteBefore.slug)).toBeTrue();
|
expect(events.has(MutatorEvents.MutatorDeleteBefore.slug)).toBeTrue();
|
||||||
expect(events.has(MutatorEvents.MutatorDeleteAfter.slug)).toBeTrue();
|
expect(events.has(MutatorEvents.MutatorDeleteAfter.slug)).toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("insertOne event return is respected", async () => {
|
||||||
|
const posts = proto.entity("posts", {
|
||||||
|
title: proto.text(),
|
||||||
|
views: proto.number()
|
||||||
|
});
|
||||||
|
|
||||||
|
const conn = getDummyConnection();
|
||||||
|
const em = new EntityManager([posts], conn.dummyConnection);
|
||||||
|
await em.schema().sync({ force: true });
|
||||||
|
|
||||||
|
const emgr = em.emgr as EventManager<any>;
|
||||||
|
|
||||||
|
emgr.onEvent(
|
||||||
|
// @ts-ignore
|
||||||
|
EntityManager.Events.MutatorInsertBefore,
|
||||||
|
async (event) => {
|
||||||
|
return {
|
||||||
|
...event.params.data,
|
||||||
|
views: 2
|
||||||
|
};
|
||||||
|
},
|
||||||
|
"sync"
|
||||||
|
);
|
||||||
|
|
||||||
|
const mutator = em.mutator("posts");
|
||||||
|
const result = await mutator.insertOne({ title: "test", views: 1 });
|
||||||
|
expect(result.data).toEqual({
|
||||||
|
id: 1,
|
||||||
|
title: "test",
|
||||||
|
views: 2
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("updateOne event return is respected", async () => {
|
||||||
|
const posts = proto.entity("posts", {
|
||||||
|
title: proto.text(),
|
||||||
|
views: proto.number()
|
||||||
|
});
|
||||||
|
|
||||||
|
const conn = getDummyConnection();
|
||||||
|
const em = new EntityManager([posts], conn.dummyConnection);
|
||||||
|
await em.schema().sync({ force: true });
|
||||||
|
|
||||||
|
const emgr = em.emgr as EventManager<any>;
|
||||||
|
|
||||||
|
emgr.onEvent(
|
||||||
|
// @ts-ignore
|
||||||
|
EntityManager.Events.MutatorUpdateBefore,
|
||||||
|
async (event) => {
|
||||||
|
return {
|
||||||
|
...event.params.data,
|
||||||
|
views: event.params.data.views + 1
|
||||||
|
};
|
||||||
|
},
|
||||||
|
"sync"
|
||||||
|
);
|
||||||
|
|
||||||
|
const mutator = em.mutator("posts");
|
||||||
|
const created = await mutator.insertOne({ title: "test", views: 1 });
|
||||||
|
const result = await mutator.updateOne(created.data.id, { views: 2 });
|
||||||
|
expect(result.data).toEqual({
|
||||||
|
id: 1,
|
||||||
|
title: "test",
|
||||||
|
views: 3
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
// @ts-ignore
|
|
||||||
import { Perf } from "@bknd/core/utils";
|
|
||||||
import type { Kysely, Transaction } from "kysely";
|
import type { Kysely, Transaction } from "kysely";
|
||||||
|
import { Perf } from "../../../src/core/utils";
|
||||||
import {
|
import {
|
||||||
Entity,
|
Entity,
|
||||||
EntityManager,
|
EntityManager,
|
||||||
@@ -24,7 +23,7 @@ async function sleep(ms: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("[Repository]", async () => {
|
describe("[Repository]", async () => {
|
||||||
test("bulk", async () => {
|
test.skip("bulk", async () => {
|
||||||
//const connection = dummyConnection;
|
//const connection = dummyConnection;
|
||||||
//const connection = getLocalLibsqlConnection();
|
//const connection = getLocalLibsqlConnection();
|
||||||
const credentials = null as any; // @todo: determine what to do here
|
const credentials = null as any; // @todo: determine what to do here
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ describe("[data] WithBuilder", async () => {
|
|||||||
const res = qb.compile();
|
const res = qb.compile();
|
||||||
|
|
||||||
expect(res.sql).toBe(
|
expect(res.sql).toBe(
|
||||||
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'content\', "agg"."content", \'author_id\', "agg"."author_id")), \'[]\') from (select "posts"."id" as "id", "posts"."content" as "content", "posts"."author_id" as "author_id" from "posts" where "users"."id" = "posts"."author_id" limit ?) as agg) as "posts" from "users"'
|
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'content\', "agg"."content", \'author_id\', "agg"."author_id")), \'[]\') from (select "posts"."id" as "id", "posts"."content" as "content", "posts"."author_id" as "author_id" from "posts" as "posts" where "posts"."author_id" = "users"."id" limit ?) as agg) as "posts" from "users"'
|
||||||
);
|
);
|
||||||
expect(res.parameters).toEqual([5]);
|
expect(res.parameters).toEqual([5]);
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ describe("[data] WithBuilder", async () => {
|
|||||||
const res2 = qb2.compile();
|
const res2 = qb2.compile();
|
||||||
|
|
||||||
expect(res2.sql).toBe(
|
expect(res2.sql).toBe(
|
||||||
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username") from (select "users"."id" as "id", "users"."username" as "username" from "users" where "posts"."author_id" = "users"."id" limit ?) as obj) as "author" from "posts"'
|
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username") from (select "author"."id" as "id", "author"."username" as "username" from "users" as "author" where "author"."id" = "posts"."author_id" limit ?) as obj) as "author" from "posts"'
|
||||||
);
|
);
|
||||||
expect(res2.parameters).toEqual([1]);
|
expect(res2.parameters).toEqual([1]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,10 +13,6 @@ describe("[data] EnumField", async () => {
|
|||||||
{ options: options(["a", "b", "c"]) }
|
{ options: options(["a", "b", "c"]) }
|
||||||
);
|
);
|
||||||
|
|
||||||
test("yields if no options", async () => {
|
|
||||||
expect(() => new EnumField("test", { options: options([]) })).toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("yields if default value is not a valid option", async () => {
|
test("yields if default value is not a valid option", async () => {
|
||||||
expect(
|
expect(
|
||||||
() => new EnumField("test", { options: options(["a", "b"]), default_value: "c" })
|
() => new EnumField("test", { options: options(["a", "b"]), default_value: "c" })
|
||||||
|
|||||||
@@ -15,11 +15,9 @@ describe("[data] Field", async () => {
|
|||||||
|
|
||||||
runBaseFieldTests(FieldSpec, { defaultValue: "test", schemaType: "text" });
|
runBaseFieldTests(FieldSpec, { defaultValue: "test", schemaType: "text" });
|
||||||
|
|
||||||
test.only("default config", async () => {
|
test("default config", async () => {
|
||||||
const field = new FieldSpec("test");
|
|
||||||
const config = Default(baseFieldConfigSchema, {});
|
const config = Default(baseFieldConfigSchema, {});
|
||||||
expect(stripMark(new FieldSpec("test").config)).toEqual(config);
|
expect(stripMark(new FieldSpec("test").config)).toEqual(config);
|
||||||
console.log("config", new TextField("test", { required: true }).toJSON());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("transformPersist (specific)", async () => {
|
test("transformPersist (specific)", async () => {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ describe("[data] JsonField", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("getValue", async () => {
|
test("getValue", async () => {
|
||||||
expect(field.getValue({ test: 1 }, "form")).toBe('{"test":1}');
|
expect(field.getValue({ test: 1 }, "form")).toBe('{\n "test": 1\n}');
|
||||||
expect(field.getValue("string", "form")).toBe('"string"');
|
expect(field.getValue("string", "form")).toBe('"string"');
|
||||||
expect(field.getValue(1, "form")).toBe("1");
|
expect(field.getValue(1, "form")).toBe("1");
|
||||||
|
|
||||||
|
|||||||
@@ -70,9 +70,9 @@ describe("[data] EntityRelation", async () => {
|
|||||||
|
|
||||||
it("required", async () => {
|
it("required", async () => {
|
||||||
const relation1 = new TestEntityRelation();
|
const relation1 = new TestEntityRelation();
|
||||||
expect(relation1.config.required).toBe(false);
|
expect(relation1.required).toBe(false);
|
||||||
|
|
||||||
const relation2 = new TestEntityRelation({ required: true });
|
const relation2 = new TestEntityRelation({ required: true });
|
||||||
expect(relation2.config.required).toBe(true);
|
expect(relation2.required).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { randomString } from "../../../src/core/utils";
|
|||||||
import { StorageCloudinaryAdapter } from "../../../src/media";
|
import { StorageCloudinaryAdapter } from "../../../src/media";
|
||||||
|
|
||||||
import { config } from "dotenv";
|
import { config } from "dotenv";
|
||||||
const dotenvOutput = config({ path: `${import.meta.dir}/../../.env` });
|
const dotenvOutput = config({ path: `${import.meta.dir}/../../../.env` });
|
||||||
const {
|
const {
|
||||||
CLOUDINARY_CLOUD_NAME,
|
CLOUDINARY_CLOUD_NAME,
|
||||||
CLOUDINARY_API_KEY,
|
CLOUDINARY_API_KEY,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ describe("StorageLocalAdapter", () => {
|
|||||||
|
|
||||||
test("puts an object", async () => {
|
test("puts an object", async () => {
|
||||||
objects = (await adapter.listObjects()).length;
|
objects = (await adapter.listObjects()).length;
|
||||||
expect(await adapter.putObject(filename, await file.arrayBuffer())).toBeString();
|
expect(await adapter.putObject(filename, file)).toBeString();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("lists objects", async () => {
|
test("lists objects", async () => {
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ import { randomString } from "../../../src/core/utils";
|
|||||||
import { StorageS3Adapter } from "../../../src/media";
|
import { StorageS3Adapter } from "../../../src/media";
|
||||||
|
|
||||||
import { config } from "dotenv";
|
import { config } from "dotenv";
|
||||||
const dotenvOutput = config({ path: `${import.meta.dir}/../../.env` });
|
const dotenvOutput = config({ path: `${import.meta.dir}/../../../.env` });
|
||||||
const { R2_ACCESS_KEY, R2_SECRET_ACCESS_KEY, R2_URL, AWS_ACCESS_KEY, AWS_SECRET_KEY, AWS_S3_URL } =
|
const { R2_ACCESS_KEY, R2_SECRET_ACCESS_KEY, R2_URL, AWS_ACCESS_KEY, AWS_SECRET_KEY, AWS_S3_URL } =
|
||||||
dotenvOutput.parsed!;
|
dotenvOutput.parsed!;
|
||||||
|
|
||||||
// @todo: mock r2/s3 responses for faster tests
|
// @todo: mock r2/s3 responses for faster tests
|
||||||
const ALL_TESTS = process.env.ALL_TESTS;
|
const ALL_TESTS = !!process.env.ALL_TESTS;
|
||||||
|
|
||||||
describe("Storage", async () => {
|
describe.skipIf(ALL_TESTS)("StorageS3Adapter", async () => {
|
||||||
console.log("ALL_TESTS", process.env.ALL_TESTS);
|
console.log("ALL_TESTS", process.env.ALL_TESTS);
|
||||||
const versions = [
|
const versions = [
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -1,2 +1,5 @@
|
|||||||
[install]
|
[install]
|
||||||
registry = "http://localhost:4873"
|
#registry = "http://localhost:4873"
|
||||||
|
|
||||||
|
[test]
|
||||||
|
coverageSkipTestFiles = true
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"test": "ALL_TESTS=1 bun test --bail",
|
"test": "ALL_TESTS=1 bun test --bail",
|
||||||
|
"test:coverage": "ALL_TESTS=1 bun test --bail --coverage",
|
||||||
"build": "NODE_ENV=production bun run build.ts --minify --types",
|
"build": "NODE_ENV=production bun run build.ts --minify --types",
|
||||||
"build:all": "rm -rf dist && bun run build:static && NODE_ENV=production bun run build.ts --minify --types --clean && bun run build:cli",
|
"build:all": "rm -rf dist && bun run build:static && NODE_ENV=production bun run build.ts --minify --types --clean && bun run build:cli",
|
||||||
"build:cli": "bun build src/cli/index.ts --target node --outdir dist/cli --minify",
|
"build:cli": "bun build src/cli/index.ts --target node --outdir dist/cli --minify",
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -1,17 +1,38 @@
|
|||||||
export abstract class Event<Params = any> {
|
export type EventClass = {
|
||||||
|
new (params: any): Event<any, any>;
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export abstract class Event<Params = any, Returning = void> {
|
||||||
|
_returning!: Returning;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unique event slug
|
* Unique event slug
|
||||||
* Must be static, because registering events is done by class
|
* Must be static, because registering events is done by class
|
||||||
*/
|
*/
|
||||||
static slug: string = "untitled-event";
|
static slug: string = "untitled-event";
|
||||||
params: Params;
|
params: Params;
|
||||||
|
returned: boolean = false;
|
||||||
|
|
||||||
|
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,
|
||||||
|
params: Params
|
||||||
|
): This {
|
||||||
|
const cloned = new (this.constructor as any)(params);
|
||||||
|
cloned.returned = true;
|
||||||
|
return cloned as This;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(params: Params) {
|
constructor(params: Params) {
|
||||||
this.params = params;
|
this.params = params;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @todo: current workaround: potentially there is none and that's the way
|
// @todo: current workaround: potentially there is "none" and that's the way
|
||||||
export class NoParamEvent extends Event<null> {
|
export class NoParamEvent extends Event<null> {
|
||||||
static override slug: string = "noparam-event";
|
static override slug: string = "noparam-event";
|
||||||
|
|
||||||
@@ -19,3 +40,19 @@ export class NoParamEvent extends Event<null> {
|
|||||||
super(null);
|
super(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class InvalidEventReturn extends Error {
|
||||||
|
constructor(expected: string, given: string) {
|
||||||
|
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`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ import type { EventClass } from "./EventManager";
|
|||||||
export const ListenerModes = ["sync", "async"] as const;
|
export const ListenerModes = ["sync", "async"] as const;
|
||||||
export type ListenerMode = (typeof ListenerModes)[number];
|
export type ListenerMode = (typeof ListenerModes)[number];
|
||||||
|
|
||||||
export type ListenerHandler<E extends Event = Event> = (
|
export type ListenerHandler<E extends Event<any, any>> = (
|
||||||
event: E,
|
event: E,
|
||||||
slug: string,
|
slug: string
|
||||||
) => Promise<void> | void;
|
) => E extends Event<any, infer R> ? R | Promise<R | void> : never;
|
||||||
|
|
||||||
export class EventListener<E extends Event = Event> {
|
export class EventListener<E extends Event = Event> {
|
||||||
mode: ListenerMode = "async";
|
mode: ListenerMode = "async";
|
||||||
event: EventClass;
|
event: EventClass;
|
||||||
handler: ListenerHandler<E>;
|
handler: ListenerHandler<E>;
|
||||||
|
once: boolean = false;
|
||||||
|
|
||||||
constructor(event: EventClass, handler: ListenerHandler<E>, mode: ListenerMode = "async") {
|
constructor(event: EventClass, handler: ListenerHandler<E>, mode: ListenerMode = "async") {
|
||||||
this.event = event;
|
this.event = event;
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
import type { Event } 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 =
|
||||||
|
| ListenerMode
|
||||||
|
| {
|
||||||
|
mode?: ListenerMode;
|
||||||
|
once?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export interface EmitsEvents {
|
export interface EmitsEvents {
|
||||||
emgr: EventManager;
|
emgr: EventManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EventClass = {
|
// for compatibility, moved it to Event.ts
|
||||||
new (params: any): Event;
|
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>
|
||||||
@@ -17,16 +22,20 @@ export class EventManager<
|
|||||||
protected listeners: EventListener[] = [];
|
protected listeners: EventListener[] = [];
|
||||||
enabled: boolean = true;
|
enabled: boolean = true;
|
||||||
|
|
||||||
constructor(events?: RegisteredEvents, listeners?: EventListener[]) {
|
constructor(
|
||||||
|
events?: RegisteredEvents,
|
||||||
|
private options?: {
|
||||||
|
listeners?: EventListener[];
|
||||||
|
onError?: (event: Event, e: unknown) => void;
|
||||||
|
onInvalidReturn?: (event: Event, e: InvalidEventReturn) => void;
|
||||||
|
asyncExecutor?: typeof Promise.all;
|
||||||
|
}
|
||||||
|
) {
|
||||||
if (events) {
|
if (events) {
|
||||||
this.registerEvents(events);
|
this.registerEvents(events);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (listeners) {
|
options?.listeners?.forEach((l) => this.addListener(l));
|
||||||
for (const listener of listeners) {
|
|
||||||
this.addListener(listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enable() {
|
enable() {
|
||||||
@@ -82,9 +91,11 @@ export class EventManager<
|
|||||||
return !!this.events.find((e) => slug === e.slug);
|
return !!this.events.find((e) => slug === e.slug);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected throwIfEventNotRegistered(event: EventClass) {
|
protected throwIfEventNotRegistered(event: EventClass | Event | string) {
|
||||||
if (!this.eventExists(event)) {
|
if (!this.eventExists(event as any)) {
|
||||||
throw new Error(`Event "${event.slug}" not registered`);
|
// @ts-expect-error
|
||||||
|
const name = event.constructor?.slug ?? event.slug ?? event;
|
||||||
|
throw new Error(`Event "${name}" not registered`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,55 +128,108 @@ export class EventManager<
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected createEventListener(
|
||||||
|
_event: EventClass | string,
|
||||||
|
handler: ListenerHandler<any>,
|
||||||
|
_config: RegisterListenerConfig = "async"
|
||||||
|
) {
|
||||||
|
const event =
|
||||||
|
typeof _event === "string" ? this.events.find((e) => e.slug === _event)! : _event;
|
||||||
|
const config = typeof _config === "string" ? { mode: _config } : _config;
|
||||||
|
const listener = new EventListener(event, handler, config.mode);
|
||||||
|
if (config.once) {
|
||||||
|
listener.once = true;
|
||||||
|
}
|
||||||
|
this.addListener(listener as any);
|
||||||
|
}
|
||||||
|
|
||||||
onEvent<ActualEvent extends EventClass, Instance extends InstanceType<ActualEvent>>(
|
onEvent<ActualEvent extends EventClass, Instance extends InstanceType<ActualEvent>>(
|
||||||
event: ActualEvent,
|
event: ActualEvent,
|
||||||
handler: ListenerHandler<Instance>,
|
handler: ListenerHandler<Instance>,
|
||||||
mode: ListenerMode = "async"
|
config?: RegisterListenerConfig
|
||||||
) {
|
) {
|
||||||
this.throwIfEventNotRegistered(event);
|
this.createEventListener(event, handler, config);
|
||||||
|
|
||||||
const listener = new EventListener(event, handler, mode);
|
|
||||||
this.addListener(listener as any);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
on<Params = any>(
|
on<Params = any>(
|
||||||
slug: string,
|
slug: string,
|
||||||
handler: ListenerHandler<Event<Params>>,
|
handler: ListenerHandler<Event<Params>>,
|
||||||
mode: ListenerMode = "async"
|
config?: RegisterListenerConfig
|
||||||
) {
|
) {
|
||||||
const event = this.events.find((e) => e.slug === slug);
|
this.createEventListener(slug, handler, config);
|
||||||
if (!event) {
|
|
||||||
throw new Error(`Event "${slug}" not registered`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.onEvent(event, handler, mode);
|
onAny(handler: ListenerHandler<Event<unknown>>, config?: RegisterListenerConfig) {
|
||||||
|
this.events.forEach((event) => this.onEvent(event, handler, config));
|
||||||
}
|
}
|
||||||
|
|
||||||
onAny(handler: ListenerHandler<Event<unknown>>, mode: ListenerMode = "async") {
|
protected executeAsyncs(promises: (() => Promise<void>)[]) {
|
||||||
this.events.forEach((event) => this.onEvent(event, handler, mode));
|
const executor = this.options?.asyncExecutor ?? ((e) => Promise.all(e));
|
||||||
|
executor(promises.map((p) => p())).then(() => void 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
async emit(event: Event) {
|
async emit<Actual extends Event<any, any>>(event: Actual): Promise<Actual> {
|
||||||
// @ts-expect-error slug is static
|
// @ts-expect-error slug is static
|
||||||
const slug = event.constructor.slug;
|
const slug = event.constructor.slug;
|
||||||
if (!this.enabled) {
|
if (!this.enabled) {
|
||||||
console.log("EventManager disabled, not emitting", slug);
|
console.log("EventManager disabled, not emitting", slug);
|
||||||
return;
|
return event;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.eventExists(event)) {
|
if (!this.eventExists(event)) {
|
||||||
throw new Error(`Event "${slug}" not registered`);
|
throw new Error(`Event "${slug}" not registered`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const listeners = this.listeners.filter((listener) => listener.event.slug === slug);
|
const syncs: EventListener[] = [];
|
||||||
//console.log("---!-- emitting", slug, listeners.length);
|
const asyncs: (() => Promise<void>)[] = [];
|
||||||
|
|
||||||
|
this.listeners = this.listeners.filter((listener) => {
|
||||||
|
// if no match, keep and ignore
|
||||||
|
if (listener.event.slug !== slug) return true;
|
||||||
|
|
||||||
for (const listener of listeners) {
|
|
||||||
if (listener.mode === "sync") {
|
if (listener.mode === "sync") {
|
||||||
await listener.handler(event, listener.event.slug);
|
syncs.push(listener);
|
||||||
} else {
|
} else {
|
||||||
listener.handler(event, listener.event.slug);
|
asyncs.push(async () => await listener.handler(event, listener.event.slug));
|
||||||
|
}
|
||||||
|
// Remove if `once` is true, otherwise keep
|
||||||
|
return !listener.once;
|
||||||
|
});
|
||||||
|
|
||||||
|
// execute asyncs
|
||||||
|
this.executeAsyncs(asyncs);
|
||||||
|
|
||||||
|
// execute syncs
|
||||||
|
let _event: Actual = event;
|
||||||
|
for (const listener of syncs) {
|
||||||
|
try {
|
||||||
|
const return_value = (await listener.handler(_event, listener.event.slug)) as any;
|
||||||
|
|
||||||
|
if (typeof return_value !== "undefined") {
|
||||||
|
const newEvent = _event.validate(return_value);
|
||||||
|
// @ts-expect-error slug is static
|
||||||
|
if (newEvent && newEvent.constructor.slug === slug) {
|
||||||
|
if (!newEvent.returned) {
|
||||||
|
throw new Error(
|
||||||
|
// @ts-expect-error slug is static
|
||||||
|
`Returned event ${newEvent.constructor.slug} must be marked as returned.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_event = newEvent as Actual;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof InvalidEventReturn) {
|
||||||
|
this.options?.onInvalidReturn?.(_event, e);
|
||||||
|
console.warn(`Invalid return of event listener for "${slug}": ${e.message}`);
|
||||||
|
} else if (this.options?.onError) {
|
||||||
|
this.options.onError(_event, e);
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return _event;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
export { Event, NoParamEvent } from "./Event";
|
export { Event, NoParamEvent, InvalidEventReturn } from "./Event";
|
||||||
export {
|
export {
|
||||||
EventListener,
|
EventListener,
|
||||||
ListenerModes,
|
ListenerModes,
|
||||||
type ListenerMode,
|
type ListenerMode,
|
||||||
type ListenerHandler,
|
type ListenerHandler
|
||||||
} from "./EventListener";
|
} from "./EventListener";
|
||||||
export { EventManager, type EmitsEvents, type EventClass } from "./EventManager";
|
export { EventManager, type EmitsEvents, type EventClass } from "./EventManager";
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -1,150 +0,0 @@
|
|||||||
Subject: [PATCH] event manager returning test
|
|
||||||
---
|
|
||||||
Index: app/__test__/core/EventManager.spec.ts
|
|
||||||
IDEA additional info:
|
|
||||||
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
|
|
||||||
<+>UTF-8
|
|
||||||
===================================================================
|
|
||||||
diff --git a/app/__test__/core/EventManager.spec.ts b/app/__test__/core/EventManager.spec.ts
|
|
||||||
--- a/app/__test__/core/EventManager.spec.ts (revision f06777256f332766de4bc76c23183725c8c7d310)
|
|
||||||
+++ b/app/__test__/core/EventManager.spec.ts (date 1731498680965)
|
|
||||||
@@ -1,8 +1,8 @@
|
|
||||||
import { describe, expect, test } from "bun:test";
|
|
||||||
-import { Event, EventManager, NoParamEvent } from "../../src/core/events";
|
|
||||||
+import { Event, EventManager, type ListenerHandler, NoParamEvent } from "../../src/core/events";
|
|
||||||
|
|
||||||
class SpecialEvent extends Event<{ foo: string }> {
|
|
||||||
- static slug = "special-event";
|
|
||||||
+ static override slug = "special-event";
|
|
||||||
|
|
||||||
isBar() {
|
|
||||||
return this.params.foo === "bar";
|
|
||||||
@@ -10,7 +10,19 @@
|
|
||||||
}
|
|
||||||
|
|
||||||
class InformationalEvent extends NoParamEvent {
|
|
||||||
- static slug = "informational-event";
|
|
||||||
+ static override slug = "informational-event";
|
|
||||||
+}
|
|
||||||
+
|
|
||||||
+class ReturnEvent extends Event<{ foo: string }, number> {
|
|
||||||
+ static override slug = "return-event";
|
|
||||||
+ static override returning = true;
|
|
||||||
+
|
|
||||||
+ override setValidatedReturn(value: number) {
|
|
||||||
+ if (typeof value !== "number") {
|
|
||||||
+ throw new Error("Invalid return value");
|
|
||||||
+ }
|
|
||||||
+ this.params.foo = value.toString();
|
|
||||||
+ }
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("EventManager", async () => {
|
|
||||||
@@ -43,4 +55,22 @@
|
|
||||||
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
+
|
|
||||||
+ test.only("piping", async () => {
|
|
||||||
+ const emgr = new EventManager();
|
|
||||||
+ emgr.registerEvents([ReturnEvent, InformationalEvent]);
|
|
||||||
+
|
|
||||||
+ type T = ListenerHandler<ReturnEvent>;
|
|
||||||
+
|
|
||||||
+ // @ts-expect-error InformationalEvent has no return value
|
|
||||||
+ emgr.onEvent(InformationalEvent, async (event, name) => {
|
|
||||||
+ console.log("Event: ", name, event.params);
|
|
||||||
+ return 1;
|
|
||||||
+ });
|
|
||||||
+
|
|
||||||
+ emgr.onEvent(ReturnEvent, async (event, name) => {
|
|
||||||
+ console.log("Event: ", name, event.params);
|
|
||||||
+ return 1;
|
|
||||||
+ });
|
|
||||||
+ });
|
|
||||||
});
|
|
||||||
Index: app/src/core/events/EventManager.ts
|
|
||||||
IDEA additional info:
|
|
||||||
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
|
|
||||||
<+>UTF-8
|
|
||||||
===================================================================
|
|
||||||
diff --git a/app/src/core/events/EventManager.ts b/app/src/core/events/EventManager.ts
|
|
||||||
--- a/app/src/core/events/EventManager.ts (revision f06777256f332766de4bc76c23183725c8c7d310)
|
|
||||||
+++ b/app/src/core/events/EventManager.ts (date 1731498680971)
|
|
||||||
@@ -6,7 +6,7 @@
|
|
||||||
}
|
|
||||||
|
|
||||||
export type EventClass = {
|
|
||||||
- new (params: any): Event;
|
|
||||||
+ new (params: any): Event<any, any>;
|
|
||||||
slug: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -137,6 +137,9 @@
|
|
||||||
throw new Error(`Event "${slug}" not registered`);
|
|
||||||
}
|
|
||||||
|
|
||||||
+ // @ts-expect-error returning is static
|
|
||||||
+ const returning = Boolean(event.constructor.returning);
|
|
||||||
+
|
|
||||||
const listeners = this.listeners.filter((listener) => listener.event.slug === slug);
|
|
||||||
//console.log("---!-- emitting", slug, listeners.length);
|
|
||||||
|
|
||||||
Index: app/src/core/events/EventListener.ts
|
|
||||||
IDEA additional info:
|
|
||||||
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
|
|
||||||
<+>UTF-8
|
|
||||||
===================================================================
|
|
||||||
diff --git a/app/src/core/events/EventListener.ts b/app/src/core/events/EventListener.ts
|
|
||||||
--- a/app/src/core/events/EventListener.ts (revision f06777256f332766de4bc76c23183725c8c7d310)
|
|
||||||
+++ b/app/src/core/events/EventListener.ts (date 1731498680968)
|
|
||||||
@@ -4,10 +4,10 @@
|
|
||||||
export const ListenerModes = ["sync", "async"] as const;
|
|
||||||
export type ListenerMode = (typeof ListenerModes)[number];
|
|
||||||
|
|
||||||
-export type ListenerHandler<E extends Event = Event> = (
|
|
||||||
+export type ListenerHandler<E extends Event<any, any>> = (
|
|
||||||
event: E,
|
|
||||||
- slug: string,
|
|
||||||
-) => Promise<void> | void;
|
|
||||||
+ slug: string
|
|
||||||
+) => E extends Event<any, infer R> ? R | Promise<R> : never;
|
|
||||||
|
|
||||||
export class EventListener<E extends Event = Event> {
|
|
||||||
mode: ListenerMode = "async";
|
|
||||||
Index: app/src/core/events/Event.ts
|
|
||||||
IDEA additional info:
|
|
||||||
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
|
|
||||||
<+>UTF-8
|
|
||||||
===================================================================
|
|
||||||
diff --git a/app/src/core/events/Event.ts b/app/src/core/events/Event.ts
|
|
||||||
--- a/app/src/core/events/Event.ts (revision f06777256f332766de4bc76c23183725c8c7d310)
|
|
||||||
+++ b/app/src/core/events/Event.ts (date 1731498680973)
|
|
||||||
@@ -1,17 +1,25 @@
|
|
||||||
-export abstract class Event<Params = any> {
|
|
||||||
+export abstract class Event<Params = any, Returning = void> {
|
|
||||||
/**
|
|
||||||
* Unique event slug
|
|
||||||
* Must be static, because registering events is done by class
|
|
||||||
*/
|
|
||||||
static slug: string = "untitled-event";
|
|
||||||
params: Params;
|
|
||||||
+ _returning!: Returning;
|
|
||||||
+ static returning: boolean = false;
|
|
||||||
+
|
|
||||||
+ setValidatedReturn(value: Returning): void {
|
|
||||||
+ if (typeof value !== "undefined") {
|
|
||||||
+ throw new Error("Invalid event return value");
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
|
|
||||||
constructor(params: Params) {
|
|
||||||
this.params = params;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
-// @todo: current workaround: potentially there is none and that's the way
|
|
||||||
+// @todo: current workaround: potentially there is "none" and that's the way
|
|
||||||
export class NoParamEvent extends Event<null> {
|
|
||||||
static override slug: string = "noparam-event";
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user