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:
dswbx
2025-02-27 14:21:00 +01:00
440 changed files with 10195 additions and 4173 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ export {
getBindings,
type BindingTypeMap,
type GetBindingType,
type BindingMap
type BindingMap,
} from "./bindings";
export function d1(config: D1ConnectionConfig) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import { registries } from "bknd";
import {
type LocalAdapterConfig,
StorageLocalAdapter
StorageLocalAdapter,
} from "../../media/storage/adapters/StorageLocalAdapter";
export * from "./node.adapter";

View File

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

View File

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

View File

@@ -20,6 +20,6 @@ export function nodeRequestToRequest(req: IncomingMessage): Request {
const method = req.method || "GET";
return new Request(url, {
method,
headers
headers,
});
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,5 +9,5 @@ export {
type PasswordStrategyOptions,
OAuthStrategy,
OAuthCallbackException,
CustomOAuthStrategy
CustomOAuthStrategy,
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,7 +12,7 @@ export {
type AuthUserResolver,
Authenticator,
authenticatorConfig,
jwtConfig
jwtConfig,
} from "./authenticate/Authenticator";
export { AppAuth, type UserFieldSchema } from "./AppAuth";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,6 @@ export {
EventListener,
ListenerModes,
type ListenerMode,
type ListenerHandler
type ListenerHandler,
} from "./EventListener";
export { EventManager, type EmitsEvents, type EventClass } from "./EventManager";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ export class Permission<Name extends string = string> {
toJSON() {
return {
name: this.name
name: this.name,
};
}
}

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
export interface Serializable<Class, Json extends object = object> {
toJSON(): Json;
fromJSON(json: Json): Class;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,9 +10,9 @@ export class SqliteConnection extends Connection {
...fn,
jsonArrayFrom,
jsonObjectFrom,
jsonBuildObject
jsonBuildObject,
},
plugins
plugins,
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -25,8 +25,8 @@ export class VirtualField extends Field<VirtualFieldConfig> {
return this.toSchemaWrapIfRequired(
Type.Any({
default: this.getDefault(),
readOnly: true
})
readOnly: true,
}),
);
}
}

View File

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