update & fix typing, updated examples

This commit is contained in:
dswbx
2024-12-23 19:28:31 +01:00
parent 70e42a02d7
commit c1e92e503b
21 changed files with 126 additions and 139 deletions

View File

@@ -3,7 +3,7 @@
"type": "module",
"sideEffects": false,
"bin": "./dist/cli/index.js",
"version": "0.3.4-alpha1",
"version": "0.4.0-rc1",
"scripts": {
"build:all": "NODE_ENV=production bun run build.ts --minify --types --clean && bun run build:cli",
"dev": "vite",

View File

@@ -38,7 +38,7 @@ export class Api {
private token_transport: "header" | "cookie" | "none" = "header";
public system!: SystemApi;
public data!: DataApi<DB>;
public data!: DataApi;
public auth!: AuthApi;
public media!: MediaApi;

View File

@@ -10,7 +10,7 @@ import * as SystemPermissions from "modules/permissions";
import { AdminController, type AdminControllerOptions } from "modules/server/AdminController";
import { SystemController } from "modules/server/SystemController";
export type AppPlugin<DB> = (app: App<DB>) => void;
export type AppPlugin = (app: App) => void;
abstract class AppEvent<A = {}> extends Event<{ app: App } & A> {}
export class AppConfigUpdatedEvent extends AppEvent {
@@ -32,13 +32,13 @@ export type CreateAppConfig = {
config: LibSqlCredentials;
};
initialConfig?: InitialModuleConfigs;
plugins?: AppPlugin<any>[];
plugins?: AppPlugin[];
options?: Omit<ModuleManagerOptions, "initial" | "onUpdated">;
};
export type AppConfig = InitialModuleConfigs;
export class App<DB = any> {
export class App {
modules: ModuleManager;
static readonly Events = AppEvents;
adminController?: AdminController;
@@ -47,7 +47,7 @@ export class App<DB = any> {
constructor(
private connection: Connection,
_initialConfig?: InitialModuleConfigs,
private plugins: AppPlugin<DB>[] = [],
private plugins: AppPlugin[] = [],
moduleManagerOptions?: ModuleManagerOptions
) {
this.modules = new ModuleManager(connection, {

View File

@@ -1,6 +1,6 @@
import { type AuthAction, Authenticator, type ProfileExchange, Role, type Strategy } from "auth";
import type { PasswordStrategy } from "auth/authenticate/strategies";
import { Exception } from "core";
import { Exception, type PrimaryFieldType } from "core";
import { type Static, secureRandomString, transformObject } from "core/utils";
import { type Entity, EntityIndex, type EntityManager } from "data";
import { type FieldSchema, entity, enumm, make, text } from "data/prototype";
@@ -10,9 +10,9 @@ import { AuthController } from "./api/AuthController";
import { type AppAuthSchema, STRATEGIES, authConfigSchema } from "./auth-schema";
export type UserFieldSchema = FieldSchema<typeof AppAuth.usersFields>;
declare global {
declare module "core" {
interface DB {
users: UserFieldSchema;
users: { id: PrimaryFieldType } & UserFieldSchema;
}
}
@@ -101,7 +101,7 @@ export class AppAuth extends Module<typeof authConfigSchema> {
return this._authenticator!;
}
get em(): EntityManager<DB> {
get em(): EntityManager {
return this.ctx.em as any;
}
@@ -161,7 +161,9 @@ export class AppAuth extends Module<typeof authConfigSchema> {
const users = this.getUsersEntity();
this.toggleStrategyValueVisibility(true);
const result = await this.em.repo(users).findOne({ email: profile.email! });
const result = await this.em
.repo(users as unknown as "users")
.findOne({ email: profile.email! });
this.toggleStrategyValueVisibility(false);
if (!result.data) {
throw new Exception("User not found", 404);

View File

@@ -1,6 +1,7 @@
import { password as $password, text as $text } from "@clack/prompts";
import type { App } from "App";
import type { BkndConfig } from "adapter";
import type { PasswordStrategy } from "auth/authenticate/strategies";
import type { App, BkndConfig } from "bknd";
import { makeConfigApp } from "cli/commands/run";
import { getConfigPath } from "cli/commands/run/platform";
import type { CliCommand } from "cli/types";
@@ -37,7 +38,7 @@ async function action(action: "create" | "update", options: any) {
async function create(app: App, options: any) {
const config = app.module.auth.toJSON(true);
const strategy = app.module.auth.authenticator.strategy("password") as PasswordStrategy;
const users_entity = config.entity_name;
const users_entity = config.entity_name as "users";
const email = await $text({
message: "Enter email",
@@ -83,7 +84,7 @@ async function create(app: App, options: any) {
async function update(app: App, options: any) {
const config = app.module.auth.toJSON(true);
const strategy = app.module.auth.authenticator.strategy("password") as PasswordStrategy;
const users_entity = config.entity_name;
const users_entity = config.entity_name as "users";
const em = app.modules.ctx().em;
const email = (await $text({

View File

@@ -5,6 +5,9 @@ import type { Generated } from "kysely";
export type PrimaryFieldType = number | Generated<number>;
// biome-ignore lint/suspicious/noEmptyInterface: <explanation>
export interface DB {}
export const config = {
data: {
default_primary_field: "id"

View File

@@ -3,7 +3,7 @@ import type { Hono, MiddlewareHandler } from "hono";
export { tbValidator } from "./server/lib/tbValidator";
export { Exception, BkndError } from "./errors";
export { isDebug } from "./env";
export { type PrimaryFieldType, config } from "./config";
export { type PrimaryFieldType, config, type DB } from "./config";
export { AwsClient } from "./clients/aws/AwsClient";
export {
SimpleRenderer,

View File

@@ -11,7 +11,7 @@ import { Module } from "modules/Module";
import { DataController } from "./api/DataController";
import { type AppDataConfig, dataConfigSchema } from "./data-schema";
export class AppData<DB> extends Module<typeof dataConfigSchema> {
export class AppData extends Module<typeof dataConfigSchema> {
override async build() {
const entities = transformObject(this.config.entities ?? {}, (entityConfig, name) => {
return constructEntity(name, entityConfig);
@@ -59,7 +59,7 @@ export class AppData<DB> extends Module<typeof dataConfigSchema> {
return dataConfigSchema;
}
get em(): EntityManager<DB> {
get em(): EntityManager {
this.throwIfNotBuilt();
return this.ctx.em;
}

View File

@@ -1,3 +1,4 @@
import type { DB } from "core";
import type { EntityData, RepoQuery, RepositoryResponse } from "data";
import { type BaseModuleApiOptions, ModuleApi, type PrimaryFieldType } from "modules";
@@ -5,7 +6,7 @@ export type DataApiOptions = BaseModuleApiOptions & {
defaultQuery?: Partial<RepoQuery>;
};
export class DataApi<DB> extends ModuleApi<DataApiOptions> {
export class DataApi extends ModuleApi<DataApiOptions> {
protected override getDefaultOptions(): Partial<DataApiOptions> {
return {
basepath: "/api/data",

View File

@@ -1,3 +1,4 @@
import type { DB as DefaultDB } from "core";
import { EventManager } from "core/events";
import { sql } from "kysely";
import { Connection } from "../connection/Connection";
@@ -14,15 +15,18 @@ import { SchemaManager } from "../schema/SchemaManager";
import { Entity } from "./Entity";
import { type EntityData, Mutator, Repository } from "./index";
type EntitySchema<E extends Entity | string, DB = any> = E extends Entity<infer Name>
? Name extends keyof DB
type EntitySchema<
TBD extends object = DefaultDB,
E extends Entity | keyof TBD | string = string
> = E extends Entity<infer Name>
? Name extends keyof TBD
? Name
: never
: E extends keyof DB
: E extends keyof TBD
? E
: never;
export class EntityManager<DB> {
export class EntityManager<TBD extends object = DefaultDB> {
connection: Connection;
private _entities: Entity[] = [];
@@ -58,7 +62,7 @@ export class EntityManager<DB> {
* Forks the EntityManager without the EventManager.
* This is useful when used inside an event handler.
*/
fork(): EntityManager<DB> {
fork(): EntityManager {
return new EntityManager(this._entities, this.connection, this._relations, this._indices);
}
@@ -95,16 +99,17 @@ export class EntityManager<DB> {
this.entities.push(entity);
}
entity(e: Entity | string): Entity {
entity(e: Entity | keyof TBD | string): Entity {
let entity: Entity | undefined;
if (typeof e === "string") {
entity = this.entities.find((entity) => entity.name === e);
} else {
} else if (e instanceof Entity) {
entity = e;
}
if (!entity) {
throw new EntityNotDefinedException(typeof e === "string" ? e : e.name);
// @ts-ignore
throw new EntityNotDefinedException(e instanceof Entity ? e.name : e);
}
return entity;
@@ -176,15 +181,17 @@ export class EntityManager<DB> {
return this.relations.relationReferencesOf(this.entity(entity_name));
}
repository<E extends Entity | string>(entity: E): Repository<DB, EntitySchema<E, DB>> {
repository<E extends Entity | keyof TBD | string>(
entity: E
): Repository<TBD, EntitySchema<TBD, E>> {
return this.repo(entity);
}
repo<E extends Entity | string>(entity: E): Repository<DB, EntitySchema<E, DB>> {
repo<E extends Entity | keyof TBD | string>(entity: E): Repository<TBD, EntitySchema<TBD, E>> {
return new Repository(this, this.entity(entity), this.emgr);
}
mutator<E extends Entity | string>(entity: E): Mutator<DB, EntitySchema<E, DB>> {
mutator<E extends Entity | keyof TBD | string>(entity: E): Mutator<TBD, EntitySchema<TBD, E>> {
return new Mutator(this, this.entity(entity), this.emgr);
}

View File

@@ -1,4 +1,4 @@
import type { PrimaryFieldType } from "core";
import type { DB as DefaultDB, PrimaryFieldType } from "core";
import { type EmitsEvents, EventManager } from "core/events";
import type { DeleteQueryBuilder, InsertQueryBuilder, UpdateQueryBuilder } from "kysely";
import { type TActionContext, WhereBuilder } from "..";
@@ -26,13 +26,13 @@ export type MutatorResponse<T = EntityData[]> = {
};
export class Mutator<
DB = any,
TB extends keyof DB = any,
Output = DB[TB],
TBD extends object = DefaultDB,
TB extends keyof TBD = any,
Output = TBD[TB],
Input = Omit<Output, "id">
> implements EmitsEvents
{
em: EntityManager<DB>;
em: EntityManager<TBD>;
entity: Entity;
static readonly Events = MutatorEvents;
emgr: EventManager<typeof MutatorEvents>;
@@ -43,7 +43,7 @@ export class Mutator<
this.__unstable_disable_system_entity_creation = value;
}
constructor(em: EntityManager<DB>, entity: Entity, emgr?: EventManager<any>) {
constructor(em: EntityManager<TBD>, entity: Entity, emgr?: EventManager<any>) {
this.em = em;
this.entity = entity;
this.emgr = emgr ?? new EventManager(MutatorEvents);
@@ -163,7 +163,7 @@ export class Mutator<
return res as any;
}
async updateOne(id: PrimaryFieldType, data: Input): Promise<MutatorResponse<Output>> {
async updateOne(id: PrimaryFieldType, data: Partial<Input>): Promise<MutatorResponse<Output>> {
const entity = this.entity;
if (!Number.isInteger(id)) {
throw new Error("ID must be provided for update");
@@ -270,7 +270,10 @@ export class Mutator<
return (await this.many(qb)) as any;
}
async updateWhere(data: Partial<Input>, where?: RepoQuery["where"]): Promise<MutatorResponse<Output[]>> {
async updateWhere(
data: Partial<Input>,
where?: RepoQuery["where"]
): Promise<MutatorResponse<Output[]>> {
const entity = this.entity;
const validatedData = await this.getValidatedData(data, "update");

View File

@@ -1,4 +1,4 @@
import type { PrimaryFieldType } from "core";
import type { DB as DefaultDB, PrimaryFieldType } from "core";
import { type EmitsEvents, EventManager } from "core/events";
import { type SelectQueryBuilder, sql } from "kysely";
import { cloneDeep } from "lodash-es";
@@ -43,13 +43,15 @@ export type RepositoryExistsResponse = RepositoryRawResponse & {
exists: boolean;
};
export class Repository<DB = any, TB extends keyof DB = any> implements EmitsEvents {
em: EntityManager<DB>;
export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = any>
implements EmitsEvents
{
em: EntityManager<TBD>;
entity: Entity;
static readonly Events = RepositoryEvents;
emgr: EventManager<typeof Repository.Events>;
constructor(em: EntityManager<DB>, entity: Entity, emgr?: EventManager<any>) {
constructor(em: EntityManager<TBD>, entity: Entity, emgr?: EventManager<any>) {
this.em = em;
this.entity = entity;
this.emgr = emgr ?? new EventManager(MutatorEvents);
@@ -272,7 +274,7 @@ export class Repository<DB = any, TB extends keyof DB = any> implements EmitsEve
async findId(
id: PrimaryFieldType,
_options?: Partial<Omit<RepoQuery, "where" | "limit" | "offset">>
): Promise<RepositoryResponse<DB[TB] | undefined>> {
): Promise<RepositoryResponse<TBD[TB] | undefined>> {
const { qb, options } = this.buildQuery(
{
..._options,
@@ -288,7 +290,7 @@ export class Repository<DB = any, TB extends keyof DB = any> implements EmitsEve
async findOne(
where: RepoQuery["where"],
_options?: Partial<Omit<RepoQuery, "where" | "limit" | "offset">>
): Promise<RepositoryResponse<DB[TB] | undefined>> {
): Promise<RepositoryResponse<TBD[TB] | undefined>> {
const { qb, options } = this.buildQuery({
..._options,
where,
@@ -298,7 +300,7 @@ export class Repository<DB = any, TB extends keyof DB = any> implements EmitsEve
return this.single(qb, options) as any;
}
async findMany(_options?: Partial<RepoQuery>): Promise<RepositoryResponse<DB[TB][]>> {
async findMany(_options?: Partial<RepoQuery>): Promise<RepositoryResponse<TBD[TB][]>> {
const { qb, options } = this.buildQuery(_options);
//console.log("findMany:options", options);

View File

@@ -1,24 +1,15 @@
import type { PrimaryFieldType } from "core";
import { EntityIndex, type EntityManager } from "data";
import { type FileUploadedEventData, Storage, type StorageAdapter } from "media";
import { Module } from "modules/Module";
import {
type FieldSchema,
type InferFields,
type Schema,
boolean,
datetime,
entity,
json,
number,
text
} from "../data/prototype";
import { type FieldSchema, boolean, datetime, entity, json, number, text } from "../data/prototype";
import { MediaController } from "./api/MediaController";
import { ADAPTERS, buildMediaSchema, type mediaConfigSchema, registry } from "./media-schema";
export type MediaFieldSchema = FieldSchema<typeof AppMedia.mediaFields>;
declare global {
declare module "core" {
interface DB {
media: MediaFieldSchema;
media: { id: PrimaryFieldType } & MediaFieldSchema;
}
}
@@ -112,14 +103,14 @@ export class AppMedia extends Module<typeof mediaConfigSchema> {
return this.em.entity(entity_name);
}
get em(): EntityManager<DB> {
get em(): EntityManager {
return this.ctx.em;
}
private setupListeners() {
//const media = this._entity;
const { emgr, em } = this.ctx;
const media = this.getMediaEntity();
const media = this.getMediaEntity().name as "media";
// when file is uploaded, sync with media entity
// @todo: need a way for singleton events!
@@ -140,10 +131,10 @@ export class AppMedia extends Module<typeof mediaConfigSchema> {
Storage.Events.FileDeletedEvent,
async (e) => {
// simple file deletion sync
const item = await em.repo(media).findOne({ path: e.params.name });
if (item.data) {
console.log("item.data", item.data);
await em.mutator(media).deleteOne(item.data.id);
const { data } = await em.repo(media).findOne({ path: e.params.name });
if (data) {
console.log("item.data", data);
await em.mutator(media).deleteOne(data.id);
}
console.log("App:storage:file deleted", e);

View File

@@ -5,10 +5,10 @@ import type { Static, TSchema } from "core/utils";
import type { Connection, EntityManager } from "data";
import type { Hono } from "hono";
export type ModuleBuildContext<DB = any> = {
export type ModuleBuildContext = {
connection: Connection;
server: Hono<any>;
em: EntityManager<DB>;
em: EntityManager;
emgr: EventManager<any>;
guard: Guard;
};

View File

@@ -1,5 +1,5 @@
import { Guard } from "auth";
import { BkndError, DebugLogger, Exception, isDebug } from "core";
import { BkndError, DebugLogger } from "core";
import { EventManager } from "core/events";
import { clone, diff } from "core/object/diff";
import {
@@ -39,7 +39,7 @@ export type { ModuleBuildContext };
export const MODULES = {
server: AppServer,
data: AppData<any>,
data: AppData,
auth: AppAuth,
media: AppMedia,
flows: AppFlows
@@ -112,9 +112,9 @@ const __bknd = entity(TABLE_NAME, {
updated_at: datetime()
});
type ConfigTable2 = Schema<typeof __bknd>;
type T_INTERNAL_EM = {
interface T_INTERNAL_EM {
__bknd: ConfigTable2;
};
}
// @todo: cleanup old diffs on upgrade
// @todo: cleanup multiple backups on upgrade
@@ -123,7 +123,7 @@ export class ModuleManager {
// internal em for __bknd config table
__em!: EntityManager<T_INTERNAL_EM>;
// ctx for modules
em!: EntityManager<any>;
em!: EntityManager;
server!: Hono;
emgr!: EventManager;
guard!: Guard;

View File

@@ -1,37 +0,0 @@
import type { DataApi } from "data/api/DataApi";
import { useApi } from "ui/client";
type OmitFirstArg<F> = F extends (x: any, ...args: infer P) => any
? (...args: P) => ReturnType<F>
: never;
/**
* Maps all DataApi functions and omits
* the first argument "entity" for convenience
* @param entity
*/
export const useData = <T extends keyof DataApi<DB>>(entity: string) => {
const api = useApi().data;
const methods = [
"readOne",
"readMany",
"readManyByReference",
"createOne",
"updateOne",
"deleteOne"
] as const;
return methods.reduce(
(acc, method) => {
// @ts-ignore
acc[method] = (...params) => {
// @ts-ignore
return api[method](entity, ...params);
};
return acc;
},
{} as {
[K in (typeof methods)[number]]: OmitFirstArg<(typeof api)[K]>;
}
);
};

View File

@@ -1,4 +1,4 @@
import type { PrimaryFieldType } from "core";
import type { DB, PrimaryFieldType } from "core";
import { encodeSearch, objectTransform } from "core/utils";
import type { EntityData, RepoQuery } from "data";
import type { ModuleApi, ResponseObject } from "modules/ModuleApi";

View File

@@ -7,7 +7,6 @@ export {
} from "./ClientProvider";
export * from "./api/use-api";
export * from "./api/use-data";
export * from "./api/use-entity";
export { useAuth } from "./schema/auth/use-auth";
export { Api } from "../../Api";

View File

@@ -1,4 +1,4 @@
import { App } from "bknd";
import { Api, App } from "bknd";
import { serve } from "bknd/adapter/astro";
import { registerLocalMediaAdapter } from "bknd/adapter/node";
import { boolean, em, entity, text } from "bknd/data";
@@ -9,6 +9,20 @@ export const prerender = false;
// since we're running in node, we can register the local media adapter
registerLocalMediaAdapter();
// the em() function makes it easy to create an initial schema
const schema = em({
todos: entity("todos", {
title: text(),
done: boolean()
})
});
// register your schema to get automatic type completion
type Database = (typeof schema)["DB"];
declare module "bknd/core" {
interface DB extends Database {}
}
export const ALL = serve({
// we can use any libsql config, and if omitted, uses in-memory
connection: {
@@ -19,13 +33,7 @@ export const ALL = serve({
},
// an initial config is only applied if the database is empty
initialConfig: {
// the em() function makes it easy to create an initial schema
data: em({
todos: entity("todos", {
title: text(),
done: boolean()
})
}).toJSON(),
data: schema.toJSON(),
// we're enabling auth ...
auth: {
enabled: true,

View File

@@ -7,6 +7,19 @@ import { secureRandomString } from "bknd/utils";
// since we're running in node, we can register the local media adapter
registerLocalMediaAdapter();
const schema = em({
todos: entity("todos", {
title: text(),
done: boolean()
})
});
// register your schema to get automatic type completion
type Database = (typeof schema)["DB"];
declare module "bknd/core" {
interface DB extends Database {}
}
const handler = serve({
// we can use any libsql config, and if omitted, uses in-memory
connection: {
@@ -17,13 +30,7 @@ const handler = serve({
},
// an initial config is only applied if the database is empty
initialConfig: {
// the em() function makes it easy to create an initial schema
data: em({
todos: entity("todos", {
title: text(),
done: boolean()
})
}).toJSON(),
data: schema.toJSON(),
// we're enabling auth ...
auth: {
enabled: true,

View File

@@ -3,22 +3,22 @@ import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
declare module "@remix-run/node" {
interface Future {
v3_singleFetch: true;
}
interface Future {
v3_singleFetch: true;
}
}
export default defineConfig({
plugins: [
remix({
future: {
v3_fetcherPersist: true,
v3_relativeSplatPath: true,
v3_throwAbortReason: true,
v3_singleFetch: true,
v3_lazyRouteDiscovery: true,
},
}),
tsconfigPaths(),
],
plugins: [
remix({
future: {
v3_fetcherPersist: true,
v3_relativeSplatPath: true,
v3_throwAbortReason: true,
v3_singleFetch: true,
v3_lazyRouteDiscovery: true
}
}) as any,
tsconfigPaths()
]
});