mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
Merge pull request #32 from bknd-io/feat/optimize-seeding
Feat: optimize seeding, adding api typing support
This commit is contained in:
@@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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());
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>();
|
||||||
|
|
||||||
|
|||||||
48
app/build.ts
48
app/build.ts
@@ -9,15 +9,43 @@ 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`;
|
await $`rm -rf dist`;
|
||||||
if (types) {
|
}
|
||||||
|
|
||||||
|
let types_running = false;
|
||||||
|
function buildTypes() {
|
||||||
|
if (types_running) return;
|
||||||
|
types_running = true;
|
||||||
|
|
||||||
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
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
log: console.log,
|
||||||
|
warn: console.warn,
|
||||||
|
error: console.error
|
||||||
|
};
|
||||||
|
disableConsoleLog(severities);
|
||||||
|
const enable = () => {
|
||||||
|
Object.entries(_oldConsoles).forEach(([severity, fn]) => {
|
||||||
|
console[severity as ConsoleSeverity] = fn;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
try {
|
||||||
const result = await fn();
|
const result = await fn();
|
||||||
enable();
|
enable();
|
||||||
return result;
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
enable();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function disableConsoleLog(severities: ConsoleSeverity[] = ["log"]) {
|
export function disableConsoleLog(severities: ConsoleSeverity[] = ["log"]) {
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
7
app/src/data/connection/DummyConnection.ts
Normal file
7
app/src/data/connection/DummyConnection.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { Connection } from "./Connection";
|
||||||
|
|
||||||
|
export class DummyConnection extends Connection {
|
||||||
|
constructor() {
|
||||||
|
super(undefined as any);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) => {
|
||||||
|
//const hidden = field.isHidden(options?.context);
|
||||||
|
const fillable = field.isFillable(options?.context);
|
||||||
|
return {
|
||||||
title: field.config.label,
|
title: field.config.label,
|
||||||
$comment: field.config.description,
|
$comment: field.config.description,
|
||||||
$field: field.type,
|
$field: field.type,
|
||||||
readOnly: !field.isFillable("update") ? true : undefined,
|
readOnly: !fillable ? true : undefined,
|
||||||
writeOnly: !field.isFillable("create") ? true : undefined,
|
|
||||||
...field.toJsonSchema()
|
...field.toJsonSchema()
|
||||||
}))
|
};
|
||||||
|
}),
|
||||||
|
{ additionalProperties: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
return clean ? JSON.parse(JSON.stringify(schema)) : schema;
|
return options?.clean ? JSON.parse(JSON.stringify(schema)) : schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>>>;
|
||||||
|
|||||||
34
app/src/data/schema/constructor.ts
Normal file
34
app/src/data/schema/constructor.ts
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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`);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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 ?? {})) {
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
18
app/src/ui/components/form/json-schema/index.tsx
Normal file
18
app/src/ui/components/form/json-schema/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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,7 +28,6 @@ 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"
|
||||||
@@ -50,7 +43,6 @@ export function EntityJsonSchemaFormField({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Suspense>
|
|
||||||
</Formy.Group>
|
</Formy.Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,7 +43,6 @@ 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)}
|
||||||
@@ -58,7 +52,6 @@ export function TriggerComponent({
|
|||||||
/*uiSchema={uiSchema}*/
|
/*uiSchema={uiSchema}*/
|
||||||
/*fields={{ template: TemplateField }}*/
|
/*fields={{ template: TemplateField }}*/
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Handle
|
<Handle
|
||||||
|
|||||||
@@ -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,7 +19,6 @@ 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}
|
||||||
@@ -36,6 +28,5 @@ export function TaskForm({ task, onChange, ...props }: TaskFormProps) {
|
|||||||
/*uiSchema={uiSchema}*/
|
/*uiSchema={uiSchema}*/
|
||||||
/*fields={{ template: TemplateField }}*/
|
/*fields={{ template: TemplateField }}*/
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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"));
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
required: ["key", "operator"]
|
||||||
},
|
},
|
||||||
],
|
minItems: 1
|
||||||
|
}
|
||||||
},
|
},
|
||||||
minItems: 1,
|
required: ["operand", "conditions"]
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["key", "operator"],
|
|
||||||
},
|
|
||||||
minItems: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["operand", "conditions"],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function QueryJsonFormTest() {
|
export default function QueryJsonFormTest() {
|
||||||
|
|||||||
@@ -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>();
|
||||||
|
|||||||
@@ -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;
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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 });
|
||||||
}}
|
}}
|
||||||
>
|
className="border border-black"
|
||||||
<input type="text" value={text} onChange={(e) => setText(e.target.value)} />
|
/>
|
||||||
<button type="submit">submit</button>
|
))}
|
||||||
</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>;
|
||||||
|
|||||||
@@ -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"]
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user