optimized module manager seeding, added type support for new api hooks and reduced amount of dist chunks

This commit is contained in:
dswbx
2024-12-18 18:22:01 +01:00
parent c4138ef823
commit 602235b372
41 changed files with 434 additions and 328 deletions

View File

@@ -3,6 +3,7 @@ import {
BooleanField,
DateField,
Entity,
EntityManager,
EnumField,
JsonField,
ManyToManyRelation,
@@ -46,12 +47,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());
});

View File

@@ -9,15 +9,43 @@ 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`;
if (types) {
}
let types_running = false;
function buildTypes() {
if (types_running) return;
types_running = true;
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
@@ -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();
}
};
}

View File

@@ -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"
},

View File

@@ -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;

View File

@@ -37,6 +37,7 @@ export type AppConfig = InitialModuleConfigs;
export class App<DB = any> {
modules: ModuleManager;
static readonly Events = AppEvents;
adminController?: AdminController;
constructor(
private connection: Connection,
@@ -94,8 +95,12 @@ export class App<DB = any> {
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 +124,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;
}

View File

@@ -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

View File

@@ -9,10 +9,25 @@ export async function withDisabledConsole<R>(
fn: () => Promise<R>,
severities: ConsoleSeverity[] = ["log"]
): 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();
enable();
return result;
} catch (e) {
enable();
throw e;
}
}
export function disableConsoleLog(severities: ConsoleSeverity[] = ["log"]) {

View File

@@ -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: Data
) {
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<Data>
) {
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
);
}

View File

@@ -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}`
}
])
);
@@ -198,7 +197,7 @@ export class DataController implements ClassController {
const schema = _entity.toSchema();
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,

View File

@@ -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) {

View File

@@ -25,7 +25,9 @@ export type MutatorResponse<T = EntityData[]> = {
data: T;
};
export class Mutator<DB> implements EmitsEvents {
export class Mutator<DB = any, TB extends keyof DB = any, Data = Omit<DB[TB], "id">>
implements EmitsEvents
{
em: EntityManager<DB>;
entity: Entity;
static readonly Events = MutatorEvents;
@@ -47,13 +49,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 +97,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 +122,7 @@ export class Mutator<DB> implements EmitsEvents {
return { ...response, data: data[0]! };
}
async insertOne(data: EntityData): Promise<MutatorResponse<EntityData>> {
async insertOne(data: Data): Promise<MutatorResponse<DB[TB]>> {
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 +156,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: Data): Promise<MutatorResponse<DB[TB]>> {
const entity = this.entity;
if (!Number.isInteger(id)) {
throw new Error("ID must be provided for update");
@@ -166,12 +168,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 +187,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<DB[TB]>> {
const entity = this.entity;
if (!Number.isInteger(id)) {
throw new Error("ID must be provided for deletion");
@@ -203,7 +209,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 +256,24 @@ 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<DB[TB][]>> {
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: Data, where?: RepoQuery["where"]): Promise<MutatorResponse<DB[TB][]>> {
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);
/*await this.emgr.emit(
new Mutator.Events.MutatorUpdateAfter({ entity, entityId: id, data: res.data })
);*/
return res;
return (await this.many(query)) as any;
}
}

View File

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

View File

@@ -1,3 +1,5 @@
import type { Generated } from "kysely";
import { MediaField, type MediaFieldConfig, type MediaItem } from "media/MediaField";
import {
BooleanField,
type BooleanFieldConfig,
@@ -25,15 +27,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 +240,7 @@ export function relation<Local extends Entity>(local: Local) {
};
}
type InferEntityFields<T> = T extends Entity<infer _N, infer Fields>
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 +285,25 @@ 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;
const n = number();
type T2 = InferField<typeof n>;
const users = entity("users", {
name: text(),
email: text(),
created_at: datetime(),
updated_at: datetime()
});
type TUsersFields = InferEntityFields<typeof users>;
type TUsers = Schema<typeof users>;
type TUsers2 = Simplify<OptionalUndefined<Schema<typeof users>>>;
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>>>;

View File

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

View File

@@ -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

View File

@@ -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;
};

View File

@@ -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>,
@@ -75,7 +77,10 @@ export type ModuleManagerOptions = {
) => 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 +299,7 @@ export class ModuleManager {
version,
json: configs,
updated_at: new Date()
},
} as any,
{
type: "config",
version
@@ -448,6 +453,9 @@ export class ModuleManager {
await this.buildModules();
await this.save();
// run initial setup
await this.setupInitial();
this.logger.clear();
return this;
}
@@ -462,6 +470,18 @@ 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);
}
get<K extends keyof Modules>(key: K): Modules[K] {
if (!(key in this.modules)) {
throw new Error(`Module "${key}" doesn't exist, cannot get`);

View File

@@ -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();

View File

@@ -1,8 +1,8 @@
import type { PrimaryFieldType } from "core";
import { objectTransform } from "core/utils";
import type { EntityData, RepoQuery } from "data";
import type { ResponseObject } from "modules/ModuleApi";
import useSWR, { type SWRConfiguration } from "swr";
import type { ModuleApi, ResponseObject } from "modules/ModuleApi";
import useSWR, { type SWRConfiguration, useSWRConfig } from "swr";
import { useApi } from "ui/client";
export class UseEntityApiError<Payload = any> extends Error {
@@ -15,9 +15,19 @@ export class UseEntityApiError<Payload = any> extends Error {
}
}
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,7 +35,7 @@ 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");
@@ -37,9 +47,12 @@ export const useEntity = <
if (!res.ok) {
throw new UseEntityApiError(res.data, res.res, "Failed to read 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");
}
@@ -63,8 +76,17 @@ export const useEntity = <
};
};
export function makeKey(api: ModuleApi, entity: string, id?: PrimaryFieldType) {
return (
"/" +
[...(api.options?.basepath?.split("/") ?? []), entity, ...(id ? [id] : [])]
.filter(Boolean)
.join("/")
);
}
export const useEntityQuery = <
Entity extends string,
Entity extends keyof DB | string,
Id extends PrimaryFieldType | undefined = undefined
>(
entity: Entity,
@@ -72,28 +94,28 @@ export const useEntityQuery = <
query?: Partial<RepoQuery>,
options?: SWRConfiguration & { enabled?: boolean }
) => {
const { mutate } = useSWRConfig();
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);
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,
...options
});
const mapped = objectTransform(actions, (action) => {
if (action === "read") return;
return async (...args: any) => {
// @ts-ignore
const res = await action(...args);
return async (...args) => {
return swr.mutate(action(...args)) as any;
// mutate the key + list key
mutate(key);
if (id) mutate(makeKey(api, entity));
return res;
};
}) as Omit<ReturnType<typeof useEntity<Entity, Id>>, "read">;
@@ -106,14 +128,14 @@ export const useEntityQuery = <
};
export const useEntityMutate = <
Entity extends string,
Entity extends keyof DB | string,
Id extends PrimaryFieldType | undefined = undefined
>(
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
});

