mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
Merge remote-tracking branch 'origin/release/0.9' into feat/app-api-exp-for-nextjs
# Conflicts: # app/src/adapter/nextjs/nextjs.adapter.ts # app/src/index.ts
This commit is contained in:
@@ -153,7 +153,7 @@ export class Api {
|
||||
return {
|
||||
token: this.token,
|
||||
user: this.user,
|
||||
verified: this.verified
|
||||
verified: this.verified,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -198,7 +198,7 @@ export class Api {
|
||||
token: this.token,
|
||||
headers: this.options.headers,
|
||||
token_transport: this.token_transport,
|
||||
verbose: this.options.verbose
|
||||
verbose: this.options.verbose,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -211,9 +211,9 @@ export class Api {
|
||||
this.auth = new AuthApi(
|
||||
{
|
||||
...baseParams,
|
||||
onTokenUpdate: (token) => this.updateToken(token, true)
|
||||
onTokenUpdate: (token) => this.updateToken(token, true),
|
||||
},
|
||||
fetcher
|
||||
fetcher,
|
||||
);
|
||||
this.media = new MediaApi(baseParams, fetcher);
|
||||
}
|
||||
|
||||
@@ -9,12 +9,15 @@ import {
|
||||
type ModuleBuildContext,
|
||||
ModuleManager,
|
||||
type ModuleManagerOptions,
|
||||
type Modules
|
||||
type Modules,
|
||||
} from "modules/ModuleManager";
|
||||
import * as SystemPermissions from "modules/permissions";
|
||||
import { AdminController, type AdminControllerOptions } from "modules/server/AdminController";
|
||||
import { SystemController } from "modules/server/SystemController";
|
||||
|
||||
// biome-ignore format: must be there
|
||||
import { Api, type ApiOptions } from "Api";
|
||||
|
||||
export type AppPlugin = (app: App) => Promise<void> | void;
|
||||
|
||||
abstract class AppEvent<A = {}> extends Event<{ app: App } & A> {}
|
||||
@@ -31,7 +34,7 @@ export const AppEvents = { AppConfigUpdatedEvent, AppBuiltEvent, AppFirstBoot }
|
||||
|
||||
export type AppOptions = {
|
||||
plugins?: AppPlugin[];
|
||||
seed?: (ctx: ModuleBuildContext) => Promise<void>;
|
||||
seed?: (ctx: ModuleBuildContext & { app: App }) => Promise<void>;
|
||||
manager?: Omit<ModuleManagerOptions, "initial" | "onUpdated" | "seed">;
|
||||
};
|
||||
export type CreateAppConfig = {
|
||||
@@ -60,13 +63,12 @@ export class App {
|
||||
constructor(
|
||||
private connection: Connection,
|
||||
_initialConfig?: InitialModuleConfigs,
|
||||
private options?: AppOptions
|
||||
private options?: AppOptions,
|
||||
) {
|
||||
this.plugins = options?.plugins ?? [];
|
||||
this.modules = new ModuleManager(connection, {
|
||||
...(options?.manager ?? {}),
|
||||
initial: _initialConfig,
|
||||
seed: options?.seed,
|
||||
onUpdated: async (key, config) => {
|
||||
// if the EventManager was disabled, we assume we shouldn't
|
||||
// respond to events, such as "onUpdated".
|
||||
@@ -90,7 +92,7 @@ export class App {
|
||||
c.set("app", this);
|
||||
await next();
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
this.modules.ctx().emgr.registerEvents(AppEvents);
|
||||
}
|
||||
@@ -114,12 +116,17 @@ export class App {
|
||||
await Promise.all(this.plugins.map((plugin) => plugin(this)));
|
||||
}
|
||||
|
||||
$console.log("App built");
|
||||
await this.emgr.emit(new AppBuiltEvent({ app: this }));
|
||||
|
||||
// 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 }));
|
||||
await this.options?.seed?.({
|
||||
...this.modules.ctx(),
|
||||
app: this,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,8 +152,8 @@ export class App {
|
||||
{
|
||||
get: (_, module: keyof Modules) => {
|
||||
return this.modules.get(module);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
) as Modules;
|
||||
}
|
||||
|
||||
@@ -202,7 +209,7 @@ export function createApp(config: CreateAppConfig = {}) {
|
||||
} else if (typeof config.connection === "object") {
|
||||
if ("type" in config.connection) {
|
||||
$console.warn(
|
||||
"Using deprecated connection type 'libsql', use the 'config' object directly."
|
||||
"Using deprecated connection type 'libsql', use the 'config' object directly.",
|
||||
);
|
||||
connection = new LibsqlConnection(config.connection.config);
|
||||
} else {
|
||||
|
||||
@@ -17,7 +17,7 @@ export type Options = {
|
||||
export async function getApi(Astro: TAstro, options: Options = { mode: "static" }) {
|
||||
const api = new Api({
|
||||
host: new URL(Astro.request.url).origin,
|
||||
headers: options.mode === "dynamic" ? Astro.request.headers : undefined
|
||||
headers: options.mode === "dynamic" ? Astro.request.headers : undefined,
|
||||
});
|
||||
await api.verifyAuth();
|
||||
return api;
|
||||
|
||||
@@ -19,7 +19,7 @@ export async function createApp({ distPath, ...config }: RuntimeBkndConfig = {})
|
||||
registerLocalMediaAdapter();
|
||||
app = await createRuntimeApp({
|
||||
...config,
|
||||
serveStatic: serveStatic({ root })
|
||||
serveStatic: serveStatic({ root }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -46,10 +46,10 @@ export function serve({
|
||||
options,
|
||||
onBuilt,
|
||||
buildConfig,
|
||||
distPath
|
||||
distPath,
|
||||
});
|
||||
return app.fetch(request);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Server is running on http://localhost:${port}`);
|
||||
|
||||
@@ -12,7 +12,7 @@ export type D1ConnectionConfig = {
|
||||
class CustomD1Dialect extends D1Dialect {
|
||||
override createIntrospector(db: Kysely<any>): DatabaseIntrospector {
|
||||
return new SqliteIntrospector(db, {
|
||||
excludeTables: ["_cf_KV"]
|
||||
excludeTables: ["_cf_KV"],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ export class D1Connection extends SqliteConnection {
|
||||
|
||||
const kysely = new Kysely({
|
||||
dialect: new CustomD1Dialect({ database: config.binding }),
|
||||
plugins
|
||||
plugins,
|
||||
});
|
||||
super(kysely, {}, plugins);
|
||||
}
|
||||
@@ -37,7 +37,7 @@ export class D1Connection extends SqliteConnection {
|
||||
}
|
||||
|
||||
protected override async batch<Queries extends QB[]>(
|
||||
queries: [...Queries]
|
||||
queries: [...Queries],
|
||||
): Promise<{
|
||||
[K in keyof Queries]: Awaited<ReturnType<Queries[K]["execute"]>>;
|
||||
}> {
|
||||
@@ -47,7 +47,7 @@ export class D1Connection extends SqliteConnection {
|
||||
queries.map((q) => {
|
||||
const { sql, parameters } = q.compile();
|
||||
return db.prepare(sql).bind(...parameters);
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
// let it run through plugins
|
||||
|
||||
@@ -8,9 +8,9 @@ import { getBindings } from "./bindings";
|
||||
export function makeSchema(bindings: string[] = []) {
|
||||
return Type.Object(
|
||||
{
|
||||
binding: bindings.length > 0 ? StringEnum(bindings) : Type.Optional(Type.String())
|
||||
binding: bindings.length > 0 ? StringEnum(bindings) : Type.Optional(Type.String()),
|
||||
},
|
||||
{ title: "R2", description: "Cloudflare R2 storage" }
|
||||
{ title: "R2", description: "Cloudflare R2 storage" },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,10 +36,10 @@ export function registerMedia(env: Record<string, any>) {
|
||||
override toJSON() {
|
||||
return {
|
||||
...super.toJSON(),
|
||||
config: this.config
|
||||
config: this.config,
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -67,13 +67,13 @@ export class StorageR2Adapter implements StorageAdapter {
|
||||
}
|
||||
}
|
||||
async listObjects(
|
||||
prefix?: string
|
||||
prefix?: string,
|
||||
): Promise<{ key: string; last_modified: Date; size: number }[]> {
|
||||
const list = await this.bucket.list({ limit: 50 });
|
||||
return list.objects.map((item) => ({
|
||||
key: item.key,
|
||||
size: item.size,
|
||||
last_modified: item.uploaded
|
||||
last_modified: item.uploaded,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ export class StorageR2Adapter implements StorageAdapter {
|
||||
let object: R2ObjectBody | null;
|
||||
const responseHeaders = new Headers({
|
||||
"Accept-Ranges": "bytes",
|
||||
"Content-Type": guess(key)
|
||||
"Content-Type": guess(key),
|
||||
});
|
||||
|
||||
//console.log("getObject:headers", headersToObject(headers));
|
||||
@@ -98,7 +98,7 @@ export class StorageR2Adapter implements StorageAdapter {
|
||||
? {} // miniflare doesn't support range requests
|
||||
: {
|
||||
range: headers,
|
||||
onlyIf: headers
|
||||
onlyIf: headers,
|
||||
};
|
||||
object = (await this.bucket.get(key, options)) as R2ObjectBody;
|
||||
|
||||
@@ -130,7 +130,7 @@ export class StorageR2Adapter implements StorageAdapter {
|
||||
|
||||
return new Response(object.body, {
|
||||
status: object.range ? 206 : 200,
|
||||
headers: responseHeaders
|
||||
headers: responseHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ export class StorageR2Adapter implements StorageAdapter {
|
||||
if (!metadata || Object.keys(metadata).length === 0) {
|
||||
// guessing is especially required for dev environment (miniflare)
|
||||
metadata = {
|
||||
contentType: guess(object.key)
|
||||
contentType: guess(object.key),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ export class StorageR2Adapter implements StorageAdapter {
|
||||
|
||||
return {
|
||||
type: String(head.httpMetadata?.contentType ?? guess(key)),
|
||||
size: head.size
|
||||
size: head.size,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ export class StorageR2Adapter implements StorageAdapter {
|
||||
toJSON(secrets?: boolean) {
|
||||
return {
|
||||
type: this.getName(),
|
||||
config: {}
|
||||
config: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export function getBindings<T extends GetBindingType>(env: any, type: T): Bindin
|
||||
if (env[key] && (env[key] as any).constructor.name === type) {
|
||||
bindings.push({
|
||||
key,
|
||||
value: env[key] as BindingTypeMap[T]
|
||||
value: env[key] as BindingTypeMap[T],
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
@@ -84,7 +84,7 @@ export function serve<Env = any>(config: CloudflareBkndConfig<Env> = {}) {
|
||||
hono.all("*", async (c, next) => {
|
||||
const res = await serveStatic({
|
||||
path: `./${pathname}`,
|
||||
manifest: config.manifest!
|
||||
manifest: config.manifest!,
|
||||
})(c as any, next);
|
||||
if (res instanceof Response) {
|
||||
const ttl = 60 * 60 * 24 * 365;
|
||||
@@ -114,6 +114,6 @@ export function serve<Env = any>(config: CloudflareBkndConfig<Env> = {}) {
|
||||
default:
|
||||
throw new Error(`Unknown mode ${mode}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export {
|
||||
getBindings,
|
||||
type BindingTypeMap,
|
||||
type GetBindingType,
|
||||
type BindingMap
|
||||
type BindingMap,
|
||||
} from "./bindings";
|
||||
|
||||
export function d1(config: D1ConnectionConfig) {
|
||||
|
||||
@@ -31,13 +31,13 @@ export async function getCached(config: CloudflareBkndConfig, { env, ctx, ...arg
|
||||
async ({ params: { app } }) => {
|
||||
saveConfig(app.toJSON(true));
|
||||
},
|
||||
"sync"
|
||||
"sync",
|
||||
);
|
||||
await config.beforeBuild?.(app);
|
||||
},
|
||||
adminOptions: { html: config.html }
|
||||
adminOptions: { html: config.html },
|
||||
},
|
||||
{ env, ctx, ...args }
|
||||
{ env, ctx, ...args },
|
||||
);
|
||||
|
||||
if (!cachedConfig) {
|
||||
|
||||
@@ -23,7 +23,7 @@ export async function getDurable(config: CloudflareBkndConfig, ctx: Context) {
|
||||
config: create_config,
|
||||
html: config.html,
|
||||
keepAliveSeconds: config.keepAliveSeconds,
|
||||
setAdminHtml: config.setAdminHtml
|
||||
setAdminHtml: config.setAdminHtml,
|
||||
});
|
||||
|
||||
const headers = new Headers(res.headers);
|
||||
@@ -32,7 +32,7 @@ export async function getDurable(config: CloudflareBkndConfig, ctx: Context) {
|
||||
return new Response(res.body, {
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
headers
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ export class DurableBkndApp extends DurableObject {
|
||||
html?: string;
|
||||
keepAliveSeconds?: number;
|
||||
setAdminHtml?: boolean;
|
||||
}
|
||||
},
|
||||
) {
|
||||
let buildtime = 0;
|
||||
if (!this.app) {
|
||||
@@ -73,7 +73,7 @@ export class DurableBkndApp extends DurableObject {
|
||||
return c.json({
|
||||
id: this.id,
|
||||
keepAliveSeconds: options?.keepAliveSeconds ?? 0,
|
||||
colo: context.colo
|
||||
colo: context.colo,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -82,7 +82,7 @@ export class DurableBkndApp extends DurableObject {
|
||||
adminOptions: { html: options.html },
|
||||
beforeBuild: async (app) => {
|
||||
await this.beforeBuild(app);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
buildtime = performance.now() - start;
|
||||
@@ -101,7 +101,7 @@ export class DurableBkndApp extends DurableObject {
|
||||
return new Response(res.body, {
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
headers
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ export async function makeApp(config: CloudflareBkndConfig, ctx: Context) {
|
||||
return await createRuntimeApp(
|
||||
{
|
||||
...makeCfConfig(config, ctx),
|
||||
adminOptions: config.html ? { html: config.html } : undefined
|
||||
adminOptions: config.html ? { html: config.html } : undefined,
|
||||
},
|
||||
ctx
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ export function makeConfig<Args = any>(config: BkndConfig<Args>, args?: Args): C
|
||||
|
||||
export async function createFrameworkApp<Args = any>(
|
||||
config: FrameworkBkndConfig,
|
||||
args?: Args
|
||||
args?: Args,
|
||||
): Promise<App> {
|
||||
const app = App.create(makeConfig(config, args));
|
||||
|
||||
@@ -44,7 +44,7 @@ export async function createFrameworkApp<Args = any>(
|
||||
async () => {
|
||||
await config.onBuilt?.(app);
|
||||
},
|
||||
"sync"
|
||||
"sync",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ export async function createRuntimeApp<Env = any>(
|
||||
serveStatic?: MiddlewareHandler | [string, MiddlewareHandler];
|
||||
adminOptions?: AdminControllerOptions | false;
|
||||
},
|
||||
env?: Env
|
||||
env?: Env,
|
||||
): Promise<App> {
|
||||
const app = App.create(makeConfig(config, env));
|
||||
|
||||
@@ -82,7 +82,7 @@ export async function createRuntimeApp<Env = any>(
|
||||
app.registerAdminController(adminOptions);
|
||||
}
|
||||
},
|
||||
"sync"
|
||||
"sync",
|
||||
);
|
||||
|
||||
await config.beforeBuild?.(app);
|
||||
|
||||
@@ -34,7 +34,7 @@ function getCleanRequest(req: Request, cleanRequest: NextjsBkndConfig["cleanRequ
|
||||
return new Request(url.toString(), {
|
||||
method: req.method,
|
||||
headers: req.headers,
|
||||
body: req.body
|
||||
body: req.body,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { registries } from "bknd";
|
||||
import {
|
||||
type LocalAdapterConfig,
|
||||
StorageLocalAdapter
|
||||
StorageLocalAdapter,
|
||||
} from "../../media/storage/adapters/StorageLocalAdapter";
|
||||
|
||||
export * from "./node.adapter";
|
||||
|
||||
@@ -24,7 +24,7 @@ export function serve({
|
||||
}: NodeBkndConfig = {}) {
|
||||
const root = path.relative(
|
||||
process.cwd(),
|
||||
path.resolve(distPath ?? relativeDistPath ?? "./node_modules/bknd/dist", "static")
|
||||
path.resolve(distPath ?? relativeDistPath ?? "./node_modules/bknd/dist", "static"),
|
||||
);
|
||||
if (relativeDistPath) {
|
||||
console.warn("relativeDistPath is deprecated, please use distPath instead");
|
||||
@@ -41,16 +41,16 @@ export function serve({
|
||||
registerLocalMediaAdapter();
|
||||
app = await createRuntimeApp({
|
||||
...config,
|
||||
serveStatic: serveStatic({ root })
|
||||
serveStatic: serveStatic({ root }),
|
||||
});
|
||||
}
|
||||
|
||||
return app.fetch(req);
|
||||
}
|
||||
},
|
||||
},
|
||||
(connInfo) => {
|
||||
console.log(`Server is running on http://localhost:${connInfo.port}`);
|
||||
listener?.(connInfo);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export async function getApp<Args extends RemixContext = RemixContext>(
|
||||
}
|
||||
|
||||
export function serve<Args extends RemixContext = RemixContext>(
|
||||
config: RemixBkndConfig<Args> = {}
|
||||
config: RemixBkndConfig<Args> = {},
|
||||
) {
|
||||
return async (args: Args) => {
|
||||
app = await getApp(config, args);
|
||||
|
||||
@@ -20,6 +20,6 @@ export function nodeRequestToRequest(req: IncomingMessage): Request {
|
||||
const method = req.method || "GET";
|
||||
return new Request(url, {
|
||||
method,
|
||||
headers
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export const devServerConfig = {
|
||||
/^\/@.+$/,
|
||||
/\/components.*?\.json.*/, // @todo: improve
|
||||
/^\/(public|assets|static)\/.+/,
|
||||
/^\/node_modules\/.*/
|
||||
/^\/node_modules\/.*/,
|
||||
] as any,
|
||||
injectClientScript: false
|
||||
injectClientScript: false,
|
||||
} as const;
|
||||
|
||||
@@ -24,7 +24,7 @@ window.__vite_plugin_react_preamble_installed__ = true
|
||||
</script>
|
||||
<script type="module" src="/@vite/client"></script>
|
||||
${addBkndContext ? "<!-- BKND_CONTEXT -->" : ""}
|
||||
</head>`
|
||||
</head>`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,12 +39,12 @@ async function createApp(config: ViteBkndConfig = {}, env?: any) {
|
||||
: {
|
||||
html: config.html,
|
||||
forceDev: config.forceDev ?? {
|
||||
mainPath: "/src/main.tsx"
|
||||
}
|
||||
mainPath: "/src/main.tsx",
|
||||
},
|
||||
},
|
||||
serveStatic: ["/assets/*", serveStatic({ root: config.distPath ?? "./" })]
|
||||
serveStatic: ["/assets/*", serveStatic({ root: config.distPath ?? "./" })],
|
||||
},
|
||||
env
|
||||
env,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ export function serveFresh(config: Omit<ViteBkndConfig, "mode"> = {}) {
|
||||
async fetch(request: Request, env: any, ctx: ExecutionContext) {
|
||||
const app = await createApp(config, env);
|
||||
return app.fetch(request, env, ctx);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ export function serveCached(config: Omit<ViteBkndConfig, "mode"> = {}) {
|
||||
}
|
||||
|
||||
return app.fetch(request, env, ctx);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -77,6 +77,6 @@ export function serve({ mode, ...config }: ViteBkndConfig = {}) {
|
||||
export function devServer(options: DevServerOptions) {
|
||||
return honoViteDevServer({
|
||||
...devServerConfig,
|
||||
...options
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@ import {
|
||||
Authenticator,
|
||||
type ProfileExchange,
|
||||
Role,
|
||||
type Strategy
|
||||
type Strategy,
|
||||
} from "auth";
|
||||
import type { PasswordStrategy } from "auth/authenticate/strategies";
|
||||
import { type DB, Exception, type PrimaryFieldType } from "core";
|
||||
import { $console, type DB, Exception, type PrimaryFieldType } from "core";
|
||||
import { type Static, secureRandomString, transformObject } from "core/utils";
|
||||
import type { Entity, EntityManager } from "data";
|
||||
import { type FieldSchema, em, entity, enumm, text } from "data/prototype";
|
||||
@@ -41,6 +41,12 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
||||
}
|
||||
}
|
||||
|
||||
// @todo: password strategy is required atm
|
||||
if (!to.strategies?.password?.enabled) {
|
||||
$console.warn("Password strategy cannot be disabled.");
|
||||
to.strategies!.password!.enabled = true;
|
||||
}
|
||||
|
||||
return to;
|
||||
}
|
||||
|
||||
@@ -56,7 +62,6 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
||||
|
||||
// register roles
|
||||
const roles = transformObject(this.config.roles ?? {}, (role, name) => {
|
||||
//console.log("role", role, name);
|
||||
return Role.create({ name, ...role });
|
||||
});
|
||||
this.ctx.guard.setRoles(Object.values(roles));
|
||||
@@ -69,15 +74,15 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Could not build strategy ${String(
|
||||
name
|
||||
)} with config ${JSON.stringify(strategy.config)}`
|
||||
name,
|
||||
)} with config ${JSON.stringify(strategy.config)}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
this._authenticator = new Authenticator(strategies, this.resolveUser.bind(this), {
|
||||
jwt: this.config.jwt,
|
||||
cookie: this.config.cookie
|
||||
cookie: this.config.cookie,
|
||||
});
|
||||
|
||||
this.registerEntities();
|
||||
@@ -88,6 +93,14 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
||||
this.ctx.guard.registerPermissions(Object.values(AuthPermissions));
|
||||
}
|
||||
|
||||
isStrategyEnabled(strategy: Strategy | string) {
|
||||
const name = typeof strategy === "string" ? strategy : strategy.getName();
|
||||
// for now, password is always active
|
||||
if (name === "password") return true;
|
||||
|
||||
return this.config.strategies?.[name]?.enabled ?? false;
|
||||
}
|
||||
|
||||
get controller(): AuthController {
|
||||
if (!this.isBuilt()) {
|
||||
throw new Error("Can't access controller, AppAuth not built yet");
|
||||
@@ -113,14 +126,8 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
||||
action: AuthAction,
|
||||
strategy: Strategy,
|
||||
identifier: string,
|
||||
profile: ProfileExchange
|
||||
profile: ProfileExchange,
|
||||
): Promise<any> {
|
||||
/*console.log("***** AppAuth:resolveUser", {
|
||||
action,
|
||||
strategy: strategy.getName(),
|
||||
identifier,
|
||||
profile
|
||||
});*/
|
||||
if (!this.config.allow_register && action === "register") {
|
||||
throw new Exception("Registration is not allowed", 403);
|
||||
}
|
||||
@@ -129,7 +136,7 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
||||
.getFillableFields("create")
|
||||
.map((f) => f.name);
|
||||
const filteredProfile = Object.fromEntries(
|
||||
Object.entries(profile).filter(([key]) => fields.includes(key))
|
||||
Object.entries(profile).filter(([key]) => fields.includes(key)),
|
||||
);
|
||||
|
||||
switch (action) {
|
||||
@@ -141,21 +148,10 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
||||
}
|
||||
|
||||
private filterUserData(user: any) {
|
||||
/*console.log(
|
||||
"--filterUserData",
|
||||
user,
|
||||
this.config.jwt.fields,
|
||||
pick(user, this.config.jwt.fields)
|
||||
);*/
|
||||
return pick(user, this.config.jwt.fields);
|
||||
}
|
||||
|
||||
private async login(strategy: Strategy, identifier: string, profile: ProfileExchange) {
|
||||
/*console.log("--- trying to login", {
|
||||
strategy: strategy.getName(),
|
||||
identifier,
|
||||
profile
|
||||
});*/
|
||||
if (!("email" in profile)) {
|
||||
throw new Exception("Profile must have email");
|
||||
}
|
||||
@@ -172,18 +168,14 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
||||
if (!result.data) {
|
||||
throw new Exception("User not found", 404);
|
||||
}
|
||||
//console.log("---login data", result.data, result);
|
||||
|
||||
// compare strategy and identifier
|
||||
//console.log("strategy comparison", result.data.strategy, strategy.getName());
|
||||
if (result.data.strategy !== strategy.getName()) {
|
||||
//console.log("!!! User registered with different strategy");
|
||||
throw new Exception("User registered with different strategy");
|
||||
}
|
||||
|
||||
//console.log("identifier comparison", result.data.strategy_value, identifier);
|
||||
if (result.data.strategy_value !== identifier) {
|
||||
//console.log("!!! Invalid credentials");
|
||||
throw new Exception("Invalid credentials");
|
||||
}
|
||||
|
||||
@@ -207,7 +199,7 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
||||
const payload: any = {
|
||||
...profile,
|
||||
strategy: strategy.getName(),
|
||||
strategy_value: identifier
|
||||
strategy_value: identifier,
|
||||
};
|
||||
|
||||
const mutator = this.em.mutator(users);
|
||||
@@ -257,13 +249,13 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
||||
email: text().required(),
|
||||
strategy: text({
|
||||
fillable: ["create"],
|
||||
hidden: ["update", "form"]
|
||||
hidden: ["update", "form"],
|
||||
}).required(),
|
||||
strategy_value: text({
|
||||
fillable: ["create"],
|
||||
hidden: ["read", "table", "update", "form"]
|
||||
hidden: ["read", "table", "update", "form"],
|
||||
}).required(),
|
||||
role: text()
|
||||
role: text(),
|
||||
};
|
||||
|
||||
registerEntities() {
|
||||
@@ -271,12 +263,12 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
||||
this.ensureSchema(
|
||||
em(
|
||||
{
|
||||
[users.name as "users"]: users
|
||||
[users.name as "users"]: users,
|
||||
},
|
||||
({ index }, { users }) => {
|
||||
index(users).on(["email"], true).on(["strategy"]).on(["strategy_value"]);
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -285,6 +277,7 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
||||
} catch (e) {}
|
||||
|
||||
try {
|
||||
// also keep disabled strategies as a choice
|
||||
const strategies = Object.keys(this.config.strategies ?? {});
|
||||
this.replaceEntityField(users, "strategy", enumm({ enum: strategies }));
|
||||
} catch (e) {}
|
||||
@@ -304,7 +297,7 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
||||
...(additional as any),
|
||||
email,
|
||||
strategy,
|
||||
strategy_value
|
||||
strategy_value,
|
||||
});
|
||||
mutator.__unstable_toggleSystemEntityCreation(true);
|
||||
return created;
|
||||
@@ -315,9 +308,16 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
||||
return this.configDefault;
|
||||
}
|
||||
|
||||
const strategies = this.authenticator.getStrategies();
|
||||
|
||||
return {
|
||||
...this.config,
|
||||
...this.authenticator.toJSON(secrets)
|
||||
...this.authenticator.toJSON(secrets),
|
||||
strategies: transformObject(strategies, (strategy) => ({
|
||||
enabled: this.isStrategyEnabled(strategy),
|
||||
type: strategy.getType(),
|
||||
config: strategy.toJSON(secrets),
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,13 +10,13 @@ export type AuthApiOptions = BaseModuleApiOptions & {
|
||||
export class AuthApi extends ModuleApi<AuthApiOptions> {
|
||||
protected override getDefaultOptions(): Partial<AuthApiOptions> {
|
||||
return {
|
||||
basepath: "/api/auth"
|
||||
basepath: "/api/auth",
|
||||
};
|
||||
}
|
||||
|
||||
async login(strategy: string, input: any) {
|
||||
const res = await this.post<AuthResponse>([strategy, "login"], input, {
|
||||
credentials: "include"
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (res.ok && res.body.token) {
|
||||
@@ -27,7 +27,7 @@ export class AuthApi extends ModuleApi<AuthApiOptions> {
|
||||
|
||||
async register(strategy: string, input: any) {
|
||||
const res = await this.post<AuthResponse>([strategy, "register"], input, {
|
||||
credentials: "include"
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (res.ok && res.body.token) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { type AppAuth, AuthPermissions, type SafeUser, type Strategy } from "auth";
|
||||
import { TypeInvalidError, parse } from "core/utils";
|
||||
import { tbValidator as tb } from "core";
|
||||
import { Type, TypeInvalidError, parse, transformObject } from "core/utils";
|
||||
import { DataPermissions } from "data";
|
||||
import type { Hono } from "hono";
|
||||
import { Controller } from "modules/Controller";
|
||||
import type { ServerEnv } from "modules/Module";
|
||||
import { Controller, type ServerEnv } from "modules/Controller";
|
||||
|
||||
export type AuthActionResponse = {
|
||||
success: boolean;
|
||||
@@ -12,6 +12,10 @@ export type AuthActionResponse = {
|
||||
errors?: any;
|
||||
};
|
||||
|
||||
const booleanLike = Type.Transform(Type.String())
|
||||
.Decode((v) => v === "1")
|
||||
.Encode((v) => (v ? "1" : "0"));
|
||||
|
||||
export class AuthController extends Controller {
|
||||
constructor(private auth: AppAuth) {
|
||||
super();
|
||||
@@ -31,6 +35,9 @@ export class AuthController extends Controller {
|
||||
}
|
||||
|
||||
private registerStrategyActions(strategy: Strategy, mainHono: Hono<ServerEnv>) {
|
||||
if (!this.auth.isStrategyEnabled(strategy)) {
|
||||
return;
|
||||
}
|
||||
const actions = strategy.getActions?.();
|
||||
if (!actions) {
|
||||
return;
|
||||
@@ -51,7 +58,7 @@ export class AuthController extends Controller {
|
||||
try {
|
||||
const body = await this.auth.authenticator.getBody(c);
|
||||
const valid = parse(create.schema, body, {
|
||||
skipMark: true
|
||||
skipMark: true,
|
||||
});
|
||||
const processed = (await create.preprocess?.(valid)) ?? valid;
|
||||
|
||||
@@ -60,7 +67,7 @@ export class AuthController extends Controller {
|
||||
mutator.__unstable_toggleSystemEntityCreation(false);
|
||||
const { data: created } = await mutator.insertOne({
|
||||
...processed,
|
||||
strategy: name
|
||||
strategy: name,
|
||||
});
|
||||
mutator.__unstable_toggleSystemEntityCreation(true);
|
||||
|
||||
@@ -68,21 +75,21 @@ export class AuthController extends Controller {
|
||||
success: true,
|
||||
action: "create",
|
||||
strategy: name,
|
||||
data: created as unknown as SafeUser
|
||||
data: created as unknown as SafeUser,
|
||||
} as AuthActionResponse);
|
||||
} catch (e) {
|
||||
if (e instanceof TypeInvalidError) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
errors: e.errors
|
||||
errors: e.errors,
|
||||
},
|
||||
400
|
||||
400,
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
hono.get("create/schema.json", async (c) => {
|
||||
return c.json(create.schema);
|
||||
@@ -98,7 +105,8 @@ export class AuthController extends Controller {
|
||||
const strategies = this.auth.authenticator.getStrategies();
|
||||
|
||||
for (const [name, strategy] of Object.entries(strategies)) {
|
||||
//console.log("registering", name, "at", `/${name}`);
|
||||
if (!this.auth.isStrategyEnabled(strategy)) continue;
|
||||
|
||||
hono.route(`/${name}`, strategy.getController(this.auth.authenticator));
|
||||
this.registerStrategyActions(strategy, hono);
|
||||
}
|
||||
@@ -127,10 +135,25 @@ export class AuthController extends Controller {
|
||||
return c.redirect("/");
|
||||
});
|
||||
|
||||
hono.get("/strategies", async (c) => {
|
||||
const { strategies, basepath } = this.auth.toJSON(false);
|
||||
return c.json({ strategies, basepath });
|
||||
});
|
||||
hono.get(
|
||||
"/strategies",
|
||||
tb("query", Type.Object({ include_disabled: Type.Optional(booleanLike) })),
|
||||
async (c) => {
|
||||
const { include_disabled } = c.req.valid("query");
|
||||
const { strategies, basepath } = this.auth.toJSON(false);
|
||||
|
||||
if (!include_disabled) {
|
||||
return c.json({
|
||||
strategies: transformObject(strategies ?? {}, (strategy, name) => {
|
||||
return this.auth.isStrategyEnabled(name) ? strategy : undefined;
|
||||
}),
|
||||
basepath,
|
||||
});
|
||||
}
|
||||
|
||||
return c.json({ strategies, basepath });
|
||||
},
|
||||
);
|
||||
|
||||
return hono.all("*", (c) => c.notFound());
|
||||
}
|
||||
|
||||
@@ -5,29 +5,30 @@ import { type Static, StringRecord, Type, objectTransform } from "core/utils";
|
||||
export const Strategies = {
|
||||
password: {
|
||||
cls: PasswordStrategy,
|
||||
schema: PasswordStrategy.prototype.getSchema()
|
||||
schema: PasswordStrategy.prototype.getSchema(),
|
||||
},
|
||||
oauth: {
|
||||
cls: OAuthStrategy,
|
||||
schema: OAuthStrategy.prototype.getSchema()
|
||||
schema: OAuthStrategy.prototype.getSchema(),
|
||||
},
|
||||
custom_oauth: {
|
||||
cls: CustomOAuthStrategy,
|
||||
schema: CustomOAuthStrategy.prototype.getSchema()
|
||||
}
|
||||
schema: CustomOAuthStrategy.prototype.getSchema(),
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const STRATEGIES = Strategies;
|
||||
const strategiesSchemaObject = objectTransform(STRATEGIES, (strategy, name) => {
|
||||
return Type.Object(
|
||||
{
|
||||
enabled: Type.Optional(Type.Boolean({ default: true })),
|
||||
type: Type.Const(name, { default: name, readOnly: true }),
|
||||
config: strategy.schema
|
||||
config: strategy.schema,
|
||||
},
|
||||
{
|
||||
title: name,
|
||||
additionalProperties: false
|
||||
}
|
||||
additionalProperties: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
const strategiesSchema = Type.Union(Object.values(strategiesSchemaObject));
|
||||
@@ -36,15 +37,15 @@ export type AppAuthOAuthStrategy = Static<typeof STRATEGIES.oauth.schema>;
|
||||
export type AppAuthCustomOAuthStrategy = Static<typeof STRATEGIES.custom_oauth.schema>;
|
||||
|
||||
const guardConfigSchema = Type.Object({
|
||||
enabled: Type.Optional(Type.Boolean({ default: false }))
|
||||
enabled: Type.Optional(Type.Boolean({ default: false })),
|
||||
});
|
||||
export const guardRoleSchema = Type.Object(
|
||||
{
|
||||
permissions: Type.Optional(Type.Array(Type.String())),
|
||||
is_default: Type.Optional(Type.Boolean()),
|
||||
implicit_allow: Type.Optional(Type.Boolean())
|
||||
implicit_allow: Type.Optional(Type.Boolean()),
|
||||
},
|
||||
{ additionalProperties: false }
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
export const authConfigSchema = Type.Object(
|
||||
@@ -61,20 +62,21 @@ export const authConfigSchema = Type.Object(
|
||||
default: {
|
||||
password: {
|
||||
type: "password",
|
||||
enabled: true,
|
||||
config: {
|
||||
hashing: "sha256"
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
hashing: "sha256",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
guard: Type.Optional(guardConfigSchema),
|
||||
roles: Type.Optional(StringRecord(guardRoleSchema, { default: {} }))
|
||||
roles: Type.Optional(StringRecord(guardRoleSchema, { default: {} })),
|
||||
},
|
||||
{
|
||||
title: "Authentication",
|
||||
additionalProperties: false
|
||||
}
|
||||
additionalProperties: false,
|
||||
},
|
||||
);
|
||||
|
||||
export type AppAuthSchema = Static<typeof authConfigSchema>;
|
||||
|
||||
@@ -7,13 +7,13 @@ import {
|
||||
Type,
|
||||
parse,
|
||||
runtimeSupports,
|
||||
transformObject
|
||||
transformObject,
|
||||
} from "core/utils";
|
||||
import type { Context, Hono } from "hono";
|
||||
import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie";
|
||||
import { sign, verify } from "hono/jwt";
|
||||
import type { CookieOptions } from "hono/utils/cookie";
|
||||
import type { ServerEnv } from "modules/Module";
|
||||
import type { ServerEnv } from "modules/Controller";
|
||||
|
||||
type Input = any; // workaround
|
||||
export type JWTPayload = Parameters<typeof sign>[0];
|
||||
@@ -71,9 +71,9 @@ export const cookieConfig = Type.Partial(
|
||||
expires: Type.Number({ default: defaultCookieExpires }), // seconds
|
||||
renew: Type.Boolean({ default: true }),
|
||||
pathSuccess: Type.String({ default: "/" }),
|
||||
pathLoggedOut: Type.String({ default: "/" })
|
||||
pathLoggedOut: Type.String({ default: "/" }),
|
||||
}),
|
||||
{ default: {}, additionalProperties: false }
|
||||
{ default: {}, additionalProperties: false },
|
||||
);
|
||||
|
||||
// @todo: maybe add a config to not allow cookie/api tokens to be used interchangably?
|
||||
@@ -86,16 +86,16 @@ export const jwtConfig = Type.Object(
|
||||
alg: Type.Optional(StringEnum(["HS256", "HS384", "HS512"], { default: "HS256" })),
|
||||
expires: Type.Optional(Type.Number()), // seconds
|
||||
issuer: Type.Optional(Type.String()),
|
||||
fields: Type.Array(Type.String(), { default: ["id", "email", "role"] })
|
||||
fields: Type.Array(Type.String(), { default: ["id", "email", "role"] }),
|
||||
},
|
||||
{
|
||||
default: {},
|
||||
additionalProperties: false
|
||||
}
|
||||
additionalProperties: false,
|
||||
},
|
||||
);
|
||||
export const authenticatorConfig = Type.Object({
|
||||
jwt: jwtConfig,
|
||||
cookie: cookieConfig
|
||||
cookie: cookieConfig,
|
||||
});
|
||||
|
||||
type AuthConfig = Static<typeof authenticatorConfig>;
|
||||
@@ -104,7 +104,7 @@ export type AuthUserResolver = (
|
||||
action: AuthAction,
|
||||
strategy: Strategy,
|
||||
identifier: string,
|
||||
profile: ProfileExchange
|
||||
profile: ProfileExchange,
|
||||
) => Promise<SafeUser | undefined>;
|
||||
type AuthClaims = SafeUser & {
|
||||
iat: number;
|
||||
@@ -127,7 +127,7 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
|
||||
action: AuthAction,
|
||||
strategy: Strategy,
|
||||
identifier: string,
|
||||
profile: ProfileExchange
|
||||
profile: ProfileExchange,
|
||||
): Promise<AuthResponse> {
|
||||
//console.log("resolve", { action, strategy: strategy.getName(), profile });
|
||||
const user = await this.userResolver(action, strategy, identifier, profile);
|
||||
@@ -135,7 +135,7 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
|
||||
if (user) {
|
||||
return {
|
||||
user,
|
||||
token: await this.jwt(user)
|
||||
token: await this.jwt(user),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
|
||||
|
||||
strategy<
|
||||
StrategyName extends keyof Strategies,
|
||||
Strat extends Strategy = Strategies[StrategyName]
|
||||
Strat extends Strategy = Strategies[StrategyName],
|
||||
>(strategy: StrategyName): Strat {
|
||||
try {
|
||||
return this.strategies[strategy] as unknown as Strat;
|
||||
@@ -168,7 +168,7 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
|
||||
|
||||
const payload: JWTPayload = {
|
||||
...user,
|
||||
iat: Math.floor(Date.now() / 1000)
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
};
|
||||
|
||||
// issuer
|
||||
@@ -194,7 +194,7 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
|
||||
const payload = await verify(
|
||||
jwt,
|
||||
this.config.jwt?.secret ?? "",
|
||||
this.config.jwt?.alg ?? "HS256"
|
||||
this.config.jwt?.alg ?? "HS256",
|
||||
);
|
||||
|
||||
// manually verify issuer (hono doesn't support it)
|
||||
@@ -215,7 +215,7 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
|
||||
|
||||
return {
|
||||
...cookieConfig,
|
||||
expires: new Date(Date.now() + expires * 1000)
|
||||
expires: new Date(Date.now() + expires * 1000),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -343,17 +343,16 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
|
||||
return {
|
||||
...this.config,
|
||||
jwt: secrets ? this.config.jwt : undefined,
|
||||
strategies: transformObject(this.getStrategies(), (s) => s.toJSON(secrets))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function createStrategyAction<S extends TObject>(
|
||||
schema: S,
|
||||
preprocess: (input: Static<S>) => Promise<Partial<DB["users"]>>
|
||||
preprocess: (input: Static<S>) => Promise<Partial<DB["users"]>>,
|
||||
) {
|
||||
return {
|
||||
schema,
|
||||
preprocess
|
||||
preprocess,
|
||||
} as StrategyAction<S>;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ type LoginSchema = { username: string; password: string } | { email: string; pas
|
||||
type RegisterSchema = { email: string; password: string; [key: string]: any };
|
||||
|
||||
const schema = Type.Object({
|
||||
hashing: StringEnum(["plain", "sha256" /*, "bcrypt"*/] as const, { default: "sha256" })
|
||||
hashing: StringEnum(["plain", "sha256" /*, "bcrypt"*/] as const, { default: "sha256" }),
|
||||
});
|
||||
|
||||
export type PasswordStrategyOptions = Static<typeof schema>;
|
||||
@@ -49,7 +49,7 @@ export class PasswordStrategy implements Strategy {
|
||||
|
||||
return {
|
||||
...input,
|
||||
password: await this.hash(input.password)
|
||||
password: await this.hash(input.password),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -62,8 +62,8 @@ export class PasswordStrategy implements Strategy {
|
||||
tb(
|
||||
"query",
|
||||
Type.Object({
|
||||
redirect: Type.Optional(Type.String())
|
||||
})
|
||||
redirect: Type.Optional(Type.String()),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const body = await authenticator.getBody(c);
|
||||
@@ -75,22 +75,22 @@ export class PasswordStrategy implements Strategy {
|
||||
"login",
|
||||
this,
|
||||
payload.password,
|
||||
payload
|
||||
payload,
|
||||
);
|
||||
|
||||
return await authenticator.respond(c, data, redirect);
|
||||
} catch (e) {
|
||||
return await authenticator.respond(c, e);
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/register",
|
||||
tb(
|
||||
"query",
|
||||
Type.Object({
|
||||
redirect: Type.Optional(Type.String())
|
||||
})
|
||||
redirect: Type.Optional(Type.String()),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const body = await authenticator.getBody(c);
|
||||
@@ -101,11 +101,11 @@ export class PasswordStrategy implements Strategy {
|
||||
"register",
|
||||
this,
|
||||
payload.password,
|
||||
payload
|
||||
payload,
|
||||
);
|
||||
|
||||
return await authenticator.respond(c, data, redirect);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -114,19 +114,19 @@ export class PasswordStrategy implements Strategy {
|
||||
create: createStrategyAction(
|
||||
Type.Object({
|
||||
email: Type.String({
|
||||
pattern: "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$"
|
||||
pattern: "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$",
|
||||
}),
|
||||
password: Type.String({
|
||||
minLength: 8 // @todo: this should be configurable
|
||||
})
|
||||
minLength: 8, // @todo: this should be configurable
|
||||
}),
|
||||
}),
|
||||
async ({ password, ...input }) => {
|
||||
return {
|
||||
...input,
|
||||
strategy_value: await this.hash(password)
|
||||
strategy_value: await this.hash(password),
|
||||
};
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -147,9 +147,6 @@ export class PasswordStrategy implements Strategy {
|
||||
}
|
||||
|
||||
toJSON(secrets?: boolean) {
|
||||
return {
|
||||
type: this.getType(),
|
||||
config: secrets ? this.options : undefined
|
||||
};
|
||||
return secrets ? this.options : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,5 +9,5 @@ export {
|
||||
type PasswordStrategyOptions,
|
||||
OAuthStrategy,
|
||||
OAuthCallbackException,
|
||||
CustomOAuthStrategy
|
||||
CustomOAuthStrategy,
|
||||
};
|
||||
|
||||
@@ -15,11 +15,11 @@ const oauthSchemaCustom = Type.Object(
|
||||
{
|
||||
client_id: Type.String(),
|
||||
client_secret: Type.String(),
|
||||
token_endpoint_auth_method: StringEnum(["client_secret_basic"])
|
||||
token_endpoint_auth_method: StringEnum(["client_secret_basic"]),
|
||||
},
|
||||
{
|
||||
additionalProperties: false
|
||||
}
|
||||
additionalProperties: false,
|
||||
},
|
||||
),
|
||||
as: Type.Object(
|
||||
{
|
||||
@@ -29,15 +29,15 @@ const oauthSchemaCustom = Type.Object(
|
||||
scope_separator: Type.Optional(Type.String({ default: " " })),
|
||||
authorization_endpoint: Type.Optional(UrlString),
|
||||
token_endpoint: Type.Optional(UrlString),
|
||||
userinfo_endpoint: Type.Optional(UrlString)
|
||||
userinfo_endpoint: Type.Optional(UrlString),
|
||||
},
|
||||
{
|
||||
additionalProperties: false
|
||||
}
|
||||
)
|
||||
additionalProperties: false,
|
||||
},
|
||||
),
|
||||
// @todo: profile mapping
|
||||
},
|
||||
{ title: "Custom OAuth", additionalProperties: false }
|
||||
{ title: "Custom OAuth", additionalProperties: false },
|
||||
);
|
||||
|
||||
type OAuthConfigCustom = Static<typeof oauthSchemaCustom>;
|
||||
@@ -57,7 +57,7 @@ export type IssuerConfig<UserInfo = any> = {
|
||||
profile: (
|
||||
info: UserInfo,
|
||||
config: Omit<IssuerConfig, "profile">,
|
||||
tokenResponse: any
|
||||
tokenResponse: any,
|
||||
) => Promise<UserProfile>;
|
||||
};
|
||||
|
||||
|
||||
@@ -18,14 +18,14 @@ const schemaProvided = Type.Object(
|
||||
client: Type.Object(
|
||||
{
|
||||
client_id: Type.String(),
|
||||
client_secret: Type.String()
|
||||
client_secret: Type.String(),
|
||||
},
|
||||
{
|
||||
additionalProperties: false
|
||||
}
|
||||
)
|
||||
additionalProperties: false,
|
||||
},
|
||||
),
|
||||
},
|
||||
{ title: "OAuth" }
|
||||
{ title: "OAuth" },
|
||||
);
|
||||
type ProvidedOAuthConfig = Static<typeof schemaProvided>;
|
||||
|
||||
@@ -56,7 +56,7 @@ export type IssuerConfig<UserInfo = any> = {
|
||||
profile: (
|
||||
info: UserInfo,
|
||||
config: Omit<IssuerConfig, "profile">,
|
||||
tokenResponse: any
|
||||
tokenResponse: any,
|
||||
) => Promise<UserProfile>;
|
||||
};
|
||||
|
||||
@@ -65,7 +65,7 @@ export class OAuthCallbackException extends Exception {
|
||||
|
||||
constructor(
|
||||
public error: any,
|
||||
public step: string
|
||||
public step: string,
|
||||
) {
|
||||
super("OAuthCallbackException on " + step);
|
||||
}
|
||||
@@ -103,8 +103,8 @@ export class OAuthStrategy implements Strategy {
|
||||
type: info.type,
|
||||
client: {
|
||||
...info.client,
|
||||
...this._config.client
|
||||
}
|
||||
...this._config.client,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ export class OAuthStrategy implements Strategy {
|
||||
|
||||
const { challenge_supported, challenge, challenge_method } = await this.getCodeChallenge(
|
||||
as,
|
||||
options.state
|
||||
options.state,
|
||||
);
|
||||
|
||||
if (!as.authorization_endpoint) {
|
||||
@@ -150,7 +150,7 @@ export class OAuthStrategy implements Strategy {
|
||||
client_id: client.client_id,
|
||||
redirect_uri: options.redirect_uri,
|
||||
response_type: "code",
|
||||
scope: scopes.join(as.scope_separator ?? " ")
|
||||
scope: scopes.join(as.scope_separator ?? " "),
|
||||
};
|
||||
if (challenge_supported) {
|
||||
params.code_challenge = challenge;
|
||||
@@ -162,13 +162,13 @@ export class OAuthStrategy implements Strategy {
|
||||
return {
|
||||
url: new URL(endpoint) + "?" + new URLSearchParams(params).toString(),
|
||||
endpoint,
|
||||
params
|
||||
params,
|
||||
};
|
||||
}
|
||||
|
||||
private async oidc(
|
||||
callbackParams: URL | URLSearchParams,
|
||||
options: { redirect_uri: string; state: string; scopes?: string[] }
|
||||
options: { redirect_uri: string; state: string; scopes?: string[] },
|
||||
) {
|
||||
const config = await this.getConfig();
|
||||
const { client, as, type } = config;
|
||||
@@ -178,7 +178,7 @@ export class OAuthStrategy implements Strategy {
|
||||
as,
|
||||
client, // no client_secret required
|
||||
callbackParams,
|
||||
oauth.expectNoState
|
||||
oauth.expectNoState,
|
||||
);
|
||||
if (oauth.isOAuth2Error(parameters)) {
|
||||
//console.log("callback.error", parameters);
|
||||
@@ -193,7 +193,7 @@ export class OAuthStrategy implements Strategy {
|
||||
client,
|
||||
parameters,
|
||||
options.redirect_uri,
|
||||
options.state
|
||||
options.state,
|
||||
);
|
||||
//console.log("callback.response", response);
|
||||
|
||||
@@ -213,7 +213,7 @@ export class OAuthStrategy implements Strategy {
|
||||
as,
|
||||
client,
|
||||
response,
|
||||
expectedNonce
|
||||
expectedNonce,
|
||||
);
|
||||
if (oauth.isOAuth2Error(result)) {
|
||||
console.log("callback.error", result);
|
||||
@@ -236,7 +236,7 @@ export class OAuthStrategy implements Strategy {
|
||||
|
||||
private async oauth2(
|
||||
callbackParams: URL | URLSearchParams,
|
||||
options: { redirect_uri: string; state: string; scopes?: string[] }
|
||||
options: { redirect_uri: string; state: string; scopes?: string[] },
|
||||
) {
|
||||
const config = await this.getConfig();
|
||||
const { client, type, as, profile } = config;
|
||||
@@ -246,7 +246,7 @@ export class OAuthStrategy implements Strategy {
|
||||
as,
|
||||
client, // no client_secret required
|
||||
callbackParams,
|
||||
oauth.expectNoState
|
||||
oauth.expectNoState,
|
||||
);
|
||||
if (oauth.isOAuth2Error(parameters)) {
|
||||
console.log("callback.error", parameters);
|
||||
@@ -254,14 +254,14 @@ export class OAuthStrategy implements Strategy {
|
||||
}
|
||||
console.log(
|
||||
"callback.parameters",
|
||||
JSON.stringify(Object.fromEntries(parameters.entries()), null, 2)
|
||||
JSON.stringify(Object.fromEntries(parameters.entries()), null, 2),
|
||||
);
|
||||
const response = await oauth.authorizationCodeGrantRequest(
|
||||
as,
|
||||
client,
|
||||
parameters,
|
||||
options.redirect_uri,
|
||||
options.state
|
||||
options.state,
|
||||
);
|
||||
|
||||
const challenges = oauth.parseWwwAuthenticateChallenges(response);
|
||||
@@ -297,7 +297,7 @@ export class OAuthStrategy implements Strategy {
|
||||
|
||||
async callback(
|
||||
callbackParams: URL | URLSearchParams,
|
||||
options: { redirect_uri: string; state: string; scopes?: string[] }
|
||||
options: { redirect_uri: string; state: string; scopes?: string[] },
|
||||
): Promise<UserProfile> {
|
||||
const type = this.getIssuerConfig().type;
|
||||
|
||||
@@ -330,7 +330,7 @@ export class OAuthStrategy implements Strategy {
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
sameSite: "Lax",
|
||||
maxAge: 60 * 5 // 5 minutes
|
||||
maxAge: 60 * 5, // 5 minutes
|
||||
});
|
||||
};
|
||||
|
||||
@@ -339,7 +339,7 @@ export class OAuthStrategy implements Strategy {
|
||||
return {
|
||||
state: c.req.header("X-State-Challenge"),
|
||||
action: c.req.header("X-State-Action"),
|
||||
mode: "token"
|
||||
mode: "token",
|
||||
} as any;
|
||||
}
|
||||
|
||||
@@ -366,7 +366,7 @@ export class OAuthStrategy implements Strategy {
|
||||
|
||||
const profile = await this.callback(params, {
|
||||
redirect_uri,
|
||||
state: state.state
|
||||
state: state.state,
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -392,7 +392,7 @@ export class OAuthStrategy implements Strategy {
|
||||
const params = new URLSearchParams(url.search);
|
||||
|
||||
return c.json({
|
||||
code: params.get("code") ?? null
|
||||
code: params.get("code") ?? null,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -410,7 +410,7 @@ export class OAuthStrategy implements Strategy {
|
||||
const state = oauth.generateRandomCodeVerifier();
|
||||
const response = await this.request({
|
||||
redirect_uri,
|
||||
state
|
||||
state,
|
||||
});
|
||||
//console.log("_state", state);
|
||||
|
||||
@@ -433,7 +433,7 @@ export class OAuthStrategy implements Strategy {
|
||||
const state = oauth.generateRandomCodeVerifier();
|
||||
const response = await this.request({
|
||||
redirect_uri,
|
||||
state
|
||||
state,
|
||||
});
|
||||
|
||||
if (isDebug()) {
|
||||
@@ -442,14 +442,14 @@ export class OAuthStrategy implements Strategy {
|
||||
redirect_uri,
|
||||
challenge: state,
|
||||
action,
|
||||
params: response.params
|
||||
params: response.params,
|
||||
});
|
||||
}
|
||||
|
||||
return c.json({
|
||||
url: response.url,
|
||||
challenge: state,
|
||||
action
|
||||
action,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -476,11 +476,8 @@ export class OAuthStrategy implements Strategy {
|
||||
const config = secrets ? this.config : filterKeys(this.config, ["secret", "client_id"]);
|
||||
|
||||
return {
|
||||
type: this.getType(),
|
||||
config: {
|
||||
type: this.getIssuerConfig().type,
|
||||
...config
|
||||
}
|
||||
type: this.getIssuerConfig().type,
|
||||
...config,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Exception, Permission } from "core";
|
||||
import { objectTransform } from "core/utils";
|
||||
import type { Context } from "hono";
|
||||
import type { ServerEnv } from "modules/Module";
|
||||
import type { ServerEnv } from "modules/Controller";
|
||||
import { Role } from "./Role";
|
||||
|
||||
export type GuardUserContext = {
|
||||
@@ -37,7 +37,7 @@ export class Guard {
|
||||
implicit_allow?: boolean;
|
||||
}
|
||||
>,
|
||||
config?: GuardConfig
|
||||
config?: GuardConfig,
|
||||
) {
|
||||
const _roles = roles
|
||||
? objectTransform(roles, ({ permissions = [], is_default, implicit_allow }, name) => {
|
||||
@@ -103,7 +103,7 @@ export class Guard {
|
||||
debug &&
|
||||
console.log("guard: role not found", {
|
||||
user: user,
|
||||
role: user?.role
|
||||
role: user?.role,
|
||||
});
|
||||
return this.getDefaultRole();
|
||||
}
|
||||
@@ -141,14 +141,14 @@ export class Guard {
|
||||
}
|
||||
|
||||
const rolePermission = role.permissions.find(
|
||||
(rolePermission) => rolePermission.permission.name === name
|
||||
(rolePermission) => rolePermission.permission.name === name,
|
||||
);
|
||||
|
||||
debug &&
|
||||
console.log("guard: rolePermission, allowing?", {
|
||||
permission: name,
|
||||
role: role.name,
|
||||
allowing: !!rolePermission
|
||||
allowing: !!rolePermission,
|
||||
});
|
||||
return !!rolePermission;
|
||||
}
|
||||
@@ -162,7 +162,7 @@ export class Guard {
|
||||
if (!this.granted(permission, c)) {
|
||||
throw new Exception(
|
||||
`Permission "${typeof permission === "string" ? permission : permission.name}" not granted`,
|
||||
403
|
||||
403,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Permission } from "core";
|
||||
export class RolePermission {
|
||||
constructor(
|
||||
public permission: Permission,
|
||||
public config?: any
|
||||
public config?: any,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -12,20 +12,20 @@ export class Role {
|
||||
public name: string,
|
||||
public permissions: RolePermission[] = [],
|
||||
public is_default: boolean = false,
|
||||
public implicit_allow: boolean = false
|
||||
public implicit_allow: boolean = false,
|
||||
) {}
|
||||
|
||||
static createWithPermissionNames(
|
||||
name: string,
|
||||
permissionNames: string[],
|
||||
is_default: boolean = false,
|
||||
implicit_allow: boolean = false
|
||||
implicit_allow: boolean = false,
|
||||
) {
|
||||
return new Role(
|
||||
name,
|
||||
permissionNames.map((name) => new RolePermission(new Permission(name))),
|
||||
is_default,
|
||||
implicit_allow
|
||||
implicit_allow,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export class Role {
|
||||
config.name,
|
||||
config.permissions?.map((name) => new RolePermission(new Permission(name))) ?? [],
|
||||
config.is_default,
|
||||
config.implicit_allow
|
||||
config.implicit_allow,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export {
|
||||
type AuthUserResolver,
|
||||
Authenticator,
|
||||
authenticatorConfig,
|
||||
jwtConfig
|
||||
jwtConfig,
|
||||
} from "./authenticate/Authenticator";
|
||||
|
||||
export { AppAuth, type UserFieldSchema } from "./AppAuth";
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Permission } from "core";
|
||||
import { patternMatch } from "core/utils";
|
||||
import type { Context } from "hono";
|
||||
import { createMiddleware } from "hono/factory";
|
||||
import type { ServerEnv } from "modules/Module";
|
||||
import type { ServerEnv } from "modules/Controller";
|
||||
|
||||
function getPath(reqOrCtx: Request | Context) {
|
||||
const req = reqOrCtx instanceof Request ? reqOrCtx : reqOrCtx.req.raw;
|
||||
@@ -36,7 +36,7 @@ export const auth = (options?: {
|
||||
registered: false,
|
||||
resolved: false,
|
||||
skip: false,
|
||||
user: undefined
|
||||
user: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ export const permission = (
|
||||
options?: {
|
||||
onGranted?: (c: Context<ServerEnv>) => Promise<Response | void | undefined>;
|
||||
onDenied?: (c: Context<ServerEnv>) => Promise<Response | void | undefined>;
|
||||
}
|
||||
},
|
||||
) =>
|
||||
// @ts-ignore
|
||||
createMiddleware<ServerEnv>(async (c, next) => {
|
||||
|
||||
@@ -13,18 +13,18 @@ import { type Template, templates } from "./templates";
|
||||
const config = {
|
||||
types: {
|
||||
runtime: "Runtime",
|
||||
framework: "Framework"
|
||||
framework: "Framework",
|
||||
},
|
||||
runtime: {
|
||||
node: "Node.js",
|
||||
bun: "Bun",
|
||||
cloudflare: "Cloudflare"
|
||||
cloudflare: "Cloudflare",
|
||||
},
|
||||
framework: {
|
||||
nextjs: "Next.js",
|
||||
remix: "Remix",
|
||||
astro: "Astro"
|
||||
}
|
||||
astro: "Astro",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const create: CliCommand = (program) => {
|
||||
@@ -41,7 +41,7 @@ function errorOutro() {
|
||||
$p.outro(color.red("Failed to create project."));
|
||||
console.log(
|
||||
color.yellow("Sorry that this happened. If you think this is a bug, please report it at: ") +
|
||||
color.cyan("https://github.com/bknd-io/bknd/issues")
|
||||
color.cyan("https://github.com/bknd-io/bknd/issues"),
|
||||
);
|
||||
console.log("");
|
||||
process.exit(1);
|
||||
@@ -53,26 +53,26 @@ async function action(options: { template?: string; dir?: string; integration?:
|
||||
|
||||
const downloadOpts = {
|
||||
dir: options.dir || "./",
|
||||
clean: false
|
||||
clean: false,
|
||||
};
|
||||
|
||||
const version = await getVersion();
|
||||
$p.intro(
|
||||
`👋 Welcome to the ${color.bold(color.cyan("bknd"))} create wizard ${color.bold(`v${version}`)}`
|
||||
`👋 Welcome to the ${color.bold(color.cyan("bknd"))} create wizard ${color.bold(`v${version}`)}`,
|
||||
);
|
||||
|
||||
await $p.stream.message(
|
||||
(async function* () {
|
||||
yield* typewriter("Thanks for choosing to create a new project with bknd!", color.dim);
|
||||
await wait();
|
||||
})()
|
||||
})(),
|
||||
);
|
||||
|
||||
if (!options.dir) {
|
||||
const dir = await $p.text({
|
||||
message: "Where to create your project?",
|
||||
placeholder: downloadOpts.dir,
|
||||
initialValue: downloadOpts.dir
|
||||
initialValue: downloadOpts.dir,
|
||||
});
|
||||
if ($p.isCancel(dir)) {
|
||||
process.exit(1);
|
||||
@@ -84,7 +84,7 @@ async function action(options: { template?: string; dir?: string; integration?:
|
||||
if (fs.existsSync(downloadOpts.dir)) {
|
||||
const clean = await $p.confirm({
|
||||
message: `Directory ${color.cyan(downloadOpts.dir)} exists. Clean it?`,
|
||||
initialValue: false
|
||||
initialValue: false,
|
||||
});
|
||||
if ($p.isCancel(clean)) {
|
||||
process.exit(1);
|
||||
@@ -115,7 +115,7 @@ async function action(options: { template?: string; dir?: string; integration?:
|
||||
await wait(2);
|
||||
yield* typewriter("Let's find the perfect template for you.", color.dim);
|
||||
await wait(2);
|
||||
})()
|
||||
})(),
|
||||
);
|
||||
|
||||
const type = await $p.select({
|
||||
@@ -123,8 +123,8 @@ async function action(options: { template?: string; dir?: string; integration?:
|
||||
options: Object.entries(config.types).map(([value, name]) => ({
|
||||
value,
|
||||
label: name,
|
||||
hint: Object.values(config[value]).join(", ")
|
||||
}))
|
||||
hint: Object.values(config[value]).join(", "),
|
||||
})),
|
||||
});
|
||||
|
||||
if ($p.isCancel(type)) {
|
||||
@@ -135,8 +135,8 @@ async function action(options: { template?: string; dir?: string; integration?:
|
||||
message: `Which ${color.cyan(config.types[type])} do you want to continue with?`,
|
||||
options: Object.entries(config[type]).map(([value, name]) => ({
|
||||
value,
|
||||
label: name
|
||||
})) as any
|
||||
label: name,
|
||||
})) as any,
|
||||
});
|
||||
if ($p.isCancel(_integration)) {
|
||||
process.exit(1);
|
||||
@@ -157,7 +157,7 @@ async function action(options: { template?: string; dir?: string; integration?:
|
||||
} else if (choices.length > 1) {
|
||||
const selected_template = await $p.select({
|
||||
message: "Pick a template",
|
||||
options: choices.map((t) => ({ value: t.key, label: t.title, hint: t.description }))
|
||||
options: choices.map((t) => ({ value: t.key, label: t.title, hint: t.description })),
|
||||
});
|
||||
|
||||
if ($p.isCancel(selected_template)) {
|
||||
@@ -196,7 +196,7 @@ async function action(options: { template?: string; dir?: string; integration?:
|
||||
try {
|
||||
await downloadTemplate(url, {
|
||||
dir: ctx.dir,
|
||||
force: downloadOpts.clean ? "clean" : true
|
||||
force: downloadOpts.clean ? "clean" : true,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
@@ -221,15 +221,15 @@ async function action(options: { template?: string; dir?: string; integration?:
|
||||
await overridePackageJson(
|
||||
(pkg) => ({
|
||||
...pkg,
|
||||
name: ctx.name
|
||||
name: ctx.name,
|
||||
}),
|
||||
{ dir: ctx.dir }
|
||||
{ dir: ctx.dir },
|
||||
);
|
||||
$p.log.success(`Updated package name to ${color.cyan(ctx.name)}`);
|
||||
|
||||
{
|
||||
const install = await $p.confirm({
|
||||
message: "Install dependencies?"
|
||||
message: "Install dependencies?",
|
||||
});
|
||||
|
||||
if ($p.isCancel(install)) {
|
||||
@@ -263,10 +263,10 @@ async function action(options: { template?: string; dir?: string; integration?:
|
||||
yield* typewriter(
|
||||
color.dim("Remember to run ") +
|
||||
color.cyan("npm install") +
|
||||
color.dim(" after setup")
|
||||
color.dim(" after setup"),
|
||||
);
|
||||
await wait();
|
||||
})()
|
||||
})(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -284,10 +284,10 @@ async function action(options: { template?: string; dir?: string; integration?:
|
||||
yield "\n\n";
|
||||
yield* typewriter(
|
||||
`Enter your project's directory using ${color.cyan("cd " + ctx.dir)}
|
||||
If you need help, check ${color.cyan("https://docs.bknd.io")} or join our Discord!`
|
||||
If you need help, check ${color.cyan("https://docs.bknd.io")} or join our Discord!`,
|
||||
);
|
||||
await wait(2);
|
||||
})()
|
||||
})(),
|
||||
);
|
||||
|
||||
$p.outro(color.green("Setup complete."));
|
||||
|
||||
@@ -16,7 +16,7 @@ export type TPackageJson = Partial<{
|
||||
export async function overrideJson<File extends object = object>(
|
||||
file: string,
|
||||
fn: (pkg: File) => Promise<File> | File,
|
||||
opts?: { dir?: string; indent?: number }
|
||||
opts?: { dir?: string; indent?: number },
|
||||
) {
|
||||
const pkgPath = path.resolve(opts?.dir ?? process.cwd(), file);
|
||||
const pkg = await readFile(pkgPath, "utf-8");
|
||||
@@ -26,7 +26,7 @@ export async function overrideJson<File extends object = object>(
|
||||
|
||||
export async function overridePackageJson(
|
||||
fn: (pkg: TPackageJson) => Promise<TPackageJson> | TPackageJson,
|
||||
opts?: { dir?: string }
|
||||
opts?: { dir?: string },
|
||||
) {
|
||||
return await overrideJson("package.json", fn, { dir: opts?.dir });
|
||||
}
|
||||
@@ -44,7 +44,7 @@ export async function getVersion(pkg: string, version: string = "latest") {
|
||||
const _deps = ["dependencies", "devDependencies", "optionalDependencies"] as const;
|
||||
export async function replacePackageJsonVersions(
|
||||
fn: (pkg: string, version: string) => Promise<string | undefined> | string | undefined,
|
||||
opts?: { include?: (keyof typeof _deps)[]; dir?: string }
|
||||
opts?: { include?: (keyof typeof _deps)[]; dir?: string },
|
||||
) {
|
||||
const deps = (opts?.include ?? _deps) as string[];
|
||||
await overridePackageJson(
|
||||
@@ -62,14 +62,14 @@ export async function replacePackageJsonVersions(
|
||||
|
||||
return json;
|
||||
},
|
||||
{ dir: opts?.dir }
|
||||
{ dir: opts?.dir },
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateBkndPackages(dir?: string, map?: Record<string, string>) {
|
||||
const versions = {
|
||||
bknd: "^" + (await sysGetVersion()),
|
||||
...(map ?? {})
|
||||
...(map ?? {}),
|
||||
};
|
||||
await replacePackageJsonVersions(
|
||||
async (pkg) => {
|
||||
@@ -78,6 +78,6 @@ export async function updateBkndPackages(dir?: string, map?: Record<string, stri
|
||||
}
|
||||
return;
|
||||
},
|
||||
{ dir }
|
||||
{ dir },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,18 +22,18 @@ export const cloudflare = {
|
||||
...json,
|
||||
name: ctx.name,
|
||||
assets: {
|
||||
directory: "node_modules/bknd/dist/static"
|
||||
}
|
||||
directory: "node_modules/bknd/dist/static",
|
||||
},
|
||||
}),
|
||||
{ dir: ctx.dir }
|
||||
{ dir: ctx.dir },
|
||||
);
|
||||
|
||||
const db = await $p.select({
|
||||
message: "What database do you want to use?",
|
||||
options: [
|
||||
{ label: "Cloudflare D1", value: "d1" },
|
||||
{ label: "LibSQL", value: "libsql" }
|
||||
]
|
||||
{ label: "LibSQL", value: "libsql" },
|
||||
],
|
||||
});
|
||||
if ($p.isCancel(db)) {
|
||||
process.exit(1);
|
||||
@@ -53,10 +53,10 @@ export const cloudflare = {
|
||||
} catch (e) {
|
||||
const message = (e as any).message || "An error occurred";
|
||||
$p.log.warn(
|
||||
"Couldn't add database. You can add it manually later. Error: " + c.red(message)
|
||||
"Couldn't add database. You can add it manually later. Error: " + c.red(message),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
} as const satisfies Template;
|
||||
|
||||
async function createD1(ctx: TemplateSetupCtx) {
|
||||
@@ -69,7 +69,7 @@ async function createD1(ctx: TemplateSetupCtx) {
|
||||
return "Invalid name";
|
||||
}
|
||||
return;
|
||||
}
|
||||
},
|
||||
});
|
||||
if ($p.isCancel(name)) {
|
||||
process.exit(1);
|
||||
@@ -83,11 +83,11 @@ async function createD1(ctx: TemplateSetupCtx) {
|
||||
{
|
||||
binding: "DB",
|
||||
database_name: name,
|
||||
database_id: uuid()
|
||||
}
|
||||
]
|
||||
database_id: uuid(),
|
||||
},
|
||||
],
|
||||
}),
|
||||
{ dir: ctx.dir }
|
||||
{ dir: ctx.dir },
|
||||
);
|
||||
|
||||
await $p.stream.info(
|
||||
@@ -96,9 +96,9 @@ async function createD1(ctx: TemplateSetupCtx) {
|
||||
await wait();
|
||||
yield* typewriter(
|
||||
`\nNote that if you deploy, you have to create a real database using ${c.cyan("npx wrangler d1 create <name>")} and update your wrangler configuration.`,
|
||||
c.dim
|
||||
c.dim,
|
||||
);
|
||||
})()
|
||||
})(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -108,10 +108,10 @@ async function createLibsql(ctx: TemplateSetupCtx) {
|
||||
(json) => ({
|
||||
...json,
|
||||
vars: {
|
||||
DB_URL: "http://127.0.0.1:8080"
|
||||
}
|
||||
DB_URL: "http://127.0.0.1:8080",
|
||||
},
|
||||
}),
|
||||
{ dir: ctx.dir }
|
||||
{ dir: ctx.dir },
|
||||
);
|
||||
|
||||
await overridePackageJson(
|
||||
@@ -120,10 +120,10 @@ async function createLibsql(ctx: TemplateSetupCtx) {
|
||||
scripts: {
|
||||
...pkg.scripts,
|
||||
db: "turso dev",
|
||||
dev: "npm run db && wrangler dev"
|
||||
}
|
||||
dev: "npm run db && wrangler dev",
|
||||
},
|
||||
}),
|
||||
{ dir: ctx.dir }
|
||||
{ dir: ctx.dir },
|
||||
);
|
||||
|
||||
await $p.stream.info(
|
||||
@@ -132,13 +132,13 @@ async function createLibsql(ctx: TemplateSetupCtx) {
|
||||
await wait();
|
||||
yield* typewriter(
|
||||
`\nYou can now run ${c.cyan("npm run db")} to start the database and ${c.cyan("npm run dev")} to start the worker.`,
|
||||
c.dim
|
||||
c.dim,
|
||||
);
|
||||
await wait();
|
||||
yield* typewriter(
|
||||
`\nAlso make sure you have Turso's CLI installed. Check their docs on how to install at ${c.cyan("https://docs.turso.tech/cli/introduction")}`,
|
||||
c.dim
|
||||
c.dim,
|
||||
);
|
||||
})()
|
||||
})(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ export const templates: Template[] = [
|
||||
integration: "node",
|
||||
description: "A basic bknd Node.js server",
|
||||
path: "gh:bknd-io/bknd/examples/node",
|
||||
ref: true
|
||||
ref: true,
|
||||
},
|
||||
{
|
||||
key: "bun",
|
||||
@@ -51,7 +51,7 @@ export const templates: Template[] = [
|
||||
integration: "bun",
|
||||
description: "A basic bknd Bun server",
|
||||
path: "gh:bknd-io/bknd/examples/bun",
|
||||
ref: true
|
||||
ref: true,
|
||||
},
|
||||
{
|
||||
key: "astro",
|
||||
@@ -59,6 +59,6 @@ export const templates: Template[] = [
|
||||
integration: "astro",
|
||||
description: "A basic bknd Astro starter",
|
||||
path: "gh:bknd-io/bknd/examples/astro",
|
||||
ref: true
|
||||
}
|
||||
ref: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -9,7 +9,7 @@ export const nextjs = {
|
||||
description: "A basic bknd Next.js starter",
|
||||
path: "gh:bknd-io/bknd/examples/nextjs",
|
||||
scripts: {
|
||||
install: "npm install --force"
|
||||
install: "npm install --force",
|
||||
},
|
||||
ref: true,
|
||||
preinstall: async (ctx) => {
|
||||
@@ -20,10 +20,10 @@ export const nextjs = {
|
||||
dependencies: {
|
||||
...pkg.dependencies,
|
||||
react: undefined,
|
||||
"react-dom": undefined
|
||||
}
|
||||
"react-dom": undefined,
|
||||
},
|
||||
}),
|
||||
{ dir: ctx.dir }
|
||||
{ dir: ctx.dir },
|
||||
);
|
||||
}
|
||||
},
|
||||
} as const satisfies Template;
|
||||
|
||||
@@ -16,10 +16,10 @@ export const remix = {
|
||||
dependencies: {
|
||||
...pkg.dependencies,
|
||||
react: "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
}
|
||||
"react-dom": "^18.2.0",
|
||||
},
|
||||
}),
|
||||
{ dir: ctx.dir }
|
||||
{ dir: ctx.dir },
|
||||
);
|
||||
}
|
||||
},
|
||||
} as const satisfies Template;
|
||||
|
||||
@@ -23,7 +23,7 @@ const subjects = {
|
||||
relativeDistPath: getRelativeDistPath(),
|
||||
cwd: process.cwd(),
|
||||
dir: path.dirname(url.fileURLToPath(import.meta.url)),
|
||||
resolvedPkg: path.resolve(getRootPath(), "package.json")
|
||||
resolvedPkg: path.resolve(getRootPath(), "package.json"),
|
||||
});
|
||||
},
|
||||
routes: async () => {
|
||||
@@ -32,7 +32,7 @@ const subjects = {
|
||||
const app = createApp({ connection: credentials });
|
||||
await app.build();
|
||||
showRoutes(app.server);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
async function action(subject: string) {
|
||||
|
||||
@@ -14,13 +14,13 @@ export async function serveStatic(server: Platform): Promise<MiddlewareHandler>
|
||||
const m = await import("@hono/node-server/serve-static");
|
||||
return m.serveStatic({
|
||||
// somehow different for node
|
||||
root: getRelativeDistPath() + "/static"
|
||||
root: getRelativeDistPath() + "/static",
|
||||
});
|
||||
}
|
||||
case "bun": {
|
||||
const m = await import("hono/bun");
|
||||
return m.serveStatic({
|
||||
root: path.resolve(getRelativeDistPath(), "static")
|
||||
root: path.resolve(getRelativeDistPath(), "static"),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -40,14 +40,14 @@ export async function startServer(server: Platform, app: any, options: { port: n
|
||||
const serve = await import("@hono/node-server").then((m) => m.serve);
|
||||
serve({
|
||||
fetch: (req) => app.fetch(req),
|
||||
port
|
||||
port,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "bun": {
|
||||
Bun.serve({
|
||||
fetch: (req) => app.fetch(req),
|
||||
port
|
||||
port,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
attachServeStatic,
|
||||
getConfigPath,
|
||||
getConnectionCredentialsFromEnv,
|
||||
startServer
|
||||
startServer,
|
||||
} from "./platform";
|
||||
|
||||
dotenv.config();
|
||||
@@ -26,26 +26,26 @@ export const run: CliCommand = (program) => {
|
||||
new Option("-p, --port <port>", "port to run on")
|
||||
.env("PORT")
|
||||
.default(config.server.default_port)
|
||||
.argParser((v) => Number.parseInt(v))
|
||||
.argParser((v) => Number.parseInt(v)),
|
||||
)
|
||||
.addOption(
|
||||
new Option("-m, --memory", "use in-memory database").conflicts([
|
||||
"config",
|
||||
"db-url",
|
||||
"db-token"
|
||||
])
|
||||
"db-token",
|
||||
]),
|
||||
)
|
||||
.addOption(new Option("-c, --config <config>", "config file"))
|
||||
.addOption(
|
||||
new Option("--db-url <db>", "database url, can be any valid libsql url").conflicts(
|
||||
"config"
|
||||
)
|
||||
"config",
|
||||
),
|
||||
)
|
||||
.addOption(new Option("--db-token <db>", "database token").conflicts("config"))
|
||||
.addOption(
|
||||
new Option("--server <server>", "server type")
|
||||
.choices(PLATFORMS)
|
||||
.default(isBun ? "bun" : "node")
|
||||
.default(isBun ? "bun" : "node"),
|
||||
)
|
||||
.action(action);
|
||||
};
|
||||
@@ -76,7 +76,7 @@ async function makeApp(config: MakeAppConfig) {
|
||||
await config.onBuilt(app);
|
||||
}
|
||||
},
|
||||
"sync"
|
||||
"sync",
|
||||
);
|
||||
|
||||
await app.build();
|
||||
@@ -95,7 +95,7 @@ export async function makeConfigApp(config: CliBkndConfig, platform?: Platform)
|
||||
|
||||
await config.onBuilt?.(app);
|
||||
},
|
||||
"sync"
|
||||
"sync",
|
||||
);
|
||||
|
||||
await config.beforeBuild?.(app);
|
||||
@@ -141,7 +141,7 @@ async function action(options: {
|
||||
console.info("Using connection", c.cyan(connection.url));
|
||||
app = await makeApp({
|
||||
connection,
|
||||
server: { platform: options.server }
|
||||
server: { platform: options.server },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ async function create(app: App, options: any) {
|
||||
return "Invalid email";
|
||||
}
|
||||
return;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const password = await $password({
|
||||
@@ -58,7 +58,7 @@ async function create(app: App, options: any) {
|
||||
return "Invalid password";
|
||||
}
|
||||
return;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (typeof email !== "string" || typeof password !== "string") {
|
||||
@@ -69,8 +69,8 @@ async function create(app: App, options: any) {
|
||||
try {
|
||||
const created = await app.createUser({
|
||||
email,
|
||||
password: await strategy.hash(password as string)
|
||||
})
|
||||
password: await strategy.hash(password as string),
|
||||
});
|
||||
console.log("Created:", created);
|
||||
} catch (e) {
|
||||
console.error("Error", e);
|
||||
@@ -90,7 +90,7 @@ async function update(app: App, options: any) {
|
||||
return "Invalid email";
|
||||
}
|
||||
return;
|
||||
}
|
||||
},
|
||||
})) as string;
|
||||
if (typeof email !== "string") {
|
||||
console.log("Cancelled");
|
||||
@@ -111,7 +111,7 @@ async function update(app: App, options: any) {
|
||||
return "Invalid password";
|
||||
}
|
||||
return;
|
||||
}
|
||||
},
|
||||
});
|
||||
if (typeof password !== "string") {
|
||||
console.log("Cancelled");
|
||||
@@ -130,7 +130,7 @@ async function update(app: App, options: any) {
|
||||
.ctx()
|
||||
.em.mutator(users_entity)
|
||||
.updateOne(user.id, {
|
||||
strategy_value: await strategy.hash(password as string)
|
||||
strategy_value: await strategy.hash(password as string),
|
||||
});
|
||||
togglePw(false);
|
||||
|
||||
@@ -138,4 +138,4 @@ async function update(app: App, options: any) {
|
||||
} catch (e) {
|
||||
console.error("Error", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function ansiRegex({ onlyFirst = false } = {}) {
|
||||
const ST = "(?:\\u0007|\\u001B\\u005C|\\u009C)";
|
||||
const pattern = [
|
||||
`[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?${ST})`,
|
||||
"(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))"
|
||||
"(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))",
|
||||
].join("|");
|
||||
|
||||
return new RegExp(pattern, onlyFirst ? undefined : "g");
|
||||
@@ -22,7 +22,7 @@ const DEFAULT_WAIT_WRITER = _SPEEDUP ? 0 : 20;
|
||||
export async function* typewriter(
|
||||
text: string,
|
||||
transform?: (char: string) => string,
|
||||
_delay?: number
|
||||
_delay?: number,
|
||||
) {
|
||||
const delay = DEFAULT_WAIT_WRITER * (_delay ?? 1);
|
||||
const regex = ansiRegex();
|
||||
|
||||
@@ -44,7 +44,7 @@ export function exec(command: string, opts?: { silent?: boolean; env?: Record<st
|
||||
const stdio = opts?.silent ? "pipe" : "inherit";
|
||||
const output = execSync(command, {
|
||||
stdio: ["inherit", stdio, stdio],
|
||||
env: { ...process.env, ...opts?.env }
|
||||
env: { ...process.env, ...opts?.env },
|
||||
});
|
||||
if (!opts?.silent) {
|
||||
return;
|
||||
@@ -54,20 +54,20 @@ export function exec(command: string, opts?: { silent?: boolean; env?: Record<st
|
||||
|
||||
export function execAsync(
|
||||
command: string,
|
||||
opts?: { silent?: boolean; env?: Record<string, string> }
|
||||
opts?: { silent?: boolean; env?: Record<string, string> },
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
nodeExec(
|
||||
command,
|
||||
{
|
||||
env: { ...process.env, ...opts?.env }
|
||||
env: { ...process.env, ...opts?.env },
|
||||
},
|
||||
(err, stdout, stderr) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(stdout);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
4
app/src/core/cache/adapters/MemoryCache.ts
vendored
4
app/src/core/cache/adapters/MemoryCache.ts
vendored
@@ -10,7 +10,7 @@ export class MemoryCache<Data = any> implements ICachePool<Data> {
|
||||
|
||||
supports = () => ({
|
||||
metadata: true,
|
||||
clear: true
|
||||
clear: true,
|
||||
});
|
||||
|
||||
async get(key: string): Promise<MemoryCacheItem<Data>> {
|
||||
@@ -61,7 +61,7 @@ export class MemoryCache<Data = any> implements ICachePool<Data> {
|
||||
async put(
|
||||
key: string,
|
||||
value: Data,
|
||||
options: { expiresAt?: Date; ttl?: number; metadata?: Record<string, string> } = {}
|
||||
options: { expiresAt?: Date; ttl?: number; metadata?: Record<string, string> } = {},
|
||||
): Promise<boolean> {
|
||||
const item = await this.get(key);
|
||||
item.set(value, options.metadata || {});
|
||||
|
||||
@@ -17,9 +17,9 @@ export const config = {
|
||||
server: {
|
||||
default_port: 1337,
|
||||
// resetted to root for now, bc bundling with vite
|
||||
assets_path: "/"
|
||||
assets_path: "/",
|
||||
},
|
||||
data: {
|
||||
default_primary_field: "id"
|
||||
}
|
||||
default_primary_field: "id",
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { datetimeStringLocal } from "core/utils";
|
||||
import colors from "picocolors";
|
||||
|
||||
function hasColors() {
|
||||
@@ -8,10 +9,10 @@ function hasColors() {
|
||||
env = p.env || {};
|
||||
return (
|
||||
!(!!env.NO_COLOR || argv.includes("--no-color")) &&
|
||||
// biome-ignore lint/complexity/useOptionalChain: <explanation>
|
||||
(!!env.FORCE_COLOR ||
|
||||
argv.includes("--color") ||
|
||||
p.platform === "win32" ||
|
||||
// biome-ignore lint/complexity/useOptionalChain: <explanation>
|
||||
((p.stdout || {}).isTTY && env.TERM !== "dumb") ||
|
||||
!!env.CI)
|
||||
);
|
||||
@@ -25,7 +26,7 @@ const originalConsoles = {
|
||||
warn: console.warn,
|
||||
info: console.info,
|
||||
log: console.log,
|
||||
debug: console.debug
|
||||
debug: console.debug,
|
||||
} as typeof console;
|
||||
|
||||
function __tty(type: any, args: any[]) {
|
||||
@@ -33,29 +34,28 @@ function __tty(type: any, args: any[]) {
|
||||
const styles = {
|
||||
error: {
|
||||
prefix: colors.red,
|
||||
args: colors.red
|
||||
args: colors.red,
|
||||
},
|
||||
warn: {
|
||||
prefix: colors.yellow,
|
||||
args: colors.yellow
|
||||
args: colors.yellow,
|
||||
},
|
||||
info: {
|
||||
prefix: colors.cyan
|
||||
prefix: colors.cyan,
|
||||
},
|
||||
log: {
|
||||
prefix: colors.gray
|
||||
prefix: colors.dim,
|
||||
},
|
||||
debug: {
|
||||
prefix: colors.yellow
|
||||
}
|
||||
prefix: colors.yellow,
|
||||
args: colors.dim,
|
||||
},
|
||||
} as const;
|
||||
const prefix = styles[type].prefix(
|
||||
`[${type.toUpperCase()}]${has ? " ".repeat(5 - type.length) : ""}`
|
||||
);
|
||||
const prefix = styles[type].prefix(`[${type.toUpperCase()}]`);
|
||||
const _args = args.map((a) =>
|
||||
"args" in styles[type] && has && typeof a === "string" ? styles[type].args(a) : a
|
||||
"args" in styles[type] && has && typeof a === "string" ? styles[type].args(a) : a,
|
||||
);
|
||||
return originalConsoles[type](prefix, ..._args);
|
||||
return originalConsoles[type](prefix, colors.gray(datetimeStringLocal()), ..._args);
|
||||
}
|
||||
|
||||
export type TConsoleSeverity = keyof typeof originalConsoles;
|
||||
@@ -79,13 +79,13 @@ export const $console = new Proxy(
|
||||
return (...args: any[]) => __tty(prop, args);
|
||||
}
|
||||
return () => null;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
) as typeof console;
|
||||
|
||||
export async function withDisabledConsole<R>(
|
||||
fn: () => Promise<R>,
|
||||
sev?: TConsoleSeverity[]
|
||||
sev?: TConsoleSeverity[],
|
||||
): Promise<R> {
|
||||
disableConsole(sev);
|
||||
try {
|
||||
|
||||
@@ -19,7 +19,7 @@ export class Exception extends Error {
|
||||
return {
|
||||
error: this.message,
|
||||
type: this.name,
|
||||
context: this._context
|
||||
context: this._context,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ export class BkndError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public details?: Record<string, any>,
|
||||
public type?: string
|
||||
public type?: string,
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
@@ -41,7 +41,7 @@ export class BkndError extends Error {
|
||||
return {
|
||||
type: this.type ?? "unknown",
|
||||
message: this.message,
|
||||
details: this.details
|
||||
details: this.details,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export abstract class Event<Params = any, Returning = void> {
|
||||
|
||||
protected clone<This extends Event<Params, Returning> = Event<Params, Returning>>(
|
||||
this: This,
|
||||
params: Params
|
||||
params: Params,
|
||||
): This {
|
||||
const cloned = new (this.constructor as any)(params);
|
||||
cloned.returned = true;
|
||||
@@ -50,7 +50,7 @@ export class InvalidEventReturn extends Error {
|
||||
export class EventReturnedWithoutValidation extends Error {
|
||||
constructor(
|
||||
event: EventClass,
|
||||
public data: any
|
||||
public data: any,
|
||||
) {
|
||||
// @ts-expect-error slug is static
|
||||
super(`Event "${event.constructor.slug}" returned without validation`);
|
||||
|
||||
@@ -6,7 +6,7 @@ export type ListenerMode = (typeof ListenerModes)[number];
|
||||
|
||||
export type ListenerHandler<E extends Event<any, any>> = (
|
||||
event: E,
|
||||
slug: string
|
||||
slug: string,
|
||||
) => E extends Event<any, infer R> ? R | Promise<R | void> : never;
|
||||
|
||||
export class EventListener<E extends Event = Event> {
|
||||
@@ -20,7 +20,7 @@ export class EventListener<E extends Event = Event> {
|
||||
event: EventClass,
|
||||
handler: ListenerHandler<E>,
|
||||
mode: ListenerMode = "async",
|
||||
id?: string
|
||||
id?: string,
|
||||
) {
|
||||
this.event = event;
|
||||
this.handler = handler;
|
||||
|
||||
@@ -17,7 +17,7 @@ export interface EmitsEvents {
|
||||
export type { EventClass };
|
||||
|
||||
export class EventManager<
|
||||
RegisteredEvents extends Record<string, EventClass> = Record<string, EventClass>
|
||||
RegisteredEvents extends Record<string, EventClass> = Record<string, EventClass>,
|
||||
> {
|
||||
protected events: EventClass[] = [];
|
||||
protected listeners: EventListener[] = [];
|
||||
@@ -30,7 +30,7 @@ export class EventManager<
|
||||
onError?: (event: Event, e: unknown) => void;
|
||||
onInvalidReturn?: (event: Event, e: InvalidEventReturn) => void;
|
||||
asyncExecutor?: typeof Promise.all;
|
||||
}
|
||||
},
|
||||
) {
|
||||
if (events) {
|
||||
this.registerEvents(events);
|
||||
@@ -69,7 +69,7 @@ export class EventManager<
|
||||
return new Proxy(this, {
|
||||
get: (_, prop: string) => {
|
||||
return this.events.find((e) => e.slug === prop);
|
||||
}
|
||||
},
|
||||
}) as any;
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ export class EventManager<
|
||||
protected createEventListener(
|
||||
_event: EventClass | string,
|
||||
handler: ListenerHandler<any>,
|
||||
_config: RegisterListenerConfig = "async"
|
||||
_config: RegisterListenerConfig = "async",
|
||||
) {
|
||||
const event =
|
||||
typeof _event === "string" ? this.events.find((e) => e.slug === _event)! : _event;
|
||||
@@ -159,7 +159,7 @@ export class EventManager<
|
||||
onEvent<ActualEvent extends EventClass, Instance extends InstanceType<ActualEvent>>(
|
||||
event: ActualEvent,
|
||||
handler: ListenerHandler<Instance>,
|
||||
config?: RegisterListenerConfig
|
||||
config?: RegisterListenerConfig,
|
||||
) {
|
||||
this.createEventListener(event, handler, config);
|
||||
}
|
||||
@@ -167,7 +167,7 @@ export class EventManager<
|
||||
on<Params = any>(
|
||||
slug: string,
|
||||
handler: ListenerHandler<Event<Params>>,
|
||||
config?: RegisterListenerConfig
|
||||
config?: RegisterListenerConfig,
|
||||
) {
|
||||
this.createEventListener(slug, handler, config);
|
||||
}
|
||||
@@ -225,7 +225,7 @@ export class EventManager<
|
||||
if (!newEvent.returned) {
|
||||
throw new Error(
|
||||
// @ts-expect-error slug is static
|
||||
`Returned event ${newEvent.constructor.slug} must be marked as returned.`
|
||||
`Returned event ${newEvent.constructor.slug} must be marked as returned.`,
|
||||
);
|
||||
}
|
||||
_event = newEvent as Actual;
|
||||
|
||||
@@ -3,6 +3,6 @@ export {
|
||||
EventListener,
|
||||
ListenerModes,
|
||||
type ListenerMode,
|
||||
type ListenerHandler
|
||||
type ListenerHandler,
|
||||
} from "./EventListener";
|
||||
export { EventManager, type EmitsEvents, type EventClass } from "./EventManager";
|
||||
|
||||
@@ -9,7 +9,7 @@ export {
|
||||
SimpleRenderer,
|
||||
type TemplateObject,
|
||||
type TemplateTypes,
|
||||
type SimpleRendererOptions
|
||||
type SimpleRendererOptions,
|
||||
} from "./template/SimpleRenderer";
|
||||
export { SchemaObject } from "./object/SchemaObject";
|
||||
export { DebugLogger } from "./utils/DebugLogger";
|
||||
@@ -22,7 +22,7 @@ export {
|
||||
isPrimitive,
|
||||
type TExpression,
|
||||
type BooleanLike,
|
||||
isBooleanLike
|
||||
isBooleanLike,
|
||||
} from "./object/query/query";
|
||||
export { Registry, type Constructor } from "./registry/Registry";
|
||||
|
||||
|
||||
@@ -6,14 +6,14 @@ import {
|
||||
getFullPathKeys,
|
||||
mergeObjectWith,
|
||||
parse,
|
||||
stripMark
|
||||
stripMark,
|
||||
} from "../utils";
|
||||
|
||||
export type SchemaObjectOptions<Schema extends TObject> = {
|
||||
onUpdate?: (config: Static<Schema>) => void | Promise<void>;
|
||||
onBeforeUpdate?: (
|
||||
from: Static<Schema>,
|
||||
to: Static<Schema>
|
||||
to: Static<Schema>,
|
||||
) => Static<Schema> | Promise<Static<Schema>>;
|
||||
restrictPaths?: string[];
|
||||
overwritePaths?: (RegExp | string)[];
|
||||
@@ -29,13 +29,13 @@ export class SchemaObject<Schema extends TObject> {
|
||||
constructor(
|
||||
private _schema: Schema,
|
||||
initial?: Partial<Static<Schema>>,
|
||||
private options?: SchemaObjectOptions<Schema>
|
||||
private options?: SchemaObjectOptions<Schema>,
|
||||
) {
|
||||
this._default = Default(_schema, {} as any) as any;
|
||||
this._value = initial
|
||||
? parse(_schema, structuredClone(initial as any), {
|
||||
forceParse: this.isForceParse(),
|
||||
skipMark: this.isForceParse()
|
||||
skipMark: this.isForceParse(),
|
||||
})
|
||||
: this._default;
|
||||
this._config = Object.freeze(this._value);
|
||||
@@ -71,7 +71,7 @@ export class SchemaObject<Schema extends TObject> {
|
||||
async set(config: Static<Schema>, noEmit?: boolean): Promise<Static<Schema>> {
|
||||
const valid = parse(this._schema, structuredClone(config) as any, {
|
||||
forceParse: true,
|
||||
skipMark: this.isForceParse()
|
||||
skipMark: this.isForceParse(),
|
||||
});
|
||||
// regardless of "noEmit" – this should always be triggered
|
||||
const updatedConfig = await this.onBeforeUpdate(this._config, valid);
|
||||
@@ -159,7 +159,7 @@ export class SchemaObject<Schema extends TObject> {
|
||||
overwritePaths.some((k2) => {
|
||||
//console.log("keep?", { k, k2 }, k2 !== k && k2.startsWith(k));
|
||||
return k2 !== k && k2.startsWith(k);
|
||||
})
|
||||
}),
|
||||
)
|
||||
: overwritePaths;
|
||||
//console.log("specific", specific);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
enum Change {
|
||||
Add = "a",
|
||||
Remove = "r",
|
||||
Edit = "e"
|
||||
Edit = "e",
|
||||
}
|
||||
|
||||
type Object = object;
|
||||
@@ -50,7 +50,7 @@ function diff(oldObj: Object, newObj: Object): DiffEntry[] {
|
||||
t: Change.Edit,
|
||||
p: path,
|
||||
o: oldValue,
|
||||
n: newValue
|
||||
n: newValue,
|
||||
});
|
||||
} else if (Array.isArray(oldValue) && Array.isArray(newValue)) {
|
||||
const maxLength = Math.max(oldValue.length, newValue.length);
|
||||
@@ -60,14 +60,14 @@ function diff(oldObj: Object, newObj: Object): DiffEntry[] {
|
||||
t: Change.Add,
|
||||
p: [...path, i],
|
||||
o: undefined,
|
||||
n: newValue[i]
|
||||
n: newValue[i],
|
||||
});
|
||||
} else if (i >= newValue.length) {
|
||||
diffs.push({
|
||||
t: Change.Remove,
|
||||
p: [...path, i],
|
||||
o: oldValue[i],
|
||||
n: undefined
|
||||
n: undefined,
|
||||
});
|
||||
} else {
|
||||
recurse(oldValue[i], newValue[i], [...path, i]);
|
||||
@@ -83,14 +83,14 @@ function diff(oldObj: Object, newObj: Object): DiffEntry[] {
|
||||
t: Change.Add,
|
||||
p: [...path, key],
|
||||
o: undefined,
|
||||
n: newValue[key]
|
||||
n: newValue[key],
|
||||
});
|
||||
} else if (!(key in newValue)) {
|
||||
diffs.push({
|
||||
t: Change.Remove,
|
||||
p: [...path, key],
|
||||
o: oldValue[key],
|
||||
n: undefined
|
||||
n: undefined,
|
||||
});
|
||||
} else {
|
||||
recurse(oldValue[key], newValue[key], [...path, key]);
|
||||
@@ -101,7 +101,7 @@ function diff(oldObj: Object, newObj: Object): DiffEntry[] {
|
||||
t: Change.Edit,
|
||||
p: path,
|
||||
o: oldValue,
|
||||
n: newValue
|
||||
n: newValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ const expressions = [
|
||||
exp(
|
||||
"$eq",
|
||||
(v: Primitive) => isPrimitive(v),
|
||||
(e, a) => e === a
|
||||
(e, a) => e === a,
|
||||
),
|
||||
exp(
|
||||
"$ne",
|
||||
(v: Primitive) => isPrimitive(v),
|
||||
(e, a) => e !== a
|
||||
(e, a) => e !== a,
|
||||
),
|
||||
exp(
|
||||
"$like",
|
||||
@@ -25,7 +25,7 @@ const expressions = [
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
exp(
|
||||
"$regex",
|
||||
@@ -39,54 +39,54 @@ const expressions = [
|
||||
return regex.test(a);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
),
|
||||
exp(
|
||||
"$isnull",
|
||||
(v: boolean | 1 | 0) => true,
|
||||
(e, a) => (e ? a === null : a !== null)
|
||||
(e, a) => (e ? a === null : a !== null),
|
||||
),
|
||||
exp(
|
||||
"$notnull",
|
||||
(v: boolean | 1 | 0) => true,
|
||||
(e, a) => (e ? a !== null : a === null)
|
||||
(e, a) => (e ? a !== null : a === null),
|
||||
),
|
||||
exp(
|
||||
"$in",
|
||||
(v: (string | number)[]) => Array.isArray(v),
|
||||
(e: any, a: any) => e.includes(a)
|
||||
(e: any, a: any) => e.includes(a),
|
||||
),
|
||||
exp(
|
||||
"$notin",
|
||||
(v: (string | number)[]) => Array.isArray(v),
|
||||
(e: any, a: any) => !e.includes(a)
|
||||
(e: any, a: any) => !e.includes(a),
|
||||
),
|
||||
exp(
|
||||
"$gt",
|
||||
(v: number) => typeof v === "number",
|
||||
(e: any, a: any) => a > e
|
||||
(e: any, a: any) => a > e,
|
||||
),
|
||||
exp(
|
||||
"$gte",
|
||||
(v: number) => typeof v === "number",
|
||||
(e: any, a: any) => a >= e
|
||||
(e: any, a: any) => a >= e,
|
||||
),
|
||||
exp(
|
||||
"$lt",
|
||||
(v: number) => typeof v === "number",
|
||||
(e: any, a: any) => a < e
|
||||
(e: any, a: any) => a < e,
|
||||
),
|
||||
exp(
|
||||
"$lte",
|
||||
(v: number) => typeof v === "number",
|
||||
(e: any, a: any) => a <= e
|
||||
(e: any, a: any) => a <= e,
|
||||
),
|
||||
exp(
|
||||
"$between",
|
||||
(v: [number, number]) =>
|
||||
Array.isArray(v) && v.length === 2 && v.every((n) => typeof n === "number"),
|
||||
(e: any, a: any) => e[0] <= a && a <= e[1]
|
||||
)
|
||||
(e: any, a: any) => e[0] <= a && a <= e[1],
|
||||
),
|
||||
];
|
||||
|
||||
export type ObjectQuery = FilterQuery<typeof expressions>;
|
||||
|
||||
@@ -13,7 +13,7 @@ export class Expression<Key, Expect = unknown, CTX = any> {
|
||||
constructor(
|
||||
public key: Key,
|
||||
public valid: (v: Expect) => boolean,
|
||||
public validate: (e: any, a: any, ctx: CTX) => any
|
||||
public validate: (e: any, a: any, ctx: CTX) => any,
|
||||
) {}
|
||||
}
|
||||
export type TExpression<Key, Expect = unknown, CTX = any> = Expression<Key, Expect, CTX>;
|
||||
@@ -21,7 +21,7 @@ export type TExpression<Key, Expect = unknown, CTX = any> = Expression<Key, Expe
|
||||
export function exp<const Key, const Expect, CTX = any>(
|
||||
key: Key,
|
||||
valid: (v: Expect) => boolean,
|
||||
validate: (e: Expect, a: unknown, ctx: CTX) => any
|
||||
validate: (e: Expect, a: unknown, ctx: CTX) => any,
|
||||
): Expression<Key, Expect, CTX> {
|
||||
return new Expression(key, valid, validate);
|
||||
}
|
||||
@@ -38,7 +38,7 @@ type ExpressionCondition<Exps extends Expressions> = {
|
||||
|
||||
function getExpression<Exps extends Expressions>(
|
||||
expressions: Exps,
|
||||
key: string
|
||||
key: string,
|
||||
): Expression<any, any> {
|
||||
const exp = expressions.find((e) => e.key === key);
|
||||
if (!exp) throw new Error(`Expression does not exist: "${key}"`);
|
||||
@@ -61,7 +61,7 @@ export type FilterQuery<Exps extends Expressions> =
|
||||
function _convert<Exps extends Expressions>(
|
||||
$query: FilterQuery<Exps>,
|
||||
expressions: Exps,
|
||||
path: string[] = []
|
||||
path: string[] = [],
|
||||
): FilterQuery<Exps> {
|
||||
//console.log("-----------------");
|
||||
const ExpressionConditionKeys = expressions.map((e) => e.key);
|
||||
@@ -98,7 +98,7 @@ function _convert<Exps extends Expressions>(
|
||||
} else if (typeof value === "object") {
|
||||
// when object is given, check if all keys are expressions
|
||||
const invalid = Object.keys(value).filter(
|
||||
(f) => !ExpressionConditionKeys.includes(f as any)
|
||||
(f) => !ExpressionConditionKeys.includes(f as any),
|
||||
);
|
||||
if (invalid.length === 0) {
|
||||
newQuery[key] = {};
|
||||
@@ -109,7 +109,7 @@ function _convert<Exps extends Expressions>(
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
`Invalid key(s) at "${key}": ${invalid.join(", ")}. Expected expressions.`
|
||||
`Invalid key(s) at "${key}": ${invalid.join(", ")}. Expected expressions.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -128,7 +128,7 @@ type BuildOptions = {
|
||||
function _build<Exps extends Expressions>(
|
||||
_query: FilterQuery<Exps>,
|
||||
expressions: Exps,
|
||||
options: BuildOptions
|
||||
options: BuildOptions,
|
||||
): ValidationResults {
|
||||
const $query = options.convert ? _convert<Exps>(_query, expressions) : _query;
|
||||
|
||||
@@ -137,7 +137,7 @@ function _build<Exps extends Expressions>(
|
||||
const result: ValidationResults = {
|
||||
$and: [],
|
||||
$or: [],
|
||||
keys: new Set<string>()
|
||||
keys: new Set<string>(),
|
||||
};
|
||||
|
||||
const { $or, ...$and } = $query;
|
||||
@@ -187,7 +187,7 @@ function _build<Exps extends Expressions>(
|
||||
function _validate(results: ValidationResults): boolean {
|
||||
const matches: { $and?: boolean; $or?: boolean } = {
|
||||
$and: undefined,
|
||||
$or: undefined
|
||||
$or: undefined,
|
||||
};
|
||||
|
||||
matches.$and = results.$and.every((r) => Boolean(r));
|
||||
@@ -204,6 +204,6 @@ export function makeValidator<Exps extends Expressions>(expressions: Exps) {
|
||||
validate: (query: FilterQuery<Exps>, options: BuildOptions) => {
|
||||
const fns = _build(query, expressions, options);
|
||||
return _validate(fns);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ export type RegisterFn<Item> = (unknown: any) => Item;
|
||||
export class Registry<
|
||||
Item,
|
||||
Items extends Record<string, Item> = Record<string, Item>,
|
||||
Fn extends RegisterFn<Item> = RegisterFn<Item>
|
||||
Fn extends RegisterFn<Item> = RegisterFn<Item>,
|
||||
> {
|
||||
private is_set: boolean = false;
|
||||
private items: Items = {} as Items;
|
||||
|
||||
@@ -5,7 +5,7 @@ export class Permission<Name extends string = string> {
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
name: this.name
|
||||
name: this.name,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ export type FlashMessageType = "error" | "warning" | "success" | "info";
|
||||
export function addFlashMessage(c: Context, message: string, type: FlashMessageType = "info") {
|
||||
if (c.req.header("Accept")?.includes("text/html")) {
|
||||
setCookie(c, flash_key, JSON.stringify({ type, message }), {
|
||||
path: "/"
|
||||
path: "/",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ function getCookieValue(name) {
|
||||
}
|
||||
|
||||
export function getFlashMessage(
|
||||
clear = true
|
||||
clear = true,
|
||||
): { type: FlashMessageType; message: string } | undefined {
|
||||
const flash = getCookieValue(flash_key);
|
||||
if (flash && clear) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { validator } from "hono/validator";
|
||||
|
||||
type Hook<T, E extends Env, P extends string> = (
|
||||
result: { success: true; data: T } | { success: false; errors: ValueError[] },
|
||||
c: Context<E, P>
|
||||
c: Context<E, P>,
|
||||
) => Response | Promise<Response> | void;
|
||||
|
||||
export function tbValidator<
|
||||
@@ -13,7 +13,7 @@ export function tbValidator<
|
||||
Target extends keyof ValidationTargets,
|
||||
E extends Env,
|
||||
P extends string,
|
||||
V extends { in: { [K in Target]: StaticDecode<T> }; out: { [K in Target]: StaticDecode<T> } }
|
||||
V extends { in: { [K in Target]: StaticDecode<T> }; out: { [K in Target]: StaticDecode<T> } },
|
||||
>(target: Target, schema: T, hook?: Hook<StaticDecode<T>, E, P>): MiddlewareHandler<E, P, V> {
|
||||
// Compile the provided schema once rather than per validation. This could be optimized further using a shared schema
|
||||
// compilation pool similar to the Fastify implementation.
|
||||
|
||||
@@ -14,7 +14,7 @@ export class SimpleRenderer {
|
||||
|
||||
constructor(
|
||||
private variables: Record<string, any> = {},
|
||||
private options: SimpleRendererOptions = {}
|
||||
private options: SimpleRendererOptions = {},
|
||||
) {}
|
||||
|
||||
another() {
|
||||
@@ -48,7 +48,7 @@ export class SimpleRenderer {
|
||||
return (await this.renderString(template)) as unknown as Given;
|
||||
} else if (Array.isArray(template)) {
|
||||
return (await Promise.all(
|
||||
template.map((item) => this.render(item))
|
||||
template.map((item) => this.render(item)),
|
||||
)) as unknown as Given;
|
||||
} else if (typeof template === "object") {
|
||||
return (await this.renderObject(template)) as unknown as Given;
|
||||
@@ -61,8 +61,8 @@ export class SimpleRenderer {
|
||||
kind: e.token.kind,
|
||||
input: e.token.input,
|
||||
begin: e.token.begin,
|
||||
end: e.token.end
|
||||
}
|
||||
end: e.token.end,
|
||||
},
|
||||
};
|
||||
|
||||
throw new BkndError(e.message, details, "liquid");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface Serializable<Class, Json extends object = object> {
|
||||
toJSON(): Json;
|
||||
fromJSON(json: Json): Class;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export const hash = {
|
||||
sha256: async (input: string, salt?: string, pepper?: string) =>
|
||||
digest("SHA-256", input, salt, pepper),
|
||||
sha1: async (input: string, salt?: string, pepper?: string) =>
|
||||
digest("SHA-1", input, salt, pepper)
|
||||
digest("SHA-1", input, salt, pepper),
|
||||
};
|
||||
|
||||
export async function checksum(s: any) {
|
||||
|
||||
@@ -11,4 +11,21 @@ declare module "dayjs" {
|
||||
|
||||
dayjs.extend(weekOfYear);
|
||||
|
||||
export function datetimeStringLocal(dateOrString?: string | Date | undefined): string {
|
||||
return dayjs(dateOrString).format("YYYY-MM-DD HH:mm:ss");
|
||||
}
|
||||
|
||||
export function datetimeStringUTC(dateOrString?: string | Date | undefined): string {
|
||||
const date = dateOrString ? new Date(dateOrString) : new Date();
|
||||
return date.toISOString().replace("T", " ").split(".")[0]!;
|
||||
}
|
||||
|
||||
export function getTimezoneOffset(): number {
|
||||
return new Date().getTimezoneOffset();
|
||||
}
|
||||
|
||||
export function getTimezone(): string {
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
}
|
||||
|
||||
export { dayjs };
|
||||
|
||||
@@ -14,7 +14,7 @@ export function isObject(value: unknown): value is Record<string, unknown> {
|
||||
|
||||
export function omitKeys<T extends object, K extends keyof T>(
|
||||
obj: T,
|
||||
keys_: readonly K[]
|
||||
keys_: readonly K[],
|
||||
): Omit<T, Extract<K, keyof T>> {
|
||||
const keys = new Set(keys_);
|
||||
const result = {} as Omit<T, Extract<K, keyof T>>;
|
||||
@@ -47,7 +47,7 @@ export function keepChanged<T extends object>(origin: T, updated: T): Partial<T>
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Partial<T>
|
||||
{} as Partial<T>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,13 +66,13 @@ export function objectKeysPascalToKebab(obj: any, ignoreKeys: string[] = []): an
|
||||
acc[kebabKey] = objectKeysPascalToKebab(obj[key], ignoreKeys);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any>
|
||||
{} as Record<string, any>,
|
||||
);
|
||||
}
|
||||
|
||||
export function filterKeys<Object extends { [key: string]: any }>(
|
||||
obj: Object,
|
||||
keysToFilter: string[]
|
||||
keysToFilter: string[],
|
||||
): Object {
|
||||
const result = {} as Object;
|
||||
|
||||
@@ -92,7 +92,7 @@ export function filterKeys<Object extends { [key: string]: any }>(
|
||||
|
||||
export function transformObject<T extends Record<string, any>, U>(
|
||||
object: T,
|
||||
transform: (value: T[keyof T], key: keyof T) => U | undefined
|
||||
transform: (value: T[keyof T], key: keyof T) => U | undefined,
|
||||
): { [K in keyof T]: U } {
|
||||
return Object.entries(object).reduce(
|
||||
(acc, [key, value]) => {
|
||||
@@ -102,20 +102,20 @@ export function transformObject<T extends Record<string, any>, U>(
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as { [K in keyof T]: U }
|
||||
{} as { [K in keyof T]: U },
|
||||
);
|
||||
}
|
||||
export const objectTransform = transformObject;
|
||||
|
||||
export function objectEach<T extends Record<string, any>, U>(
|
||||
object: T,
|
||||
each: (value: T[keyof T], key: keyof T) => U
|
||||
each: (value: T[keyof T], key: keyof T) => U,
|
||||
): void {
|
||||
Object.entries(object).forEach(
|
||||
([key, value]) => {
|
||||
each(value, key);
|
||||
},
|
||||
{} as { [K in keyof T]: U }
|
||||
{} as { [K in keyof T]: U },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -291,7 +291,7 @@ export function isEqual(value1: any, value2: any): boolean {
|
||||
return "plainObject";
|
||||
if (value instanceof Function) return "function";
|
||||
throw new Error(
|
||||
`deeply comparing an instance of type ${value1.constructor?.name} is not supported.`
|
||||
`deeply comparing an instance of type ${value1.constructor?.name} is not supported.`,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -336,7 +336,7 @@ export function isEqual(value1: any, value2: any): boolean {
|
||||
export function getPath(
|
||||
object: object,
|
||||
_path: string | (string | number)[],
|
||||
defaultValue = undefined
|
||||
defaultValue = undefined,
|
||||
): any {
|
||||
const path = typeof _path === "string" ? _path.split(/[.\[\]\"]+/).filter((x) => x) : _path;
|
||||
|
||||
|
||||
@@ -161,11 +161,11 @@ const FILE_SIGNATURES: Record<string, string> = {
|
||||
FFF9: "audio/aac",
|
||||
"52494646????41564920": "audio/wav",
|
||||
"52494646????57415645": "audio/wave",
|
||||
"52494646????415550": "audio/aiff"
|
||||
"52494646????415550": "audio/aiff",
|
||||
};
|
||||
|
||||
async function detectMimeType(
|
||||
input: ReadableStream | ArrayBuffer | ArrayBufferView | string | Blob | File | null
|
||||
input: ReadableStream | ArrayBuffer | ArrayBufferView | string | Blob | File | null,
|
||||
): Promise<string | undefined> {
|
||||
if (!input) return;
|
||||
|
||||
@@ -202,7 +202,7 @@ async function detectMimeType(
|
||||
|
||||
export async function blobToFile(
|
||||
blob: Blob | File | unknown,
|
||||
overrides: FilePropertyBag & { name?: string } = {}
|
||||
overrides: FilePropertyBag & { name?: string } = {},
|
||||
): Promise<File> {
|
||||
if (isFile(blob)) return blob;
|
||||
if (!isBlob(blob)) throw new Error("Not a Blob");
|
||||
@@ -215,7 +215,7 @@ export async function blobToFile(
|
||||
|
||||
return new File([blob], name, {
|
||||
type: type || guess(name),
|
||||
lastModified: Date.now()
|
||||
lastModified: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -340,5 +340,5 @@ export const enum HttpStatus {
|
||||
INSUFFICIENT_STORAGE = 507,
|
||||
LOOP_DETECTED = 508,
|
||||
NOT_EXTENDED = 510,
|
||||
NETWORK_AUTHENTICATION_REQUIRED = 511
|
||||
NETWORK_AUTHENTICATION_REQUIRED = 511,
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export function getRuntimeKey(): string {
|
||||
const features = {
|
||||
// supports the redirect of not full qualified addresses
|
||||
// not supported in nextjs
|
||||
redirects_non_fq: true
|
||||
redirects_non_fq: true,
|
||||
};
|
||||
|
||||
export function runtimeSupports(feature: keyof typeof features) {
|
||||
|
||||
@@ -2,17 +2,17 @@ type ConsoleSeverity = "log" | "warn" | "error";
|
||||
const _oldConsoles = {
|
||||
log: console.log,
|
||||
warn: console.warn,
|
||||
error: console.error
|
||||
error: console.error,
|
||||
};
|
||||
|
||||
export async function withDisabledConsole<R>(
|
||||
fn: () => Promise<R>,
|
||||
severities: ConsoleSeverity[] = ["log", "warn", "error"]
|
||||
severities: ConsoleSeverity[] = ["log", "warn", "error"],
|
||||
): Promise<R> {
|
||||
const _oldConsoles = {
|
||||
log: console.log,
|
||||
warn: console.warn,
|
||||
error: console.error
|
||||
error: console.error,
|
||||
};
|
||||
disableConsoleLog(severities);
|
||||
const enable = () => {
|
||||
@@ -57,6 +57,6 @@ export function formatMemoryUsage() {
|
||||
rss: usage.rss / 1024 / 1024,
|
||||
heapUsed: usage.heapUsed / 1024 / 1024,
|
||||
external: usage.external / 1024 / 1024,
|
||||
arrayBuffers: usage.arrayBuffers / 1024 / 1024
|
||||
arrayBuffers: usage.arrayBuffers / 1024 / 1024,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ const IsSArray = (value: unknown): value is SArray =>
|
||||
!Type.ValueGuard.IsArray(value.items) &&
|
||||
Type.ValueGuard.IsObject(value.items);
|
||||
const IsSConst = (value: unknown): value is SConst =>
|
||||
// biome-ignore lint/complexity/useLiteralKeys: <explanation>
|
||||
Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsObject(value["const"]);
|
||||
const IsSString = (value: unknown): value is SString =>
|
||||
Type.ValueGuard.IsObject(value) && IsExact(value.type, "string");
|
||||
@@ -68,7 +69,7 @@ const IsSBoolean = (value: unknown): value is SBoolean =>
|
||||
const IsSNull = (value: unknown): value is SBoolean =>
|
||||
Type.ValueGuard.IsObject(value) && IsExact(value.type, "null");
|
||||
const IsSProperties = (value: unknown): value is SProperties => Type.ValueGuard.IsObject(value);
|
||||
// prettier-ignore
|
||||
// biome-ignore format: keep
|
||||
const IsSObject = (value: unknown): value is SObject => Type.ValueGuard.IsObject(value) && IsExact(value.type, 'object') && IsSProperties(value.properties) && (value.required === undefined || Type.ValueGuard.IsArray(value.required) && value.required.every((value: unknown) => Type.ValueGuard.IsString(value)))
|
||||
type SValue = string | number | boolean;
|
||||
type SEnum = Readonly<{ enum: readonly SValue[] }>;
|
||||
@@ -88,8 +89,9 @@ type SNull = Readonly<{ type: "null" }>;
|
||||
// ------------------------------------------------------------------
|
||||
// FromRest
|
||||
// ------------------------------------------------------------------
|
||||
// prettier-ignore
|
||||
// biome-ignore format: keep
|
||||
type TFromRest<T extends readonly unknown[], Acc extends Type.TSchema[] = []> = (
|
||||
// biome-ignore lint/complexity/noUselessTypeConstraint: <explanation>
|
||||
T extends readonly [infer L extends unknown, ...infer R extends unknown[]]
|
||||
? TFromSchema<L> extends infer S extends Type.TSchema
|
||||
? TFromRest<R, [...Acc, S]>
|
||||
@@ -102,7 +104,7 @@ function FromRest<T extends readonly unknown[]>(T: T): TFromRest<T> {
|
||||
// ------------------------------------------------------------------
|
||||
// FromEnumRest
|
||||
// ------------------------------------------------------------------
|
||||
// prettier-ignore
|
||||
// biome-ignore format: keep
|
||||
type TFromEnumRest<T extends readonly SValue[], Acc extends Type.TSchema[] = []> = (
|
||||
T extends readonly [infer L extends SValue, ...infer R extends SValue[]]
|
||||
? TFromEnumRest<R, [...Acc, Type.TLiteral<L>]>
|
||||
@@ -114,7 +116,7 @@ function FromEnumRest<T extends readonly SValue[]>(T: T): TFromEnumRest<T> {
|
||||
// ------------------------------------------------------------------
|
||||
// AllOf
|
||||
// ------------------------------------------------------------------
|
||||
// prettier-ignore
|
||||
// biome-ignore format: keep
|
||||
type TFromAllOf<T extends SAllOf> = (
|
||||
TFromRest<T['allOf']> extends infer Rest extends Type.TSchema[]
|
||||
? Type.TIntersectEvaluated<Rest>
|
||||
@@ -126,7 +128,7 @@ function FromAllOf<T extends SAllOf>(T: T): TFromAllOf<T> {
|
||||
// ------------------------------------------------------------------
|
||||
// AnyOf
|
||||
// ------------------------------------------------------------------
|
||||
// prettier-ignore
|
||||
// biome-ignore format: keep
|
||||
type TFromAnyOf<T extends SAnyOf> = (
|
||||
TFromRest<T['anyOf']> extends infer Rest extends Type.TSchema[]
|
||||
? Type.TUnionEvaluated<Rest>
|
||||
@@ -138,7 +140,7 @@ function FromAnyOf<T extends SAnyOf>(T: T): TFromAnyOf<T> {
|
||||
// ------------------------------------------------------------------
|
||||
// OneOf
|
||||
// ------------------------------------------------------------------
|
||||
// prettier-ignore
|
||||
// biome-ignore format: keep
|
||||
type TFromOneOf<T extends SOneOf> = (
|
||||
TFromRest<T['oneOf']> extends infer Rest extends Type.TSchema[]
|
||||
? Type.TUnionEvaluated<Rest>
|
||||
@@ -150,7 +152,7 @@ function FromOneOf<T extends SOneOf>(T: T): TFromOneOf<T> {
|
||||
// ------------------------------------------------------------------
|
||||
// Enum
|
||||
// ------------------------------------------------------------------
|
||||
// prettier-ignore
|
||||
// biome-ignore format: keep
|
||||
type TFromEnum<T extends SEnum> = (
|
||||
TFromEnumRest<T['enum']> extends infer Elements extends Type.TSchema[]
|
||||
? Type.TUnionEvaluated<Elements>
|
||||
@@ -162,33 +164,33 @@ function FromEnum<T extends SEnum>(T: T): TFromEnum<T> {
|
||||
// ------------------------------------------------------------------
|
||||
// Tuple
|
||||
// ------------------------------------------------------------------
|
||||
// prettier-ignore
|
||||
// biome-ignore format: keep
|
||||
type TFromTuple<T extends STuple> = (
|
||||
TFromRest<T['items']> extends infer Elements extends Type.TSchema[]
|
||||
? Type.TTuple<Elements>
|
||||
: Type.TTuple<[]>
|
||||
)
|
||||
// prettier-ignore
|
||||
// biome-ignore format: keep
|
||||
function FromTuple<T extends STuple>(T: T): TFromTuple<T> {
|
||||
return Type.Tuple(FromRest(T.items), T) as never
|
||||
}
|
||||
// ------------------------------------------------------------------
|
||||
// Array
|
||||
// ------------------------------------------------------------------
|
||||
// prettier-ignore
|
||||
// biome-ignore format: keep
|
||||
type TFromArray<T extends SArray> = (
|
||||
TFromSchema<T['items']> extends infer Items extends Type.TSchema
|
||||
? Type.TArray<Items>
|
||||
: Type.TArray<Type.TUnknown>
|
||||
)
|
||||
// prettier-ignore
|
||||
// biome-ignore format: keep
|
||||
function FromArray<T extends SArray>(T: T): TFromArray<T> {
|
||||
return Type.Array(FromSchema(T.items), T) as never
|
||||
}
|
||||
// ------------------------------------------------------------------
|
||||
// Const
|
||||
// ------------------------------------------------------------------
|
||||
// prettier-ignore
|
||||
// biome-ignore format: keep
|
||||
type TFromConst<T extends SConst> = (
|
||||
Type.Ensure<Type.TLiteral<T['const']>>
|
||||
)
|
||||
@@ -202,13 +204,13 @@ type TFromPropertiesIsOptional<
|
||||
K extends PropertyKey,
|
||||
R extends string | unknown,
|
||||
> = unknown extends R ? true : K extends R ? false : true;
|
||||
// prettier-ignore
|
||||
// biome-ignore format: keep
|
||||
type TFromProperties<T extends SProperties, R extends string | unknown> = Type.Evaluate<{
|
||||
-readonly [K in keyof T]: TFromPropertiesIsOptional<K, R> extends true
|
||||
? Type.TOptional<TFromSchema<T[K]>>
|
||||
: TFromSchema<T[K]>
|
||||
}>
|
||||
// prettier-ignore
|
||||
// biome-ignore format: keep
|
||||
type TFromObject<T extends SObject> = (
|
||||
TFromProperties<T['properties'], Exclude<T['required'], undefined>[number]> extends infer Properties extends Type.TProperties
|
||||
? Type.TObject<Properties>
|
||||
@@ -217,11 +219,11 @@ type TFromObject<T extends SObject> = (
|
||||
function FromObject<T extends SObject>(T: T): TFromObject<T> {
|
||||
const properties = globalThis.Object.getOwnPropertyNames(T.properties).reduce((Acc, K) => {
|
||||
return {
|
||||
// biome-ignore lint/performance/noAccumulatingSpread: <explanation>
|
||||
...Acc,
|
||||
[K]:
|
||||
T.required && T.required.includes(K)
|
||||
? FromSchema(T.properties[K])
|
||||
: Type.Optional(FromSchema(T.properties[K])),
|
||||
[K]: T.required?.includes(K)
|
||||
? FromSchema(T.properties[K])
|
||||
: Type.Optional(FromSchema(T.properties[K])),
|
||||
};
|
||||
}, {} as Type.TProperties);
|
||||
return Type.Object(properties, T) as never;
|
||||
@@ -229,7 +231,7 @@ function FromObject<T extends SObject>(T: T): TFromObject<T> {
|
||||
// ------------------------------------------------------------------
|
||||
// FromSchema
|
||||
// ------------------------------------------------------------------
|
||||
// prettier-ignore
|
||||
// biome-ignore format: keep
|
||||
export type TFromSchema<T> = (
|
||||
T extends SAllOf ? TFromAllOf<T> :
|
||||
T extends SAnyOf ? TFromAnyOf<T> :
|
||||
@@ -248,7 +250,7 @@ export type TFromSchema<T> = (
|
||||
)
|
||||
/** Parses a TypeBox type from raw JsonSchema */
|
||||
export function FromSchema<T>(T: T): TFromSchema<T> {
|
||||
// prettier-ignore
|
||||
// biome-ignore format: keep
|
||||
return (
|
||||
IsSAllOf(T) ? FromAllOf(T) :
|
||||
IsSAnyOf(T) ? FromAnyOf(T) :
|
||||
|
||||
@@ -12,13 +12,13 @@ import {
|
||||
type TSchema,
|
||||
type TString,
|
||||
Type,
|
||||
TypeRegistry
|
||||
TypeRegistry,
|
||||
} from "@sinclair/typebox";
|
||||
import {
|
||||
DefaultErrorFunction,
|
||||
Errors,
|
||||
SetErrorFunction,
|
||||
type ValueErrorIterator
|
||||
type ValueErrorIterator,
|
||||
} from "@sinclair/typebox/errors";
|
||||
import { Check, Default, Value, type ValueError } from "@sinclair/typebox/value";
|
||||
|
||||
@@ -45,7 +45,7 @@ export class TypeInvalidError extends Error {
|
||||
constructor(
|
||||
public schema: TSchema,
|
||||
public data: unknown,
|
||||
message?: string
|
||||
message?: string,
|
||||
) {
|
||||
//console.warn("errored schema", JSON.stringify(schema, null, 2));
|
||||
super(message ?? `Invalid: ${JSON.stringify(data)}`);
|
||||
@@ -66,7 +66,7 @@ export class TypeInvalidError extends Error {
|
||||
message: this.message,
|
||||
schema: this.schema,
|
||||
data: this.data,
|
||||
errors: this.errors
|
||||
errors: this.errors,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -95,7 +95,7 @@ export function mark(obj: any, validated = true) {
|
||||
export function parse<Schema extends TSchema = TSchema>(
|
||||
schema: Schema,
|
||||
data: RecursivePartial<Static<Schema>>,
|
||||
options?: ParseOptions
|
||||
options?: ParseOptions,
|
||||
): Static<Schema> {
|
||||
if (!options?.forceParse && typeof data === "object" && validationSymbol in data) {
|
||||
if (options?.useDefaults === false) {
|
||||
@@ -124,7 +124,7 @@ export function parse<Schema extends TSchema = TSchema>(
|
||||
|
||||
export function parseDecode<Schema extends TSchema = TSchema>(
|
||||
schema: Schema,
|
||||
data: RecursivePartial<StaticDecode<Schema>>
|
||||
data: RecursivePartial<StaticDecode<Schema>>,
|
||||
): StaticDecode<Schema> {
|
||||
//console.log("parseDecode", schema, data);
|
||||
const parsed = Default(schema, data);
|
||||
@@ -140,7 +140,7 @@ export function parseDecode<Schema extends TSchema = TSchema>(
|
||||
export function strictParse<Schema extends TSchema = TSchema>(
|
||||
schema: Schema,
|
||||
data: Static<Schema>,
|
||||
options?: ParseOptions
|
||||
options?: ParseOptions,
|
||||
): Static<Schema> {
|
||||
return parse(schema, data as any, options);
|
||||
}
|
||||
@@ -157,7 +157,7 @@ export const StringEnum = <const T extends readonly string[]>(values: T, options
|
||||
[Kind]: "StringEnum",
|
||||
type: "string",
|
||||
enum: values,
|
||||
...options
|
||||
...options,
|
||||
});
|
||||
|
||||
// key value record compatible with RJSF and typebox inference
|
||||
@@ -175,7 +175,7 @@ export const Const = <T extends TLiteralValue = TLiteralValue>(value: T, options
|
||||
export const StringIdentifier = Type.String({
|
||||
pattern: "^[a-zA-Z_][a-zA-Z0-9_]*$",
|
||||
minLength: 2,
|
||||
maxLength: 150
|
||||
maxLength: 150,
|
||||
});
|
||||
|
||||
SetErrorFunction((error) => {
|
||||
@@ -202,5 +202,5 @@ export {
|
||||
Value,
|
||||
Default,
|
||||
Errors,
|
||||
Check
|
||||
Check,
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
EntityIndex,
|
||||
type EntityManager,
|
||||
constructEntity,
|
||||
constructRelation
|
||||
constructRelation,
|
||||
} from "data";
|
||||
import { Module } from "modules/Module";
|
||||
import { DataController } from "./api/DataController";
|
||||
@@ -16,7 +16,7 @@ export class AppData extends Module<typeof dataConfigSchema> {
|
||||
const {
|
||||
entities: _entities = {},
|
||||
relations: _relations = {},
|
||||
indices: _indices = {}
|
||||
indices: _indices = {},
|
||||
} = this.config;
|
||||
|
||||
this.ctx.logger.context("AppData").log("building with entities", Object.keys(_entities));
|
||||
@@ -33,7 +33,7 @@ export class AppData extends Module<typeof dataConfigSchema> {
|
||||
};
|
||||
|
||||
const relations = transformObject(_relations, (relation) =>
|
||||
constructRelation(relation, _entity)
|
||||
constructRelation(relation, _entity),
|
||||
);
|
||||
|
||||
const indices = transformObject(_indices, (index, name) => {
|
||||
@@ -56,7 +56,7 @@ export class AppData extends Module<typeof dataConfigSchema> {
|
||||
|
||||
this.ctx.server.route(
|
||||
this.basepath,
|
||||
new DataController(this.ctx, this.config).getController()
|
||||
new DataController(this.ctx, this.config).getController(),
|
||||
);
|
||||
this.ctx.guard.registerPermissions(Object.values(DataPermissions));
|
||||
|
||||
@@ -84,7 +84,7 @@ export class AppData extends Module<typeof dataConfigSchema> {
|
||||
override toJSON(secrets?: boolean): AppDataConfig {
|
||||
return {
|
||||
...this.config,
|
||||
...this.em.toJSON()
|
||||
...this.em.toJSON(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,25 +13,25 @@ export class DataApi extends ModuleApi<DataApiOptions> {
|
||||
basepath: "/api/data",
|
||||
queryLengthLimit: 1000,
|
||||
defaultQuery: {
|
||||
limit: 10
|
||||
}
|
||||
limit: 10,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
readOne<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
|
||||
entity: E,
|
||||
id: PrimaryFieldType,
|
||||
query: Omit<RepoQueryIn, "where" | "limit" | "offset"> = {}
|
||||
query: Omit<RepoQueryIn, "where" | "limit" | "offset"> = {},
|
||||
) {
|
||||
return this.get<Pick<RepositoryResponse<Data>, "meta" | "data">>(
|
||||
["entity", entity as any, id],
|
||||
query
|
||||
query,
|
||||
);
|
||||
}
|
||||
|
||||
readMany<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
|
||||
entity: E,
|
||||
query: RepoQueryIn = {}
|
||||
query: RepoQueryIn = {},
|
||||
) {
|
||||
type T = Pick<RepositoryResponse<Data[]>, "meta" | "data">;
|
||||
|
||||
@@ -48,17 +48,17 @@ export class DataApi extends ModuleApi<DataApiOptions> {
|
||||
readManyByReference<
|
||||
E extends keyof DB | string,
|
||||
R extends keyof DB | string,
|
||||
Data = R extends keyof DB ? DB[R] : EntityData
|
||||
Data = R extends keyof DB ? DB[R] : EntityData,
|
||||
>(entity: E, id: PrimaryFieldType, reference: R, query: RepoQueryIn = {}) {
|
||||
return this.get<Pick<RepositoryResponse<Data[]>, "meta" | "data">>(
|
||||
["entity", entity as any, id, reference],
|
||||
query ?? this.options.defaultQuery
|
||||
query ?? this.options.defaultQuery,
|
||||
);
|
||||
}
|
||||
|
||||
createOne<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
|
||||
entity: E,
|
||||
input: Omit<Data, "id">
|
||||
input: Omit<Data, "id">,
|
||||
) {
|
||||
return this.post<RepositoryResponse<Data>>(["entity", entity as any], input);
|
||||
}
|
||||
@@ -66,14 +66,14 @@ export class DataApi extends ModuleApi<DataApiOptions> {
|
||||
updateOne<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
|
||||
entity: E,
|
||||
id: PrimaryFieldType,
|
||||
input: Partial<Omit<Data, "id">>
|
||||
input: Partial<Omit<Data, "id">>,
|
||||
) {
|
||||
return this.patch<RepositoryResponse<Data>>(["entity", entity as any, id], input);
|
||||
}
|
||||
|
||||
deleteOne<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
|
||||
entity: E,
|
||||
id: PrimaryFieldType
|
||||
id: PrimaryFieldType,
|
||||
) {
|
||||
return this.delete<RepositoryResponse<Data>>(["entity", entity as any, id]);
|
||||
}
|
||||
@@ -81,7 +81,7 @@ export class DataApi extends ModuleApi<DataApiOptions> {
|
||||
count<E extends keyof DB | string>(entity: E, where: RepoQueryIn["where"] = {}) {
|
||||
return this.post<RepositoryResponse<{ entity: E; count: number }>>(
|
||||
["entity", entity as any, "fn", "count"],
|
||||
where
|
||||
where,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
type MutatorResponse,
|
||||
type RepoQuery,
|
||||
type RepositoryResponse,
|
||||
querySchema
|
||||
querySchema,
|
||||
} from "data";
|
||||
import type { Handler } from "hono/types";
|
||||
import type { ModuleBuildContext } from "modules";
|
||||
@@ -18,7 +18,7 @@ import type { AppDataConfig } from "../data-schema";
|
||||
export class DataController extends Controller {
|
||||
constructor(
|
||||
private readonly ctx: ModuleBuildContext,
|
||||
private readonly config: AppDataConfig
|
||||
private readonly config: AppDataConfig,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -32,7 +32,7 @@ export class DataController extends Controller {
|
||||
}
|
||||
|
||||
repoResult<T extends RepositoryResponse<any> = RepositoryResponse>(
|
||||
res: T
|
||||
res: T,
|
||||
): Pick<T, "meta" | "data"> {
|
||||
let meta: Partial<RepositoryResponse["meta"]> = {};
|
||||
|
||||
@@ -48,7 +48,7 @@ export class DataController extends Controller {
|
||||
//return objectCleanEmpty(template) as any;
|
||||
// filter empty
|
||||
return Object.fromEntries(
|
||||
Object.entries(template).filter(([_, v]) => typeof v !== "undefined" && v !== null)
|
||||
Object.entries(template).filter(([_, v]) => typeof v !== "undefined" && v !== null),
|
||||
) as any;
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ export class DataController extends Controller {
|
||||
handler("data info", (c) => {
|
||||
// sample implementation
|
||||
return c.json(this.em.toJSON());
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
// sync endpoint
|
||||
@@ -103,7 +103,7 @@ export class DataController extends Controller {
|
||||
//console.log("tables", tables);
|
||||
const changes = await this.em.schema().sync({
|
||||
force,
|
||||
drop
|
||||
drop,
|
||||
});
|
||||
return c.json({ tables: tables.map((t) => t.name), changes });
|
||||
});
|
||||
@@ -111,56 +111,56 @@ export class DataController extends Controller {
|
||||
/**
|
||||
* Schema endpoints
|
||||
*/
|
||||
hono
|
||||
// read entity schema
|
||||
.get("/schema.json", permission(DataPermissions.entityRead), async (c) => {
|
||||
const $id = `${this.config.basepath}/schema.json`;
|
||||
const schemas = Object.fromEntries(
|
||||
this.em.entities.map((e) => [
|
||||
e.name,
|
||||
{
|
||||
$ref: `${this.config.basepath}/schemas/${e.name}`
|
||||
}
|
||||
])
|
||||
);
|
||||
return c.json({
|
||||
$schema: "https://json-schema.org/draft/2020-12/schema",
|
||||
$id,
|
||||
properties: schemas
|
||||
});
|
||||
})
|
||||
// read schema
|
||||
.get(
|
||||
"/schemas/:entity/:context?",
|
||||
permission(DataPermissions.entityRead),
|
||||
tb(
|
||||
"param",
|
||||
Type.Object({
|
||||
entity: Type.String(),
|
||||
context: Type.Optional(StringEnum(["create", "update"]))
|
||||
})
|
||||
),
|
||||
async (c) => {
|
||||
//console.log("request", c.req.raw);
|
||||
const { entity, context } = c.req.param();
|
||||
if (!this.entityExists(entity)) {
|
||||
console.warn("not found:", entity, definedEntities);
|
||||
return c.notFound();
|
||||
}
|
||||
const _entity = this.em.entity(entity);
|
||||
const schema = _entity.toSchema({ context } as any);
|
||||
const url = new URL(c.req.url);
|
||||
const base = `${url.origin}${this.config.basepath}`;
|
||||
const $id = `${this.config.basepath}/schemas/${entity}`;
|
||||
return c.json({
|
||||
$schema: `${base}/schema.json`,
|
||||
$id,
|
||||
title: _entity.label,
|
||||
$comment: _entity.config.description,
|
||||
...schema
|
||||
});
|
||||
}
|
||||
// read entity schema
|
||||
hono.get("/schema.json", permission(DataPermissions.entityRead), async (c) => {
|
||||
const $id = `${this.config.basepath}/schema.json`;
|
||||
const schemas = Object.fromEntries(
|
||||
this.em.entities.map((e) => [
|
||||
e.name,
|
||||
{
|
||||
$ref: `${this.config.basepath}/schemas/${e.name}`,
|
||||
},
|
||||
]),
|
||||
);
|
||||
return c.json({
|
||||
$schema: "https://json-schema.org/draft/2020-12/schema",
|
||||
$id,
|
||||
properties: schemas,
|
||||
});
|
||||
});
|
||||
|
||||
// read schema
|
||||
hono.get(
|
||||
"/schemas/:entity/:context?",
|
||||
permission(DataPermissions.entityRead),
|
||||
tb(
|
||||
"param",
|
||||
Type.Object({
|
||||
entity: Type.String(),
|
||||
context: Type.Optional(StringEnum(["create", "update"])),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
//console.log("request", c.req.raw);
|
||||
const { entity, context } = c.req.param();
|
||||
if (!this.entityExists(entity)) {
|
||||
console.warn("not found:", entity, definedEntities);
|
||||
return this.notFound(c);
|
||||
}
|
||||
const _entity = this.em.entity(entity);
|
||||
const schema = _entity.toSchema({ context } as any);
|
||||
const url = new URL(c.req.url);
|
||||
const base = `${url.origin}${this.config.basepath}`;
|
||||
const $id = `${this.config.basepath}/schemas/${entity}`;
|
||||
return c.json({
|
||||
$schema: `${base}/schema.json`,
|
||||
$id,
|
||||
title: _entity.label,
|
||||
$comment: _entity.config.description,
|
||||
...schema,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// entity endpoints
|
||||
hono.route("/entity", this.getEntityRoutes());
|
||||
@@ -171,14 +171,14 @@ export class DataController extends Controller {
|
||||
hono.get("/info/:entity", async (c) => {
|
||||
const { entity } = c.req.param();
|
||||
if (!this.entityExists(entity)) {
|
||||
return c.notFound();
|
||||
return this.notFound(c);
|
||||
}
|
||||
const _entity = this.em.entity(entity);
|
||||
const fields = _entity.fields.map((f) => f.name);
|
||||
const $rels = (r: any) =>
|
||||
r.map((r: any) => ({
|
||||
entity: r.other(_entity).entity.name,
|
||||
ref: r.other(_entity).reference
|
||||
ref: r.other(_entity).reference,
|
||||
}));
|
||||
|
||||
return c.json({
|
||||
@@ -188,8 +188,8 @@ export class DataController extends Controller {
|
||||
all: $rels(this.em.relations.relationsOf(_entity)),
|
||||
listable: $rels(this.em.relations.listableRelationsOf(_entity)),
|
||||
source: $rels(this.em.relations.sourceRelationsOf(_entity)),
|
||||
target: $rels(this.em.relations.targetRelationsOf(_entity))
|
||||
}
|
||||
target: $rels(this.em.relations.targetRelationsOf(_entity)),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -208,203 +208,230 @@ export class DataController extends Controller {
|
||||
/**
|
||||
* Function endpoints
|
||||
*/
|
||||
hono
|
||||
// fn: count
|
||||
.post(
|
||||
"/:entity/fn/count",
|
||||
permission(DataPermissions.entityRead),
|
||||
tb("param", Type.Object({ entity: Type.String() })),
|
||||
async (c) => {
|
||||
const { entity } = c.req.valid("param");
|
||||
if (!this.entityExists(entity)) {
|
||||
return c.notFound();
|
||||
}
|
||||
|
||||
const where = c.req.json() as any;
|
||||
const result = await this.em.repository(entity).count(where);
|
||||
return c.json({ entity, count: result.count });
|
||||
// fn: count
|
||||
hono.post(
|
||||
"/:entity/fn/count",
|
||||
permission(DataPermissions.entityRead),
|
||||
tb("param", Type.Object({ entity: Type.String() })),
|
||||
async (c) => {
|
||||
const { entity } = c.req.valid("param");
|
||||
if (!this.entityExists(entity)) {
|
||||
return this.notFound(c);
|
||||
}
|
||||
)
|
||||
// fn: exists
|
||||
.post(
|
||||
"/:entity/fn/exists",
|
||||
permission(DataPermissions.entityRead),
|
||||
tb("param", Type.Object({ entity: Type.String() })),
|
||||
async (c) => {
|
||||
const { entity } = c.req.valid("param");
|
||||
if (!this.entityExists(entity)) {
|
||||
return c.notFound();
|
||||
}
|
||||
|
||||
const where = c.req.json() as any;
|
||||
const result = await this.em.repository(entity).exists(where);
|
||||
return c.json({ entity, exists: result.exists });
|
||||
const where = (await c.req.json()) as any;
|
||||
const result = await this.em.repository(entity).count(where);
|
||||
return c.json({ entity, count: result.count });
|
||||
},
|
||||
);
|
||||
|
||||
// fn: exists
|
||||
hono.post(
|
||||
"/:entity/fn/exists",
|
||||
permission(DataPermissions.entityRead),
|
||||
tb("param", Type.Object({ entity: Type.String() })),
|
||||
async (c) => {
|
||||
const { entity } = c.req.valid("param");
|
||||
if (!this.entityExists(entity)) {
|
||||
return this.notFound(c);
|
||||
}
|
||||
);
|
||||
|
||||
const where = c.req.json() as any;
|
||||
const result = await this.em.repository(entity).exists(where);
|
||||
return c.json({ entity, exists: result.exists });
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Read endpoints
|
||||
*/
|
||||
hono
|
||||
// read many
|
||||
.get(
|
||||
"/:entity",
|
||||
permission(DataPermissions.entityRead),
|
||||
tb("param", Type.Object({ entity: Type.String() })),
|
||||
tb("query", querySchema),
|
||||
async (c) => {
|
||||
//console.log("request", c.req.raw);
|
||||
const { entity } = c.req.param();
|
||||
if (!this.entityExists(entity)) {
|
||||
console.warn("not found:", entity, definedEntities);
|
||||
return c.notFound();
|
||||
}
|
||||
const options = c.req.valid("query") as RepoQuery;
|
||||
//console.log("before", this.ctx.emgr.Events);
|
||||
const result = await this.em.repository(entity).findMany(options);
|
||||
|
||||
return c.json(this.repoResult(result), { status: result.data ? 200 : 404 });
|
||||
// read many
|
||||
hono.get(
|
||||
"/:entity",
|
||||
permission(DataPermissions.entityRead),
|
||||
tb("param", Type.Object({ entity: Type.String() })),
|
||||
tb("query", querySchema),
|
||||
async (c) => {
|
||||
const { entity } = c.req.param();
|
||||
if (!this.entityExists(entity)) {
|
||||
console.warn("not found:", entity, definedEntities);
|
||||
return this.notFound(c);
|
||||
}
|
||||
)
|
||||
const options = c.req.valid("query") as RepoQuery;
|
||||
const result = await this.em.repository(entity).findMany(options);
|
||||
|
||||
// read one
|
||||
.get(
|
||||
"/:entity/:id",
|
||||
permission(DataPermissions.entityRead),
|
||||
tb(
|
||||
"param",
|
||||
Type.Object({
|
||||
entity: Type.String(),
|
||||
id: tbNumber
|
||||
})
|
||||
),
|
||||
tb("query", querySchema),
|
||||
async (c) => {
|
||||
const { entity, id } = c.req.param();
|
||||
if (!this.entityExists(entity)) {
|
||||
return c.notFound();
|
||||
}
|
||||
const options = c.req.valid("query") as RepoQuery;
|
||||
const result = await this.em.repository(entity).findId(Number(id), options);
|
||||
return c.json(this.repoResult(result), { status: result.data ? 200 : 404 });
|
||||
},
|
||||
);
|
||||
|
||||
return c.json(this.repoResult(result), { status: result.data ? 200 : 404 });
|
||||
// read one
|
||||
hono.get(
|
||||
"/:entity/:id",
|
||||
permission(DataPermissions.entityRead),
|
||||
tb(
|
||||
"param",
|
||||
Type.Object({
|
||||
entity: Type.String(),
|
||||
id: tbNumber,
|
||||
}),
|
||||
),
|
||||
tb("query", querySchema),
|
||||
async (c) => {
|
||||
const { entity, id } = c.req.param();
|
||||
if (!this.entityExists(entity)) {
|
||||
return this.notFound(c);
|
||||
}
|
||||
)
|
||||
// read many by reference
|
||||
.get(
|
||||
"/:entity/:id/:reference",
|
||||
permission(DataPermissions.entityRead),
|
||||
tb(
|
||||
"param",
|
||||
Type.Object({
|
||||
entity: Type.String(),
|
||||
id: tbNumber,
|
||||
reference: Type.String()
|
||||
})
|
||||
),
|
||||
tb("query", querySchema),
|
||||
async (c) => {
|
||||
const { entity, id, reference } = c.req.param();
|
||||
if (!this.entityExists(entity)) {
|
||||
return c.notFound();
|
||||
}
|
||||
const options = c.req.valid("query") as RepoQuery;
|
||||
const result = await this.em.repository(entity).findId(Number(id), options);
|
||||
|
||||
const options = c.req.valid("query") as RepoQuery;
|
||||
const result = await this.em
|
||||
.repository(entity)
|
||||
.findManyByReference(Number(id), reference, options);
|
||||
return c.json(this.repoResult(result), { status: result.data ? 200 : 404 });
|
||||
},
|
||||
);
|
||||
|
||||
return c.json(this.repoResult(result), { status: result.data ? 200 : 404 });
|
||||
// read many by reference
|
||||
hono.get(
|
||||
"/:entity/:id/:reference",
|
||||
permission(DataPermissions.entityRead),
|
||||
tb(
|
||||
"param",
|
||||
Type.Object({
|
||||
entity: Type.String(),
|
||||
id: tbNumber,
|
||||
reference: Type.String(),
|
||||
}),
|
||||
),
|
||||
tb("query", querySchema),
|
||||
async (c) => {
|
||||
const { entity, id, reference } = c.req.param();
|
||||
if (!this.entityExists(entity)) {
|
||||
return this.notFound(c);
|
||||
}
|
||||
)
|
||||
// func query
|
||||
.post(
|
||||
"/:entity/query",
|
||||
permission(DataPermissions.entityRead),
|
||||
tb("param", Type.Object({ entity: Type.String() })),
|
||||
tb("json", querySchema),
|
||||
async (c) => {
|
||||
const { entity } = c.req.param();
|
||||
if (!this.entityExists(entity)) {
|
||||
return c.notFound();
|
||||
}
|
||||
const options = (await c.req.valid("json")) as RepoQuery;
|
||||
//console.log("options", options);
|
||||
const result = await this.em.repository(entity).findMany(options);
|
||||
|
||||
return c.json(this.repoResult(result), { status: result.data ? 200 : 404 });
|
||||
const options = c.req.valid("query") as RepoQuery;
|
||||
const result = await this.em
|
||||
.repository(entity)
|
||||
.findManyByReference(Number(id), reference, options);
|
||||
|
||||
return c.json(this.repoResult(result), { status: result.data ? 200 : 404 });
|
||||
},
|
||||
);
|
||||
|
||||
// func query
|
||||
hono.post(
|
||||
"/:entity/query",
|
||||
permission(DataPermissions.entityRead),
|
||||
tb("param", Type.Object({ entity: Type.String() })),
|
||||
tb("json", querySchema),
|
||||
async (c) => {
|
||||
const { entity } = c.req.param();
|
||||
if (!this.entityExists(entity)) {
|
||||
return this.notFound(c);
|
||||
}
|
||||
);
|
||||
const options = (await c.req.valid("json")) as RepoQuery;
|
||||
//console.log("options", options);
|
||||
const result = await this.em.repository(entity).findMany(options);
|
||||
|
||||
return c.json(this.repoResult(result), { status: result.data ? 200 : 404 });
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Mutation endpoints
|
||||
*/
|
||||
// insert one
|
||||
hono
|
||||
.post(
|
||||
"/:entity",
|
||||
permission(DataPermissions.entityCreate),
|
||||
tb("param", Type.Object({ entity: Type.String() })),
|
||||
async (c) => {
|
||||
const { entity } = c.req.param();
|
||||
if (!this.entityExists(entity)) {
|
||||
return c.notFound();
|
||||
}
|
||||
const body = (await c.req.json()) as EntityData;
|
||||
const result = await this.em.mutator(entity).insertOne(body);
|
||||
|
||||
return c.json(this.mutatorResult(result), 201);
|
||||
hono.post(
|
||||
"/:entity",
|
||||
permission(DataPermissions.entityCreate),
|
||||
tb("param", Type.Object({ entity: Type.String() })),
|
||||
async (c) => {
|
||||
const { entity } = c.req.param();
|
||||
if (!this.entityExists(entity)) {
|
||||
return this.notFound(c);
|
||||
}
|
||||
)
|
||||
// update one
|
||||
.patch(
|
||||
"/:entity/:id",
|
||||
permission(DataPermissions.entityUpdate),
|
||||
tb("param", Type.Object({ entity: Type.String(), id: tbNumber })),
|
||||
async (c) => {
|
||||
const { entity, id } = c.req.param();
|
||||
if (!this.entityExists(entity)) {
|
||||
return c.notFound();
|
||||
}
|
||||
const body = (await c.req.json()) as EntityData;
|
||||
const result = await this.em.mutator(entity).updateOne(Number(id), body);
|
||||
const body = (await c.req.json()) as EntityData;
|
||||
const result = await this.em.mutator(entity).insertOne(body);
|
||||
|
||||
return c.json(this.mutatorResult(result));
|
||||
return c.json(this.mutatorResult(result), 201);
|
||||
},
|
||||
);
|
||||
|
||||
// update many
|
||||
hono.patch(
|
||||
"/:entity",
|
||||
permission(DataPermissions.entityUpdate),
|
||||
tb("param", Type.Object({ entity: Type.String() })),
|
||||
tb(
|
||||
"json",
|
||||
Type.Object({
|
||||
update: Type.Object({}),
|
||||
where: querySchema.properties.where,
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const { entity } = c.req.param();
|
||||
if (!this.entityExists(entity)) {
|
||||
return this.notFound(c);
|
||||
}
|
||||
)
|
||||
// delete one
|
||||
.delete(
|
||||
"/:entity/:id",
|
||||
permission(DataPermissions.entityDelete),
|
||||
tb("param", Type.Object({ entity: Type.String(), id: tbNumber })),
|
||||
async (c) => {
|
||||
const { entity, id } = c.req.param();
|
||||
if (!this.entityExists(entity)) {
|
||||
return c.notFound();
|
||||
}
|
||||
const result = await this.em.mutator(entity).deleteOne(Number(id));
|
||||
const { update, where } = (await c.req.json()) as {
|
||||
update: EntityData;
|
||||
where: RepoQuery["where"];
|
||||
};
|
||||
const result = await this.em.mutator(entity).updateWhere(update, where);
|
||||
|
||||
return c.json(this.mutatorResult(result));
|
||||
return c.json(this.mutatorResult(result));
|
||||
},
|
||||
);
|
||||
|
||||
// update one
|
||||
hono.patch(
|
||||
"/:entity/:id",
|
||||
permission(DataPermissions.entityUpdate),
|
||||
tb("param", Type.Object({ entity: Type.String(), id: tbNumber })),
|
||||
async (c) => {
|
||||
const { entity, id } = c.req.param();
|
||||
if (!this.entityExists(entity)) {
|
||||
return this.notFound(c);
|
||||
}
|
||||
)
|
||||
const body = (await c.req.json()) as EntityData;
|
||||
const result = await this.em.mutator(entity).updateOne(Number(id), body);
|
||||
|
||||
// delete many
|
||||
.delete(
|
||||
"/:entity",
|
||||
permission(DataPermissions.entityDelete),
|
||||
tb("param", Type.Object({ entity: Type.String() })),
|
||||
tb("json", querySchema.properties.where),
|
||||
async (c) => {
|
||||
const { entity } = c.req.param();
|
||||
if (!this.entityExists(entity)) {
|
||||
return c.notFound();
|
||||
}
|
||||
const where = c.req.valid("json") as RepoQuery["where"];
|
||||
const result = await this.em.mutator(entity).deleteWhere(where);
|
||||
return c.json(this.mutatorResult(result));
|
||||
},
|
||||
);
|
||||
|
||||
return c.json(this.mutatorResult(result));
|
||||
// delete one
|
||||
hono.delete(
|
||||
"/:entity/:id",
|
||||
permission(DataPermissions.entityDelete),
|
||||
tb("param", Type.Object({ entity: Type.String(), id: tbNumber })),
|
||||
async (c) => {
|
||||
const { entity, id } = c.req.param();
|
||||
if (!this.entityExists(entity)) {
|
||||
return this.notFound(c);
|
||||
}
|
||||
);
|
||||
const result = await this.em.mutator(entity).deleteOne(Number(id));
|
||||
|
||||
return c.json(this.mutatorResult(result));
|
||||
},
|
||||
);
|
||||
|
||||
// delete many
|
||||
hono.delete(
|
||||
"/:entity",
|
||||
permission(DataPermissions.entityDelete),
|
||||
tb("param", Type.Object({ entity: Type.String() })),
|
||||
tb("json", querySchema.properties.where),
|
||||
async (c) => {
|
||||
const { entity } = c.req.param();
|
||||
if (!this.entityExists(entity)) {
|
||||
return this.notFound(c);
|
||||
}
|
||||
const where = c.req.valid("json") as RepoQuery["where"];
|
||||
const result = await this.em.mutator(entity).deleteWhere(where);
|
||||
|
||||
return c.json(this.mutatorResult(result));
|
||||
},
|
||||
);
|
||||
|
||||
return hono;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
type SelectQueryBuilder,
|
||||
type SelectQueryNode,
|
||||
type Simplify,
|
||||
sql
|
||||
sql,
|
||||
} from "kysely";
|
||||
|
||||
export type QB = SelectQueryBuilder<any, any, any>;
|
||||
@@ -33,7 +33,7 @@ export type DbFunctions = {
|
||||
jsonObjectFrom<O>(expr: SelectQueryBuilderExpression<O>): RawBuilder<Simplify<O> | null>;
|
||||
jsonArrayFrom<O>(expr: SelectQueryBuilderExpression<O>): RawBuilder<Simplify<O>[]>;
|
||||
jsonBuildObject<O extends Record<string, Expression<unknown>>>(
|
||||
obj: O
|
||||
obj: O,
|
||||
): RawBuilder<
|
||||
Simplify<{
|
||||
[K in keyof O]: O[K] extends Expression<infer V> ? V : never;
|
||||
@@ -49,7 +49,7 @@ export abstract class Connection<DB = any> {
|
||||
constructor(
|
||||
kysely: Kysely<DB>,
|
||||
public fn: Partial<DbFunctions> = {},
|
||||
protected plugins: KyselyPlugin[] = []
|
||||
protected plugins: KyselyPlugin[] = [],
|
||||
) {
|
||||
this.kysely = kysely;
|
||||
this[CONN_SYMBOL] = true;
|
||||
@@ -73,6 +73,7 @@ export abstract class Connection<DB = any> {
|
||||
return false;
|
||||
}
|
||||
|
||||
// @todo: add if only first field is used in index
|
||||
supportsIndices(): boolean {
|
||||
return false;
|
||||
}
|
||||
@@ -83,7 +84,7 @@ export abstract class Connection<DB = any> {
|
||||
}
|
||||
|
||||
protected async batch<Queries extends QB[]>(
|
||||
queries: [...Queries]
|
||||
queries: [...Queries],
|
||||
): Promise<{
|
||||
[K in keyof Queries]: Awaited<ReturnType<Queries[K]["execute"]>>;
|
||||
}> {
|
||||
@@ -91,7 +92,7 @@ export abstract class Connection<DB = any> {
|
||||
}
|
||||
|
||||
async batchQuery<Queries extends QB[]>(
|
||||
queries: [...Queries]
|
||||
queries: [...Queries],
|
||||
): Promise<{
|
||||
[K in keyof Queries]: Awaited<ReturnType<Queries[K]["execute"]>>;
|
||||
}> {
|
||||
|
||||
@@ -15,7 +15,7 @@ export type LibSqlCredentials = Config & {
|
||||
class CustomLibsqlDialect extends LibsqlDialect {
|
||||
override createIntrospector(db: Kysely<any>): DatabaseIntrospector {
|
||||
return new SqliteIntrospector(db, {
|
||||
excludeTables: ["libsql_wasm_func_table"]
|
||||
excludeTables: ["libsql_wasm_func_table"],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ export class LibsqlConnection extends SqliteConnection {
|
||||
const kysely = new Kysely({
|
||||
// @ts-expect-error libsql has type issues
|
||||
dialect: new CustomLibsqlDialect({ client }),
|
||||
plugins
|
||||
plugins,
|
||||
});
|
||||
|
||||
super(kysely, {}, plugins);
|
||||
@@ -64,7 +64,7 @@ export class LibsqlConnection extends SqliteConnection {
|
||||
}
|
||||
|
||||
protected override async batch<Queries extends QB[]>(
|
||||
queries: [...Queries]
|
||||
queries: [...Queries],
|
||||
): Promise<{
|
||||
[K in keyof Queries]: Awaited<ReturnType<Queries[K]["execute"]>>;
|
||||
}> {
|
||||
@@ -72,7 +72,7 @@ export class LibsqlConnection extends SqliteConnection {
|
||||
const compiled = q.compile();
|
||||
return {
|
||||
sql: compiled.sql,
|
||||
args: compiled.parameters as any[]
|
||||
args: compiled.parameters as any[],
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ export class SqliteConnection extends Connection {
|
||||
...fn,
|
||||
jsonArrayFrom,
|
||||
jsonObjectFrom,
|
||||
jsonBuildObject
|
||||
jsonBuildObject,
|
||||
},
|
||||
plugins
|
||||
plugins,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
ExpressionBuilder,
|
||||
Kysely,
|
||||
SchemaMetadata,
|
||||
TableMetadata
|
||||
TableMetadata,
|
||||
} from "kysely";
|
||||
import { DEFAULT_MIGRATION_LOCK_TABLE, DEFAULT_MIGRATION_TABLE, sql } from "kysely";
|
||||
import type { ConnectionIntrospector, IndexMetadata } from "./Connection";
|
||||
@@ -62,7 +62,7 @@ export class SqliteIntrospector implements DatabaseIntrospector, ConnectionIntro
|
||||
seqno: number;
|
||||
cid: number;
|
||||
name: string;
|
||||
}>`pragma_index_info(${index})`.as("index_info")
|
||||
}>`pragma_index_info(${index})`.as("index_info"),
|
||||
)
|
||||
.select(["seqno", "cid", "name"])
|
||||
.orderBy("cid")
|
||||
@@ -74,8 +74,8 @@ export class SqliteIntrospector implements DatabaseIntrospector, ConnectionIntro
|
||||
isUnique: isUnique,
|
||||
columns: columns.map((col) => ({
|
||||
name: col.name,
|
||||
order: col.seqno
|
||||
}))
|
||||
order: col.seqno,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ export class SqliteIntrospector implements DatabaseIntrospector, ConnectionIntro
|
||||
}
|
||||
|
||||
async getTables(
|
||||
options: DatabaseMetadataOptions = { withInternalKyselyTables: false }
|
||||
options: DatabaseMetadataOptions = { withInternalKyselyTables: false },
|
||||
): Promise<TableMetadata[]> {
|
||||
let query = this.#db
|
||||
.selectFrom("sqlite_master")
|
||||
@@ -99,7 +99,7 @@ export class SqliteIntrospector implements DatabaseIntrospector, ConnectionIntro
|
||||
|
||||
if (!options.withInternalKyselyTables) {
|
||||
query = query.where(
|
||||
this.excludeTables([DEFAULT_MIGRATION_TABLE, DEFAULT_MIGRATION_LOCK_TABLE])
|
||||
this.excludeTables([DEFAULT_MIGRATION_TABLE, DEFAULT_MIGRATION_LOCK_TABLE]),
|
||||
);
|
||||
}
|
||||
if (this._excludeTables.length > 0) {
|
||||
@@ -112,7 +112,7 @@ export class SqliteIntrospector implements DatabaseIntrospector, ConnectionIntro
|
||||
|
||||
async getMetadata(options?: DatabaseMetadataOptions): Promise<DatabaseMetadata> {
|
||||
return {
|
||||
tables: await this.getTables(options)
|
||||
tables: await this.getTables(options),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ export class SqliteIntrospector implements DatabaseIntrospector, ConnectionIntro
|
||||
type: string;
|
||||
notnull: 0 | 1;
|
||||
dflt_value: any;
|
||||
}>`pragma_table_info(${table})`.as("table_info")
|
||||
}>`pragma_table_info(${table})`.as("table_info"),
|
||||
)
|
||||
.select(["name", "type", "notnull", "dflt_value"])
|
||||
.orderBy("cid")
|
||||
@@ -157,8 +157,8 @@ export class SqliteIntrospector implements DatabaseIntrospector, ConnectionIntro
|
||||
isNullable: !col.notnull,
|
||||
isAutoIncrementing: col.name === autoIncrementCol,
|
||||
hasDefaultValue: col.dflt_value != null,
|
||||
comment: undefined
|
||||
}))
|
||||
comment: undefined,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { SqliteIntrospector } from "./SqliteIntrospector";
|
||||
class CustomSqliteDialect extends SqliteDialect {
|
||||
override createIntrospector(db: Kysely<any>): DatabaseIntrospector {
|
||||
return new SqliteIntrospector(db, {
|
||||
excludeTables: ["test_table"]
|
||||
excludeTables: ["test_table"],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ export class SqliteLocalConnection extends SqliteConnection {
|
||||
const plugins = [new ParseJSONResultsPlugin()];
|
||||
const kysely = new Kysely({
|
||||
dialect: new CustomSqliteDialect({ database }),
|
||||
plugins
|
||||
plugins,
|
||||
//log: ["query"],
|
||||
});
|
||||
|
||||
|
||||
@@ -4,14 +4,14 @@ import {
|
||||
RelationClassMap,
|
||||
RelationFieldClassMap,
|
||||
entityConfigSchema,
|
||||
entityTypes
|
||||
entityTypes,
|
||||
} from "data";
|
||||
import { MediaField, mediaFieldConfigSchema } from "../media/MediaField";
|
||||
|
||||
export const FIELDS = {
|
||||
...FieldClassMap,
|
||||
...RelationFieldClassMap,
|
||||
media: { schema: mediaFieldConfigSchema, field: MediaField }
|
||||
media: { schema: mediaFieldConfigSchema, field: MediaField },
|
||||
};
|
||||
export type FieldType = keyof typeof FIELDS;
|
||||
|
||||
@@ -21,11 +21,11 @@ export const fieldsSchemaObject = objectTransform(FIELDS, (field, name) => {
|
||||
return Type.Object(
|
||||
{
|
||||
type: Type.Const(name, { default: name, readOnly: true }),
|
||||
config: Type.Optional(field.schema)
|
||||
config: Type.Optional(field.schema),
|
||||
},
|
||||
{
|
||||
title: name
|
||||
}
|
||||
title: name,
|
||||
},
|
||||
);
|
||||
});
|
||||
export const fieldsSchema = Type.Union(Object.values(fieldsSchemaObject));
|
||||
@@ -37,7 +37,7 @@ export const entitiesSchema = Type.Object({
|
||||
//name: Type.String(),
|
||||
type: Type.Optional(Type.String({ enum: entityTypes, default: "regular", readOnly: true })),
|
||||
config: Type.Optional(entityConfigSchema),
|
||||
fields: Type.Optional(entityFields)
|
||||
fields: Type.Optional(entityFields),
|
||||
});
|
||||
export type TAppDataEntity = Static<typeof entitiesSchema>;
|
||||
|
||||
@@ -47,11 +47,11 @@ export const relationsSchema = Object.entries(RelationClassMap).map(([name, rela
|
||||
type: Type.Const(name, { default: name, readOnly: true }),
|
||||
source: Type.String(),
|
||||
target: Type.String(),
|
||||
config: Type.Optional(relationClass.schema)
|
||||
config: Type.Optional(relationClass.schema),
|
||||
},
|
||||
{
|
||||
title: name
|
||||
}
|
||||
title: name,
|
||||
},
|
||||
);
|
||||
});
|
||||
export type TAppDataRelation = Static<(typeof relationsSchema)[number]>;
|
||||
@@ -61,11 +61,11 @@ export const indicesSchema = Type.Object(
|
||||
entity: Type.String(),
|
||||
fields: Type.Array(Type.String(), { minItems: 1 }),
|
||||
//name: Type.Optional(Type.String()),
|
||||
unique: Type.Optional(Type.Boolean({ default: false }))
|
||||
unique: Type.Optional(Type.Boolean({ default: false })),
|
||||
},
|
||||
{
|
||||
additionalProperties: false
|
||||
}
|
||||
additionalProperties: false,
|
||||
},
|
||||
);
|
||||
|
||||
export const dataConfigSchema = Type.Object(
|
||||
@@ -73,11 +73,11 @@ export const dataConfigSchema = Type.Object(
|
||||
basepath: Type.Optional(Type.String({ default: "/api/data" })),
|
||||
entities: Type.Optional(StringRecord(entitiesSchema, { default: {} })),
|
||||
relations: Type.Optional(StringRecord(Type.Union(relationsSchema), { default: {} })),
|
||||
indices: Type.Optional(StringRecord(indicesSchema, { default: {} }))
|
||||
indices: Type.Optional(StringRecord(indicesSchema, { default: {} })),
|
||||
},
|
||||
{
|
||||
additionalProperties: false
|
||||
}
|
||||
additionalProperties: false,
|
||||
},
|
||||
);
|
||||
|
||||
export type AppDataConfig = Static<typeof dataConfigSchema>;
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
Type,
|
||||
parse,
|
||||
snakeToPascalWithSpaces,
|
||||
transformObject
|
||||
transformObject,
|
||||
} from "core/utils";
|
||||
import { type Field, PrimaryField, type TActionContext, type TRenderContext } from "../fields";
|
||||
|
||||
@@ -16,11 +16,11 @@ export const entityConfigSchema = Type.Object(
|
||||
name_singular: Type.Optional(Type.String()),
|
||||
description: Type.Optional(Type.String()),
|
||||
sort_field: Type.Optional(Type.String({ default: config.data.default_primary_field })),
|
||||
sort_dir: Type.Optional(StringEnum(["asc", "desc"], { default: "asc" }))
|
||||
sort_dir: Type.Optional(StringEnum(["asc", "desc"], { default: "asc" })),
|
||||
},
|
||||
{
|
||||
additionalProperties: false
|
||||
}
|
||||
additionalProperties: false,
|
||||
},
|
||||
);
|
||||
|
||||
export type EntityConfig = Static<typeof entityConfigSchema>;
|
||||
@@ -42,7 +42,7 @@ export type TEntityType = (typeof entityTypes)[number];
|
||||
*/
|
||||
export class Entity<
|
||||
EntityName extends string = string,
|
||||
Fields extends Record<string, Field<any, any, any>> = Record<string, Field<any, any, any>>
|
||||
Fields extends Record<string, Field<any, any, any>> = Record<string, Field<any, any, any>>,
|
||||
> {
|
||||
readonly #_name!: EntityName;
|
||||
readonly #_fields!: Fields; // only for types
|
||||
@@ -99,14 +99,14 @@ export class Entity<
|
||||
getDefaultSort() {
|
||||
return {
|
||||
by: this.config.sort_field ?? "id",
|
||||
dir: this.config.sort_dir ?? "asc"
|
||||
dir: this.config.sort_dir ?? "asc",
|
||||
};
|
||||
}
|
||||
|
||||
getAliasedSelectFrom(
|
||||
select: string[],
|
||||
_alias?: string,
|
||||
context?: TActionContext | TRenderContext
|
||||
context?: TActionContext | TRenderContext,
|
||||
): string[] {
|
||||
const alias = _alias ?? this.name;
|
||||
return this.getFields()
|
||||
@@ -114,7 +114,7 @@ export class Entity<
|
||||
(field) =>
|
||||
!field.isVirtual() &&
|
||||
!field.isHidden(context ?? "read") &&
|
||||
select.includes(field.name)
|
||||
select.includes(field.name),
|
||||
)
|
||||
.map((field) => (alias ? `${alias}.${field.name} as ${field.name}` : field.name));
|
||||
}
|
||||
@@ -206,7 +206,7 @@ export class Entity<
|
||||
options?: {
|
||||
explain?: boolean;
|
||||
ignoreUnknown?: boolean;
|
||||
}
|
||||
},
|
||||
): boolean {
|
||||
if (typeof data !== "object") {
|
||||
if (options?.explain) {
|
||||
@@ -224,7 +224,7 @@ export class Entity<
|
||||
if (unknown_keys.length > 0) {
|
||||
if (options?.explain) {
|
||||
throw new Error(
|
||||
`Entity "${this.name}" data must only contain known keys, unknown: "${unknown_keys}"`
|
||||
`Entity "${this.name}" data must only contain known keys, unknown: "${unknown_keys}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -265,10 +265,10 @@ export class Entity<
|
||||
$comment: field.config.description,
|
||||
$field: field.type,
|
||||
readOnly: !fillable ? true : undefined,
|
||||
...field.toJsonSchema()
|
||||
...field.toJsonSchema(),
|
||||
};
|
||||
}),
|
||||
{ additionalProperties: false }
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
return options?.clean ? JSON.parse(JSON.stringify(schema)) : schema;
|
||||
@@ -280,7 +280,7 @@ export class Entity<
|
||||
type: this.type,
|
||||
//fields: transformObject(this.fields, (field) => field.toJSON()),
|
||||
fields: Object.fromEntries(this.fields.map((field) => [field.name, field.toJSON()])),
|
||||
config: this.config
|
||||
config: this.config,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,10 @@ import { Connection } from "../connection/Connection";
|
||||
import {
|
||||
EntityNotDefinedException,
|
||||
TransformRetrieveFailedException,
|
||||
UnableToConnectException
|
||||
UnableToConnectException,
|
||||
} from "../errors";
|
||||
import { MutatorEvents, RepositoryEvents } from "../events";
|
||||
import type { Field } from "../fields/Field";
|
||||
import type { EntityIndex } from "../fields/indices/EntityIndex";
|
||||
import type { EntityRelation } from "../relations";
|
||||
import { RelationAccessor } from "../relations/RelationAccessor";
|
||||
@@ -17,7 +18,7 @@ import { type EntityData, Mutator, Repository } from "./index";
|
||||
|
||||
type EntitySchema<
|
||||
TBD extends object = DefaultDB,
|
||||
E extends Entity | keyof TBD | string = string
|
||||
E extends Entity | keyof TBD | string = string,
|
||||
> = E extends Entity<infer Name>
|
||||
? Name extends keyof TBD
|
||||
? Name
|
||||
@@ -41,7 +42,7 @@ export class EntityManager<TBD extends object = DefaultDB> {
|
||||
connection: Connection,
|
||||
relations: EntityRelation[] = [],
|
||||
indices: EntityIndex[] = [],
|
||||
emgr?: EventManager<any>
|
||||
emgr?: EventManager<any>,
|
||||
) {
|
||||
// add entities & relations
|
||||
entities.forEach((entity) => this.addEntity(entity));
|
||||
@@ -113,11 +114,11 @@ export class EntityManager<TBD extends object = DefaultDB> {
|
||||
|
||||
entity<Silent extends true | false = false>(
|
||||
e: Entity | keyof TBD | string,
|
||||
silent?: Silent
|
||||
silent?: Silent,
|
||||
): Silent extends true ? Entity | undefined : Entity {
|
||||
// make sure to always retrieve by name
|
||||
const entity = this.entities.find((entity) =>
|
||||
e instanceof Entity ? entity.name === e.name : entity.name === e
|
||||
e instanceof Entity ? entity.name === e.name : entity.name === e,
|
||||
);
|
||||
|
||||
if (!entity) {
|
||||
@@ -142,6 +143,16 @@ export class EntityManager<TBD extends object = DefaultDB> {
|
||||
return this.indices.some((e) => e.name === name);
|
||||
}
|
||||
|
||||
// @todo: add to Connection whether first index is used or not
|
||||
getIndexedFields(_entity: Entity | string): Field[] {
|
||||
const entity = this.entity(_entity);
|
||||
const indices = this.getIndicesOf(entity);
|
||||
const rel_fields = entity.fields.filter((f) => f.type === "relation");
|
||||
// assuming only first
|
||||
const idx_fields = indices.map((index) => index.fields[0]);
|
||||
return [entity.getPrimaryField(), ...rel_fields, ...idx_fields].filter(Boolean) as Field[];
|
||||
}
|
||||
|
||||
addRelation(relation: EntityRelation) {
|
||||
// check if entities are registered
|
||||
if (!this.entity(relation.source.entity.name) || !this.entity(relation.target.entity.name)) {
|
||||
@@ -166,7 +177,7 @@ export class EntityManager<TBD extends object = DefaultDB> {
|
||||
if (found) {
|
||||
throw new Error(
|
||||
`Relation "${relation.type}" between "${relation.source.entity.name}" ` +
|
||||
`and "${relation.target.entity.name}" already exists`
|
||||
`and "${relation.target.entity.name}" already exists`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -195,7 +206,7 @@ export class EntityManager<TBD extends object = DefaultDB> {
|
||||
}
|
||||
|
||||
repository<E extends Entity | keyof TBD | string>(
|
||||
entity: E
|
||||
entity: E,
|
||||
): Repository<TBD, EntitySchema<TBD, E>> {
|
||||
return this.repo(entity);
|
||||
}
|
||||
@@ -266,7 +277,7 @@ export class EntityManager<TBD extends object = DefaultDB> {
|
||||
row[key] = field.transformRetrieve(value as any);
|
||||
} catch (e: any) {
|
||||
throw new TransformRetrieveFailedException(
|
||||
`"${field.type}" field "${key}" on entity "${entity.name}": ${e.message}`
|
||||
`"${field.type}" field "${key}" on entity "${entity.name}": ${e.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -282,7 +293,7 @@ export class EntityManager<TBD extends object = DefaultDB> {
|
||||
entities: Object.fromEntries(this.entities.map((e) => [e.name, e.toJSON()])),
|
||||
relations: Object.fromEntries(this.relations.all.map((r) => [r.getName(), r.toJSON()])),
|
||||
//relations: this.relations.all.map((r) => r.toJSON()),
|
||||
indices: Object.fromEntries(this.indices.map((i) => [i.name, i.toJSON()]))
|
||||
indices: Object.fromEntries(this.indices.map((i) => [i.name, i.toJSON()])),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export class Mutator<
|
||||
TBD extends object = DefaultDB,
|
||||
TB extends keyof TBD = any,
|
||||
Output = TBD[TB],
|
||||
Input = Omit<Output, "id">
|
||||
Input = Omit<Output, "id">,
|
||||
> implements EmitsEvents
|
||||
{
|
||||
em: EntityManager<TBD>;
|
||||
@@ -85,7 +85,7 @@ export class Mutator<
|
||||
`Field "${key}" not found on entity "${entity.name}". Fields: ${entity
|
||||
.getFillableFields()
|
||||
.map((f) => f.name)
|
||||
.join(", ")}`
|
||||
.join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ export class Mutator<
|
||||
sql,
|
||||
parameters: [...parameters],
|
||||
result: result,
|
||||
data
|
||||
data,
|
||||
};
|
||||
} catch (e) {
|
||||
// @todo: redact
|
||||
@@ -139,14 +139,14 @@ export class Mutator<
|
||||
}
|
||||
|
||||
const result = await this.emgr.emit(
|
||||
new Mutator.Events.MutatorInsertBefore({ entity, data: data as any })
|
||||
new Mutator.Events.MutatorInsertBefore({ entity, data: data as any }),
|
||||
);
|
||||
|
||||
// if listener returned, take what's returned
|
||||
const _data = result.returned ? result.params.data : data;
|
||||
const validatedData = {
|
||||
...entity.getDefaultObject(),
|
||||
...(await this.getValidatedData(_data, "create"))
|
||||
...(await this.getValidatedData(_data, "create")),
|
||||
};
|
||||
|
||||
// check if required fields are present
|
||||
@@ -182,8 +182,8 @@ export class Mutator<
|
||||
new Mutator.Events.MutatorUpdateBefore({
|
||||
entity,
|
||||
entityId: id,
|
||||
data
|
||||
})
|
||||
data,
|
||||
}),
|
||||
);
|
||||
|
||||
const _data = result.returned ? result.params.data : data;
|
||||
@@ -198,7 +198,7 @@ export class Mutator<
|
||||
const res = await this.single(query);
|
||||
|
||||
await this.emgr.emit(
|
||||
new Mutator.Events.MutatorUpdateAfter({ entity, entityId: id, data: res.data })
|
||||
new Mutator.Events.MutatorUpdateAfter({ entity, entityId: id, data: res.data }),
|
||||
);
|
||||
|
||||
return res as any;
|
||||
@@ -220,7 +220,7 @@ export class Mutator<
|
||||
const res = await this.single(query);
|
||||
|
||||
await this.emgr.emit(
|
||||
new Mutator.Events.MutatorDeleteAfter({ entity, entityId: id, data: res.data })
|
||||
new Mutator.Events.MutatorDeleteAfter({ entity, entityId: id, data: res.data }),
|
||||
);
|
||||
|
||||
return res as any;
|
||||
@@ -274,7 +274,7 @@ export class Mutator<
|
||||
const entity = this.entity;
|
||||
|
||||
const qb = this.appendWhere(this.conn.deleteFrom(entity.name), where).returning(
|
||||
entity.getSelect()
|
||||
entity.getSelect(),
|
||||
);
|
||||
|
||||
return (await this.many(qb)) as any;
|
||||
@@ -282,7 +282,7 @@ export class Mutator<
|
||||
|
||||
async updateWhere(
|
||||
data: Partial<Input>,
|
||||
where?: RepoQuery["where"]
|
||||
where?: RepoQuery["where"],
|
||||
): Promise<MutatorResponse<Output[]>> {
|
||||
const entity = this.entity;
|
||||
const validatedData = await this.getValidatedData(data, "update");
|
||||
@@ -304,7 +304,7 @@ export class Mutator<
|
||||
for (const row of data) {
|
||||
const validatedData = {
|
||||
...entity.getDefaultObject(),
|
||||
...(await this.getValidatedData(row, "create"))
|
||||
...(await this.getValidatedData(row, "create")),
|
||||
};
|
||||
|
||||
// check if required fields are present
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
type EntityData,
|
||||
type EntityManager,
|
||||
WhereBuilder,
|
||||
WithBuilder
|
||||
WithBuilder,
|
||||
} from "../index";
|
||||
import { JoinBuilder } from "./JoinBuilder";
|
||||
|
||||
@@ -66,13 +66,20 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
||||
return this.em.connection.kysely;
|
||||
}
|
||||
|
||||
private checkIndex(entity: string, field: string, clause: string) {
|
||||
const indexed = this.em.getIndexedFields(entity).map((f) => f.name);
|
||||
if (!indexed.includes(field)) {
|
||||
$console.warn(`Field "${entity}.${field}" used in "${clause}" is not indexed`);
|
||||
}
|
||||
}
|
||||
|
||||
getValidOptions(options?: Partial<RepoQuery>): RepoQuery {
|
||||
const entity = this.entity;
|
||||
// @todo: if not cloned deep, it will keep references and error if multiple requests come in
|
||||
const validated = {
|
||||
...cloneDeep(defaultQuerySchema),
|
||||
sort: entity.getDefaultSort(),
|
||||
select: entity.getSelect()
|
||||
select: entity.getSelect(),
|
||||
};
|
||||
|
||||
if (!options) return validated;
|
||||
@@ -85,6 +92,7 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
||||
throw new InvalidSearchParamsException(`Invalid sort direction "${options.sort.dir}"`);
|
||||
}
|
||||
|
||||
this.checkIndex(entity.name, options.sort.by, "sort");
|
||||
validated.sort = options.sort;
|
||||
}
|
||||
|
||||
@@ -93,10 +101,10 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
||||
|
||||
if (invalid.length > 0) {
|
||||
throw new InvalidSearchParamsException(
|
||||
`Invalid select field(s): ${invalid.join(", ")}`
|
||||
`Invalid select field(s): ${invalid.join(", ")}`,
|
||||
).context({
|
||||
entity: entity.name,
|
||||
valid: validated.select
|
||||
valid: validated.select,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -114,7 +122,7 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
||||
const related = this.em.relationOf(entity.name, entry);
|
||||
if (!related) {
|
||||
throw new InvalidSearchParamsException(
|
||||
`JOIN: "${entry}" is not a relation of "${entity.name}"`
|
||||
`JOIN: "${entry}" is not a relation of "${entity.name}"`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -137,15 +145,17 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
||||
return true;
|
||||
}
|
||||
|
||||
this.checkIndex(alias, prop, "where");
|
||||
return !this.em.entity(alias).getField(prop);
|
||||
}
|
||||
|
||||
this.checkIndex(entity.name, field, "where");
|
||||
return typeof entity.getField(field) === "undefined";
|
||||
});
|
||||
|
||||
if (invalid.length > 0) {
|
||||
throw new InvalidSearchParamsException(
|
||||
`Invalid where field(s): ${invalid.join(", ")}`
|
||||
`Invalid where field(s): ${invalid.join(", ")}`,
|
||||
).context({ aliases, entity: entity.name });
|
||||
}
|
||||
|
||||
@@ -156,13 +166,15 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
||||
if (options.limit) validated.limit = options.limit;
|
||||
if (options.offset) validated.offset = options.offset;
|
||||
|
||||
//$console.debug("Repository: options", { entity: entity.name, options, validated });
|
||||
|
||||
return validated;
|
||||
}
|
||||
|
||||
protected async performQuery(qb: RepositoryQB): Promise<RepositoryResponse> {
|
||||
const entity = this.entity;
|
||||
const compiled = qb.compile();
|
||||
//$console.log("performQuery", compiled.sql, compiled.parameters);
|
||||
//$console.debug(`Repository: query\n${compiled.sql}\n`, compiled.parameters);
|
||||
|
||||
const start = performance.now();
|
||||
const selector = (as = "count") => this.conn.fn.countAll<number>().as(as);
|
||||
@@ -179,7 +191,7 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
||||
const [_count, _total, result] = await this.em.connection.batchQuery([
|
||||
countQuery,
|
||||
totalQuery,
|
||||
qb
|
||||
qb,
|
||||
]);
|
||||
//$console.log("result", { _count, _total });
|
||||
|
||||
@@ -197,8 +209,8 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
||||
count: _count[0]?.count ?? 0, // @todo: better graceful method
|
||||
items: result.length,
|
||||
time,
|
||||
query: { sql: compiled.sql, parameters: compiled.parameters }
|
||||
}
|
||||
query: { sql: compiled.sql, parameters: compiled.parameters },
|
||||
},
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
@@ -211,10 +223,10 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
||||
|
||||
protected async single(
|
||||
qb: RepositoryQB,
|
||||
options: RepoQuery
|
||||
options: RepoQuery,
|
||||
): Promise<RepositoryResponse<EntityData>> {
|
||||
await this.emgr.emit(
|
||||
new Repository.Events.RepositoryFindOneBefore({ entity: this.entity, options })
|
||||
new Repository.Events.RepositoryFindOneBefore({ entity: this.entity, options }),
|
||||
);
|
||||
|
||||
const { data, ...response } = await this.performQuery(qb);
|
||||
@@ -223,8 +235,8 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
||||
new Repository.Events.RepositoryFindOneAfter({
|
||||
entity: this.entity,
|
||||
options,
|
||||
data: data[0]!
|
||||
})
|
||||
data: data[0]!,
|
||||
}),
|
||||
);
|
||||
|
||||
return { ...response, data: data[0]! };
|
||||
@@ -238,7 +250,7 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
||||
ignore?: (keyof RepoQuery)[];
|
||||
alias?: string;
|
||||
defaults?: Pick<RepoQuery, "limit" | "offset">;
|
||||
}
|
||||
},
|
||||
) {
|
||||
const entity = this.entity;
|
||||
let qb = _qb ?? (this.conn.selectFrom(entity.name) as RepositoryQB);
|
||||
@@ -252,15 +264,9 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
||||
const defaults = {
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
...config?.defaults
|
||||
...config?.defaults,
|
||||
};
|
||||
|
||||
/*$console.log("build query options", {
|
||||
entity: entity.name,
|
||||
options,
|
||||
config
|
||||
});*/
|
||||
|
||||
if (!ignore.includes("select") && options.select) {
|
||||
qb = qb.select(entity.getAliasedSelectFrom(options.select, alias));
|
||||
}
|
||||
@@ -291,7 +297,7 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
||||
|
||||
private buildQuery(
|
||||
_options?: Partial<RepoQuery>,
|
||||
ignore: (keyof RepoQuery)[] = []
|
||||
ignore: (keyof RepoQuery)[] = [],
|
||||
): { qb: RepositoryQB; options: RepoQuery } {
|
||||
const entity = this.entity;
|
||||
const options = this.getValidOptions(_options);
|
||||
@@ -299,23 +305,25 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
||||
return {
|
||||
qb: this.addOptionsToQueryBuilder(undefined, options, {
|
||||
ignore,
|
||||
alias: entity.name
|
||||
alias: entity.name,
|
||||
// already done
|
||||
validate: false,
|
||||
}),
|
||||
options
|
||||
options,
|
||||
};
|
||||
}
|
||||
|
||||
async findId(
|
||||
id: PrimaryFieldType,
|
||||
_options?: Partial<Omit<RepoQuery, "where" | "limit" | "offset">>
|
||||
_options?: Partial<Omit<RepoQuery, "where" | "limit" | "offset">>,
|
||||
): Promise<RepositoryResponse<TBD[TB] | undefined>> {
|
||||
const { qb, options } = this.buildQuery(
|
||||
{
|
||||
..._options,
|
||||
where: { [this.entity.getPrimaryField().name]: id },
|
||||
limit: 1
|
||||
limit: 1,
|
||||
},
|
||||
["offset", "sort"]
|
||||
["offset", "sort"],
|
||||
);
|
||||
|
||||
return this.single(qb, options) as any;
|
||||
@@ -323,12 +331,12 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
||||
|
||||
async findOne(
|
||||
where: RepoQuery["where"],
|
||||
_options?: Partial<Omit<RepoQuery, "where" | "limit" | "offset">>
|
||||
_options?: Partial<Omit<RepoQuery, "where" | "limit" | "offset">>,
|
||||
): Promise<RepositoryResponse<TBD[TB] | undefined>> {
|
||||
const { qb, options } = this.buildQuery({
|
||||
..._options,
|
||||
where,
|
||||
limit: 1
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
return this.single(qb, options) as any;
|
||||
@@ -338,7 +346,7 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
||||
const { qb, options } = this.buildQuery(_options);
|
||||
|
||||
await this.emgr.emit(
|
||||
new Repository.Events.RepositoryFindManyBefore({ entity: this.entity, options })
|
||||
new Repository.Events.RepositoryFindManyBefore({ entity: this.entity, options }),
|
||||
);
|
||||
|
||||
const res = await this.performQuery(qb);
|
||||
@@ -347,8 +355,8 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
||||
new Repository.Events.RepositoryFindManyAfter({
|
||||
entity: this.entity,
|
||||
options,
|
||||
data: res.data
|
||||
})
|
||||
data: res.data,
|
||||
}),
|
||||
);
|
||||
|
||||
return res as any;
|
||||
@@ -358,7 +366,7 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
||||
async findManyByReference(
|
||||
id: PrimaryFieldType,
|
||||
reference: string,
|
||||
_options?: Partial<Omit<RepoQuery, "limit" | "offset">>
|
||||
_options?: Partial<Omit<RepoQuery, "limit" | "offset">>,
|
||||
): Promise<RepositoryResponse<EntityData>> {
|
||||
const entity = this.entity;
|
||||
const listable_relations = this.em.relations.listableRelationsOf(entity);
|
||||
@@ -366,7 +374,7 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
||||
|
||||
if (!relation) {
|
||||
throw new Error(
|
||||
`Relation "${reference}" not found or not listable on entity "${entity.name}"`
|
||||
`Relation "${reference}" not found or not listable on entity "${entity.name}"`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -374,7 +382,7 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
||||
const refQueryOptions = relation.getReferenceQuery(newEntity, id as number, reference);
|
||||
if (!("where" in refQueryOptions) || Object.keys(refQueryOptions.where as any).length === 0) {
|
||||
throw new Error(
|
||||
`Invalid reference query for "${reference}" on entity "${newEntity.name}"`
|
||||
`Invalid reference query for "${reference}" on entity "${newEntity.name}"`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -383,8 +391,8 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
||||
...refQueryOptions,
|
||||
where: {
|
||||
...refQueryOptions.where,
|
||||
..._options?.where
|
||||
}
|
||||
..._options?.where,
|
||||
},
|
||||
};
|
||||
|
||||
return this.cloneFor(newEntity).findMany(findManyOptions);
|
||||
@@ -409,7 +417,7 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
||||
sql: compiled.sql,
|
||||
parameters: [...compiled.parameters],
|
||||
result,
|
||||
count: result[0]?.count ?? 0
|
||||
count: result[0]?.count ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -435,7 +443,7 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
||||
sql: compiled.sql,
|
||||
parameters: [...compiled.parameters],
|
||||
result,
|
||||
exists: result[0]!.count > 0
|
||||
exists: result[0]!.count > 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,14 @@ import {
|
||||
exp,
|
||||
isBooleanLike,
|
||||
isPrimitive,
|
||||
makeValidator
|
||||
makeValidator,
|
||||
} from "core";
|
||||
import type {
|
||||
DeleteQueryBuilder,
|
||||
ExpressionBuilder,
|
||||
ExpressionWrapper,
|
||||
SelectQueryBuilder,
|
||||
UpdateQueryBuilder
|
||||
UpdateQueryBuilder,
|
||||
} from "kysely";
|
||||
|
||||
type Builder = ExpressionBuilder<any, any>;
|
||||
@@ -34,58 +34,58 @@ const expressions = [
|
||||
exp(
|
||||
"$eq",
|
||||
(v: Primitive) => isPrimitive(v),
|
||||
(v, k, eb: Builder) => eb(key(k), "=", v)
|
||||
(v, k, eb: Builder) => eb(key(k), "=", v),
|
||||
),
|
||||
exp(
|
||||
"$ne",
|
||||
(v: Primitive) => isPrimitive(v),
|
||||
(v, k, eb: Builder) => eb(key(k), "!=", v)
|
||||
(v, k, eb: Builder) => eb(key(k), "!=", v),
|
||||
),
|
||||
exp(
|
||||
"$gt",
|
||||
(v: Primitive) => isPrimitive(v),
|
||||
(v, k, eb: Builder) => eb(key(k), ">", v)
|
||||
(v, k, eb: Builder) => eb(key(k), ">", v),
|
||||
),
|
||||
exp(
|
||||
"$gte",
|
||||
(v: Primitive) => isPrimitive(v),
|
||||
(v, k, eb: Builder) => eb(key(k), ">=", v)
|
||||
(v, k, eb: Builder) => eb(key(k), ">=", v),
|
||||
),
|
||||
exp(
|
||||
"$lt",
|
||||
(v: Primitive) => isPrimitive(v),
|
||||
(v, k, eb: Builder) => eb(key(k), "<", v)
|
||||
(v, k, eb: Builder) => eb(key(k), "<", v),
|
||||
),
|
||||
exp(
|
||||
"$lte",
|
||||
(v: Primitive) => isPrimitive(v),
|
||||
(v, k, eb: Builder) => eb(key(k), "<=", v)
|
||||
(v, k, eb: Builder) => eb(key(k), "<=", v),
|
||||
),
|
||||
exp(
|
||||
"$isnull",
|
||||
(v: BooleanLike) => isBooleanLike(v),
|
||||
(v, k, eb: Builder) => eb(key(k), v ? "is" : "is not", null)
|
||||
(v, k, eb: Builder) => eb(key(k), v ? "is" : "is not", null),
|
||||
),
|
||||
exp(
|
||||
"$in",
|
||||
(v: any[]) => Array.isArray(v),
|
||||
(v, k, eb: Builder) => eb(key(k), "in", v)
|
||||
(v, k, eb: Builder) => eb(key(k), "in", v),
|
||||
),
|
||||
exp(
|
||||
"$notin",
|
||||
(v: any[]) => Array.isArray(v),
|
||||
(v, k, eb: Builder) => eb(key(k), "not in", v)
|
||||
(v, k, eb: Builder) => eb(key(k), "not in", v),
|
||||
),
|
||||
exp(
|
||||
"$between",
|
||||
(v: [number, number]) => Array.isArray(v) && v.length === 2,
|
||||
(v, k, eb: Builder) => eb.between(key(k), v[0], v[1])
|
||||
(v, k, eb: Builder) => eb.between(key(k), v[0], v[1]),
|
||||
),
|
||||
exp(
|
||||
"$like",
|
||||
(v: Primitive) => isPrimitive(v),
|
||||
(v, k, eb: Builder) => eb(key(k), "like", String(v).replace(/\*/g, "%"))
|
||||
)
|
||||
(v, k, eb: Builder) => eb(key(k), "like", String(v).replace(/\*/g, "%")),
|
||||
),
|
||||
];
|
||||
|
||||
export type WhereQuery = FilterQuery<typeof expressions>;
|
||||
@@ -103,7 +103,7 @@ export class WhereBuilder {
|
||||
const fns = validator.build(query, {
|
||||
value_is_kv: true,
|
||||
exp_ctx: eb,
|
||||
convert: true
|
||||
convert: true,
|
||||
});
|
||||
|
||||
if (fns.$or.length > 0 && fns.$and.length > 0) {
|
||||
@@ -124,7 +124,7 @@ export class WhereBuilder {
|
||||
const { keys } = validator.build(query, {
|
||||
value_is_kv: true,
|
||||
exp_ctx: () => null,
|
||||
convert: true
|
||||
convert: true,
|
||||
});
|
||||
return Array.from(keys);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export class WithBuilder {
|
||||
em: EntityManager<any>,
|
||||
qb: RepositoryQB,
|
||||
entity: Entity,
|
||||
withs: RepoQuery["with"]
|
||||
withs: RepoQuery["with"],
|
||||
) {
|
||||
if (!withs || !isObject(withs)) {
|
||||
console.warn(`'withs' undefined or invalid, given: ${JSON.stringify(withs)}`);
|
||||
@@ -36,8 +36,8 @@ export class WithBuilder {
|
||||
if (query) {
|
||||
subQuery = em.repo(other.entity).addOptionsToQueryBuilder(subQuery, query as any, {
|
||||
ignore: ["with", "join", cardinality === 1 ? "limit" : undefined].filter(
|
||||
Boolean
|
||||
) as any
|
||||
Boolean,
|
||||
) as any,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ export class WithBuilder {
|
||||
const related = em.relationOf(entity, ref);
|
||||
if (!related) {
|
||||
throw new InvalidSearchParamsException(
|
||||
`WITH: "${ref}" is not a relation of "${entity}"`
|
||||
`WITH: "${ref}" is not a relation of "${entity}"`,
|
||||
);
|
||||
}
|
||||
depth++;
|
||||
|
||||
@@ -42,11 +42,11 @@ export class InvalidFieldConfigException extends Exception {
|
||||
constructor(
|
||||
field: Field<any, any, any>,
|
||||
public given: any,
|
||||
error: TypeInvalidError
|
||||
error: TypeInvalidError,
|
||||
) {
|
||||
console.error("InvalidFieldConfigException", {
|
||||
given,
|
||||
error: error.firstToString()
|
||||
error: error.firstToString(),
|
||||
});
|
||||
super(`Invalid Field config given for field "${field.name}": ${error.firstToString()}`);
|
||||
}
|
||||
@@ -71,7 +71,7 @@ export class EntityNotFoundException extends Exception {
|
||||
|
||||
constructor(entity: Entity | string, id: any) {
|
||||
super(
|
||||
`Entity "${typeof entity !== "string" ? entity.name : entity}" with id "${id}" not found`
|
||||
`Entity "${typeof entity !== "string" ? entity.name : entity}" with id "${id}" not found`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export class MutatorInsertBefore extends Event<{ entity: Entity; data: EntityDat
|
||||
|
||||
return this.clone({
|
||||
entity,
|
||||
data
|
||||
data,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export class MutatorUpdateBefore extends Event<
|
||||
return this.clone({
|
||||
...rest,
|
||||
entity,
|
||||
data
|
||||
data,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ export const MutatorEvents = {
|
||||
MutatorUpdateBefore,
|
||||
MutatorUpdateAfter,
|
||||
MutatorDeleteBefore,
|
||||
MutatorDeleteAfter
|
||||
MutatorDeleteAfter,
|
||||
};
|
||||
|
||||
export class RepositoryFindOneBefore extends Event<{ entity: Entity; options: RepoQuery }> {
|
||||
@@ -98,5 +98,5 @@ export const RepositoryEvents = {
|
||||
RepositoryFindOneBefore,
|
||||
RepositoryFindOneAfter,
|
||||
RepositoryFindManyBefore,
|
||||
RepositoryFindManyAfter
|
||||
RepositoryFindManyAfter,
|
||||
};
|
||||
|
||||
@@ -5,9 +5,9 @@ import { Field, type TActionContext, type TRenderContext, baseFieldConfigSchema
|
||||
|
||||
export const booleanFieldConfigSchema = Type.Composite([
|
||||
Type.Object({
|
||||
default_value: Type.Optional(Type.Boolean({ default: false }))
|
||||
default_value: Type.Optional(Type.Boolean({ default: false })),
|
||||
}),
|
||||
baseFieldConfigSchema
|
||||
baseFieldConfigSchema,
|
||||
]);
|
||||
|
||||
export type BooleanFieldConfig = Static<typeof booleanFieldConfigSchema>;
|
||||
@@ -40,7 +40,7 @@ export class BooleanField<Required extends true | false = false> extends Field<
|
||||
override getHtmlConfig() {
|
||||
return {
|
||||
...super.getHtmlConfig(),
|
||||
element: "boolean"
|
||||
element: "boolean",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ export class BooleanField<Required extends true | false = false> extends Field<
|
||||
override async transformPersist(
|
||||
val: unknown,
|
||||
em: EntityManager<any>,
|
||||
context: TActionContext
|
||||
context: TActionContext,
|
||||
): Promise<boolean | undefined> {
|
||||
const value = await super.transformPersist(val, em, context);
|
||||
if (this.nullish(value)) {
|
||||
|
||||
@@ -9,13 +9,13 @@ export const dateFieldConfigSchema = Type.Composite(
|
||||
type: StringEnum(["date", "datetime", "week"] as const, { default: "date" }),
|
||||
timezone: Type.Optional(Type.String()),
|
||||
min_date: Type.Optional(Type.String()),
|
||||
max_date: Type.Optional(Type.String())
|
||||
max_date: Type.Optional(Type.String()),
|
||||
}),
|
||||
baseFieldConfigSchema
|
||||
baseFieldConfigSchema,
|
||||
],
|
||||
{
|
||||
additionalProperties: false
|
||||
}
|
||||
additionalProperties: false,
|
||||
},
|
||||
);
|
||||
|
||||
export type DateFieldConfig = Static<typeof dateFieldConfigSchema>;
|
||||
@@ -43,8 +43,8 @@ export class DateField<Required extends true | false = false> extends Field<
|
||||
...super.getHtmlConfig(),
|
||||
element: "date",
|
||||
props: {
|
||||
type: htmlType
|
||||
}
|
||||
type: htmlType,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ export class DateField<Required extends true | false = false> extends Field<
|
||||
if (this.config.type === "week" && value.includes("-W")) {
|
||||
const [year, week] = value.split("-W").map((n) => Number.parseInt(n, 10)) as [
|
||||
number,
|
||||
number
|
||||
number,
|
||||
];
|
||||
//console.log({ year, week });
|
||||
// @ts-ignore causes errors on build?
|
||||
@@ -129,7 +129,7 @@ export class DateField<Required extends true | false = false> extends Field<
|
||||
override async transformPersist(
|
||||
_value: any,
|
||||
em: EntityManager<any>,
|
||||
context: TActionContext
|
||||
context: TActionContext,
|
||||
): Promise<string | undefined> {
|
||||
const value = await super.transformPersist(_value, em, context);
|
||||
if (this.nullish(value)) return value;
|
||||
|
||||
@@ -12,9 +12,9 @@ export const enumFieldConfigSchema = Type.Composite(
|
||||
Type.Object(
|
||||
{
|
||||
type: Const("strings"),
|
||||
values: Type.Array(Type.String())
|
||||
values: Type.Array(Type.String()),
|
||||
},
|
||||
{ title: "Strings" }
|
||||
{ title: "Strings" },
|
||||
),
|
||||
Type.Object(
|
||||
{
|
||||
@@ -22,23 +22,23 @@ export const enumFieldConfigSchema = Type.Composite(
|
||||
values: Type.Array(
|
||||
Type.Object({
|
||||
label: Type.String(),
|
||||
value: Type.String()
|
||||
})
|
||||
)
|
||||
value: Type.String(),
|
||||
}),
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Objects",
|
||||
additionalProperties: false
|
||||
}
|
||||
)
|
||||
])
|
||||
)
|
||||
additionalProperties: false,
|
||||
},
|
||||
),
|
||||
]),
|
||||
),
|
||||
}),
|
||||
baseFieldConfigSchema
|
||||
baseFieldConfigSchema,
|
||||
],
|
||||
{
|
||||
additionalProperties: false
|
||||
}
|
||||
additionalProperties: false,
|
||||
},
|
||||
);
|
||||
|
||||
export type EnumFieldConfig = Static<typeof enumFieldConfigSchema>;
|
||||
@@ -123,7 +123,7 @@ export class EnumField<Required extends true | false = false, TypeOverride = str
|
||||
override async transformPersist(
|
||||
_value: any,
|
||||
em: EntityManager<any>,
|
||||
context: TActionContext
|
||||
context: TActionContext,
|
||||
): Promise<string | undefined> {
|
||||
const value = await super.transformPersist(_value, em, context);
|
||||
if (this.nullish(value)) return value;
|
||||
@@ -132,7 +132,7 @@ export class EnumField<Required extends true | false = false, TypeOverride = str
|
||||
throw new TransformPersistFailedException(
|
||||
`Field "${this.name}" must be one of the following values: ${this.getOptions()
|
||||
.map((o) => o.value)
|
||||
.join(", ")}`
|
||||
.join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -146,8 +146,8 @@ export class EnumField<Required extends true | false = false, TypeOverride = str
|
||||
[];
|
||||
return this.toSchemaWrapIfRequired(
|
||||
StringEnum(values, {
|
||||
default: this.getDefault()
|
||||
})
|
||||
default: this.getDefault(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
Type,
|
||||
TypeInvalidError,
|
||||
parse,
|
||||
snakeToPascalWithSpaces
|
||||
snakeToPascalWithSpaces,
|
||||
} from "core/utils";
|
||||
import type { ColumnBuilderCallback, ColumnDataType, ColumnDefinitionBuilder } from "kysely";
|
||||
import type { HTMLInputTypeAttribute, InputHTMLAttributes } from "react";
|
||||
@@ -38,32 +38,32 @@ export const baseFieldConfigSchema = Type.Object(
|
||||
Type.Union(
|
||||
[
|
||||
Type.Boolean({ title: "Boolean", default: DEFAULT_FILLABLE }),
|
||||
Type.Array(StringEnum(ActionContext), { title: "Context", uniqueItems: true })
|
||||
Type.Array(StringEnum(ActionContext), { title: "Context", uniqueItems: true }),
|
||||
],
|
||||
{
|
||||
default: DEFAULT_FILLABLE
|
||||
}
|
||||
)
|
||||
default: DEFAULT_FILLABLE,
|
||||
},
|
||||
),
|
||||
),
|
||||
hidden: Type.Optional(
|
||||
Type.Union(
|
||||
[
|
||||
Type.Boolean({ title: "Boolean", default: DEFAULT_HIDDEN }),
|
||||
// @todo: tmp workaround
|
||||
Type.Array(StringEnum(TmpContext), { title: "Context", uniqueItems: true })
|
||||
Type.Array(StringEnum(TmpContext), { title: "Context", uniqueItems: true }),
|
||||
],
|
||||
{
|
||||
default: DEFAULT_HIDDEN
|
||||
}
|
||||
)
|
||||
default: DEFAULT_HIDDEN,
|
||||
},
|
||||
),
|
||||
),
|
||||
// if field is virtual, it will not call transformPersist & transformRetrieve
|
||||
virtual: Type.Optional(Type.Boolean()),
|
||||
default_value: Type.Optional(Type.Any())
|
||||
default_value: Type.Optional(Type.Any()),
|
||||
},
|
||||
{
|
||||
additionalProperties: false
|
||||
}
|
||||
additionalProperties: false,
|
||||
},
|
||||
);
|
||||
export type BaseFieldConfig = Static<typeof baseFieldConfigSchema>;
|
||||
|
||||
@@ -72,7 +72,7 @@ export type SchemaResponse = [string, ColumnDataType, ColumnBuilderCallback] | u
|
||||
export abstract class Field<
|
||||
Config extends BaseFieldConfig = BaseFieldConfig,
|
||||
Type = any,
|
||||
Required extends true | false = false
|
||||
Required extends true | false = false,
|
||||
> {
|
||||
_required!: Required;
|
||||
_type!: Type;
|
||||
@@ -108,7 +108,7 @@ export abstract class Field<
|
||||
|
||||
protected useSchemaHelper(
|
||||
type: ColumnDataType,
|
||||
builder?: (col: ColumnDefinitionBuilder) => ColumnDefinitionBuilder
|
||||
builder?: (col: ColumnDefinitionBuilder) => ColumnDefinitionBuilder,
|
||||
): SchemaResponse {
|
||||
return [
|
||||
this.name,
|
||||
@@ -116,7 +116,7 @@ export abstract class Field<
|
||||
(col: ColumnDefinitionBuilder) => {
|
||||
if (builder) return builder(col);
|
||||
return col;
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ export abstract class Field<
|
||||
getHtmlConfig(): { element: HTMLInputTypeAttribute | string; props?: InputHTMLAttributes<any> } {
|
||||
return {
|
||||
element: "input",
|
||||
props: { type: "text" }
|
||||
props: { type: "text" },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ export abstract class Field<
|
||||
async transformPersist(
|
||||
value: unknown,
|
||||
em: EntityManager<any>,
|
||||
context: TActionContext
|
||||
context: TActionContext,
|
||||
): Promise<any> {
|
||||
if (this.nullish(value)) {
|
||||
if (this.isRequired() && !this.hasDefault()) {
|
||||
@@ -245,7 +245,7 @@ export abstract class Field<
|
||||
return {
|
||||
// @todo: current workaround because of fixed string type
|
||||
type: this.type as any,
|
||||
config: this.config
|
||||
config: this.config,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ export class JsonField<Required extends true | false = false, TypeOverride = obj
|
||||
override async transformPersist(
|
||||
_value: any,
|
||||
em: EntityManager<any>,
|
||||
context: TActionContext
|
||||
context: TActionContext,
|
||||
): Promise<string | undefined> {
|
||||
const value = await super.transformPersist(_value, em, context);
|
||||
//console.log("value", value);
|
||||
@@ -91,7 +91,7 @@ export class JsonField<Required extends true | false = false, TypeOverride = obj
|
||||
|
||||
if (!this.isSerializable(value)) {
|
||||
throw new TransformPersistFailedException(
|
||||
`Field "${this.name}" must be serializable to JSON.`
|
||||
`Field "${this.name}" must be serializable to JSON.`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,20 +9,20 @@ export const jsonSchemaFieldConfigSchema = Type.Composite(
|
||||
Type.Object({
|
||||
schema: Type.Object({}, { default: {} }),
|
||||
ui_schema: Type.Optional(Type.Object({})),
|
||||
default_from_schema: Type.Optional(Type.Boolean())
|
||||
default_from_schema: Type.Optional(Type.Boolean()),
|
||||
}),
|
||||
baseFieldConfigSchema
|
||||
baseFieldConfigSchema,
|
||||
],
|
||||
{
|
||||
additionalProperties: false
|
||||
}
|
||||
additionalProperties: false,
|
||||
},
|
||||
);
|
||||
|
||||
export type JsonSchemaFieldConfig = Static<typeof jsonSchemaFieldConfigSchema>;
|
||||
|
||||
export class JsonSchemaField<
|
||||
Required extends true | false = false,
|
||||
TypeOverride = object
|
||||
TypeOverride = object,
|
||||
> extends Field<JsonSchemaFieldConfig, TypeOverride, Required> {
|
||||
override readonly type = "jsonschema";
|
||||
private validator: Validator;
|
||||
@@ -107,7 +107,7 @@ export class JsonSchemaField<
|
||||
override async transformPersist(
|
||||
_value: any,
|
||||
em: EntityManager<any>,
|
||||
context: TActionContext
|
||||
context: TActionContext,
|
||||
): Promise<string | undefined> {
|
||||
const value = await super.transformPersist(_value, em, context);
|
||||
if (this.nullish(value)) return value;
|
||||
@@ -130,8 +130,8 @@ export class JsonSchemaField<
|
||||
return this.toSchemaWrapIfRequired(
|
||||
FromSchema({
|
||||
default: this.getDefault(),
|
||||
...schema
|
||||
})
|
||||
...schema,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,13 +11,13 @@ export const numberFieldConfigSchema = Type.Composite(
|
||||
maximum: Type.Optional(Type.Number()),
|
||||
exclusiveMinimum: Type.Optional(Type.Number()),
|
||||
exclusiveMaximum: Type.Optional(Type.Number()),
|
||||
multipleOf: Type.Optional(Type.Number())
|
||||
multipleOf: Type.Optional(Type.Number()),
|
||||
}),
|
||||
baseFieldConfigSchema
|
||||
baseFieldConfigSchema,
|
||||
],
|
||||
{
|
||||
additionalProperties: false
|
||||
}
|
||||
additionalProperties: false,
|
||||
},
|
||||
);
|
||||
|
||||
export type NumberFieldConfig = Static<typeof numberFieldConfigSchema>;
|
||||
@@ -39,8 +39,8 @@ export class NumberField<Required extends true | false = false> extends Field<
|
||||
props: {
|
||||
type: "number",
|
||||
pattern: "d*",
|
||||
inputMode: "numeric"
|
||||
} as any // @todo: react expects "inputMode", but type dictates "inputmode"
|
||||
inputMode: "numeric",
|
||||
} as any, // @todo: react expects "inputMode", but type dictates "inputmode"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ export class NumberField<Required extends true | false = false> extends Field<
|
||||
override async transformPersist(
|
||||
_value: unknown,
|
||||
em: EntityManager<any>,
|
||||
context: TActionContext
|
||||
context: TActionContext,
|
||||
): Promise<number | undefined> {
|
||||
const value = await super.transformPersist(_value, em, context);
|
||||
|
||||
@@ -72,13 +72,13 @@ export class NumberField<Required extends true | false = false> extends Field<
|
||||
|
||||
if (this.config.maximum && (value as number) > this.config.maximum) {
|
||||
throw new TransformPersistFailedException(
|
||||
`Field "${this.name}" cannot be greater than ${this.config.maximum}`
|
||||
`Field "${this.name}" cannot be greater than ${this.config.maximum}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.config.minimum && (value as number) < this.config.minimum) {
|
||||
throw new TransformPersistFailedException(
|
||||
`Field "${this.name}" cannot be less than ${this.config.minimum}`
|
||||
`Field "${this.name}" cannot be less than ${this.config.minimum}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -93,8 +93,8 @@ export class NumberField<Required extends true | false = false> extends Field<
|
||||
maximum: this.config?.maximum,
|
||||
exclusiveMinimum: this.config?.exclusiveMinimum,
|
||||
exclusiveMaximum: this.config?.exclusiveMaximum,
|
||||
multipleOf: this.config?.multipleOf
|
||||
})
|
||||
multipleOf: this.config?.multipleOf,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import { Field, baseFieldConfigSchema } from "./Field";
|
||||
export const primaryFieldConfigSchema = Type.Composite([
|
||||
Type.Omit(baseFieldConfigSchema, ["required"]),
|
||||
Type.Object({
|
||||
required: Type.Optional(Type.Literal(false))
|
||||
})
|
||||
required: Type.Optional(Type.Literal(false)),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type PrimaryFieldConfig = Static<typeof primaryFieldConfigSchema>;
|
||||
|
||||
@@ -19,19 +19,19 @@ export const textFieldConfigSchema = Type.Composite(
|
||||
{
|
||||
additionalProperties: Type.Union([
|
||||
Type.String({ title: "String" }),
|
||||
Type.Number({ title: "Number" })
|
||||
])
|
||||
}
|
||||
)
|
||||
)
|
||||
})
|
||||
)
|
||||
Type.Number({ title: "Number" }),
|
||||
]),
|
||||
},
|
||||
),
|
||||
),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
baseFieldConfigSchema
|
||||
baseFieldConfigSchema,
|
||||
],
|
||||
{
|
||||
additionalProperties: false
|
||||
}
|
||||
additionalProperties: false,
|
||||
},
|
||||
);
|
||||
|
||||
export type TextFieldConfig = Static<typeof textFieldConfigSchema>;
|
||||
@@ -81,7 +81,7 @@ export class TextField<Required extends true | false = false> extends Field<
|
||||
override async transformPersist(
|
||||
_value: any,
|
||||
em: EntityManager<any>,
|
||||
context: TActionContext
|
||||
context: TActionContext,
|
||||
): Promise<string | undefined> {
|
||||
let value = await super.transformPersist(_value, em, context);
|
||||
|
||||
@@ -94,19 +94,19 @@ export class TextField<Required extends true | false = false> extends Field<
|
||||
|
||||
if (this.config.maxLength && value?.length > this.config.maxLength) {
|
||||
throw new TransformPersistFailedException(
|
||||
`Field "${this.name}" must be at most ${this.config.maxLength} character(s)`
|
||||
`Field "${this.name}" must be at most ${this.config.maxLength} character(s)`,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.config.minLength && value?.length < this.config.minLength) {
|
||||
throw new TransformPersistFailedException(
|
||||
`Field "${this.name}" must be at least ${this.config.minLength} character(s)`
|
||||
`Field "${this.name}" must be at least ${this.config.minLength} character(s)`,
|
||||
);
|
||||
}
|
||||
|
||||
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}`
|
||||
`Field "${this.name}" must match the pattern ${this.config.pattern}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -119,8 +119,8 @@ export class TextField<Required extends true | false = false> extends Field<
|
||||
default: this.getDefault(),
|
||||
minLength: this.config?.minLength,
|
||||
maxLength: this.config?.maxLength,
|
||||
pattern: this.config?.pattern
|
||||
})
|
||||
pattern: this.config?.pattern,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ export class VirtualField extends Field<VirtualFieldConfig> {
|
||||
return this.toSchemaWrapIfRequired(
|
||||
Type.Any({
|
||||
default: this.getDefault(),
|
||||
readOnly: true
|
||||
})
|
||||
readOnly: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { JsonField, type JsonFieldConfig, jsonFieldConfigSchema } from "./JsonFi
|
||||
import {
|
||||
JsonSchemaField,
|
||||
type JsonSchemaFieldConfig,
|
||||
jsonSchemaFieldConfigSchema
|
||||
jsonSchemaFieldConfigSchema,
|
||||
} from "./JsonSchemaField";
|
||||
import { NumberField, type NumberFieldConfig, numberFieldConfigSchema } from "./NumberField";
|
||||
import { PrimaryField, type PrimaryFieldConfig, primaryFieldConfigSchema } from "./PrimaryField";
|
||||
@@ -35,7 +35,7 @@ export {
|
||||
type NumberFieldConfig,
|
||||
TextField,
|
||||
textFieldConfigSchema,
|
||||
type TextFieldConfig
|
||||
type TextFieldConfig,
|
||||
};
|
||||
|
||||
export * from "./Field";
|
||||
@@ -51,5 +51,5 @@ export const FieldClassMap = {
|
||||
date: { schema: dateFieldConfigSchema, field: DateField },
|
||||
enum: { schema: enumFieldConfigSchema, field: EnumField },
|
||||
json: { schema: jsonFieldConfigSchema, field: JsonField },
|
||||
jsonschema: { schema: jsonSchemaFieldConfigSchema, field: JsonSchemaField }
|
||||
jsonschema: { schema: jsonSchemaFieldConfigSchema, field: JsonSchemaField },
|
||||
} as const;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user