Merge pull request #32 from bknd-io/feat/optimize-seeding

Feat: optimize seeding, adding api typing support
This commit is contained in:
dswbx
2024-12-23 11:45:15 +01:00
committed by GitHub
54 changed files with 883 additions and 443 deletions

View File

@@ -16,7 +16,7 @@ describe("Mutator simple", async () => {
new TextField("label", { required: true, minLength: 1 }), new TextField("label", { required: true, minLength: 1 }),
new NumberField("count", { default_value: 0 }) new NumberField("count", { default_value: 0 })
]); ]);
const em = new EntityManager([items], connection); const em = new EntityManager<any>([items], connection);
await em.connection.kysely.schema await em.connection.kysely.schema
.createTable("items") .createTable("items")
@@ -175,4 +175,18 @@ describe("Mutator simple", async () => {
{ id: 8, label: "keep", count: 0 } { id: 8, label: "keep", count: 0 }
]); ]);
}); });
test("insertMany", async () => {
const oldCount = (await em.repo(items).count()).count;
const inserts = [{ label: "insert 1" }, { label: "insert 2" }];
const { data } = await em.mutator(items).insertMany(inserts);
expect(data.length).toBe(2);
expect(data.map((d) => ({ label: d.label }))).toEqual(inserts);
const newCount = (await em.repo(items).count()).count;
expect(newCount).toBe(oldCount + inserts.length);
const { data: data2 } = await em.repo(items).findMany({ offset: oldCount });
expect(data2).toEqual(data);
});
}); });

View File

@@ -3,6 +3,8 @@ import {
BooleanField, BooleanField,
DateField, DateField,
Entity, Entity,
EntityIndex,
EntityManager,
EnumField, EnumField,
JsonField, JsonField,
ManyToManyRelation, ManyToManyRelation,
@@ -12,6 +14,7 @@ import {
PolymorphicRelation, PolymorphicRelation,
TextField TextField
} from "../../src/data"; } from "../../src/data";
import { DummyConnection } from "../../src/data/connection/DummyConnection";
import { import {
FieldPrototype, FieldPrototype,
type FieldSchema, type FieldSchema,
@@ -20,6 +23,7 @@ import {
boolean, boolean,
date, date,
datetime, datetime,
em,
entity, entity,
enumm, enumm,
json, json,
@@ -46,12 +50,17 @@ describe("prototype", () => {
}); });
test("...2", async () => { test("...2", async () => {
const user = entity("users", { const users = entity("users", {
name: text().required(), name: text(),
bio: text(), bio: text(),
age: number(), age: number(),
some: number().required() some: number()
}); });
type db = {
users: Schema<typeof users>;
};
const obj: Schema<typeof users> = {} as any;
//console.log("user", user.toJSON()); //console.log("user", user.toJSON());
}); });
@@ -266,4 +275,38 @@ describe("prototype", () => {
const obj: Schema<typeof test> = {} as any; const obj: Schema<typeof test> = {} as any;
}); });
test("schema", async () => {
const _em = em(
{
posts: entity("posts", { name: text(), slug: text().required() }),
comments: entity("comments", { some: text() }),
users: entity("users", { email: text() })
},
({ relation, index }, { posts, comments, users }) => {
relation(posts).manyToOne(comments).manyToOne(users);
index(posts).on(["name"]).on(["slug"], true);
}
);
type LocalDb = (typeof _em)["DB"];
const es = [
new Entity("posts", [new TextField("name"), new TextField("slug", { required: true })]),
new Entity("comments", [new TextField("some")]),
new Entity("users", [new TextField("email")])
];
const _em2 = new EntityManager(
es,
new DummyConnection(),
[new ManyToOneRelation(es[0], es[1]), new ManyToOneRelation(es[0], es[2])],
[
new EntityIndex(es[0], [es[0].field("name")!]),
new EntityIndex(es[0], [es[0].field("slug")!], true)
]
);
// @ts-ignore
expect(_em2.toJSON()).toEqual(_em.toJSON());
});
}); });

View File

