mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-15 20:17:22 +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 NumberField("count", { default_value: 0 })
|
||||
]);
|
||||
const em = new EntityManager([items], connection);
|
||||
const em = new EntityManager<any>([items], connection);
|
||||
|
||||
await em.connection.kysely.schema
|
||||
.createTable("items")
|
||||
@@ -175,4 +175,18 @@ describe("Mutator simple", async () => {
|
||||
{ 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,
|
||||
DateField,
|
||||
Entity,
|
||||
EntityIndex,
|
||||
EntityManager,
|
||||
EnumField,
|
||||
JsonField,
|
||||
ManyToManyRelation,
|
||||
@@ -12,6 +14,7 @@ import {
|
||||
PolymorphicRelation,
|
||||
TextField
|
||||
} from "../../src/data";
|
||||
import { DummyConnection } from "../../src/data/connection/DummyConnection";
|
||||
import {
|
||||
FieldPrototype,
|
||||
type FieldSchema,
|
||||
@@ -20,6 +23,7 @@ import {
|
||||
boolean,
|
||||
date,
|
||||
datetime,
|
||||
em,
|
||||
entity,
|
||||
enumm,
|
||||
json,
|
||||
@@ -46,12 +50,17 @@ describe("prototype", () => {
|
||||
});
|
||||
|
||||
test("...2", async () => {
|
||||
const user = entity("users", {
|
||||
name: text().required(),
|
||||
const users = entity("users", {
|
||||
name: text(),
|
||||
bio: text(),
|
||||
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());
|
||||
});
|
||||
@@ -266,4 +275,38 @@ describe("prototype", () => {
|
||||
|
||||
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("not_fillable", { fillable: false })
|
||||
]);
|
||||
const em = new EntityManager([entity], dummyConnection);
|
||||
const em = new EntityManager<any>([entity], dummyConnection);
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
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 users = new Entity("users", [new TextField("username")]);
|
||||
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 });
|
||||
|
||||
test("RelationMutator", async () => {
|
||||
@@ -192,7 +192,7 @@ describe("[data] Mutator (OneToOne)", async () => {
|
||||
const users = new Entity("users", [new TextField("username")]);
|
||||
const settings = new Entity("settings", [new TextField("theme")]);
|
||||
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 });
|
||||
|
||||
test("insertOne: missing ref", async () => {
|
||||
@@ -276,7 +276,7 @@ describe("[data] Mutator (ManyToMany)", async () => {
|
||||
|
||||
describe("[data] Mutator (Events)", async () => {
|
||||
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 });
|
||||
const events = new Map<string, any>();
|
||||
|
||||
|
||||
50
app/build.ts
50
app/build.ts
@@ -9,16 +9,44 @@ const watch = args.includes("--watch");
|
||||
const minify = args.includes("--minify");
|
||||
const types = args.includes("--types");
|
||||
const sourcemap = args.includes("--sourcemap");
|
||||
const clean = args.includes("--clean");
|
||||
|
||||
if (clean) {
|
||||
console.log("Cleaning dist");
|
||||
await $`rm -rf dist`;
|
||||
}
|
||||
|
||||
let types_running = false;
|
||||
function buildTypes() {
|
||||
if (types_running) return;
|
||||
types_running = true;
|
||||
|
||||
await $`rm -rf dist`;
|
||||
if (types) {
|
||||
Bun.spawn(["bun", "build:types"], {
|
||||
onExit: () => {
|
||||
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
|
||||
* Using esbuild because tsup doesn't include "react"
|
||||
@@ -46,7 +74,8 @@ const result = await esbuild.build({
|
||||
__isDev: "0",
|
||||
"process.env.NODE_ENV": '"production"'
|
||||
},
|
||||
chunkNames: "chunks/[name]-[hash]"
|
||||
chunkNames: "chunks/[name]-[hash]",
|
||||
logLevel: "error"
|
||||
});
|
||||
|
||||
// Write manifest
|
||||
@@ -96,6 +125,9 @@ await tsup.build({
|
||||
treeshake: true,
|
||||
loader: {
|
||||
".svg": "dataurl"
|
||||
},
|
||||
onSuccess: async () => {
|
||||
delayTypes();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -117,11 +149,12 @@ await tsup.build({
|
||||
loader: {
|
||||
".svg": "dataurl"
|
||||
},
|
||||
onSuccess: async () => {
|
||||
console.log("--- ui built");
|
||||
},
|
||||
esbuildOptions: (options) => {
|
||||
options.logLevel = "silent";
|
||||
options.chunkNames = "chunks/[name]-[hash]";
|
||||
},
|
||||
onSuccess: async () => {
|
||||
delayTypes();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -148,7 +181,10 @@ function baseConfig(adapter: string): tsup.Options {
|
||||
],
|
||||
metafile: true,
|
||||
splitting: false,
|
||||
treeshake: true
|
||||
treeshake: true,
|
||||
onSuccess: async () => {
|
||||
delayTypes();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
"bin": "./dist/cli/index.js",
|
||||
"version": "0.3.4-alpha1",
|
||||
"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",
|
||||
"test": "ALL_TESTS=1 bun test --bail",
|
||||
"build": "NODE_ENV=production bun run build.ts --minify --types",
|
||||
"watch": "bun run build.ts --types --watch",
|
||||
"types": "bun tsc --noEmit",
|
||||
"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",
|
||||
"watch:css": "bun tailwindcss --watch -i src/ui/main.css -o ./dist/styles.css",
|
||||
"updater": "bun x npm-check-updates -ui",
|
||||
@@ -75,6 +75,7 @@
|
||||
"tailwind-merge": "^2.5.4",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tsc-alias": "^1.8.10",
|
||||
"tsup": "^8.3.5",
|
||||
"vite": "^5.4.10",
|
||||
"vite-plugin-static-copy": "^2.0.0",
|
||||
@@ -90,75 +91,75 @@
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"./ui": {
|
||||
"types": "./dist/ui/index.d.ts",
|
||||
"types": "./dist/types/ui/index.d.ts",
|
||||
"import": "./dist/ui/index.js",
|
||||
"require": "./dist/ui/index.cjs"
|
||||
},
|
||||
"./client": {
|
||||
"types": "./dist/ui/client/index.d.ts",
|
||||
"types": "./dist/types/ui/client/index.d.ts",
|
||||
"import": "./dist/ui/client/index.js",
|
||||
"require": "./dist/ui/client/index.cjs"
|
||||
},
|
||||
"./data": {
|
||||
"types": "./dist/data/index.d.ts",
|
||||
"types": "./dist/types/data/index.d.ts",
|
||||
"import": "./dist/data/index.js",
|
||||
"require": "./dist/data/index.cjs"
|
||||
},
|
||||
"./core": {
|
||||
"types": "./dist/core/index.d.ts",
|
||||
"types": "./dist/types/core/index.d.ts",
|
||||
"import": "./dist/core/index.js",
|
||||
"require": "./dist/core/index.cjs"
|
||||
},
|
||||
"./utils": {
|
||||
"types": "./dist/core/utils/index.d.ts",
|
||||
"types": "./dist/types/core/utils/index.d.ts",
|
||||
"import": "./dist/core/utils/index.js",
|
||||
"require": "./dist/core/utils/index.cjs"
|
||||
},
|
||||
"./cli": {
|
||||
"types": "./dist/cli/index.d.ts",
|
||||
"types": "./dist/types/cli/index.d.ts",
|
||||
"import": "./dist/cli/index.js",
|
||||
"require": "./dist/cli/index.cjs"
|
||||
},
|
||||
"./adapter/cloudflare": {
|
||||
"types": "./dist/adapter/cloudflare/index.d.ts",
|
||||
"types": "./dist/types/adapter/cloudflare/index.d.ts",
|
||||
"import": "./dist/adapter/cloudflare/index.js",
|
||||
"require": "./dist/adapter/cloudflare/index.cjs"
|
||||
},
|
||||
"./adapter/vite": {
|
||||
"types": "./dist/adapter/vite/index.d.ts",
|
||||
"types": "./dist/types/adapter/vite/index.d.ts",
|
||||
"import": "./dist/adapter/vite/index.js",
|
||||
"require": "./dist/adapter/vite/index.cjs"
|
||||
},
|
||||
"./adapter/nextjs": {
|
||||
"types": "./dist/adapter/nextjs/index.d.ts",
|
||||
"types": "./dist/types/adapter/nextjs/index.d.ts",
|
||||
"import": "./dist/adapter/nextjs/index.js",
|
||||
"require": "./dist/adapter/nextjs/index.cjs"
|
||||
},
|
||||
"./adapter/remix": {
|
||||
"types": "./dist/adapter/remix/index.d.ts",
|
||||
"types": "./dist/types/adapter/remix/index.d.ts",
|
||||
"import": "./dist/adapter/remix/index.js",
|
||||
"require": "./dist/adapter/remix/index.cjs"
|
||||
},
|
||||
"./adapter/bun": {
|
||||
"types": "./dist/adapter/bun/index.d.ts",
|
||||
"types": "./dist/types/adapter/bun/index.d.ts",
|
||||
"import": "./dist/adapter/bun/index.js",
|
||||
"require": "./dist/adapter/bun/index.cjs"
|
||||
},
|
||||
"./adapter/node": {
|
||||
"types": "./dist/adapter/node/index.d.ts",
|
||||
"types": "./dist/types/adapter/node/index.d.ts",
|
||||
"import": "./dist/adapter/node/index.js",
|
||||
"require": "./dist/adapter/node/index.cjs"
|
||||
},
|
||||
"./adapter/astro": {
|
||||
"types": "./dist/adapter/astro/index.d.ts",
|
||||
"types": "./dist/types/adapter/astro/index.d.ts",
|
||||
"import": "./dist/adapter/astro/index.js",
|
||||
"require": "./dist/adapter/astro/index.cjs"
|
||||
},
|
||||
|
||||
@@ -38,7 +38,7 @@ export class Api {
|
||||
private token_transport: "header" | "cookie" | "none" = "header";
|
||||
|
||||
public system!: SystemApi;
|
||||
public data!: DataApi;
|
||||
public data!: DataApi<DB>;
|
||||
public auth!: AuthApi;
|
||||
public media!: MediaApi;
|
||||
|
||||
|
||||
@@ -12,13 +12,17 @@ import { SystemController } from "modules/server/SystemController";
|
||||
|
||||
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";
|
||||
}
|
||||
export class AppBuiltEvent extends Event<{ app: App }> {
|
||||
export class AppBuiltEvent extends AppEvent {
|
||||
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 = {
|
||||
connection?:
|
||||
@@ -37,6 +41,8 @@ export type AppConfig = InitialModuleConfigs;
|
||||
export class App<DB = any> {
|
||||
modules: ModuleManager;
|
||||
static readonly Events = AppEvents;
|
||||
adminController?: AdminController;
|
||||
private trigger_first_boot = false;
|
||||
|
||||
constructor(
|
||||
private connection: Connection,
|
||||
@@ -48,9 +54,20 @@ export class App<DB = any> {
|
||||
...moduleManagerOptions,
|
||||
initial: _initialConfig,
|
||||
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.emgr.emit(new AppConfigUpdatedEvent({ app: this }));
|
||||
},
|
||||
onFirstBoot: async () => {
|
||||
console.log("[APP] first boot");
|
||||
this.trigger_first_boot = true;
|
||||
}
|
||||
});
|
||||
this.modules.ctx().emgr.registerEvents(AppEvents);
|
||||
@@ -88,14 +105,24 @@ export class App<DB = any> {
|
||||
if (options?.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) {
|
||||
return this.modules.get(module).schema();
|
||||
}
|
||||
|
||||
get server() {
|
||||
return this.modules.server;
|
||||
}
|
||||
|
||||
get fetch(): any {
|
||||
return this.modules.server.fetch;
|
||||
return this.server.fetch;
|
||||
}
|
||||
|
||||
get module() {
|
||||
@@ -119,7 +146,8 @@ export class App<DB = any> {
|
||||
|
||||
registerAdminController(config?: AdminControllerOptions) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -197,7 +197,7 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
||||
throw new Exception("User already exists");
|
||||
}
|
||||
|
||||
const payload = {
|
||||
const payload: any = {
|
||||
...profile,
|
||||
strategy: strategy.getName(),
|
||||
strategy_value: identifier
|
||||
|
||||
@@ -15,6 +15,7 @@ export class EventManager<
|
||||
> {
|
||||
protected events: EventClass[] = [];
|
||||
protected listeners: EventListener[] = [];
|
||||
enabled: boolean = true;
|
||||
|
||||
constructor(events?: RegisteredEvents, listeners?: EventListener[]) {
|
||||
if (events) {
|
||||
@@ -28,6 +29,16 @@ export class EventManager<
|
||||
}
|
||||
}
|
||||
|
||||
enable() {
|
||||
this.enabled = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
disable() {
|
||||
this.enabled = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
clearEvents() {
|
||||
this.events = [];
|
||||
return this;
|
||||
@@ -39,6 +50,10 @@ export class EventManager<
|
||||
return this;
|
||||
}
|
||||
|
||||
getListeners(): EventListener[] {
|
||||
return [...this.listeners];
|
||||
}
|
||||
|
||||
get Events(): { [K in keyof RegisteredEvents]: RegisteredEvents[K] } {
|
||||
// proxy class to access events
|
||||
return new Proxy(this, {
|
||||
@@ -133,6 +148,11 @@ export class EventManager<
|
||||
async emit(event: Event) {
|
||||
// @ts-expect-error slug is static
|
||||
const slug = event.constructor.slug;
|
||||
if (!this.enabled) {
|
||||
console.log("EventManager disabled, not emitting", slug);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.eventExists(event)) {
|
||||
throw new Error(`Event "${slug}" not registered`);
|
||||
}
|
||||
|
||||
@@ -9,10 +9,25 @@ export async function withDisabledConsole<R>(
|
||||
fn: () => Promise<R>,
|
||||
severities: ConsoleSeverity[] = ["log"]
|
||||
): Promise<R> {
|
||||
const enable = disableConsoleLog(severities);
|
||||
const result = await fn();
|
||||
enable();
|
||||
return result;
|
||||
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();
|
||||
enable();
|
||||
return result;
|
||||
} catch (e) {
|
||||
enable();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export function disableConsoleLog(severities: ConsoleSeverity[] = ["log"]) {
|
||||
|
||||
@@ -1,52 +1,20 @@
|
||||
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 { DataController } from "./api/DataController";
|
||||
import {
|
||||
type AppDataConfig,
|
||||
FIELDS,
|
||||
RELATIONS,
|
||||
type TAppDataEntity,
|
||||
type TAppDataRelation,
|
||||
dataConfigSchema
|
||||
} from "./data-schema";
|
||||
import { type AppDataConfig, dataConfigSchema } from "./data-schema";
|
||||
|
||||
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() {
|
||||
const entities = transformObject(this.config.entities ?? {}, (entityConfig, name) => {
|
||||
return AppData.constructEntity(name, entityConfig);
|
||||
return constructEntity(name, entityConfig);
|
||||
});
|
||||
|
||||
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) =>
|
||||
AppData.constructRelation(relation, _entity)
|
||||
constructRelation(relation, _entity)
|
||||
);
|
||||
|
||||
const indices = transformObject(this.config.indices ?? {}, (index, name) => {
|
||||
|
||||
@@ -5,7 +5,7 @@ export type DataApiOptions = BaseModuleApiOptions & {
|
||||
defaultQuery?: Partial<RepoQuery>;
|
||||
};
|
||||
|
||||
export class DataApi extends ModuleApi<DataApiOptions> {
|
||||
export class DataApi<DB> extends ModuleApi<DataApiOptions> {
|
||||
protected override getDefaultOptions(): Partial<DataApiOptions> {
|
||||
return {
|
||||
basepath: "/api/data",
|
||||
@@ -15,48 +15,60 @@ export class DataApi extends ModuleApi<DataApiOptions> {
|
||||
};
|
||||
}
|
||||
|
||||
readOne(
|
||||
entity: string,
|
||||
readOne<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
|
||||
entity: E,
|
||||
id: PrimaryFieldType,
|
||||
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> = {}) {
|
||||
return this.get<Pick<RepositoryResponse, "meta" | "data">>(
|
||||
[entity],
|
||||
query ?? this.options.defaultQuery
|
||||
);
|
||||
}
|
||||
|
||||
readManyByReference(
|
||||
entity: string,
|
||||
id: PrimaryFieldType,
|
||||
reference: string,
|
||||
readMany<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
|
||||
entity: E,
|
||||
query: Partial<RepoQuery> = {}
|
||||
) {
|
||||
return this.get<Pick<RepositoryResponse, "meta" | "data">>(
|
||||
[entity, id, reference],
|
||||
return this.get<Pick<RepositoryResponse<Data[]>, "meta" | "data">>(
|
||||
[entity as any],
|
||||
query ?? this.options.defaultQuery
|
||||
);
|
||||
}
|
||||
|
||||
createOne(entity: string, input: EntityData) {
|
||||
return this.post<RepositoryResponse<EntityData>>([entity], input);
|
||||
readManyByReference<
|
||||
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) {
|
||||
return this.patch<RepositoryResponse<EntityData>>([entity, id], input);
|
||||
createOne<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
|
||||
entity: E,
|
||||
input: Omit<Data, "id">
|
||||
) {
|
||||
return this.post<RepositoryResponse<Data>>([entity as any], input);
|
||||
}
|
||||
|
||||
deleteOne(entity: string, id: PrimaryFieldType) {
|
||||
return this.delete<RepositoryResponse<EntityData>>([entity, id]);
|
||||
updateOne<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
|
||||
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"] = {}) {
|
||||
return this.post<RepositoryResponse<{ entity: string; count: number }>>(
|
||||
[entity, "fn", "count"],
|
||||
deleteOne<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
|
||||
entity: E,
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 {
|
||||
DataPermissions,
|
||||
type EntityData,
|
||||
@@ -165,13 +165,12 @@ export class DataController implements ClassController {
|
||||
// read entity schema
|
||||
.get("/schema.json", async (c) => {
|
||||
this.guard.throwUnlessGranted(DataPermissions.entityRead);
|
||||
const url = new URL(c.req.url);
|
||||
const $id = `${url.origin}${this.config.basepath}/schema.json`;
|
||||
const $id = `${this.config.basepath}/schema.json`;
|
||||
const schemas = Object.fromEntries(
|
||||
this.em.entities.map((e) => [
|
||||
e.name,
|
||||
{
|
||||
$ref: `schemas/${e.name}`
|
||||
$ref: `${this.config.basepath}/schemas/${e.name}`
|
||||
}
|
||||
])
|
||||
);
|
||||
@@ -183,22 +182,28 @@ export class DataController implements ClassController {
|
||||
})
|
||||
// read schema
|
||||
.get(
|
||||
"/schemas/:entity",
|
||||
tb("param", Type.Object({ entity: Type.String() })),
|
||||
"/schemas/:entity/:context?",
|
||||
tb(
|
||||
"param",
|
||||
Type.Object({
|
||||
entity: Type.String(),
|
||||
context: Type.Optional(StringEnum(["create", "update"]))
|
||||
})
|
||||
),
|
||||
async (c) => {
|
||||
this.guard.throwUnlessGranted(DataPermissions.entityRead);
|
||||
|
||||
//console.log("request", c.req.raw);
|
||||
const { entity } = c.req.param();
|
||||
const { entity, context } = c.req.param();
|
||||
if (!this.entityExists(entity)) {
|
||||
console.log("not found", entity, definedEntities);
|
||||
return c.notFound();
|
||||
}
|
||||
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 base = `${url.origin}${this.config.basepath}`;
|
||||
const $id = `${base}/schemas/${entity}`;
|
||||
const $id = `${this.config.basepath}/schemas/${entity}`;
|
||||
return c.json({
|
||||
$schema: `${base}/schema.json`,
|
||||
$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 {
|
||||
return snakeToPascalWithSpaces(this.config.name || this.name);
|
||||
return this.config.name ?? snakeToPascalWithSpaces(this.name);
|
||||
}
|
||||
|
||||
field(name: string): Field | undefined {
|
||||
@@ -210,20 +210,34 @@ export class Entity<
|
||||
return true;
|
||||
}
|
||||
|
||||
toSchema(clean?: boolean): object {
|
||||
const fields = Object.fromEntries(this.fields.map((field) => [field.name, field]));
|
||||
toSchema(options?: { clean: boolean; context?: "create" | "update" }): object {
|
||||
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(
|
||||
transformObject(fields, (field) => ({
|
||||
title: field.config.label,
|
||||
$comment: field.config.description,
|
||||
$field: field.type,
|
||||
readOnly: !field.isFillable("update") ? true : undefined,
|
||||
writeOnly: !field.isFillable("create") ? true : undefined,
|
||||
...field.toJsonSchema()
|
||||
}))
|
||||
transformObject(_fields, (field) => {
|
||||
//const hidden = field.isHidden(options?.context);
|
||||
const fillable = field.isFillable(options?.context);
|
||||
return {
|
||||
title: field.config.label,
|
||||
$comment: field.config.description,
|
||||
$field: field.type,
|
||||
readOnly: !fillable ? true : undefined,
|
||||
...field.toJsonSchema()
|
||||
};
|
||||
}),
|
||||
{ additionalProperties: false }
|
||||
);
|
||||
|
||||
return clean ? JSON.parse(JSON.stringify(schema)) : schema;
|
||||
return options?.clean ? JSON.parse(JSON.stringify(schema)) : schema;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
|
||||
@@ -14,6 +14,14 @@ import { SchemaManager } from "../schema/SchemaManager";
|
||||
import { Entity } from "./Entity";
|
||||
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> {
|
||||
connection: Connection;
|
||||
|
||||
@@ -87,10 +95,16 @@ export class EntityManager<DB> {
|
||||
this.entities.push(entity);
|
||||
}
|
||||
|
||||
entity(name: string): Entity {
|
||||
const entity = this.entities.find((e) => e.name === name);
|
||||
entity(e: Entity | string): Entity {
|
||||
let entity: Entity | undefined;
|
||||
if (typeof e === "string") {
|
||||
entity = this.entities.find((entity) => entity.name === e);
|
||||
} else {
|
||||
entity = e;
|
||||
}
|
||||
|
||||
if (!entity) {
|
||||
throw new EntityNotDefinedException(name);
|
||||
throw new EntityNotDefinedException(typeof e === "string" ? e : e.name);
|
||||
}
|
||||
|
||||
return entity;
|
||||
@@ -162,28 +176,16 @@ export class EntityManager<DB> {
|
||||
return this.relations.relationReferencesOf(this.entity(entity_name));
|
||||
}
|
||||
|
||||
repository(_entity: Entity | string) {
|
||||
const entity = _entity instanceof Entity ? _entity : this.entity(_entity);
|
||||
return new Repository(this, entity, this.emgr);
|
||||
repository<E extends Entity | string>(entity: E): Repository<DB, EntitySchema<E, DB>> {
|
||||
return this.repo(entity);
|
||||
}
|
||||
|
||||
repo<E extends Entity>(
|
||||
_entity: E
|
||||
): Repository<
|
||||
DB,
|
||||
E extends Entity<infer Name> ? (Name extends keyof DB ? Name : never) : never
|
||||
> {
|
||||
return new Repository(this, _entity, this.emgr);
|
||||
repo<E extends Entity | string>(entity: E): Repository<DB, EntitySchema<E, DB>> {
|
||||
return new Repository(this, this.entity(entity), this.emgr);
|
||||
}
|
||||
|
||||
_repo<TB extends keyof DB>(_entity: TB): Repository<DB, TB> {
|
||||
const entity = this.entity(_entity as any);
|
||||
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);
|
||||
mutator<E extends Entity | string>(entity: E): Mutator<DB, EntitySchema<E, DB>> {
|
||||
return new Mutator(this, this.entity(entity), this.emgr);
|
||||
}
|
||||
|
||||
addIndex(index: EntityIndex, force = false) {
|
||||
|
||||
@@ -25,7 +25,13 @@ export type MutatorResponse<T = EntityData[]> = {
|
||||
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>;
|
||||
entity: Entity;
|
||||
static readonly Events = MutatorEvents;
|
||||
@@ -47,13 +53,13 @@ export class Mutator<DB> implements EmitsEvents {
|
||||
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;
|
||||
if (!context) {
|
||||
throw new Error("Context must be provided for validation");
|
||||
}
|
||||
|
||||
const keys = Object.keys(data);
|
||||
const keys = Object.keys(data as any);
|
||||
const validatedData: EntityData = {};
|
||||
|
||||
// get relational references/keys
|
||||
@@ -95,7 +101,7 @@ export class Mutator<DB> implements EmitsEvents {
|
||||
throw new Error(`No data left to update "${entity.name}"`);
|
||||
}
|
||||
|
||||
return validatedData;
|
||||
return validatedData as Given;
|
||||
}
|
||||
|
||||
protected async many(qb: MutatorQB): Promise<MutatorResponse> {
|
||||
@@ -120,7 +126,7 @@ export class Mutator<DB> implements EmitsEvents {
|
||||
return { ...response, data: data[0]! };
|
||||
}
|
||||
|
||||
async insertOne(data: EntityData): Promise<MutatorResponse<EntityData>> {
|
||||
async insertOne(data: Input): Promise<MutatorResponse<Output>> {
|
||||
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`);
|
||||
@@ -154,10 +160,10 @@ export class Mutator<DB> implements EmitsEvents {
|
||||
|
||||
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;
|
||||
if (!Number.isInteger(id)) {
|
||||
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");
|
||||
|
||||
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
|
||||
.updateTable(entity.name)
|
||||
.set(validatedData)
|
||||
.set(validatedData as any)
|
||||
.where(entity.id().name, "=", id)
|
||||
.returning(entity.getSelect());
|
||||
|
||||
@@ -181,10 +191,10 @@ export class Mutator<DB> implements EmitsEvents {
|
||||
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;
|
||||
if (!Number.isInteger(id)) {
|
||||
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 })
|
||||
);
|
||||
|
||||
return res;
|
||||
return res as any;
|
||||
}
|
||||
|
||||
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)
|
||||
async deleteWhere(where?: RepoQuery["where"]): Promise<MutatorResponse<EntityData>> {
|
||||
async deleteWhere(where?: RepoQuery["where"]): Promise<MutatorResponse<Output[]>> {
|
||||
const entity = this.entity;
|
||||
|
||||
const qb = this.appendWhere(this.conn.deleteFrom(entity.name), where).returning(
|
||||
entity.getSelect()
|
||||
);
|
||||
|
||||
//await this.emgr.emit(new Mutator.Events.MutatorDeleteBefore({ entity, entityId: id }));
|
||||
|
||||
const res = await this.many(qb);
|
||||
|
||||
/*await this.emgr.emit(
|
||||
new Mutator.Events.MutatorDeleteAfter({ entity, entityId: id, data: res.data })
|
||||
);*/
|
||||
|
||||
return res;
|
||||
return (await this.many(qb)) as any;
|
||||
}
|
||||
|
||||
async updateWhere(
|
||||
data: EntityData,
|
||||
where?: RepoQuery["where"]
|
||||
): Promise<MutatorResponse<EntityData>> {
|
||||
async updateWhere(data: Partial<Input>, where?: RepoQuery["where"]): Promise<MutatorResponse<Output[]>> {
|
||||
const entity = this.entity;
|
||||
|
||||
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)
|
||||
.set(validatedData)
|
||||
//.where(entity.id().name, "=", id)
|
||||
.set(validatedData as any)
|
||||
.returning(entity.getSelect());
|
||||
|
||||
const res = await this.many(query);
|
||||
return (await this.many(query)) as any;
|
||||
}
|
||||
|
||||
/*await this.emgr.emit(
|
||||
new Mutator.Events.MutatorUpdateAfter({ entity, entityId: id, data: res.data })
|
||||
);*/
|
||||
async insertMany(data: Input[]): Promise<MutatorResponse<Output[]>> {
|
||||
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(
|
||||
id: PrimaryFieldType,
|
||||
_options?: Partial<Omit<RepoQuery, "where" | "limit" | "offset">>
|
||||
): Promise<RepositoryResponse<DB[TB]>> {
|
||||
): Promise<RepositoryResponse<DB[TB] | undefined>> {
|
||||
const { qb, options } = this.buildQuery(
|
||||
{
|
||||
..._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;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ export { LibsqlConnection, type LibSqlCredentials } from "./connection/LibsqlCon
|
||||
export { SqliteConnection } from "./connection/SqliteConnection";
|
||||
export { SqliteLocalConnection } from "./connection/SqliteLocalConnection";
|
||||
|
||||
export { constructEntity, constructRelation } from "./schema/constructor";
|
||||
|
||||
export const DatabaseEvents = {
|
||||
...MutatorEvents,
|
||||
...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 {
|
||||
BooleanField,
|
||||
type BooleanFieldConfig,
|
||||
@@ -5,6 +10,8 @@ import {
|
||||
type DateFieldConfig,
|
||||
Entity,
|
||||
type EntityConfig,
|
||||
EntityIndex,
|
||||
type EntityRelation,
|
||||
EnumField,
|
||||
type EnumFieldConfig,
|
||||
type Field,
|
||||
@@ -25,15 +32,14 @@ import {
|
||||
type TEntityType,
|
||||
TextField,
|
||||
type TextFieldConfig
|
||||
} from "data";
|
||||
import type { Generated } from "kysely";
|
||||
import { MediaField, type MediaFieldConfig, type MediaItem } from "media/MediaField";
|
||||
} from "../index";
|
||||
|
||||
type Options<Config = any> = {
|
||||
entity: { name: string; fields: Record<string, Field<any, any, any>> };
|
||||
field_name: string;
|
||||
config: Config;
|
||||
is_required: boolean;
|
||||
another?: string;
|
||||
};
|
||||
|
||||
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 }
|
||||
? 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
|
||||
? Type
|
||||
: Type | undefined
|
||||
: 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 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>>>;
|
||||
|
||||
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,
|
||||
getDefaultSchema,
|
||||
type ModuleConfigs,
|
||||
type ModuleSchemas
|
||||
} from "modules/ModuleManager";
|
||||
type ModuleSchemas,
|
||||
type ModuleManagerOptions,
|
||||
type ModuleBuildContext
|
||||
} from "./modules/ModuleManager";
|
||||
|
||||
export { registries } from "modules/registries";
|
||||
|
||||
|
||||
@@ -174,7 +174,7 @@ export class MediaController implements ClassController {
|
||||
const result = await mutator.insertOne({
|
||||
...this.media.uploadedEventDataToMediaPayload(info),
|
||||
...mediaRef
|
||||
});
|
||||
} as any);
|
||||
mutator.__unstable_toggleSystemEntityCreation(true);
|
||||
|
||||
// delete items if needed
|
||||
|
||||
@@ -5,10 +5,10 @@ import type { Static, TSchema } from "core/utils";
|
||||
import type { Connection, EntityManager } from "data";
|
||||
import type { Hono } from "hono";
|
||||
|
||||
export type ModuleBuildContext = {
|
||||
export type ModuleBuildContext<DB = any> = {
|
||||
connection: Connection;
|
||||
server: Hono<any>;
|
||||
em: EntityManager<any>;
|
||||
em: EntityManager<DB>;
|
||||
emgr: EventManager<any>;
|
||||
guard: Guard;
|
||||
};
|
||||
|
||||
@@ -35,6 +35,8 @@ import { AppFlows } from "../flows/AppFlows";
|
||||
import { AppMedia } from "../media/AppMedia";
|
||||
import type { Module, ModuleBuildContext } from "./Module";
|
||||
|
||||
export type { ModuleBuildContext };
|
||||
|
||||
export const MODULES = {
|
||||
server: AppServer,
|
||||
data: AppData<any>,
|
||||
@@ -73,9 +75,14 @@ export type ModuleManagerOptions = {
|
||||
module: Module,
|
||||
config: ModuleConfigs[Module]
|
||||
) => Promise<void>;
|
||||
// triggered when no config table existed
|
||||
onFirstBoot?: () => Promise<void>;
|
||||
// base path for the hono instance
|
||||
basePath?: string;
|
||||
// doesn't perform validity checks for given/fetched config
|
||||
trustFetched?: boolean;
|
||||
// runs when initial config provided on a fresh database
|
||||
seed?: (ctx: ModuleBuildContext) => Promise<void>;
|
||||
};
|
||||
|
||||
type ConfigTable<Json = ModuleConfigs> = {
|
||||
@@ -294,7 +301,7 @@ export class ModuleManager {
|
||||
version,
|
||||
json: configs,
|
||||
updated_at: new Date()
|
||||
},
|
||||
} as any,
|
||||
{
|
||||
type: "config",
|
||||
version
|
||||
@@ -448,6 +455,9 @@ export class ModuleManager {
|
||||
await this.buildModules();
|
||||
await this.save();
|
||||
|
||||
// run initial setup
|
||||
await this.setupInitial();
|
||||
|
||||
this.logger.clear();
|
||||
return this;
|
||||
}
|
||||
@@ -462,6 +472,21 @@ export class ModuleManager {
|
||||
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] {
|
||||
if (!(key in this.modules)) {
|
||||
throw new Error(`Module "${key}" doesn't exist, cannot get`);
|
||||
|
||||
@@ -5,14 +5,14 @@ import { useApi } from "ui/client";
|
||||
|
||||
export const useApiQuery = <
|
||||
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>,
|
||||
options?: SWRConfiguration & { enabled?: boolean; refine?: RefineFn }
|
||||
) => {
|
||||
const api = useApi();
|
||||
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 key = promise.key();
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ type OmitFirstArg<F> = F extends (x: any, ...args: infer P) => any
|
||||
* the first argument "entity" for convenience
|
||||
* @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 methods = [
|
||||
"readOne",
|
||||
|
||||
@@ -1,23 +1,40 @@
|
||||
import type { PrimaryFieldType } from "core";
|
||||
import { objectTransform } from "core/utils";
|
||||
import { encodeSearch, objectTransform } from "core/utils";
|
||||
import type { EntityData, RepoQuery } from "data";
|
||||
import type { ResponseObject } from "modules/ModuleApi";
|
||||
import useSWR, { type SWRConfiguration } from "swr";
|
||||
import { useApi } from "ui/client";
|
||||
import type { ModuleApi, ResponseObject } from "modules/ModuleApi";
|
||||
import useSWR, { type SWRConfiguration, mutate } from "swr";
|
||||
import { type Api, useApi } from "ui/client";
|
||||
|
||||
export class UseEntityApiError<Payload = any> extends Error {
|
||||
constructor(
|
||||
public payload: Payload,
|
||||
public response: Response,
|
||||
message?: string
|
||||
public response: ResponseObject<Payload>,
|
||||
fallback?: string
|
||||
) {
|
||||
let message = fallback;
|
||||
if ("error" in response) {
|
||||
message = response.error as string;
|
||||
if (fallback) {
|
||||
message = `${fallback}: ${message}`;
|
||||
}
|
||||
}
|
||||
|
||||
super(message ?? "UseEntityApiError");
|
||||
}
|
||||
}
|
||||
|
||||
function Test() {
|
||||
const { read } = useEntity("users");
|
||||
async () => {
|
||||
const data = await read();
|
||||
};
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export const useEntity = <
|
||||
Entity extends string,
|
||||
Id extends PrimaryFieldType | undefined = undefined
|
||||
Entity extends keyof DB | string,
|
||||
Id extends PrimaryFieldType | undefined = undefined,
|
||||
Data = Entity extends keyof DB ? DB[Entity] : EntityData
|
||||
>(
|
||||
entity: Entity,
|
||||
id?: Id
|
||||
@@ -25,27 +42,30 @@ export const useEntity = <
|
||||
const api = useApi().data;
|
||||
|
||||
return {
|
||||
create: async (input: EntityData) => {
|
||||
create: async (input: Omit<Data, "id">) => {
|
||||
const res = await api.createOne(entity, input);
|
||||
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;
|
||||
},
|
||||
read: async (query: Partial<RepoQuery> = {}) => {
|
||||
const res = id ? await api.readOne(entity, id!, query) : await api.readMany(entity, query);
|
||||
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) {
|
||||
throw new Error("id is required");
|
||||
}
|
||||
const res = await api.updateOne(entity, _id, input);
|
||||
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;
|
||||
},
|
||||
@@ -56,44 +76,67 @@ export const useEntity = <
|
||||
|
||||
const res = await api.deleteOne(entity, _id);
|
||||
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;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// @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 = <
|
||||
Entity extends string,
|
||||
Entity extends keyof DB | string,
|
||||
Id extends PrimaryFieldType | undefined = undefined
|
||||
>(
|
||||
entity: Entity,
|
||||
id?: Id,
|
||||
query?: Partial<RepoQuery>,
|
||||
options?: SWRConfiguration & { enabled?: boolean }
|
||||
options?: SWRConfiguration & { enabled?: boolean; revalidateOnMutate?: boolean }
|
||||
) => {
|
||||
const api = useApi().data;
|
||||
const key =
|
||||
options?.enabled !== false
|
||||
? [...(api.options?.basepath?.split("/") ?? []), entity, ...(id ? [id] : [])].filter(
|
||||
Boolean
|
||||
)
|
||||
: null;
|
||||
const { read, ...actions } = useEntity(entity, id) as any;
|
||||
const key = makeKey(api, entity, id, query);
|
||||
const { read, ...actions } = useEntity<Entity, Id>(entity, id);
|
||||
const fetcher = () => read(query);
|
||||
|
||||
type T = Awaited<ReturnType<(typeof api)[Id extends undefined ? "readMany" : "readOne"]>>;
|
||||
const swr = useSWR<T>(key, fetcher, {
|
||||
type T = Awaited<ReturnType<typeof fetcher>>;
|
||||
const swr = useSWR<T>(options?.enabled === false ? null : key, fetcher as any, {
|
||||
revalidateOnFocus: false,
|
||||
keepPreviousData: false,
|
||||
keepPreviousData: true,
|
||||
...options
|
||||
});
|
||||
|
||||
const mapped = objectTransform(actions, (action) => {
|
||||
if (action === "read") return;
|
||||
const mutateAll = async () => {
|
||||
const entityKey = makeKey(api, entity);
|
||||
return mutate((key) => typeof key === "string" && key.startsWith(entityKey), undefined, {
|
||||
revalidate: true
|
||||
});
|
||||
};
|
||||
|
||||
return async (...args) => {
|
||||
return swr.mutate(action(...args)) as any;
|
||||
const mapped = objectTransform(actions, (action) => {
|
||||
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">;
|
||||
|
||||
@@ -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 = <
|
||||
Entity extends string,
|
||||
Id extends PrimaryFieldType | undefined = undefined
|
||||
Entity extends keyof DB | string,
|
||||
Id extends PrimaryFieldType | undefined = undefined,
|
||||
Data = Entity extends keyof DB ? Omit<DB[Entity], "id"> : EntityData
|
||||
>(
|
||||
entity: Entity,
|
||||
id?: Id,
|
||||
options?: SWRConfiguration
|
||||
) => {
|
||||
const { data, ...$q } = useEntityQuery(entity, id, undefined, {
|
||||
const { data, ...$q } = useEntityQuery<Entity, Id>(entity, id, undefined, {
|
||||
...options,
|
||||
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 { Entity } from "data";
|
||||
import { AppData } from "data/AppData";
|
||||
import { constructEntity } from "data";
|
||||
import {
|
||||
type TAppDataEntity,
|
||||
type TAppDataEntityFields,
|
||||
@@ -19,7 +18,7 @@ export function useBkndData() {
|
||||
|
||||
// @todo: potentially store in ref, so it doesn't get recomputed? or use memo?
|
||||
const entities = transformObject(config.data.entities ?? {}, (entity, name) => {
|
||||
return AppData.constructEntity(name, entity);
|
||||
return constructEntity(name, entity);
|
||||
});
|
||||
|
||||
const actions = {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { App } from "App";
|
||||
import type { Entity, EntityRelation } from "data";
|
||||
import { AppData } from "data/AppData";
|
||||
import { type Entity, type EntityRelation, constructEntity, constructRelation } from "data";
|
||||
import { RelationAccessor } from "data/relations/RelationAccessor";
|
||||
import { Flow, TaskMap } from "flows";
|
||||
|
||||
@@ -20,11 +19,11 @@ export class AppReduced {
|
||||
//console.log("received appjson", appJson);
|
||||
|
||||
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]) => {
|
||||
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 ?? {})) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { ReactCodeMirrorProps } from "@uiw/react-codemirror";
|
||||
import { Suspense, lazy } from "react";
|
||||
import { default as CodeMirror, type ReactCodeMirrorProps } from "@uiw/react-codemirror";
|
||||
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
const CodeMirror = lazy(() => import("@uiw/react-codemirror"));
|
||||
|
||||
export default function CodeEditor({ editable, basicSetup, ...props }: ReactCodeMirrorProps) {
|
||||
const b = useBknd();
|
||||
@@ -15,13 +14,11 @@ export default function CodeEditor({ editable, basicSetup, ...props }: ReactCode
|
||||
: basicSetup;
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
<CodeMirror
|
||||
theme={theme === "dark" ? "dark" : "light"}
|
||||
editable={editable}
|
||||
basicSetup={_basicSetup}
|
||||
{...props}
|
||||
/>
|
||||
</Suspense>
|
||||
<CodeMirror
|
||||
theme={theme === "dark" ? "dark" : "light"}
|
||||
editable={editable}
|
||||
basicSetup={_basicSetup}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import type { Schema } from "@cfworker/json-schema";
|
||||
import Form from "@rjsf/core";
|
||||
import type { RJSFSchema, UiSchema } from "@rjsf/utils";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import { forwardRef, useId, useImperativeHandle, useRef, useState } from "react";
|
||||
//import { JsonSchemaValidator } from "./JsonSchemaValidator";
|
||||
import { fields as Fields } from "./fields";
|
||||
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 { widgets as Widgets } from "./widgets";
|
||||
|
||||
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,
|
||||
type JsonSchemaFormProps,
|
||||
type JsonSchemaFormRef
|
||||
} from "ui/components/form/json-schema/JsonSchemaForm";
|
||||
} from "ui/components/form/json-schema";
|
||||
|
||||
import type { ContextModalProps } from "@mantine/modals";
|
||||
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import type { FieldApi } from "@tanstack/react-form";
|
||||
import type { EntityData, JsonSchemaField } from "data";
|
||||
import { Suspense, lazy } from "react";
|
||||
import * as Formy from "ui/components/form/Formy";
|
||||
import { FieldLabel } from "ui/components/form/Formy";
|
||||
|
||||
const JsonSchemaForm = lazy(() =>
|
||||
import("ui/components/form/json-schema/JsonSchemaForm").then((m) => ({
|
||||
default: m.JsonSchemaForm
|
||||
}))
|
||||
);
|
||||
import { JsonSchemaForm } from "ui/components/form/json-schema";
|
||||
|
||||
export function EntityJsonSchemaFormField({
|
||||
fieldApi,
|
||||
@@ -34,23 +28,21 @@ export function EntityJsonSchemaFormField({
|
||||
return (
|
||||
<Formy.Group>
|
||||
<FieldLabel htmlFor={fieldApi.name} field={field} />
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<div
|
||||
data-disabled={disabled ? 1 : undefined}
|
||||
className="data-[disabled]:opacity-70 data-[disabled]:pointer-events-none"
|
||||
>
|
||||
<JsonSchemaForm
|
||||
schema={field.getJsonSchema()}
|
||||
onChange={handleChange}
|
||||
direction="horizontal"
|
||||
formData={formData}
|
||||
uiSchema={{
|
||||
"ui:globalOptions": { flexDirection: "row" },
|
||||
...field.getJsonUiSchema()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Suspense>
|
||||
<div
|
||||
data-disabled={disabled ? 1 : undefined}
|
||||
className="data-[disabled]:opacity-70 data-[disabled]:pointer-events-none"
|
||||
>
|
||||
<JsonSchemaForm
|
||||
schema={field.getJsonSchema()}
|
||||
onChange={handleChange}
|
||||
direction="horizontal"
|
||||
formData={formData}
|
||||
uiSchema={{
|
||||
"ui:globalOptions": { flexDirection: "row" },
|
||||
...field.getJsonUiSchema()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Formy.Group>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import { Handle, type Node, type NodeProps, Position } from "@xyflow/react";
|
||||
import { Const, Type, transformObject } from "core/utils";
|
||||
import { type TaskRenderProps, type Trigger, TriggerMap } from "flows";
|
||||
import { Suspense, lazy } from "react";
|
||||
import { type Trigger, TriggerMap } from "flows";
|
||||
import type { IconType } from "react-icons";
|
||||
import { TbCircleLetterT } from "react-icons/tb";
|
||||
const JsonSchemaForm = lazy(() =>
|
||||
import("ui/components/form/json-schema/JsonSchemaForm").then((m) => ({
|
||||
default: m.JsonSchemaForm
|
||||
}))
|
||||
);
|
||||
import { JsonSchemaForm } from "ui/components/form/json-schema";
|
||||
|
||||
export type TaskComponentProps = NodeProps<Node<{ trigger: Trigger }>> & {
|
||||
Icon?: IconType;
|
||||
@@ -48,17 +43,15 @@ export function TriggerComponent({
|
||||
</div>
|
||||
<div className="w-full h-px bg-primary/10" />
|
||||
<div className="flex flex-col gap-2 px-3 py-2">
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<JsonSchemaForm
|
||||
className="legacy"
|
||||
schema={Type.Union(triggerSchemas)}
|
||||
onChange={console.log}
|
||||
formData={trigger}
|
||||
{...props}
|
||||
/*uiSchema={uiSchema}*/
|
||||
/*fields={{ template: TemplateField }}*/
|
||||
/>
|
||||
</Suspense>
|
||||
<JsonSchemaForm
|
||||
className="legacy"
|
||||
schema={Type.Union(triggerSchemas)}
|
||||
onChange={console.log}
|
||||
formData={trigger}
|
||||
{...props}
|
||||
/*uiSchema={uiSchema}*/
|
||||
/*fields={{ template: TemplateField }}*/
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Handle
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import type { Task } from "flows";
|
||||
import { Suspense, lazy } from "react";
|
||||
import { TemplateField } from "./TemplateField";
|
||||
|
||||
const JsonSchemaForm = lazy(() =>
|
||||
import("ui/components/form/json-schema/JsonSchemaForm").then((m) => ({
|
||||
default: m.JsonSchemaForm
|
||||
}))
|
||||
);
|
||||
import { JsonSchemaForm } from "ui/components/form/json-schema";
|
||||
|
||||
export type TaskFormProps = {
|
||||
task: Task;
|
||||
@@ -26,16 +19,14 @@ export function TaskForm({ task, onChange, ...props }: TaskFormProps) {
|
||||
//console.log("uiSchema", uiSchema);
|
||||
|
||||
return (
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<JsonSchemaForm
|
||||
className="legacy"
|
||||
schema={schema}
|
||||
onChange={onChange}
|
||||
formData={params}
|
||||
{...props}
|
||||
/*uiSchema={uiSchema}*/
|
||||
/*fields={{ template: TemplateField }}*/
|
||||
/>
|
||||
</Suspense>
|
||||
<JsonSchemaForm
|
||||
className="legacy"
|
||||
schema={schema}
|
||||
onChange={onChange}
|
||||
formData={params}
|
||||
{...props}
|
||||
/*uiSchema={uiSchema}*/
|
||||
/*fields={{ template: TemplateField }}*/
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 { Button } from "ui/components/buttons/Button";
|
||||
import { Alert } from "ui/components/display/Alert";
|
||||
import {
|
||||
JsonSchemaForm,
|
||||
type JsonSchemaFormRef
|
||||
} from "ui/components/form/json-schema/JsonSchemaForm";
|
||||
import { JsonSchemaForm, type JsonSchemaFormRef } from "ui/components/form/json-schema";
|
||||
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
||||
import { useNavigate } from "ui/lib/routes";
|
||||
import { extractSchema } from "../settings/utils/schema";
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { cloneDeep, omit } from "lodash-es";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
import { JsonSchemaForm } from "ui/components/form/json-schema/JsonSchemaForm";
|
||||
import * as AppShell from "../../layouts/AppShell/AppShell";
|
||||
import { extractSchema } from "../settings/utils/schema";
|
||||
|
||||
export function AuthStrategiesList() {
|
||||
useBknd({ withSecrets: true });
|
||||
|
||||
@@ -101,7 +101,7 @@ export function DataEntityUpdate({ params }) {
|
||||
data: {
|
||||
data: data as any,
|
||||
entity: entity.toJSON(),
|
||||
schema: entity.toSchema(true),
|
||||
schema: entity.toSchema({ clean: true }),
|
||||
form: Form.state.values,
|
||||
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 { IconButton } from "ui/components/buttons/IconButton";
|
||||
import { Empty } from "ui/components/display/Empty";
|
||||
import {
|
||||
JsonSchemaForm,
|
||||
type JsonSchemaFormRef
|
||||
} from "ui/components/form/json-schema/JsonSchemaForm";
|
||||
import { JsonSchemaForm, type JsonSchemaFormRef } from "ui/components/form/json-schema";
|
||||
import { Dropdown } from "ui/components/overlay/Dropdown";
|
||||
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
||||
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 { JsonViewer } from "ui/components/code/JsonViewer";
|
||||
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 { Popover } from "ui/components/overlay/Popover";
|
||||
import { fieldSpecs } from "ui/modules/data/components/fields-specs";
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import { Suspense, lazy } from "react";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
import { Route, Router, Switch } from "wouter";
|
||||
import AuthRoutes from "./auth";
|
||||
import { AuthLogin } from "./auth/auth.login";
|
||||
import DataRoutes from "./data";
|
||||
import FlowRoutes from "./flows";
|
||||
import MediaRoutes from "./media";
|
||||
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 MediaRoutes = lazy(() => import("./media"));
|
||||
const FlowRoutes = lazy(() => import("./flows"));
|
||||
const SettingsRoutes = lazy(() => import("./settings"));
|
||||
const SettingsRoutes = lazy(() => import("./settings"));*/
|
||||
|
||||
// @ts-ignore
|
||||
const TestRoutes = lazy(() => import("./test"));
|
||||
|
||||
@@ -8,10 +8,7 @@ import { Button } from "ui/components/buttons/Button";
|
||||
import { IconButton } from "ui/components/buttons/IconButton";
|
||||
import { Alert } from "ui/components/display/Alert";
|
||||
import { Empty } from "ui/components/display/Empty";
|
||||
import {
|
||||
JsonSchemaForm,
|
||||
type JsonSchemaFormRef
|
||||
} from "ui/components/form/json-schema/JsonSchemaForm";
|
||||
import { JsonSchemaForm, type JsonSchemaFormRef } from "ui/components/form/json-schema";
|
||||
import { Dropdown } from "ui/components/overlay/Dropdown";
|
||||
import { DataTable } from "ui/components/table/DataTable";
|
||||
import { useEvent } from "ui/hooks/use-event";
|
||||
|
||||
@@ -3,16 +3,13 @@ import type { TObject } from "core/utils";
|
||||
import { omit } from "lodash-es";
|
||||
import { useRef, useState } from "react";
|
||||
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 { 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 = {
|
||||
schema: TObject;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { parse } from "core/utils";
|
||||
import { AppFlows } from "flows/AppFlows";
|
||||
import { useState } from "react";
|
||||
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";
|
||||
|
||||
export default function FlowCreateSchemaTest() {
|
||||
|
||||
@@ -2,12 +2,9 @@ import Form from "@rjsf/core";
|
||||
import type { RJSFSchema, UiSchema } from "@rjsf/utils";
|
||||
import { useRef } from "react";
|
||||
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 {
|
||||
JsonSchemaForm,
|
||||
type JsonSchemaFormRef
|
||||
} from "../../../../components/form/json-schema/JsonSchemaForm";
|
||||
import * as AppShell from "../../../../layouts/AppShell/AppShell";
|
||||
|
||||
class CfJsonSchemaValidator {}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Schema } from "@cfworker/json-schema";
|
||||
import { useState } from "react";
|
||||
import { JsonSchemaForm } from "../../../components/form/json-schema/JsonSchemaForm";
|
||||
import { Scrollable } from "../../../layouts/AppShell/AppShell";
|
||||
import { JsonSchemaForm } from "ui/components/form/json-schema";
|
||||
import { Scrollable } from "ui/layouts/AppShell/AppShell";
|
||||
|
||||
const schema: Schema = {
|
||||
definitions: {
|
||||
@@ -9,52 +9,52 @@ const schema: Schema = {
|
||||
anyOf: [
|
||||
{
|
||||
title: "String",
|
||||
type: "string",
|
||||
type: "string"
|
||||
},
|
||||
{
|
||||
title: "Number",
|
||||
type: "number",
|
||||
type: "number"
|
||||
},
|
||||
{
|
||||
title: "Boolean",
|
||||
type: "boolean",
|
||||
},
|
||||
],
|
||||
type: "boolean"
|
||||
}
|
||||
]
|
||||
},
|
||||
numeric: {
|
||||
anyOf: [
|
||||
{
|
||||
title: "Number",
|
||||
type: "number",
|
||||
type: "number"
|
||||
},
|
||||
{
|
||||
title: "Datetime",
|
||||
type: "string",
|
||||
format: "date-time",
|
||||
format: "date-time"
|
||||
},
|
||||
{
|
||||
title: "Date",
|
||||
type: "string",
|
||||
format: "date",
|
||||
format: "date"
|
||||
},
|
||||
{
|
||||
title: "Time",
|
||||
type: "string",
|
||||
format: "time",
|
||||
},
|
||||
],
|
||||
format: "time"
|
||||
}
|
||||
]
|
||||
},
|
||||
boolean: {
|
||||
title: "Boolean",
|
||||
type: "boolean",
|
||||
},
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
type: "object",
|
||||
properties: {
|
||||
operand: {
|
||||
enum: ["$and", "$or"],
|
||||
default: "$and",
|
||||
type: "string",
|
||||
type: "string"
|
||||
},
|
||||
conditions: {
|
||||
type: "array",
|
||||
@@ -64,10 +64,10 @@ const schema: Schema = {
|
||||
operand: {
|
||||
enum: ["$and", "$or"],
|
||||
default: "$and",
|
||||
type: "string",
|
||||
type: "string"
|
||||
},
|
||||
key: {
|
||||
type: "string",
|
||||
type: "string"
|
||||
},
|
||||
operator: {
|
||||
type: "array",
|
||||
@@ -78,30 +78,30 @@ const schema: Schema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
$eq: {
|
||||
$ref: "#/definitions/primitive",
|
||||
},
|
||||
$ref: "#/definitions/primitive"
|
||||
}
|
||||
},
|
||||
required: ["$eq"],
|
||||
required: ["$eq"]
|
||||
},
|
||||
{
|
||||
title: "Lower than",
|
||||
type: "object",
|
||||
properties: {
|
||||
$lt: {
|
||||
$ref: "#/definitions/numeric",
|
||||
},
|
||||
$ref: "#/definitions/numeric"
|
||||
}
|
||||
},
|
||||
required: ["$lt"],
|
||||
required: ["$lt"]
|
||||
},
|
||||
{
|
||||
title: "Greather than",
|
||||
type: "object",
|
||||
properties: {
|
||||
$gt: {
|
||||
$ref: "#/definitions/numeric",
|
||||
},
|
||||
$ref: "#/definitions/numeric"
|
||||
}
|
||||
},
|
||||
required: ["$gt"],
|
||||
required: ["$gt"]
|
||||
},
|
||||
{
|
||||
title: "Between",
|
||||
@@ -110,13 +110,13 @@ const schema: Schema = {
|
||||
$between: {
|
||||
type: "array",
|
||||
items: {
|
||||
$ref: "#/definitions/numeric",
|
||||
$ref: "#/definitions/numeric"
|
||||
},
|
||||
minItems: 2,
|
||||
maxItems: 2,
|
||||
},
|
||||
maxItems: 2
|
||||
}
|
||||
},
|
||||
required: ["$between"],
|
||||
required: ["$between"]
|
||||
},
|
||||
{
|
||||
title: "In",
|
||||
@@ -125,23 +125,23 @@ const schema: Schema = {
|
||||
$in: {
|
||||
type: "array",
|
||||
items: {
|
||||
$ref: "#/definitions/primitive",
|
||||
$ref: "#/definitions/primitive"
|
||||
},
|
||||
minItems: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
minItems: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
minItems: 1,
|
||||
},
|
||||
minItems: 1
|
||||
}
|
||||
},
|
||||
required: ["key", "operator"],
|
||||
required: ["key", "operator"]
|
||||
},
|
||||
minItems: 1,
|
||||
},
|
||||
minItems: 1
|
||||
}
|
||||
},
|
||||
required: ["operand", "conditions"],
|
||||
required: ["operand", "conditions"]
|
||||
};
|
||||
|
||||
export default function QueryJsonFormTest() {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { useBknd } from "../../../client/BkndProvider";
|
||||
import { JsonSchemaForm } from "../../../components/form/json-schema/JsonSchemaForm";
|
||||
import { Scrollable } from "../../../layouts/AppShell/AppShell";
|
||||
import { useBknd } from "ui/client/BkndProvider";
|
||||
import { JsonSchemaForm } from "ui/components/form/json-schema";
|
||||
import { Scrollable } from "ui/layouts/AppShell/AppShell";
|
||||
|
||||
function useSchema() {
|
||||
const [schema, setSchema] = useState<any>();
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useApiQuery } from "ui/client";
|
||||
import { useApi, useApiQuery } from "ui/client";
|
||||
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() {
|
||||
const [text, setText] = useState("");
|
||||
const { data, ...r } = useApiQuery((api) => api.data.readOne("comments", 1), {
|
||||
@@ -16,7 +29,7 @@ export default function SWRAndAPI() {
|
||||
|
||||
return (
|
||||
<Scrollable>
|
||||
<pre>{JSON.stringify(r.promise.keyArray({ search: false }))}</pre>
|
||||
<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>}
|
||||
@@ -26,12 +39,12 @@ export default function SWRAndAPI() {
|
||||
e.preventDefault();
|
||||
if (!comment) return;
|
||||
|
||||
await r.mutate(async () => {
|
||||
/*await r.mutate(async () => {
|
||||
const res = await r.api.data.updateOne("comments", comment.id, {
|
||||
content: text
|
||||
});
|
||||
return res.data;
|
||||
});
|
||||
});*/
|
||||
|
||||
return false;
|
||||
}}
|
||||
|
||||
@@ -1,54 +1,72 @@
|
||||
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";
|
||||
|
||||
export default function SwrAndDataApi() {
|
||||
return (
|
||||
<div>
|
||||
<Scrollable>
|
||||
asdf
|
||||
<DirectDataApi />
|
||||
<QueryDataApi />
|
||||
</div>
|
||||
<QueryMutateDataApi />
|
||||
</Scrollable>
|
||||
);
|
||||
}
|
||||
|
||||
function QueryDataApi() {
|
||||
const [text, setText] = useState("");
|
||||
const { data, update, ...r } = useEntityQuery("comments", 1, {});
|
||||
const comment = data ? data : null;
|
||||
|
||||
useEffect(() => {
|
||||
setText(comment?.content ?? "");
|
||||
}, [comment]);
|
||||
function QueryMutateDataApi() {
|
||||
const { mutate } = useEntityMutate("comments");
|
||||
const { data, ...r } = useEntityQuery("comments", undefined, {
|
||||
limit: 2
|
||||
});
|
||||
|
||||
return (
|
||||
<Scrollable>
|
||||
<div>
|
||||
bla
|
||||
<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>}
|
||||
{data && (
|
||||
<form
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
if (!comment) return;
|
||||
await update({ content: text });
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
<input type="text" value={text} onChange={(e) => setText(e.target.value)} />
|
||||
<button type="submit">submit</button>
|
||||
</form>
|
||||
<div>
|
||||
{data.map((comment) => (
|
||||
<input
|
||||
key={String(comment.id)}
|
||||
type="text"
|
||||
value={comment.content}
|
||||
onChange={async (e) => {
|
||||
await mutate(comment.id, { content: e.target.value });
|
||||
}}
|
||||
className="border border-black"
|
||||
/>
|
||||
))}
|
||||
</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() {
|
||||
const [data, setData] = useState<any>();
|
||||
const { create, read, update, _delete } = useEntity("comments", 1);
|
||||
const { create, read, update, _delete } = useEntity("comments");
|
||||
|
||||
useEffect(() => {
|
||||
read().then(setData);
|
||||
read().then((data) => setData(data));
|
||||
}, []);
|
||||
|
||||
return <pre>{JSON.stringify(data, null, 2)}</pre>;
|
||||
|
||||
@@ -26,14 +26,13 @@
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo",
|
||||
"outDir": "./dist/types",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"*": ["./src/*"],
|
||||
"bknd": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["./src/**/*.ts", "./src/**/*.tsx", "./env.d.ts"],
|
||||
"exclude": ["node_modules", "dist/**/*", "../examples/bun"]
|
||||
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
|
||||
"exclude": ["node_modules", "dist", "dist/types", "**/*.d.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user