View File

@@ -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>
);
}

View File

@@ -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();

View File

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

View File

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

View File

@@ -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,7 +28,6 @@ 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"
@@ -50,7 +43,6 @@ export function EntityJsonSchemaFormField({
}}
/>
</div>
</Suspense>
</Formy.Group>
);
}

View File

@@ -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,7 +43,6 @@ 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)}
@@ -58,7 +52,6 @@ export function TriggerComponent({
/*uiSchema={uiSchema}*/
/*fields={{ template: TemplateField }}*/
/>
</Suspense>
</div>
</div>
<Handle

View File

@@ -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,7 +19,6 @@ export function TaskForm({ task, onChange, ...props }: TaskFormProps) {
//console.log("uiSchema", uiSchema);
return (
<Suspense fallback={<div>Loading...</div>}>
<JsonSchemaForm
className="legacy"
schema={schema}
@@ -36,6 +28,5 @@ export function TaskForm({ task, onChange, ...props }: TaskFormProps) {
/*uiSchema={uiSchema}*/
/*fields={{ template: TemplateField }}*/
/>
</Suspense>
);
}

View File

@@ -5,10 +5,7 @@ import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth";
import { useBkndData } from "ui/client/schema/data/use-bknd-data";
import { 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";

View File

@@ -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 });

View File

@@ -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";

View File

@@ -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";

View File

@@ -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"));

View File

@@ -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";

View File

@@ -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;

View File

@@ -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() {

View File

@@ -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 {}

View File

@@ -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
}
},
required: ["key", "operator"]
},
],
minItems: 1
}
},
minItems: 1,
},
},
required: ["key", "operator"],
},
minItems: 1,
},
},
required: ["operand", "conditions"],
required: ["operand", "conditions"]
};
export default function QueryJsonFormTest() {

View File

@@ -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>();

View File

@@ -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>}

View File

@@ -13,7 +13,7 @@ export default function SwrAndDataApi() {
function QueryDataApi() {
const [text, setText] = useState("");
const { data, update, ...r } = useEntityQuery("comments", 1, {});
const { data, update, ...r } = useEntityQuery("comments", 2);
const comment = data ? data : null;
useEffect(() => {
@@ -45,10 +45,10 @@ function QueryDataApi() {
function DirectDataApi() {
const [data, setData] = useState<any>();
const { create, read, update, _delete } = useEntity("comments", 1);
const { create, read, update, _delete } = useEntity("users");
useEffect(() => {
read().then(setData);
read().then((data) => setData(data));
}, []);
return <pre>{JSON.stringify(data, null, 2)}</pre>;

View File

@@ -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"]
}

BIN
bun.lockb

Binary file not shown.