@@ -22,7 +22,7 @@ describe("[data] Mutator (base)", async () => {
new TextField("hidden", { hidden: true }), new TextField("hidden", { hidden: true }),
new TextField("not_fillable", { fillable: false }) new TextField("not_fillable", { fillable: false })
]); ]);
const em = new EntityManager([entity], dummyConnection); const em = new EntityManager<any>([entity], dummyConnection);
await em.schema().sync({ force: true }); await em.schema().sync({ force: true });
const payload = { label: "item 1", count: 1 }; const payload = { label: "item 1", count: 1 };
@@ -61,7 +61,7 @@ describe("[data] Mutator (ManyToOne)", async () => {
const posts = new Entity("posts", [new TextField("title")]); const posts = new Entity("posts", [new TextField("title")]);
const users = new Entity("users", [new TextField("username")]); const users = new Entity("users", [new TextField("username")]);
const relations = [new ManyToOneRelation(posts, users)]; const relations = [new ManyToOneRelation(posts, users)];
const em = new EntityManager([posts, users], dummyConnection, relations); const em = new EntityManager<any>([posts, users], dummyConnection, relations);
await em.schema().sync({ force: true }); await em.schema().sync({ force: true });
test("RelationMutator", async () => { test("RelationMutator", async () => {
@@ -192,7 +192,7 @@ describe("[data] Mutator (OneToOne)", async () => {
const users = new Entity("users", [new TextField("username")]); const users = new Entity("users", [new TextField("username")]);
const settings = new Entity("settings", [new TextField("theme")]); const settings = new Entity("settings", [new TextField("theme")]);
const relations = [new OneToOneRelation(users, settings)]; const relations = [new OneToOneRelation(users, settings)];
const em = new EntityManager([users, settings], dummyConnection, relations); const em = new EntityManager<any>([users, settings], dummyConnection, relations);
await em.schema().sync({ force: true }); await em.schema().sync({ force: true });
test("insertOne: missing ref", async () => { test("insertOne: missing ref", async () => {
@@ -276,7 +276,7 @@ describe("[data] Mutator (ManyToMany)", async () => {
describe("[data] Mutator (Events)", async () => { describe("[data] Mutator (Events)", async () => {
const entity = new Entity("test", [new TextField("label")]); const entity = new Entity("test", [new TextField("label")]);
const em = new EntityManager([entity], dummyConnection); const em = new EntityManager<any>([entity], dummyConnection);
await em.schema().sync({ force: true }); await em.schema().sync({ force: true });
const events = new Map<string, any>(); const events = new Map<string, any>();

View File

@@ -9,16 +9,44 @@ const watch = args.includes("--watch");
const minify = args.includes("--minify"); const minify = args.includes("--minify");
const types = args.includes("--types"); const types = args.includes("--types");
const sourcemap = args.includes("--sourcemap"); const sourcemap = args.includes("--sourcemap");
const clean = args.includes("--clean");
if (clean) {
console.log("Cleaning dist");
await $`rm -rf dist`;
}
let types_running = false;
function buildTypes() {
if (types_running) return;
types_running = true;
await $`rm -rf dist`;
if (types) {
Bun.spawn(["bun", "build:types"], { Bun.spawn(["bun", "build:types"], {
onExit: () => { onExit: () => {
console.log("Types built"); console.log("Types built");
Bun.spawn(["bun", "tsc-alias"], {
onExit: () => {
console.log("Types aliased");
types_running = false;
}
});
} }
}); });
} }
let watcher_timeout: any;
function delayTypes() {
if (!watch) return;
if (watcher_timeout) {
clearTimeout(watcher_timeout);
}
watcher_timeout = setTimeout(buildTypes, 1000);
}
if (types && !watch) {
buildTypes();
}
/** /**
* Build static assets * Build static assets
* Using esbuild because tsup doesn't include "react" * Using esbuild because tsup doesn't include "react"
@@ -46,7 +74,8 @@ const result = await esbuild.build({
__isDev: "0", __isDev: "0",
"process.env.NODE_ENV": '"production"' "process.env.NODE_ENV": '"production"'
}, },
chunkNames: "chunks/[name]-[hash]" chunkNames: "chunks/[name]-[hash]",
logLevel: "error"
}); });
// Write manifest // Write manifest
@@ -96,6 +125,9 @@ await tsup.build({
treeshake: true, treeshake: true,
loader: { loader: {
".svg": "dataurl" ".svg": "dataurl"
},
onSuccess: async () => {
delayTypes();
} }
}); });
@@ -117,11 +149,12 @@ await tsup.build({
loader: { loader: {
".svg": "dataurl" ".svg": "dataurl"
}, },
onSuccess: async () => {
console.log("--- ui built");
},
esbuildOptions: (options) => { esbuildOptions: (options) => {
options.logLevel = "silent";
options.chunkNames = "chunks/[name]-[hash]"; options.chunkNames = "chunks/[name]-[hash]";
},
onSuccess: async () => {
delayTypes();
} }
}); });
@@ -148,7 +181,10 @@ function baseConfig(adapter: string): tsup.Options {
], ],
metafile: true, metafile: true,
splitting: false, splitting: false,
treeshake: true treeshake: true,
onSuccess: async () => {
delayTypes();
}
}; };
} }

View File

@@ -5,14 +5,14 @@
"bin": "./dist/cli/index.js", "bin": "./dist/cli/index.js",
"version": "0.3.4-alpha1", "version": "0.3.4-alpha1",
"scripts": { "scripts": {
"build:all": "bun run build && bun run build:cli", "build:all": "NODE_ENV=production bun run build.ts --minify --types --clean && bun run build:cli",
"dev": "vite", "dev": "vite",
"test": "ALL_TESTS=1 bun test --bail", "test": "ALL_TESTS=1 bun test --bail",
"build": "NODE_ENV=production bun run build.ts --minify --types", "build": "NODE_ENV=production bun run build.ts --minify --types",
"watch": "bun run build.ts --types --watch", "watch": "bun run build.ts --types --watch",
"types": "bun tsc --noEmit", "types": "bun tsc --noEmit",
"clean:types": "find ./dist -name '*.d.ts' -delete && rm -f ./dist/tsconfig.tsbuildinfo", "clean:types": "find ./dist -name '*.d.ts' -delete && rm -f ./dist/tsconfig.tsbuildinfo",
"build:types": "tsc --emitDeclarationOnly", "build:types": "tsc --emitDeclarationOnly && tsc-alias",
"build:css": "bun tailwindcss -i src/ui/main.css -o ./dist/static/styles.css", "build:css": "bun tailwindcss -i src/ui/main.css -o ./dist/static/styles.css",
"watch:css": "bun tailwindcss --watch -i src/ui/main.css -o ./dist/styles.css", "watch:css": "bun tailwindcss --watch -i src/ui/main.css -o ./dist/styles.css",
"updater": "bun x npm-check-updates -ui", "updater": "bun x npm-check-updates -ui",
@@ -75,6 +75,7 @@
"tailwind-merge": "^2.5.4", "tailwind-merge": "^2.5.4",
"tailwindcss": "^3.4.14", "tailwindcss": "^3.4.14",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"tsc-alias": "^1.8.10",
"tsup": "^8.3.5", "tsup": "^8.3.5",
"vite": "^5.4.10", "vite": "^5.4.10",
"vite-plugin-static-copy": "^2.0.0", "vite-plugin-static-copy": "^2.0.0",
@@ -90,75 +91,75 @@
}, },
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/types/index.d.ts",
"exports": { "exports": {
".": { ".": {
"types": "./dist/index.d.ts", "types": "./dist/types/index.d.ts",
"import": "./dist/index.js", "import": "./dist/index.js",
"require": "./dist/index.cjs" "require": "./dist/index.cjs"
}, },
"./ui": { "./ui": {
"types": "./dist/ui/index.d.ts", "types": "./dist/types/ui/index.d.ts",
"import": "./dist/ui/index.js", "import": "./dist/ui/index.js",
"require": "./dist/ui/index.cjs" "require": "./dist/ui/index.cjs"
}, },
"./client": { "./client": {
"types": "./dist/ui/client/index.d.ts", "types": "./dist/types/ui/client/index.d.ts",
"import": "./dist/ui/client/index.js", "import": "./dist/ui/client/index.js",
"require": "./dist/ui/client/index.cjs" "require": "./dist/ui/client/index.cjs"
}, },
"./data": { "./data": {
"types": "./dist/data/index.d.ts", "types": "./dist/types/data/index.d.ts",
"import": "./dist/data/index.js", "import": "./dist/data/index.js",
"require": "./dist/data/index.cjs" "require": "./dist/data/index.cjs"
}, },
"./core": { "./core": {
"types": "./dist/core/index.d.ts", "types": "./dist/types/core/index.d.ts",
"import": "./dist/core/index.js", "import": "./dist/core/index.js",
"require": "./dist/core/index.cjs" "require": "./dist/core/index.cjs"
}, },
"./utils": { "./utils": {
"types": "./dist/core/utils/index.d.ts", "types": "./dist/types/core/utils/index.d.ts",
"import": "./dist/core/utils/index.js", "import": "./dist/core/utils/index.js",
"require": "./dist/core/utils/index.cjs" "require": "./dist/core/utils/index.cjs"
}, },
"./cli": { "./cli": {
"types": "./dist/cli/index.d.ts", "types": "./dist/types/cli/index.d.ts",
"import": "./dist/cli/index.js", "import": "./dist/cli/index.js",
"require": "./dist/cli/index.cjs" "require": "./dist/cli/index.cjs"
}, },
"./adapter/cloudflare": { "./adapter/cloudflare": {
"types": "./dist/adapter/cloudflare/index.d.ts", "types": "./dist/types/adapter/cloudflare/index.d.ts",
"import": "./dist/adapter/cloudflare/index.js", "import": "./dist/adapter/cloudflare/index.js",
"require": "./dist/adapter/cloudflare/index.cjs" "require": "./dist/adapter/cloudflare/index.cjs"
}, },
"./adapter/vite": { "./adapter/vite": {
"types": "./dist/adapter/vite/index.d.ts", "types": "./dist/types/adapter/vite/index.d.ts",
"import": "./dist/adapter/vite/index.js", "import": "./dist/adapter/vite/index.js",
"require": "./dist/adapter/vite/index.cjs" "require": "./dist/adapter/vite/index.cjs"
}, },
"./adapter/nextjs": { "./adapter/nextjs": {
"types": "./dist/adapter/nextjs/index.d.ts", "types": "./dist/types/adapter/nextjs/index.d.ts",
"import": "./dist/adapter/nextjs/index.js", "import": "./dist/adapter/nextjs/index.js",
"require": "./dist/adapter/nextjs/index.cjs" "require": "./dist/adapter/nextjs/index.cjs"
}, },
"./adapter/remix": { "./adapter/remix": {
"types": "./dist/adapter/remix/index.d.ts", "types": "./dist/types/adapter/remix/index.d.ts",
"import": "./dist/adapter/remix/index.js", "import": "./dist/adapter/remix/index.js",
"require": "./dist/adapter/remix/index.cjs" "require": "./dist/adapter/remix/index.cjs"
}, },
"./adapter/bun": { "./adapter/bun": {
"types": "./dist/adapter/bun/index.d.ts", "types": "./dist/types/adapter/bun/index.d.ts",
"import": "./dist/adapter/bun/index.js", "import": "./dist/adapter/bun/index.js",
"require": "./dist/adapter/bun/index.cjs" "require": "./dist/adapter/bun/index.cjs"
}, },
"./adapter/node": { "./adapter/node": {
"types": "./dist/adapter/node/index.d.ts", "types": "./dist/types/adapter/node/index.d.ts",
"import": "./dist/adapter/node/index.js", "import": "./dist/adapter/node/index.js",
"require": "./dist/adapter/node/index.cjs" "require": "./dist/adapter/node/index.cjs"
}, },
"./adapter/astro": { "./adapter/astro": {
"types": "./dist/adapter/astro/index.d.ts", "types": "./dist/types/adapter/astro/index.d.ts",
"import": "./dist/adapter/astro/index.js", "import": "./dist/adapter/astro/index.js",
"require": "./dist/adapter/astro/index.cjs" "require": "./dist/adapter/astro/index.cjs"
}, },

View File

@@ -38,7 +38,7 @@ export class Api {
private token_transport: "header" | "cookie" | "none" = "header"; private token_transport: "header" | "cookie" | "none" = "header";
public system!: SystemApi; public system!: SystemApi;
public data!: DataApi; public data!: DataApi<DB>;
public auth!: AuthApi; public auth!: AuthApi;
public media!: MediaApi; public media!: MediaApi;

View File

@@ -12,13 +12,17 @@ import { SystemController } from "modules/server/SystemController";
export type AppPlugin<DB> = (app: App<DB>) => void; export type AppPlugin<DB> = (app: App<DB>) => void;
export class AppConfigUpdatedEvent extends Event<{ app: App }> { abstract class AppEvent<A = {}> extends Event<{ app: App } & A> {}
export class AppConfigUpdatedEvent extends AppEvent {
static override slug = "app-config-updated"; static override slug = "app-config-updated";
} }
export class AppBuiltEvent extends Event<{ app: App }> { export class AppBuiltEvent extends AppEvent {
static override slug = "app-built"; static override slug = "app-built";
} }
export const AppEvents = { AppConfigUpdatedEvent, AppBuiltEvent } as const; export class AppFirstBoot extends AppEvent {
static override slug = "app-first-boot";
}
export const AppEvents = { AppConfigUpdatedEvent, AppBuiltEvent, AppFirstBoot } as const;
export type CreateAppConfig = { export type CreateAppConfig = {
connection?: connection?:
@@ -37,6 +41,8 @@ export type AppConfig = InitialModuleConfigs;
export class App<DB = any> { export class App<DB = any> {
modules: ModuleManager; modules: ModuleManager;
static readonly Events = AppEvents; static readonly Events = AppEvents;
adminController?: AdminController;
private trigger_first_boot = false;
constructor( constructor(
private connection: Connection, private connection: Connection,
@@ -48,9 +54,20 @@ export class App<DB = any> {
...moduleManagerOptions, ...moduleManagerOptions,
initial: _initialConfig, initial: _initialConfig,
onUpdated: async (key, config) => { onUpdated: async (key, config) => {
//console.log("[APP] config updated", key, config); // if the EventManager was disabled, we assume we shouldn't
// respond to events, such as "onUpdated".
if (!this.emgr.enabled) {
console.warn("[APP] config updated, but event manager is disabled, skip.");
return;
}
console.log("[APP] config updated", key);
await this.build({ sync: true, save: true }); await this.build({ sync: true, save: true });
await this.emgr.emit(new AppConfigUpdatedEvent({ app: this })); await this.emgr.emit(new AppConfigUpdatedEvent({ app: this }));
},
onFirstBoot: async () => {
console.log("[APP] first boot");
this.trigger_first_boot = true;
} }
}); });
this.modules.ctx().emgr.registerEvents(AppEvents); this.modules.ctx().emgr.registerEvents(AppEvents);
@@ -88,14 +105,24 @@ export class App<DB = any> {
if (options?.save) { if (options?.save) {
await this.modules.save(); await this.modules.save();
} }
// first boot is set from ModuleManager when there wasn't a config table
if (this.trigger_first_boot) {
this.trigger_first_boot = false;
await this.emgr.emit(new AppFirstBoot({ app: this }));
}
} }
mutateConfig<Module extends keyof Modules>(module: Module) { mutateConfig<Module extends keyof Modules>(module: Module) {
return this.modules.get(module).schema(); return this.modules.get(module).schema();
} }
get server() {
return this.modules.server;
}
get fetch(): any { get fetch(): any {
return this.modules.server.fetch; return this.server.fetch;
} }
get module() { get module() {
@@ -119,7 +146,8 @@ export class App<DB = any> {
registerAdminController(config?: AdminControllerOptions) { registerAdminController(config?: AdminControllerOptions) {
// register admin // register admin
this.modules.server.route("/", new AdminController(this, config).getController()); this.adminController = new AdminController(this, config);
this.modules.server.route("/", this.adminController.getController());
return this; return this;
} }

View File

@@ -197,7 +197,7 @@ export class AppAuth extends Module<typeof authConfigSchema> {
throw new Exception("User already exists"); throw new Exception("User already exists");
} }
const payload = { const payload: any = {
...profile, ...profile,
strategy: strategy.getName(), strategy: strategy.getName(),
strategy_value: identifier strategy_value: identifier

View File

@@ -15,6 +15,7 @@ export class EventManager<
> { > {
protected events: EventClass[] = []; protected events: EventClass[] = [];
protected listeners: EventListener[] = []; protected listeners: EventListener[] = [];
enabled: boolean = true;
constructor(events?: RegisteredEvents, listeners?: EventListener[]) { constructor(events?: RegisteredEvents, listeners?: EventListener[]) {
if (events) { if (events) {
@@ -28,6 +29,16 @@ export class EventManager<
} }
} }
enable() {
this.enabled = true;
return this;
}
disable() {
this.enabled = false;
return this;
}
clearEvents() { clearEvents() {
this.events = []; this.events = [];
return this; return this;
@@ -39,6 +50,10 @@ export class EventManager<
return this; return this;
} }
getListeners(): EventListener[] {
return [...this.listeners];
}
get Events(): { [K in keyof RegisteredEvents]: RegisteredEvents[K] } { get Events(): { [K in keyof RegisteredEvents]: RegisteredEvents[K] } {
// proxy class to access events // proxy class to access events
return new Proxy(this, { return new Proxy(this, {
@@ -133,6 +148,11 @@ export class EventManager<
async emit(event: Event) { async emit(event: Event) {
// @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) {
console.log("EventManager disabled, not emitting", slug);
return;
}
if (!this.eventExists(event)) { if (!this.eventExists(event)) {
throw new Error(`Event "${slug}" not registered`); throw new Error(`Event "${slug}" not registered`);
} }

View File

@@ -9,10 +9,25 @@ export async function withDisabledConsole<R>(
fn: () => Promise<R>, fn: () => Promise<R>,
severities: ConsoleSeverity[] = ["log"] severities: ConsoleSeverity[] = ["log"]
): Promise<R> { ): Promise<R> {
const enable = disableConsoleLog(severities); const _oldConsoles = {
const result = await fn(); log: console.log,
enable(); warn: console.warn,
return result; error: console.error
};
disableConsoleLog(severities);
const enable = () => {
Object.entries(_oldConsoles).forEach(([severity, fn]) => {
console[severity as ConsoleSeverity] = fn;
});
};
try {
const result = await fn();
enable();
return result;
} catch (e) {
enable();
throw e;
}
} }
export function disableConsoleLog(severities: ConsoleSeverity[] = ["log"]) { export function disableConsoleLog(severities: ConsoleSeverity[] = ["log"]) {

View File

@@ -1,52 +1,20 @@
import { transformObject } from "core/utils"; import { transformObject } from "core/utils";
import { DataPermissions, Entity, EntityIndex, type EntityManager, type Field } from "data"; import {
DataPermissions,
type Entity,
EntityIndex,
type EntityManager,
constructEntity,
constructRelation
} from "data";
import { Module } from "modules/Module"; import { Module } from "modules/Module";
import { DataController } from "./api/DataController"; import { DataController } from "./api/DataController";
import { import { type AppDataConfig, dataConfigSchema } from "./data-schema";
type AppDataConfig,
FIELDS,
RELATIONS,
type TAppDataEntity,
type TAppDataRelation,
dataConfigSchema
} from "./data-schema";
export class AppData<DB> extends Module<typeof dataConfigSchema> { export class AppData<DB> extends Module<typeof dataConfigSchema> {
static constructEntity(name: string, entityConfig: TAppDataEntity) {
const fields = transformObject(entityConfig.fields ?? {}, (fieldConfig, name) => {
const { type } = fieldConfig;
if (!(type in FIELDS)) {
throw new Error(`Field type "${type}" not found`);
}
const { field } = FIELDS[type as any];
const returnal = new field(name, fieldConfig.config) as Field;
return returnal;
});
// @todo: entity must be migrated to typebox
return new Entity(
name,
Object.values(fields),
entityConfig.config as any,
entityConfig.type as any
);
}
static constructRelation(
relationConfig: TAppDataRelation,
resolver: (name: Entity | string) => Entity
) {
return new RELATIONS[relationConfig.type].cls(
resolver(relationConfig.source),
resolver(relationConfig.target),
relationConfig.config
);
}
override async build() { override async build() {
const entities = transformObject(this.config.entities ?? {}, (entityConfig, name) => { const entities = transformObject(this.config.entities ?? {}, (entityConfig, name) => {
return AppData.constructEntity(name, entityConfig); return constructEntity(name, entityConfig);
}); });
const _entity = (_e: Entity | string): Entity => { const _entity = (_e: Entity | string): Entity => {
@@ -57,7 +25,7 @@ export class AppData<DB> extends Module<typeof dataConfigSchema> {
}; };
const relations = transformObject(this.config.relations ?? {}, (relation) => const relations = transformObject(this.config.relations ?? {}, (relation) =>
AppData.constructRelation(relation, _entity) constructRelation(relation, _entity)
); );
const indices = transformObject(this.config.indices ?? {}, (index, name) => { const indices = transformObject(this.config.indices ?? {}, (index, name) => {

View File

@@ -5,7 +5,7 @@ export type DataApiOptions = BaseModuleApiOptions & {
defaultQuery?: Partial<RepoQuery>; defaultQuery?: Partial<RepoQuery>;
}; };
export class DataApi extends ModuleApi<DataApiOptions> { export class DataApi<DB> extends ModuleApi<DataApiOptions> {
protected override getDefaultOptions(): Partial<DataApiOptions> { protected override getDefaultOptions(): Partial<DataApiOptions> {
return { return {
basepath: "/api/data", basepath: "/api/data",
@@ -15,48 +15,60 @@ export class DataApi extends ModuleApi<DataApiOptions> {
}; };
} }
readOne( readOne<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
entity: string, entity: E,
id: PrimaryFieldType, id: PrimaryFieldType,
query: Partial<Omit<RepoQuery, "where" | "limit" | "offset">> = {} query: Partial<Omit<RepoQuery, "where" | "limit" | "offset">> = {}
) { ) {
return this.get<RepositoryResponse<EntityData>>([entity, id], query); return this.get<Pick<RepositoryResponse<Data>, "meta" | "data">>([entity as any, id], query);
} }
readMany(entity: string, query: Partial<RepoQuery> = {}) { readMany<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
return this.get<Pick<RepositoryResponse, "meta" | "data">>( entity: E,
[entity],
query ?? this.options.defaultQuery
);
}
readManyByReference(
entity: string,
id: PrimaryFieldType,
reference: string,
query: Partial<RepoQuery> = {} query: Partial<RepoQuery> = {}
) { ) {
return this.get<Pick<RepositoryResponse, "meta" | "data">>( return this.get<Pick<RepositoryResponse<Data[]>, "meta" | "data">>(
[entity, id, reference], [entity as any],
query ?? this.options.defaultQuery query ?? this.options.defaultQuery
); );
} }
createOne(entity: string, input: EntityData) { readManyByReference<
return this.post<RepositoryResponse<EntityData>>([entity], input); E extends keyof DB | string,
R extends keyof DB | string,
Data = R extends keyof DB ? DB[R] : EntityData
>(entity: E, id: PrimaryFieldType, reference: R, query: Partial<RepoQuery> = {}) {
return this.get<Pick<RepositoryResponse<Data[]>, "meta" | "data">>(
[entity as any, id, reference],
query ?? this.options.defaultQuery
);
} }
updateOne(entity: string, id: PrimaryFieldType, input: EntityData) { createOne<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
return this.patch<RepositoryResponse<EntityData>>([entity, id], input); entity: E,
input: Omit<Data, "id">
) {
return this.post<RepositoryResponse<Data>>([entity as any], input);
} }
deleteOne(entity: string, id: PrimaryFieldType) { updateOne<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
return this.delete<RepositoryResponse<EntityData>>([entity, id]); entity: E,
id: PrimaryFieldType,
input: Partial<Omit<Data, "id">>
) {
return this.patch<RepositoryResponse<Data>>([entity as any, id], input);
} }
count(entity: string, where: RepoQuery["where"] = {}) { deleteOne<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
return this.post<RepositoryResponse<{ entity: string; count: number }>>( entity: E,
[entity, "fn", "count"], id: PrimaryFieldType
) {
return this.delete<RepositoryResponse<Data>>([entity as any, id]);
}
count<E extends keyof DB | string>(entity: E, where: RepoQuery["where"] = {}) {
return this.post<RepositoryResponse<{ entity: E; count: number }>>(
[entity as any, "fn", "count"],
where where
); );
} }

View File

@@ -1,5 +1,5 @@
import { type ClassController, isDebug, tbValidator as tb } from "core"; import { type ClassController, isDebug, tbValidator as tb } from "core";
import { Type, objectCleanEmpty, objectTransform } from "core/utils"; import { StringEnum, Type, objectCleanEmpty, objectTransform } from "core/utils";
import { import {
DataPermissions, DataPermissions,
type EntityData, type EntityData,
@@ -165,13 +165,12 @@ export class DataController implements ClassController {
// read entity schema // read entity schema
.get("/schema.json", async (c) => { .get("/schema.json", async (c) => {
this.guard.throwUnlessGranted(DataPermissions.entityRead); this.guard.throwUnlessGranted(DataPermissions.entityRead);
const url = new URL(c.req.url); const $id = `${this.config.basepath}/schema.json`;
const $id = `${url.origin}${this.config.basepath}/schema.json`;
const schemas = Object.fromEntries( const schemas = Object.fromEntries(
this.em.entities.map((e) => [ this.em.entities.map((e) => [
e.name, e.name,
{ {
$ref: `schemas/${e.name}` $ref: `${this.config.basepath}/schemas/${e.name}`
} }
]) ])
); );
@@ -183,22 +182,28 @@ export class DataController implements ClassController {
}) })
// read schema // read schema
.get( .get(
"/schemas/:entity", "/schemas/:entity/:context?",
tb("param", Type.Object({ entity: Type.String() })), tb(
"param",
Type.Object({
entity: Type.String(),
context: Type.Optional(StringEnum(["create", "update"]))
})
),
async (c) => { async (c) => {
this.guard.throwUnlessGranted(DataPermissions.entityRead); this.guard.throwUnlessGranted(DataPermissions.entityRead);
//console.log("request", c.req.raw); //console.log("request", c.req.raw);
const { entity } = c.req.param(); const { entity, context } = c.req.param();
if (!this.entityExists(entity)) { if (!this.entityExists(entity)) {
console.log("not found", entity, definedEntities); console.log("not found", entity, definedEntities);
return c.notFound(); return c.notFound();
} }
const _entity = this.em.entity(entity); const _entity = this.em.entity(entity);
const schema = _entity.toSchema(); const schema = _entity.toSchema({ context } as any);
const url = new URL(c.req.url); const url = new URL(c.req.url);
const base = `${url.origin}${this.config.basepath}`; const base = `${url.origin}${this.config.basepath}`;
const $id = `${base}/schemas/${entity}`; const $id = `${this.config.basepath}/schemas/${entity}`;
return c.json({ return c.json({
$schema: `${base}/schema.json`, $schema: `${base}/schema.json`,
$id, $id,

View File

@@ -0,0 +1,7 @@
import { Connection } from "./Connection";
export class DummyConnection extends Connection {
constructor() {
super(undefined as any);
}
}

View File

@@ -158,7 +158,7 @@ export class Entity<
} }
get label(): string { get label(): string {
return snakeToPascalWithSpaces(this.config.name || this.name); return this.config.name ?? snakeToPascalWithSpaces(this.name);
} }
field(name: string): Field | undefined { field(name: string): Field | undefined {
@@ -210,20 +210,34 @@ export class Entity<
return true; return true;
} }
toSchema(clean?: boolean): object { toSchema(options?: { clean: boolean; context?: "create" | "update" }): object {
const fields = Object.fromEntries(this.fields.map((field) => [field.name, field])); let fields: Field[];
switch (options?.context) {
case "create":
case "update":
fields = this.getFillableFields(options.context);
break;
default:
fields = this.getFields(true);
}
const _fields = Object.fromEntries(fields.map((field) => [field.name, field]));
const schema = Type.Object( const schema = Type.Object(
transformObject(fields, (field) => ({ transformObject(_fields, (field) => {
title: field.config.label, //const hidden = field.isHidden(options?.context);
$comment: field.config.description, const fillable = field.isFillable(options?.context);
$field: field.type, return {
readOnly: !field.isFillable("update") ? true : undefined, title: field.config.label,
writeOnly: !field.isFillable("create") ? true : undefined, $comment: field.config.description,
...field.toJsonSchema() $field: field.type,
})) readOnly: !fillable ? true : undefined,
...field.toJsonSchema()
};
}),
{ additionalProperties: false }
); );
return clean ? JSON.parse(JSON.stringify(schema)) : schema; return options?.clean ? JSON.parse(JSON.stringify(schema)) : schema;
} }
toJSON() { toJSON() {

View File

@@ -14,6 +14,14 @@ import { SchemaManager } from "../schema/SchemaManager";
import { Entity } from "./Entity"; import { Entity } from "./Entity";
import { type EntityData, Mutator, Repository } from "./index"; import { type EntityData, Mutator, Repository } from "./index";
type EntitySchema<E extends Entity | string, DB = any> = E extends Entity<infer Name>
? Name extends keyof DB
? Name
: never
: E extends keyof DB
? E
: never;
export class EntityManager<DB> { export class EntityManager<DB> {
connection: Connection; connection: Connection;
@@ -87,10 +95,16 @@ export class EntityManager<DB> {
this.entities.push(entity); this.entities.push(entity);
} }
entity(name: string): Entity { entity(e: Entity | string): Entity {
const entity = this.entities.find((e) => e.name === name); let entity: Entity | undefined;
if (typeof e === "string") {
entity = this.entities.find((entity) => entity.name === e);
} else {
entity = e;
}
if (!entity) { if (!entity) {
throw new EntityNotDefinedException(name); throw new EntityNotDefinedException(typeof e === "string" ? e : e.name);
} }
return entity; return entity;
@@ -162,28 +176,16 @@ export class EntityManager<DB> {
return this.relations.relationReferencesOf(this.entity(entity_name)); return this.relations.relationReferencesOf(this.entity(entity_name));
} }
repository(_entity: Entity | string) { repository<E extends Entity | string>(entity: E): Repository<DB, EntitySchema<E, DB>> {
const entity = _entity instanceof Entity ? _entity : this.entity(_entity); return this.repo(entity);
return new Repository(this, entity, this.emgr);
} }
repo<E extends Entity>( repo<E extends Entity | string>(entity: E): Repository<DB, EntitySchema<E, DB>> {
_entity: E return new Repository(this, this.entity(entity), this.emgr);
): Repository<
DB,
E extends Entity<infer Name> ? (Name extends keyof DB ? Name : never) : never
> {
return new Repository(this, _entity, this.emgr);
} }
_repo<TB extends keyof DB>(_entity: TB): Repository<DB, TB> { mutator<E extends Entity | string>(entity: E): Mutator<DB, EntitySchema<E, DB>> {
const entity = this.entity(_entity as any); return new Mutator(this, this.entity(entity), this.emgr);
return new Repository(this, entity, this.emgr);
}
mutator(_entity: Entity | string) {
const entity = _entity instanceof Entity ? _entity : this.entity(_entity);
return new Mutator(this, entity, this.emgr);
} }
addIndex(index: EntityIndex, force = false) { addIndex(index: EntityIndex, force = false) {

View File

@@ -25,7 +25,13 @@ export type MutatorResponse<T = EntityData[]> = {
data: T; data: T;
}; };
export class Mutator<DB> implements EmitsEvents { export class Mutator<
DB = any,
TB extends keyof DB = any,
Output = DB[TB],
Input = Omit<Output, "id">
> implements EmitsEvents
{
em: EntityManager<DB>; em: EntityManager<DB>;
entity: Entity; entity: Entity;
static readonly Events = MutatorEvents; static readonly Events = MutatorEvents;
@@ -47,13 +53,13 @@ export class Mutator<DB> implements EmitsEvents {
return this.em.connection.kysely; return this.em.connection.kysely;
} }
async getValidatedData(data: EntityData, context: TActionContext): Promise<EntityData> { async getValidatedData<Given = any>(data: Given, context: TActionContext): Promise<Given> {
const entity = this.entity; const entity = this.entity;
if (!context) { if (!context) {
throw new Error("Context must be provided for validation"); throw new Error("Context must be provided for validation");
} }
const keys = Object.keys(data); const keys = Object.keys(data as any);
const validatedData: EntityData = {}; const validatedData: EntityData = {};
// get relational references/keys // get relational references/keys
@@ -95,7 +101,7 @@ export class Mutator<DB> implements EmitsEvents {
throw new Error(`No data left to update "${entity.name}"`); throw new Error(`No data left to update "${entity.name}"`);
} }
return validatedData; return validatedData as Given;
} }
protected async many(qb: MutatorQB): Promise<MutatorResponse> { protected async many(qb: MutatorQB): Promise<MutatorResponse> {
@@ -120,7 +126,7 @@ export class Mutator<DB> implements EmitsEvents {
return { ...response, data: data[0]! }; return { ...response, data: data[0]! };
} }
async insertOne(data: EntityData): Promise<MutatorResponse<EntityData>> { async insertOne(data: Input): Promise<MutatorResponse<Output>> {
const entity = this.entity; const entity = this.entity;
if (entity.type === "system" && this.__unstable_disable_system_entity_creation) { if (entity.type === "system" && this.__unstable_disable_system_entity_creation) {
throw new Error(`Creation of system entity "${entity.name}" is disabled`); throw new Error(`Creation of system entity "${entity.name}" is disabled`);
@@ -154,10 +160,10 @@ export class Mutator<DB> implements EmitsEvents {
await this.emgr.emit(new Mutator.Events.MutatorInsertAfter({ entity, data: res.data })); await this.emgr.emit(new Mutator.Events.MutatorInsertAfter({ entity, data: res.data }));
return res; return res as any;
} }
async updateOne(id: PrimaryFieldType, data: EntityData): Promise<MutatorResponse<EntityData>> { async updateOne(id: PrimaryFieldType, data: Input): Promise<MutatorResponse<Output>> {
const entity = this.entity; const entity = this.entity;
if (!Number.isInteger(id)) { if (!Number.isInteger(id)) {
throw new Error("ID must be provided for update"); throw new Error("ID must be provided for update");
@@ -166,12 +172,16 @@ export class Mutator<DB> implements EmitsEvents {
const validatedData = await this.getValidatedData(data, "update"); const validatedData = await this.getValidatedData(data, "update");
await this.emgr.emit( await this.emgr.emit(
new Mutator.Events.MutatorUpdateBefore({ entity, entityId: id, data: validatedData }) new Mutator.Events.MutatorUpdateBefore({
entity,
entityId: id,
data: validatedData as any
})
); );
const query = this.conn const query = this.conn
.updateTable(entity.name) .updateTable(entity.name)
.set(validatedData) .set(validatedData as any)
.where(entity.id().name, "=", id) .where(entity.id().name, "=", id)
.returning(entity.getSelect()); .returning(entity.getSelect());
@@ -181,10 +191,10 @@ export class Mutator<DB> implements EmitsEvents {
new Mutator.Events.MutatorUpdateAfter({ entity, entityId: id, data: res.data }) new Mutator.Events.MutatorUpdateAfter({ entity, entityId: id, data: res.data })
); );
return res; return res as any;
} }
async deleteOne(id: PrimaryFieldType): Promise<MutatorResponse<EntityData>> { async deleteOne(id: PrimaryFieldType): Promise<MutatorResponse<Output>> {
const entity = this.entity; const entity = this.entity;
if (!Number.isInteger(id)) { if (!Number.isInteger(id)) {
throw new Error("ID must be provided for deletion"); throw new Error("ID must be provided for deletion");
@@ -203,7 +213,7 @@ export class Mutator<DB> implements EmitsEvents {
new Mutator.Events.MutatorDeleteAfter({ entity, entityId: id, data: res.data }) new Mutator.Events.MutatorDeleteAfter({ entity, entityId: id, data: res.data })
); );
return res; return res as any;
} }
private getValidOptions(options?: Partial<RepoQuery>): Partial<RepoQuery> { private getValidOptions(options?: Partial<RepoQuery>): Partial<RepoQuery> {
@@ -250,47 +260,59 @@ export class Mutator<DB> implements EmitsEvents {
} }
// @todo: decide whether entries should be deleted all at once or one by one (for events) // @todo: decide whether entries should be deleted all at once or one by one (for events)
async deleteWhere(where?: RepoQuery["where"]): Promise<MutatorResponse<EntityData>> { async deleteWhere(where?: RepoQuery["where"]): Promise<MutatorResponse<Output[]>> {
const entity = this.entity; const entity = this.entity;
const qb = this.appendWhere(this.conn.deleteFrom(entity.name), where).returning( const qb = this.appendWhere(this.conn.deleteFrom(entity.name), where).returning(
entity.getSelect() entity.getSelect()
); );
//await this.emgr.emit(new Mutator.Events.MutatorDeleteBefore({ entity, entityId: id })); return (await this.many(qb)) as any;
const res = await this.many(qb);
/*await this.emgr.emit(
new Mutator.Events.MutatorDeleteAfter({ entity, entityId: id, data: res.data })
);*/
return res;
} }
async updateWhere( async updateWhere(data: Partial<Input>, where?: RepoQuery["where"]): Promise<MutatorResponse<Output[]>> {
data: EntityData,
where?: RepoQuery["where"]
): Promise<MutatorResponse<EntityData>> {
const entity = this.entity; const entity = this.entity;
const validatedData = await this.getValidatedData(data, "update"); const validatedData = await this.getValidatedData(data, "update");
/*await this.emgr.emit(
new Mutator.Events.MutatorUpdateBefore({ entity, entityId: id, data: validatedData })
);*/
const query = this.appendWhere(this.conn.updateTable(entity.name), where) const query = this.appendWhere(this.conn.updateTable(entity.name), where)
.set(validatedData) .set(validatedData as any)
//.where(entity.id().name, "=", id)
.returning(entity.getSelect()); .returning(entity.getSelect());
const res = await this.many(query); return (await this.many(query)) as any;
}
/*await this.emgr.emit( async insertMany(data: Input[]): Promise<MutatorResponse<Output[]>> {
new Mutator.Events.MutatorUpdateAfter({ entity, entityId: id, data: res.data }) const entity = this.entity;
);*/ if (entity.type === "system" && this.__unstable_disable_system_entity_creation) {
throw new Error(`Creation of system entity "${entity.name}" is disabled`);
}
return res; const validated: any[] = [];
for (const row of data) {
const validatedData = {
...entity.getDefaultObject(),
...(await this.getValidatedData(row, "create"))
};
// check if required fields are present
const required = entity.getRequiredFields();
for (const field of required) {
if (
typeof validatedData[field.name] === "undefined" ||
validatedData[field.name] === null
) {
throw new Error(`Field "${field.name}" is required`);
}
}
validated.push(validatedData);
}
const query = this.conn
.insertInto(entity.name)
.values(validated)
.returning(entity.getSelect());
return (await this.many(query)) as any;
} }
} }

View File

@@ -272,7 +272,7 @@ export class Repository<DB = any, TB extends keyof DB = any> implements EmitsEve
async findId( async findId(
id: PrimaryFieldType, id: PrimaryFieldType,
_options?: Partial<Omit<RepoQuery, "where" | "limit" | "offset">> _options?: Partial<Omit<RepoQuery, "where" | "limit" | "offset">>
): Promise<RepositoryResponse<DB[TB]>> { ): Promise<RepositoryResponse<DB[TB] | undefined>> {
const { qb, options } = this.buildQuery( const { qb, options } = this.buildQuery(
{ {
..._options, ..._options,

View File

@@ -104,6 +104,12 @@ export class TextField<Required extends true | false = false> extends Field<
); );
} }
if (this.config.pattern && value && !new RegExp(this.config.pattern).test(value)) {
throw new TransformPersistFailedException(
`Field "${this.name}" must match the pattern ${this.config.pattern}`
);
}
return value; return value;
} }

View File

@@ -18,6 +18,8 @@ export { LibsqlConnection, type LibSqlCredentials } from "./connection/LibsqlCon
export { SqliteConnection } from "./connection/SqliteConnection"; export { SqliteConnection } from "./connection/SqliteConnection";
export { SqliteLocalConnection } from "./connection/SqliteLocalConnection"; export { SqliteLocalConnection } from "./connection/SqliteLocalConnection";
export { constructEntity, constructRelation } from "./schema/constructor";
export const DatabaseEvents = { export const DatabaseEvents = {
...MutatorEvents, ...MutatorEvents,
...RepositoryEvents ...RepositoryEvents

View File

@@ -1,3 +1,8 @@
import { DummyConnection } from "data/connection/DummyConnection";
import { EntityManager } from "data/entities/EntityManager";
import type { Generated } from "kysely";
import { MediaField, type MediaFieldConfig, type MediaItem } from "media/MediaField";
import type { ModuleConfigs } from "modules";
import { import {
BooleanField, BooleanField,
type BooleanFieldConfig, type BooleanFieldConfig,
@@ -5,6 +10,8 @@ import {
type DateFieldConfig, type DateFieldConfig,
Entity, Entity,
type EntityConfig, type EntityConfig,
EntityIndex,
type EntityRelation,
EnumField, EnumField,
type EnumFieldConfig, type EnumFieldConfig,
type Field, type Field,
@@ -25,15 +32,14 @@ import {
type TEntityType, type TEntityType,
TextField, TextField,
type TextFieldConfig type TextFieldConfig
} from "data"; } from "../index";
import type { Generated } from "kysely";
import { MediaField, type MediaFieldConfig, type MediaItem } from "media/MediaField";
type Options<Config = any> = { type Options<Config = any> = {
entity: { name: string; fields: Record<string, Field<any, any, any>> }; entity: { name: string; fields: Record<string, Field<any, any, any>> };
field_name: string; field_name: string;
config: Config; config: Config;
is_required: boolean; is_required: boolean;
another?: string;
}; };
const FieldMap = { const FieldMap = {
@@ -239,7 +245,89 @@ export function relation<Local extends Entity>(local: Local) {
}; };
} }
type InferEntityFields<T> = T extends Entity<infer _N, infer Fields> export function index<E extends Entity>(entity: E) {
return {
on: (fields: (keyof InsertSchema<E>)[], unique?: boolean) => {
const _fields = fields.map((f) => {
const field = entity.field(f as any);
if (!field) {
throw new Error(`Field "${String(f)}" not found on entity "${entity.name}"`);
}
return field;
});
return new EntityIndex(entity, _fields, unique);
}
};
}
class EntityManagerPrototype<Entities extends Record<string, Entity>> extends EntityManager<
Schema<Entities>
> {
constructor(
public __entities: Entities,
relations: EntityRelation[] = [],
indices: EntityIndex[] = []
) {
super(Object.values(__entities), new DummyConnection(), relations, indices);
}
}
type Chained<Fn extends (...args: any[]) => any, Rt = ReturnType<Fn>> = <E extends Entity>(
e: E
) => {
[K in keyof Rt]: Rt[K] extends (...args: any[]) => any
? (...args: Parameters<Rt[K]>) => Rt
: never;
};
export function em<Entities extends Record<string, Entity>>(
entities: Entities,
schema?: (
fns: { relation: Chained<typeof relation>; index: Chained<typeof index> },
entities: Entities
) => void
) {
const relations: EntityRelation[] = [];
const indices: EntityIndex[] = [];
const relationProxy = (e: Entity) => {
return new Proxy(relation(e), {
get(target, prop) {
return (...args: any[]) => {
relations.push(target[prop](...args));
return relationProxy(e);
};
}
}) as any;
};
const indexProxy = (e: Entity) => {
return new Proxy(index(e), {
get(target, prop) {
return (...args: any[]) => {
indices.push(target[prop](...args));
return indexProxy(e);
};
}
}) as any;
};
if (schema) {
schema({ relation: relationProxy, index: indexProxy }, entities);
}
const e = new EntityManagerPrototype(entities, relations, indices);
return {
DB: e.__entities as unknown as Schemas<Entities>,
entities: e.__entities,
relations,
indices,
toJSON: () =>
e.toJSON() as unknown as Pick<ModuleConfigs["data"], "entities" | "relations" | "indices">
};
}
export type InferEntityFields<T> = T extends Entity<infer _N, infer Fields>
? { ? {
[K in keyof Fields]: Fields[K] extends { _type: infer Type; _required: infer Required } [K in keyof Fields]: Fields[K] extends { _type: infer Type; _required: infer Required }
? Required extends true ? Required extends true
@@ -284,12 +372,16 @@ type OptionalUndefined<
} }
>; >;
type InferField<Field> = Field extends { _type: infer Type; _required: infer Required } export type InferField<Field> = Field extends { _type: infer Type; _required: infer Required }
? Required extends true ? Required extends true
? Type ? Type
: Type | undefined : Type | undefined
: never; : never;
export type Schemas<T extends Record<string, Entity>> = {
[K in keyof T]: Schema<T[K]>;
};
export type InsertSchema<T> = Simplify<OptionalUndefined<InferEntityFields<T>>>; export type InsertSchema<T> = Simplify<OptionalUndefined<InferEntityFields<T>>>;
export type Schema<T> = { id: Generated<number> } & InsertSchema<T>; export type Schema<T> = Simplify<{ id: Generated<number> } & InsertSchema<T>>;
export type FieldSchema<T> = Simplify<OptionalUndefined<InferFields<T>>>; export type FieldSchema<T> = Simplify<OptionalUndefined<InferFields<T>>>;

View File

@@ -0,0 +1,34 @@
import { transformObject } from "core/utils";
import { Entity, type Field } from "data";
import { FIELDS, RELATIONS, type TAppDataEntity, type TAppDataRelation } from "data/data-schema";
export function constructEntity(name: string, entityConfig: TAppDataEntity) {
const fields = transformObject(entityConfig.fields ?? {}, (fieldConfig, name) => {
const { type } = fieldConfig;
if (!(type in FIELDS)) {
throw new Error(`Field type "${type}" not found`);
}
const { field } = FIELDS[type as any];
const returnal = new field(name, fieldConfig.config) as Field;
return returnal;
});
return new Entity(
name,
Object.values(fields),
entityConfig.config as any,
entityConfig.type as any
);
}
export function constructRelation(
relationConfig: TAppDataRelation,
resolver: (name: Entity | string) => Entity
) {
return new RELATIONS[relationConfig.type].cls(
resolver(relationConfig.source),
resolver(relationConfig.target),
relationConfig.config
);
}

View File

@@ -4,8 +4,10 @@ export {
getDefaultConfig, getDefaultConfig,
getDefaultSchema, getDefaultSchema,
type ModuleConfigs, type ModuleConfigs,
type ModuleSchemas type ModuleSchemas,
} from "modules/ModuleManager"; type ModuleManagerOptions,
type ModuleBuildContext
} from "./modules/ModuleManager";
export { registries } from "modules/registries"; export { registries } from "modules/registries";

View File

@@ -174,7 +174,7 @@ export class MediaController implements ClassController {
const result = await mutator.insertOne({ const result = await mutator.insertOne({
...this.media.uploadedEventDataToMediaPayload(info), ...this.media.uploadedEventDataToMediaPayload(info),
...mediaRef ...mediaRef
}); } as any);
mutator.__unstable_toggleSystemEntityCreation(true); mutator.__unstable_toggleSystemEntityCreation(true);
// delete items if needed // delete items if needed

View File

@@ -5,10 +5,10 @@ import type { Static, TSchema } from "core/utils";
import type { Connection, EntityManager } from "data"; import type { Connection, EntityManager } from "data";
import type { Hono } from "hono"; import type { Hono } from "hono";
export type ModuleBuildContext = { export type ModuleBuildContext<DB = any> = {
connection: Connection; connection: Connection;
server: Hono<any>; server: Hono<any>;
em: EntityManager<any>; em: EntityManager<DB>;
emgr: EventManager<any>; emgr: EventManager<any>;
guard: Guard; guard: Guard;
}; };

View File

@@ -35,6 +35,8 @@ import { AppFlows } from "../flows/AppFlows";
import { AppMedia } from "../media/AppMedia"; import { AppMedia } from "../media/AppMedia";
import type { Module, ModuleBuildContext } from "./Module"; import type { Module, ModuleBuildContext } from "./Module";
export type { ModuleBuildContext };
export const MODULES = { export const MODULES = {
server: AppServer, server: AppServer,
data: AppData<any>, data: AppData<any>,
@@ -73,9 +75,14 @@ export type ModuleManagerOptions = {
module: Module, module: Module,
config: ModuleConfigs[Module] config: ModuleConfigs[Module]
) => Promise<void>; ) => Promise<void>;
// triggered when no config table existed
onFirstBoot?: () => Promise<void>;
// base path for the hono instance // base path for the hono instance
basePath?: string; basePath?: string;
// doesn't perform validity checks for given/fetched config
trustFetched?: boolean; trustFetched?: boolean;
// runs when initial config provided on a fresh database
seed?: (ctx: ModuleBuildContext) => Promise<void>;
}; };
type ConfigTable<Json = ModuleConfigs> = { type ConfigTable<Json = ModuleConfigs> = {
@@ -294,7 +301,7 @@ export class ModuleManager {
version, version,
json: configs, json: configs,
updated_at: new Date() updated_at: new Date()
}, } as any,
{ {
type: "config", type: "config",
version version
@@ -448,6 +455,9 @@ export class ModuleManager {
await this.buildModules(); await this.buildModules();
await this.save(); await this.save();
// run initial setup
await this.setupInitial();
this.logger.clear(); this.logger.clear();
return this; return this;
} }
@@ -462,6 +472,21 @@ export class ModuleManager {
return this; return this;
} }
protected async setupInitial() {
const ctx = {
...this.ctx(),
// disable events for initial setup
em: this.ctx().em.fork()
};
// perform a sync
await ctx.em.schema().sync({ force: true });
await this.options?.seed?.(ctx);
// run first boot event
await this.options?.onFirstBoot?.();
}
get<K extends keyof Modules>(key: K): Modules[K] { get<K extends keyof Modules>(key: K): Modules[K] {
if (!(key in this.modules)) { if (!(key in this.modules)) {
throw new Error(`Module "${key}" doesn't exist, cannot get`); throw new Error(`Module "${key}" doesn't exist, cannot get`);

View File

@@ -5,14 +5,14 @@ import { useApi } from "ui/client";
export const useApiQuery = < export const useApiQuery = <
Data, Data,
RefineFn extends (data: ResponseObject<Data>) => any = (data: ResponseObject<Data>) => Data RefineFn extends (data: ResponseObject<Data>) => unknown = (data: ResponseObject<Data>) => Data
>( >(
fn: (api: Api) => FetchPromise<Data>, fn: (api: Api) => FetchPromise<Data>,
options?: SWRConfiguration & { enabled?: boolean; refine?: RefineFn } options?: SWRConfiguration & { enabled?: boolean; refine?: RefineFn }
) => { ) => {
const api = useApi(); const api = useApi();
const promise = fn(api); const promise = fn(api);
const refine = options?.refine ?? ((data: ResponseObject<Data>) => data); const refine = options?.refine ?? ((data: any) => data);
const fetcher = () => promise.execute().then(refine); const fetcher = () => promise.execute().then(refine);
const key = promise.key(); const key = promise.key();

View File

@@ -10,7 +10,7 @@ type OmitFirstArg<F> = F extends (x: any, ...args: infer P) => any
* the first argument "entity" for convenience * the first argument "entity" for convenience
* @param entity * @param entity
*/ */
export const useData = <T extends keyof DataApi>(entity: string) => { export const useData = <T extends keyof DataApi<DB>>(entity: string) => {
const api = useApi().data; const api = useApi().data;
const methods = [ const methods = [
"readOne", "readOne",

View File

@@ -1,23 +1,40 @@
import type { PrimaryFieldType } from "core"; import type { PrimaryFieldType } from "core";
import { objectTransform } from "core/utils"; import { encodeSearch, objectTransform } from "core/utils";
import type { EntityData, RepoQuery } from "data"; import type { EntityData, RepoQuery } from "data";
import type { ResponseObject } from "modules/ModuleApi"; import type { ModuleApi, ResponseObject } from "modules/ModuleApi";
import useSWR, { type SWRConfiguration } from "swr"; import useSWR, { type SWRConfiguration, mutate } from "swr";
import { useApi } from "ui/client"; import { type Api, useApi } from "ui/client";
export class UseEntityApiError<Payload = any> extends Error { export class UseEntityApiError<Payload = any> extends Error {
constructor( constructor(
public payload: Payload, public response: ResponseObject<Payload>,
public response: Response, fallback?: string
message?: string
) { ) {
let message = fallback;
if ("error" in response) {
message = response.error as string;
if (fallback) {
message = `${fallback}: ${message}`;
}
}
super(message ?? "UseEntityApiError"); super(message ?? "UseEntityApiError");
} }
} }
function Test() {
const { read } = useEntity("users");
async () => {
const data = await read();
};
return null;
}
export const useEntity = < export const useEntity = <
Entity extends string, Entity extends keyof DB | string,
Id extends PrimaryFieldType | undefined = undefined Id extends PrimaryFieldType | undefined = undefined,
Data = Entity extends keyof DB ? DB[Entity] : EntityData
>( >(
entity: Entity, entity: Entity,
id?: Id id?: Id
@@ -25,27 +42,30 @@ export const useEntity = <
const api = useApi().data; const api = useApi().data;
return { return {
create: async (input: EntityData) => { create: async (input: Omit<Data, "id">) => {
const res = await api.createOne(entity, input); const res = await api.createOne(entity, input);
if (!res.ok) { if (!res.ok) {
throw new UseEntityApiError(res.data, res.res, "Failed to create entity"); throw new UseEntityApiError(res, `Failed to create entity "${entity}"`);
} }
return res; return res;
}, },
read: async (query: Partial<RepoQuery> = {}) => { read: async (query: Partial<RepoQuery> = {}) => {
const res = id ? await api.readOne(entity, id!, query) : await api.readMany(entity, query); const res = id ? await api.readOne(entity, id!, query) : await api.readMany(entity, query);
if (!res.ok) { if (!res.ok) {
throw new UseEntityApiError(res.data, res.res, "Failed to read entity"); throw new UseEntityApiError(res as any, `Failed to read entity "${entity}"`);
} }
return res; // must be manually typed
return res as unknown as Id extends undefined
? ResponseObject<Data[]>
: ResponseObject<Data>;
}, },
update: async (input: Partial<EntityData>, _id: PrimaryFieldType | undefined = id) => { update: async (input: Partial<Omit<Data, "id">>, _id: PrimaryFieldType | undefined = id) => {
if (!_id) { if (!_id) {
throw new Error("id is required"); throw new Error("id is required");
} }
const res = await api.updateOne(entity, _id, input); const res = await api.updateOne(entity, _id, input);
if (!res.ok) { if (!res.ok) {
throw new UseEntityApiError(res.data, res.res, "Failed to update entity"); throw new UseEntityApiError(res, `Failed to update entity "${entity}"`);
} }
return res; return res;
}, },
@@ -56,44 +76,67 @@ export const useEntity = <
const res = await api.deleteOne(entity, _id); const res = await api.deleteOne(entity, _id);
if (!res.ok) { if (!res.ok) {
throw new UseEntityApiError(res.data, res.res, "Failed to delete entity"); throw new UseEntityApiError(res, `Failed to delete entity "${entity}"`);
} }
return res; return res;
} }
}; };
}; };
// @todo: try to get from ModuleApi directly
export function makeKey(
api: ModuleApi,
entity: string,
id?: PrimaryFieldType,
query?: Partial<RepoQuery>
) {
return (
"/" +
[...(api.options?.basepath?.split("/") ?? []), entity, ...(id ? [id] : [])]
.filter(Boolean)
.join("/") +
(query ? "?" + encodeSearch(query) : "")
);
}
export const useEntityQuery = < export const useEntityQuery = <
Entity extends string, Entity extends keyof DB | string,
Id extends PrimaryFieldType | undefined = undefined Id extends PrimaryFieldType | undefined = undefined
>( >(
entity: Entity, entity: Entity,
id?: Id, id?: Id,
query?: Partial<RepoQuery>, query?: Partial<RepoQuery>,
options?: SWRConfiguration & { enabled?: boolean } options?: SWRConfiguration & { enabled?: boolean; revalidateOnMutate?: boolean }
) => { ) => {
const api = useApi().data; const api = useApi().data;
const key = const key = makeKey(api, entity, id, query);
options?.enabled !== false const { read, ...actions } = useEntity<Entity, Id>(entity, id);
? [...(api.options?.basepath?.split("/") ?? []), entity, ...(id ? [id] : [])].filter(
Boolean
)
: null;
const { read, ...actions } = useEntity(entity, id) as any;
const fetcher = () => read(query); const fetcher = () => read(query);
type T = Awaited<ReturnType<(typeof api)[Id extends undefined ? "readMany" : "readOne"]>>; type T = Awaited<ReturnType<typeof fetcher>>;
const swr = useSWR<T>(key, fetcher, { const swr = useSWR<T>(options?.enabled === false ? null : key, fetcher as any, {
revalidateOnFocus: false, revalidateOnFocus: false,
keepPreviousData: false, keepPreviousData: true,
...options ...options
}); });
const mapped = objectTransform(actions, (action) => { const mutateAll = async () => {
if (action === "read") return; const entityKey = makeKey(api, entity);
return mutate((key) => typeof key === "string" && key.startsWith(entityKey), undefined, {
revalidate: true
});
};
return async (...args) => { const mapped = objectTransform(actions, (action) => {
return swr.mutate(action(...args)) as any; return async (...args: any) => {
// @ts-ignore
const res = await action(...args);
// mutate all keys of entity by default
if (options?.revalidateOnMutate !== false) {
await mutateAll();
}
return res;
}; };
}) as Omit<ReturnType<typeof useEntity<Entity, Id>>, "read">; }) as Omit<ReturnType<typeof useEntity<Entity, Id>>, "read">;
@@ -105,17 +148,62 @@ export const useEntityQuery = <
}; };
}; };
export async function mutateEntityCache<
Entity extends keyof DB | string,
Data = Entity extends keyof DB ? Omit<DB[Entity], "id"> : EntityData
>(api: Api["data"], entity: Entity, id: PrimaryFieldType, partialData: Partial<Data>) {
function update(prev: any, partialNext: any) {
if (
typeof prev !== "undefined" &&
typeof partialNext !== "undefined" &&
"id" in prev &&
prev.id === id
) {
return { ...prev, ...partialNext };
}
return prev;
}
const entityKey = makeKey(api, entity);
return mutate(
(key) => typeof key === "string" && key.startsWith(entityKey),
async (data) => {
if (typeof data === "undefined") return;
if (Array.isArray(data)) {
return data.map((item) => update(item, partialData));
}
return update(data, partialData);
},
{
revalidate: false
}
);
}
export const useEntityMutate = < export const useEntityMutate = <
Entity extends string, Entity extends keyof DB | string,
Id extends PrimaryFieldType | undefined = undefined Id extends PrimaryFieldType | undefined = undefined,
Data = Entity extends keyof DB ? Omit<DB[Entity], "id"> : EntityData
>( >(
entity: Entity, entity: Entity,
id?: Id, id?: Id,
options?: SWRConfiguration options?: SWRConfiguration
) => { ) => {
const { data, ...$q } = useEntityQuery(entity, id, undefined, { const { data, ...$q } = useEntityQuery<Entity, Id>(entity, id, undefined, {
...options, ...options,
enabled: false enabled: false
}); });
return $q;
const _mutate = id
? (data) => mutateEntityCache($q.api, entity, id, data)
: (id, data) => mutateEntityCache($q.api, entity, id, data);
return {
...$q,
mutate: _mutate as unknown as Id extends undefined
? (id: PrimaryFieldType, data: Partial<Data>) => Promise<void>
: (data: Partial<Data>) => Promise<void>
};
}; };

View File

@@ -1,6 +1,5 @@
import { Type, TypeInvalidError, parse, transformObject } from "core/utils"; import { Type, TypeInvalidError, parse, transformObject } from "core/utils";
import type { Entity } from "data"; import { constructEntity } from "data";
import { AppData } from "data/AppData";
import { import {
type TAppDataEntity, type TAppDataEntity,
type TAppDataEntityFields, type TAppDataEntityFields,
@@ -19,7 +18,7 @@ export function useBkndData() {
// @todo: potentially store in ref, so it doesn't get recomputed? or use memo? // @todo: potentially store in ref, so it doesn't get recomputed? or use memo?
const entities = transformObject(config.data.entities ?? {}, (entity, name) => { const entities = transformObject(config.data.entities ?? {}, (entity, name) => {
return AppData.constructEntity(name, entity); return constructEntity(name, entity);
}); });
const actions = { const actions = {

View File

@@ -1,6 +1,5 @@
import type { App } from "App"; import type { App } from "App";
import type { Entity, EntityRelation } from "data"; import { type Entity, type EntityRelation, constructEntity, constructRelation } from "data";
import { AppData } from "data/AppData";
import { RelationAccessor } from "data/relations/RelationAccessor"; import { RelationAccessor } from "data/relations/RelationAccessor";
import { Flow, TaskMap } from "flows"; import { Flow, TaskMap } from "flows";
@@ -20,11 +19,11 @@ export class AppReduced {
//console.log("received appjson", appJson); //console.log("received appjson", appJson);
this._entities = Object.entries(this.appJson.data.entities ?? {}).map(([name, entity]) => { this._entities = Object.entries(this.appJson.data.entities ?? {}).map(([name, entity]) => {
return AppData.constructEntity(name, entity); return constructEntity(name, entity);
}); });
this._relations = Object.entries(this.appJson.data.relations ?? {}).map(([, relation]) => { this._relations = Object.entries(this.appJson.data.relations ?? {}).map(([, relation]) => {
return AppData.constructRelation(relation, this.entity.bind(this)); return constructRelation(relation, this.entity.bind(this));
}); });
for (const [name, obj] of Object.entries(this.appJson.flows.flows ?? {})) { for (const [name, obj] of Object.entries(this.appJson.flows.flows ?? {})) {

View File

@@ -1,7 +1,6 @@
import type { ReactCodeMirrorProps } from "@uiw/react-codemirror"; import { default as CodeMirror, type ReactCodeMirrorProps } from "@uiw/react-codemirror";
import { Suspense, lazy } from "react";
import { useBknd } from "ui/client/bknd"; import { useBknd } from "ui/client/bknd";
const CodeMirror = lazy(() => import("@uiw/react-codemirror"));
export default function CodeEditor({ editable, basicSetup, ...props }: ReactCodeMirrorProps) { export default function CodeEditor({ editable, basicSetup, ...props }: ReactCodeMirrorProps) {
const b = useBknd(); const b = useBknd();
@@ -15,13 +14,11 @@ export default function CodeEditor({ editable, basicSetup, ...props }: ReactCode
: basicSetup; : basicSetup;
return ( return (
<Suspense> <CodeMirror
<CodeMirror theme={theme === "dark" ? "dark" : "light"}
theme={theme === "dark" ? "dark" : "light"} editable={editable}
editable={editable} basicSetup={_basicSetup}
basicSetup={_basicSetup} {...props}
{...props} />
/>
</Suspense>
); );
} }

View File

@@ -1,15 +1,12 @@
import type { Schema } from "@cfworker/json-schema"; import type { Schema } from "@cfworker/json-schema";
import Form from "@rjsf/core"; import Form from "@rjsf/core";
import type { RJSFSchema, UiSchema } from "@rjsf/utils"; import type { RJSFSchema, UiSchema } from "@rjsf/utils";
import { cloneDeep } from "lodash-es";
import { forwardRef, useId, useImperativeHandle, useRef, useState } from "react"; import { forwardRef, useId, useImperativeHandle, useRef, useState } from "react";
//import { JsonSchemaValidator } from "./JsonSchemaValidator";
import { fields as Fields } from "./fields"; import { fields as Fields } from "./fields";
import { templates as Templates } from "./templates"; import { templates as Templates } from "./templates";
import { widgets as Widgets } from "./widgets";
import "./styles.css";
import { filterKeys } from "core/utils";
import { cloneDeep } from "lodash-es";
import { RJSFTypeboxValidator } from "./typebox/RJSFTypeboxValidator"; import { RJSFTypeboxValidator } from "./typebox/RJSFTypeboxValidator";
import { widgets as Widgets } from "./widgets";
const validator = new RJSFTypeboxValidator(); const validator = new RJSFTypeboxValidator();

View File

@@ -0,0 +1,18 @@
import { Suspense, forwardRef, lazy } from "react";
import type { JsonSchemaFormProps, JsonSchemaFormRef } from "./JsonSchemaForm";
export type { JsonSchemaFormProps, JsonSchemaFormRef };
const Module = lazy(() =>
import("./JsonSchemaForm").then((m) => ({
default: m.JsonSchemaForm
}))
);
export const JsonSchemaForm = forwardRef<JsonSchemaFormRef, JsonSchemaFormProps>((props, ref) => {
return (
<Suspense>
<Module ref={ref} {...props} />
</Suspense>
);
});

View File

@@ -4,7 +4,7 @@ import {
JsonSchemaForm, JsonSchemaForm,
type JsonSchemaFormProps, type JsonSchemaFormProps,
type JsonSchemaFormRef type JsonSchemaFormRef
} from "ui/components/form/json-schema/JsonSchemaForm"; } from "ui/components/form/json-schema";
import type { ContextModalProps } from "@mantine/modals"; import type { ContextModalProps } from "@mantine/modals";

View File

@@ -1,14 +1,8 @@
import type { FieldApi } from "@tanstack/react-form"; import type { FieldApi } from "@tanstack/react-form";
import type { EntityData, JsonSchemaField } from "data"; import type { EntityData, JsonSchemaField } from "data";
import { Suspense, lazy } from "react";
import * as Formy from "ui/components/form/Formy"; import * as Formy from "ui/components/form/Formy";
import { FieldLabel } from "ui/components/form/Formy"; import { FieldLabel } from "ui/components/form/Formy";
import { JsonSchemaForm } from "ui/components/form/json-schema";
const JsonSchemaForm = lazy(() =>
import("ui/components/form/json-schema/JsonSchemaForm").then((m) => ({
default: m.JsonSchemaForm
}))
);
export function EntityJsonSchemaFormField({ export function EntityJsonSchemaFormField({
fieldApi, fieldApi,
@@ -34,23 +28,21 @@ export function EntityJsonSchemaFormField({
return ( return (
<Formy.Group> <Formy.Group>
<FieldLabel htmlFor={fieldApi.name} field={field} /> <FieldLabel htmlFor={fieldApi.name} field={field} />
<Suspense fallback={<div>Loading...</div>}> <div
<div data-disabled={disabled ? 1 : undefined}
data-disabled={disabled ? 1 : undefined} className="data-[disabled]:opacity-70 data-[disabled]:pointer-events-none"
className="data-[disabled]:opacity-70 data-[disabled]:pointer-events-none" >
> <JsonSchemaForm
<JsonSchemaForm schema={field.getJsonSchema()}
schema={field.getJsonSchema()} onChange={handleChange}
onChange={handleChange} direction="horizontal"
direction="horizontal" formData={formData}
formData={formData} uiSchema={{
uiSchema={{ "ui:globalOptions": { flexDirection: "row" },
"ui:globalOptions": { flexDirection: "row" }, ...field.getJsonUiSchema()
...field.getJsonUiSchema() }}
}} />
/> </div>
</div>
</Suspense>
</Formy.Group> </Formy.Group>
); );
} }

View File

@@ -1,14 +1,9 @@
import { Handle, type Node, type NodeProps, Position } from "@xyflow/react"; import { Handle, type Node, type NodeProps, Position } from "@xyflow/react";
import { Const, Type, transformObject } from "core/utils"; import { Const, Type, transformObject } from "core/utils";
import { type TaskRenderProps, type Trigger, TriggerMap } from "flows"; import { type Trigger, TriggerMap } from "flows";
import { Suspense, lazy } from "react";
import type { IconType } from "react-icons"; import type { IconType } from "react-icons";
import { TbCircleLetterT } from "react-icons/tb"; import { TbCircleLetterT } from "react-icons/tb";
const JsonSchemaForm = lazy(() => import { JsonSchemaForm } from "ui/components/form/json-schema";
import("ui/components/form/json-schema/JsonSchemaForm").then((m) => ({
default: m.JsonSchemaForm
}))
);
export type TaskComponentProps = NodeProps<Node<{ trigger: Trigger }>> & { export type TaskComponentProps = NodeProps<Node<{ trigger: Trigger }>> & {
Icon?: IconType; Icon?: IconType;
@@ -48,17 +43,15 @@ export function TriggerComponent({
</div> </div>
<div className="w-full h-px bg-primary/10" /> <div className="w-full h-px bg-primary/10" />
<div className="flex flex-col gap-2 px-3 py-2"> <div className="flex flex-col gap-2 px-3 py-2">
<Suspense fallback={<div>Loading...</div>}> <JsonSchemaForm
<JsonSchemaForm className="legacy"
className="legacy" schema={Type.Union(triggerSchemas)}
schema={Type.Union(triggerSchemas)} onChange={console.log}
onChange={console.log} formData={trigger}
formData={trigger} {...props}
{...props} /*uiSchema={uiSchema}*/
/*uiSchema={uiSchema}*/ /*fields={{ template: TemplateField }}*/
/*fields={{ template: TemplateField }}*/ />
/>
</Suspense>
</div> </div>
</div> </div>
<Handle <Handle

View File

@@ -1,12 +1,5 @@
import type { Task } from "flows"; import type { Task } from "flows";
import { Suspense, lazy } from "react"; import { JsonSchemaForm } from "ui/components/form/json-schema";
import { TemplateField } from "./TemplateField";
const JsonSchemaForm = lazy(() =>
import("ui/components/form/json-schema/JsonSchemaForm").then((m) => ({
default: m.JsonSchemaForm
}))
);
export type TaskFormProps = { export type TaskFormProps = {
task: Task; task: Task;
@@ -26,16 +19,14 @@ export function TaskForm({ task, onChange, ...props }: TaskFormProps) {
//console.log("uiSchema", uiSchema); //console.log("uiSchema", uiSchema);
return ( return (
<Suspense fallback={<div>Loading...</div>}> <JsonSchemaForm
<JsonSchemaForm className="legacy"
className="legacy" schema={schema}
schema={schema} onChange={onChange}
onChange={onChange} formData={params}
formData={params} {...props}
{...props} /*uiSchema={uiSchema}*/
/*uiSchema={uiSchema}*/ /*fields={{ template: TemplateField }}*/
/*fields={{ template: TemplateField }}*/ />
/>
</Suspense>
); );
} }

View File

@@ -5,10 +5,7 @@ import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth";
import { useBkndData } from "ui/client/schema/data/use-bknd-data"; import { useBkndData } from "ui/client/schema/data/use-bknd-data";
import { Button } from "ui/components/buttons/Button"; import { Button } from "ui/components/buttons/Button";
import { Alert } from "ui/components/display/Alert"; import { Alert } from "ui/components/display/Alert";
import { import { JsonSchemaForm, type JsonSchemaFormRef } from "ui/components/form/json-schema";
JsonSchemaForm,
type JsonSchemaFormRef
} from "ui/components/form/json-schema/JsonSchemaForm";
import * as AppShell from "ui/layouts/AppShell/AppShell"; import * as AppShell from "ui/layouts/AppShell/AppShell";
import { useNavigate } from "ui/lib/routes"; import { useNavigate } from "ui/lib/routes";
import { extractSchema } from "../settings/utils/schema"; import { extractSchema } from "../settings/utils/schema";

View File

@@ -1,9 +1,7 @@
import { cloneDeep, omit } from "lodash-es"; import { cloneDeep, omit } from "lodash-es";
import { useBknd } from "ui/client/bknd"; import { useBknd } from "ui/client/bknd";
import { Button } from "ui/components/buttons/Button"; import { Button } from "ui/components/buttons/Button";
import { JsonSchemaForm } from "ui/components/form/json-schema/JsonSchemaForm";
import * as AppShell from "../../layouts/AppShell/AppShell"; import * as AppShell from "../../layouts/AppShell/AppShell";
import { extractSchema } from "../settings/utils/schema";
export function AuthStrategiesList() { export function AuthStrategiesList() {
useBknd({ withSecrets: true }); useBknd({ withSecrets: true });

View File

@@ -101,7 +101,7 @@ export function DataEntityUpdate({ params }) {
data: { data: {
data: data as any, data: data as any,
entity: entity.toJSON(), entity: entity.toJSON(),
schema: entity.toSchema(true), schema: entity.toSchema({ clean: true }),
form: Form.state.values, form: Form.state.values,
state: Form.state state: Form.state
} }

View File

@@ -13,10 +13,7 @@ import { useBkndData } from "ui/client/schema/data/use-bknd-data";
import { Button } from "ui/components/buttons/Button"; import { Button } from "ui/components/buttons/Button";
import { IconButton } from "ui/components/buttons/IconButton"; import { IconButton } from "ui/components/buttons/IconButton";
import { Empty } from "ui/components/display/Empty"; import { Empty } from "ui/components/display/Empty";
import { import { JsonSchemaForm, type JsonSchemaFormRef } from "ui/components/form/json-schema";
JsonSchemaForm,
type JsonSchemaFormRef
} from "ui/components/form/json-schema/JsonSchemaForm";
import { Dropdown } from "ui/components/overlay/Dropdown"; import { Dropdown } from "ui/components/overlay/Dropdown";
import * as AppShell from "ui/layouts/AppShell/AppShell"; import * as AppShell from "ui/layouts/AppShell/AppShell";
import { Breadcrumbs2 } from "ui/layouts/AppShell/Breadcrumbs2"; import { Breadcrumbs2 } from "ui/layouts/AppShell/Breadcrumbs2";

View File

@@ -22,7 +22,7 @@ import { Button } from "ui/components/buttons/Button";
import { IconButton } from "ui/components/buttons/IconButton"; import { IconButton } from "ui/components/buttons/IconButton";
import { JsonViewer } from "ui/components/code/JsonViewer"; import { JsonViewer } from "ui/components/code/JsonViewer";
import { MantineSwitch } from "ui/components/form/hook-form-mantine/MantineSwitch"; import { MantineSwitch } from "ui/components/form/hook-form-mantine/MantineSwitch";
import { JsonSchemaForm } from "ui/components/form/json-schema/JsonSchemaForm"; import { JsonSchemaForm } from "ui/components/form/json-schema";
import { type SortableItemProps, SortableList } from "ui/components/list/SortableList"; import { type SortableItemProps, SortableList } from "ui/components/list/SortableList";
import { Popover } from "ui/components/overlay/Popover"; import { Popover } from "ui/components/overlay/Popover";
import { fieldSpecs } from "ui/modules/data/components/fields-specs"; import { fieldSpecs } from "ui/modules/data/components/fields-specs";

View File

@@ -1,14 +1,19 @@
import { Suspense, lazy } from "react"; import { Suspense, lazy } from "react";
import { useBknd } from "ui/client/bknd"; import { useBknd } from "ui/client/bknd";
import { Route, Router, Switch } from "wouter"; import { Route, Router, Switch } from "wouter";
import AuthRoutes from "./auth";
import { AuthLogin } from "./auth/auth.login"; import { AuthLogin } from "./auth/auth.login";
import DataRoutes from "./data";
import FlowRoutes from "./flows";
import MediaRoutes from "./media";
import { Root, RootEmpty } from "./root"; import { Root, RootEmpty } from "./root";
import SettingsRoutes from "./settings";
const DataRoutes = lazy(() => import("./data")); /*const DataRoutes = lazy(() => import("./data"));
const AuthRoutes = lazy(() => import("./auth")); const AuthRoutes = lazy(() => import("./auth"));
const MediaRoutes = lazy(() => import("./media")); const MediaRoutes = lazy(() => import("./media"));
const FlowRoutes = lazy(() => import("./flows")); const FlowRoutes = lazy(() => import("./flows"));
const SettingsRoutes = lazy(() => import("./settings")); const SettingsRoutes = lazy(() => import("./settings"));*/
// @ts-ignore // @ts-ignore
const TestRoutes = lazy(() => import("./test")); const TestRoutes = lazy(() => import("./test"));

View File

@@ -8,10 +8,7 @@ import { Button } from "ui/components/buttons/Button";
import { IconButton } from "ui/components/buttons/IconButton"; import { IconButton } from "ui/components/buttons/IconButton";
import { Alert } from "ui/components/display/Alert"; import { Alert } from "ui/components/display/Alert";
import { Empty } from "ui/components/display/Empty"; import { Empty } from "ui/components/display/Empty";
import { import { JsonSchemaForm, type JsonSchemaFormRef } from "ui/components/form/json-schema";
JsonSchemaForm,
type JsonSchemaFormRef
} from "ui/components/form/json-schema/JsonSchemaForm";
import { Dropdown } from "ui/components/overlay/Dropdown"; import { Dropdown } from "ui/components/overlay/Dropdown";
import { DataTable } from "ui/components/table/DataTable"; import { DataTable } from "ui/components/table/DataTable";
import { useEvent } from "ui/hooks/use-event"; import { useEvent } from "ui/hooks/use-event";

View File

@@ -3,16 +3,13 @@ import type { TObject } from "core/utils";
import { omit } from "lodash-es"; import { omit } from "lodash-es";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { TbCirclePlus, TbVariable } from "react-icons/tb"; import { TbCirclePlus, TbVariable } from "react-icons/tb";
import { useBknd } from "ui/client/BkndProvider";
import { Button } from "ui/components/buttons/Button";
import * as Formy from "ui/components/form/Formy";
import { JsonSchemaForm, type JsonSchemaFormRef } from "ui/components/form/json-schema";
import { Dropdown } from "ui/components/overlay/Dropdown";
import { Modal } from "ui/components/overlay/Modal";
import { useLocation } from "wouter"; import { useLocation } from "wouter";
import { useBknd } from "../../../client/BkndProvider";
import { Button } from "../../../components/buttons/Button";
import * as Formy from "../../../components/form/Formy";
import {
JsonSchemaForm,
type JsonSchemaFormRef
} from "../../../components/form/json-schema/JsonSchemaForm";
import { Dropdown } from "../../../components/overlay/Dropdown";
import { Modal } from "../../../components/overlay/Modal";
export type SettingsNewModalProps = { export type SettingsNewModalProps = {
schema: TObject; schema: TObject;

View File

@@ -2,7 +2,7 @@ import { parse } from "core/utils";
import { AppFlows } from "flows/AppFlows"; import { AppFlows } from "flows/AppFlows";
import { useState } from "react"; import { useState } from "react";
import { JsonViewer } from "../../../components/code/JsonViewer"; import { JsonViewer } from "../../../components/code/JsonViewer";
import { JsonSchemaForm } from "../../../components/form/json-schema/JsonSchemaForm"; import { JsonSchemaForm } from "../../../components/form/json-schema";
import { Scrollable } from "../../../layouts/AppShell/AppShell"; import { Scrollable } from "../../../layouts/AppShell/AppShell";
export default function FlowCreateSchemaTest() { export default function FlowCreateSchemaTest() {

View File

@@ -2,12 +2,9 @@ import Form from "@rjsf/core";
import type { RJSFSchema, UiSchema } from "@rjsf/utils"; import type { RJSFSchema, UiSchema } from "@rjsf/utils";
import { useRef } from "react"; import { useRef } from "react";
import { TbPlus, TbTrash } from "react-icons/tb"; import { TbPlus, TbTrash } from "react-icons/tb";
import { Button } from "../../../../components/buttons/Button"; import { Button } from "ui/components/buttons/Button";
import { JsonSchemaForm, type JsonSchemaFormRef } from "ui/components/form/json-schema";
import * as Formy from "../../../../components/form/Formy"; import * as Formy from "../../../../components/form/Formy";
import {
JsonSchemaForm,
type JsonSchemaFormRef
} from "../../../../components/form/json-schema/JsonSchemaForm";
import * as AppShell from "../../../../layouts/AppShell/AppShell"; import * as AppShell from "../../../../layouts/AppShell/AppShell";
class CfJsonSchemaValidator {} class CfJsonSchemaValidator {}

View File

@@ -1,7 +1,7 @@
import type { Schema } from "@cfworker/json-schema"; import type { Schema } from "@cfworker/json-schema";
import { useState } from "react"; import { useState } from "react";
import { JsonSchemaForm } from "../../../components/form/json-schema/JsonSchemaForm"; import { JsonSchemaForm } from "ui/components/form/json-schema";
import { Scrollable } from "../../../layouts/AppShell/AppShell"; import { Scrollable } from "ui/layouts/AppShell/AppShell";
const schema: Schema = { const schema: Schema = {
definitions: { definitions: {
@@ -9,52 +9,52 @@ const schema: Schema = {
anyOf: [ anyOf: [
{ {
title: "String", title: "String",
type: "string", type: "string"
}, },
{ {
title: "Number", title: "Number",
type: "number", type: "number"
}, },
{ {
title: "Boolean", title: "Boolean",
type: "boolean", type: "boolean"
}, }
], ]
}, },
numeric: { numeric: {
anyOf: [ anyOf: [
{ {
title: "Number", title: "Number",
type: "number", type: "number"
}, },
{ {
title: "Datetime", title: "Datetime",
type: "string", type: "string",
format: "date-time", format: "date-time"
}, },
{ {
title: "Date", title: "Date",
type: "string", type: "string",
format: "date", format: "date"
}, },
{ {
title: "Time", title: "Time",
type: "string", type: "string",
format: "time", format: "time"
}, }
], ]
}, },
boolean: { boolean: {
title: "Boolean", title: "Boolean",
type: "boolean", type: "boolean"
}, }
}, },
type: "object", type: "object",
properties: { properties: {
operand: { operand: {
enum: ["$and", "$or"], enum: ["$and", "$or"],
default: "$and", default: "$and",
type: "string", type: "string"
}, },
conditions: { conditions: {
type: "array", type: "array",
@@ -64,10 +64,10 @@ const schema: Schema = {
operand: { operand: {
enum: ["$and", "$or"], enum: ["$and", "$or"],
default: "$and", default: "$and",
type: "string", type: "string"
}, },
key: { key: {
type: "string", type: "string"
}, },
operator: { operator: {
type: "array", type: "array",
@@ -78,30 +78,30 @@ const schema: Schema = {
type: "object", type: "object",
properties: { properties: {
$eq: { $eq: {
$ref: "#/definitions/primitive", $ref: "#/definitions/primitive"
}, }
}, },
required: ["$eq"], required: ["$eq"]
}, },
{ {
title: "Lower than", title: "Lower than",
type: "object", type: "object",
properties: { properties: {
$lt: { $lt: {
$ref: "#/definitions/numeric", $ref: "#/definitions/numeric"
}, }
}, },
required: ["$lt"], required: ["$lt"]
}, },
{ {
title: "Greather than", title: "Greather than",
type: "object", type: "object",
properties: { properties: {
$gt: { $gt: {
$ref: "#/definitions/numeric", $ref: "#/definitions/numeric"
}, }
}, },
required: ["$gt"], required: ["$gt"]
}, },
{ {
title: "Between", title: "Between",
@@ -110,13 +110,13 @@ const schema: Schema = {
$between: { $between: {
type: "array", type: "array",
items: { items: {
$ref: "#/definitions/numeric", $ref: "#/definitions/numeric"
}, },
minItems: 2, minItems: 2,
maxItems: 2, maxItems: 2
}, }
}, },
required: ["$between"], required: ["$between"]
}, },
{ {
title: "In", title: "In",
@@ -125,23 +125,23 @@ const schema: Schema = {
$in: { $in: {
type: "array", type: "array",
items: { items: {
$ref: "#/definitions/primitive", $ref: "#/definitions/primitive"
}, },
minItems: 1, minItems: 1
}, }
}, }
}, }
], ]
}, },
minItems: 1, minItems: 1
}, }
}, },
required: ["key", "operator"], required: ["key", "operator"]
}, },
minItems: 1, minItems: 1
}, }
}, },
required: ["operand", "conditions"], required: ["operand", "conditions"]
}; };
export default function QueryJsonFormTest() { export default function QueryJsonFormTest() {

View File

@@ -1,8 +1,8 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { useBknd } from "../../../client/BkndProvider"; import { useBknd } from "ui/client/BkndProvider";
import { JsonSchemaForm } from "../../../components/form/json-schema/JsonSchemaForm"; import { JsonSchemaForm } from "ui/components/form/json-schema";
import { Scrollable } from "../../../layouts/AppShell/AppShell"; import { Scrollable } from "ui/layouts/AppShell/AppShell";
function useSchema() { function useSchema() {
const [schema, setSchema] = useState<any>(); const [schema, setSchema] = useState<any>();

View File

@@ -1,7 +1,20 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useApiQuery } from "ui/client"; import { useApi, useApiQuery } from "ui/client";
import { Scrollable } from "ui/layouts/AppShell/AppShell"; import { Scrollable } from "ui/layouts/AppShell/AppShell";
function Bla() {
const api = useApi();
useEffect(() => {
(async () => {
const one = await api.data.readOne("users", 1);
const many = await api.data.readMany("users");
})();
}, []);
return null;
}
export default function SWRAndAPI() { export default function SWRAndAPI() {
const [text, setText] = useState(""); const [text, setText] = useState("");
const { data, ...r } = useApiQuery((api) => api.data.readOne("comments", 1), { const { data, ...r } = useApiQuery((api) => api.data.readOne("comments", 1), {
@@ -16,7 +29,7 @@ export default function SWRAndAPI() {
return ( return (
<Scrollable> <Scrollable>
<pre>{JSON.stringify(r.promise.keyArray({ search: false }))}</pre> <pre>{JSON.stringify(r.key)}</pre>
{r.error && <div>failed to load</div>} {r.error && <div>failed to load</div>}
{r.isLoading && <div>loading...</div>} {r.isLoading && <div>loading...</div>}
{data && <pre>{JSON.stringify(data, null, 2)}</pre>} {data && <pre>{JSON.stringify(data, null, 2)}</pre>}
@@ -26,12 +39,12 @@ export default function SWRAndAPI() {
e.preventDefault(); e.preventDefault();
if (!comment) return; if (!comment) return;
await r.mutate(async () => { /*await r.mutate(async () => {
const res = await r.api.data.updateOne("comments", comment.id, { const res = await r.api.data.updateOne("comments", comment.id, {
content: text content: text
}); });
return res.data; return res.data;
}); });*/
return false; return false;
}} }}

View File

@@ -1,54 +1,72 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useEntity, useEntityQuery } from "ui/client/api/use-entity"; import { useEntity, useEntityMutate, useEntityQuery } from "ui/client/api/use-entity";
import { Scrollable } from "ui/layouts/AppShell/AppShell"; import { Scrollable } from "ui/layouts/AppShell/AppShell";
export default function SwrAndDataApi() { export default function SwrAndDataApi() {
return ( return (
<div> <Scrollable>
asdf
<DirectDataApi /> <DirectDataApi />
<QueryDataApi /> <QueryDataApi />
</div> <QueryMutateDataApi />
</Scrollable>
); );
} }
function QueryDataApi() { function QueryMutateDataApi() {
const [text, setText] = useState(""); const { mutate } = useEntityMutate("comments");
const { data, update, ...r } = useEntityQuery("comments", 1, {}); const { data, ...r } = useEntityQuery("comments", undefined, {
const comment = data ? data : null; limit: 2
});
useEffect(() => {
setText(comment?.content ?? "");
}, [comment]);
return ( return (
<Scrollable> <div>
bla
<pre>{JSON.stringify(r.key)}</pre> <pre>{JSON.stringify(r.key)}</pre>
{r.error && <div>failed to load</div>} {r.error && <div>failed to load</div>}
{r.isLoading && <div>loading...</div>} {r.isLoading && <div>loading...</div>}
{data && <pre>{JSON.stringify(data, null, 2)}</pre>} {data && <pre>{JSON.stringify(data, null, 2)}</pre>}
{data && ( {data && (
<form <div>
onSubmit={async (e) => { {data.map((comment) => (
e.preventDefault(); <input
if (!comment) return; key={String(comment.id)}
await update({ content: text }); type="text"
return false; value={comment.content}
}} onChange={async (e) => {
> await mutate(comment.id, { content: e.target.value });
<input type="text" value={text} onChange={(e) => setText(e.target.value)} /> }}
<button type="submit">submit</button> className="border border-black"
</form> />
))}
</div>
)} )}
</Scrollable> </div>
);
}
function QueryDataApi() {
const { data, update, ...r } = useEntityQuery("comments", undefined, {
sort: { by: "id", dir: "asc" },
limit: 3
});
return (
<div>
<pre>{JSON.stringify(r.key)}</pre>
{r.error && <div>failed to load</div>}
{r.isLoading && <div>loading...</div>}
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
</div>
); );
} }
function DirectDataApi() { function DirectDataApi() {
const [data, setData] = useState<any>(); const [data, setData] = useState<any>();
const { create, read, update, _delete } = useEntity("comments", 1); const { create, read, update, _delete } = useEntity("comments");
useEffect(() => { useEffect(() => {
read().then(setData); read().then((data) => setData(data));
}, []); }, []);
return <pre>{JSON.stringify(data, null, 2)}</pre>; return <pre>{JSON.stringify(data, null, 2)}</pre>;

View File

@@ -26,14 +26,13 @@
"esModuleInterop": true, "esModuleInterop": true,
"skipLibCheck": true, "skipLibCheck": true,
"rootDir": "./src", "rootDir": "./src",
"outDir": "./dist", "outDir": "./dist/types",
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo",
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"*": ["./src/*"], "*": ["./src/*"],
"bknd": ["./src/*"] "bknd": ["./src/*"]
} }
}, },
"include": ["./src/**/*.ts", "./src/**/*.tsx", "./env.d.ts"], "include": ["./src/**/*.ts", "./src/**/*.tsx"],
"exclude": ["node_modules", "dist/**/*", "../examples/bun"] "exclude": ["node_modules", "dist", "dist/types", "**/*.d.ts"]
} }

BIN
bun.lockb

Binary file not shown.