From c1e92e503beee52971ab789a9e77ecca35dd27bf Mon Sep 17 00:00:00 2001 From: dswbx Date: Mon, 23 Dec 2024 19:28:31 +0100 Subject: [PATCH] update & fix typing, updated examples --- app/package.json | 2 +- app/src/Api.ts | 2 +- app/src/App.ts | 8 ++--- app/src/auth/AppAuth.ts | 12 +++++--- app/src/cli/commands/user.ts | 7 +++-- app/src/core/config.ts | 3 ++ app/src/core/index.ts | 2 +- app/src/data/AppData.ts | 4 +-- app/src/data/api/DataApi.ts | 3 +- app/src/data/entities/EntityManager.ts | 29 +++++++++++------- app/src/data/entities/Mutator.ts | 19 +++++++----- app/src/data/entities/query/Repository.ts | 16 +++++----- app/src/media/AppMedia.ts | 29 ++++++------------ app/src/modules/Module.ts | 4 +-- app/src/modules/ModuleManager.ts | 10 +++--- app/src/ui/client/api/use-data.ts | 37 ----------------------- app/src/ui/client/api/use-entity.ts | 2 +- app/src/ui/client/index.ts | 1 - examples/astro/src/pages/api/[...api].ts | 24 ++++++++++----- examples/remix/app/routes/api.$.ts | 21 ++++++++----- examples/remix/vite.config.ts | 30 +++++++++--------- 21 files changed, 126 insertions(+), 139 deletions(-) delete mode 100644 app/src/ui/client/api/use-data.ts diff --git a/app/package.json b/app/package.json index 4e188a1..c451ce8 100644 --- a/app/package.json +++ b/app/package.json @@ -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", diff --git a/app/src/Api.ts b/app/src/Api.ts index bf31449..5e288fe 100644 --- a/app/src/Api.ts +++ b/app/src/Api.ts @@ -38,7 +38,7 @@ export class Api { private token_transport: "header" | "cookie" | "none" = "header"; public system!: SystemApi; - public data!: DataApi; + public data!: DataApi; public auth!: AuthApi; public media!: MediaApi; diff --git a/app/src/App.ts b/app/src/App.ts index 45c6ef7..738ad50 100644 --- a/app/src/App.ts +++ b/app/src/App.ts @@ -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 = (app: App) => void; +export type AppPlugin = (app: App) => void; abstract class AppEvent extends Event<{ app: App } & A> {} export class AppConfigUpdatedEvent extends AppEvent { @@ -32,13 +32,13 @@ export type CreateAppConfig = { config: LibSqlCredentials; }; initialConfig?: InitialModuleConfigs; - plugins?: AppPlugin[]; + plugins?: AppPlugin[]; options?: Omit; }; export type AppConfig = InitialModuleConfigs; -export class App { +export class App { modules: ModuleManager; static readonly Events = AppEvents; adminController?: AdminController; @@ -47,7 +47,7 @@ export class App { constructor( private connection: Connection, _initialConfig?: InitialModuleConfigs, - private plugins: AppPlugin[] = [], + private plugins: AppPlugin[] = [], moduleManagerOptions?: ModuleManagerOptions ) { this.modules = new ModuleManager(connection, { diff --git a/app/src/auth/AppAuth.ts b/app/src/auth/AppAuth.ts index 9f6d901..c123290 100644 --- a/app/src/auth/AppAuth.ts +++ b/app/src/auth/AppAuth.ts @@ -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; -declare global { +declare module "core" { interface DB { - users: UserFieldSchema; + users: { id: PrimaryFieldType } & UserFieldSchema; } } @@ -101,7 +101,7 @@ export class AppAuth extends Module { return this._authenticator!; } - get em(): EntityManager { + get em(): EntityManager { return this.ctx.em as any; } @@ -161,7 +161,9 @@ export class AppAuth extends Module { 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); diff --git a/app/src/cli/commands/user.ts b/app/src/cli/commands/user.ts index cdf51a1..3a04d06 100644 --- a/app/src/cli/commands/user.ts +++ b/app/src/cli/commands/user.ts @@ -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({ diff --git a/app/src/core/config.ts b/app/src/core/config.ts index 445b67c..fadff04 100644 --- a/app/src/core/config.ts +++ b/app/src/core/config.ts @@ -5,6 +5,9 @@ import type { Generated } from "kysely"; export type PrimaryFieldType = number | Generated; +// biome-ignore lint/suspicious/noEmptyInterface: +export interface DB {} + export const config = { data: { default_primary_field: "id" diff --git a/app/src/core/index.ts b/app/src/core/index.ts index e296c1d..330e9fe 100644 --- a/app/src/core/index.ts +++ b/app/src/core/index.ts @@ -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, diff --git a/app/src/data/AppData.ts b/app/src/data/AppData.ts index 6f885f0..df90b57 100644 --- a/app/src/data/AppData.ts +++ b/app/src/data/AppData.ts @@ -11,7 +11,7 @@ import { Module } from "modules/Module"; import { DataController } from "./api/DataController"; import { type AppDataConfig, dataConfigSchema } from "./data-schema"; -export class AppData extends Module { +export class AppData extends Module { override async build() { const entities = transformObject(this.config.entities ?? {}, (entityConfig, name) => { return constructEntity(name, entityConfig); @@ -59,7 +59,7 @@ export class AppData extends Module { return dataConfigSchema; } - get em(): EntityManager { + get em(): EntityManager { this.throwIfNotBuilt(); return this.ctx.em; } diff --git a/app/src/data/api/DataApi.ts b/app/src/data/api/DataApi.ts index ad7c670..47144c2 100644 --- a/app/src/data/api/DataApi.ts +++ b/app/src/data/api/DataApi.ts @@ -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; }; -export class DataApi extends ModuleApi { +export class DataApi extends ModuleApi { protected override getDefaultOptions(): Partial { return { basepath: "/api/data", diff --git a/app/src/data/entities/EntityManager.ts b/app/src/data/entities/EntityManager.ts index d34c728..fea93aa 100644 --- a/app/src/data/entities/EntityManager.ts +++ b/app/src/data/entities/EntityManager.ts @@ -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 - ? Name extends keyof DB +type EntitySchema< + TBD extends object = DefaultDB, + E extends Entity | keyof TBD | string = string +> = E extends Entity + ? Name extends keyof TBD ? Name : never - : E extends keyof DB + : E extends keyof TBD ? E : never; -export class EntityManager { +export class EntityManager { connection: Connection; private _entities: Entity[] = []; @@ -58,7 +62,7 @@ export class EntityManager { * Forks the EntityManager without the EventManager. * This is useful when used inside an event handler. */ - fork(): EntityManager { + fork(): EntityManager { return new EntityManager(this._entities, this.connection, this._relations, this._indices); } @@ -95,16 +99,17 @@ export class EntityManager { 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 { return this.relations.relationReferencesOf(this.entity(entity_name)); } - repository(entity: E): Repository> { + repository( + entity: E + ): Repository> { return this.repo(entity); } - repo(entity: E): Repository> { + repo(entity: E): Repository> { return new Repository(this, this.entity(entity), this.emgr); } - mutator(entity: E): Mutator> { + mutator(entity: E): Mutator> { return new Mutator(this, this.entity(entity), this.emgr); } diff --git a/app/src/data/entities/Mutator.ts b/app/src/data/entities/Mutator.ts index cb25ddf..d9bff38 100644 --- a/app/src/data/entities/Mutator.ts +++ b/app/src/data/entities/Mutator.ts @@ -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 = { }; 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 > implements EmitsEvents { - em: EntityManager; + em: EntityManager; entity: Entity; static readonly Events = MutatorEvents; emgr: EventManager; @@ -43,7 +43,7 @@ export class Mutator< this.__unstable_disable_system_entity_creation = value; } - constructor(em: EntityManager, entity: Entity, emgr?: EventManager) { + constructor(em: EntityManager, entity: Entity, emgr?: EventManager) { 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> { + async updateOne(id: PrimaryFieldType, data: Partial): Promise> { 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, where?: RepoQuery["where"]): Promise> { + async updateWhere( + data: Partial, + where?: RepoQuery["where"] + ): Promise> { const entity = this.entity; const validatedData = await this.getValidatedData(data, "update"); diff --git a/app/src/data/entities/query/Repository.ts b/app/src/data/entities/query/Repository.ts index 4391b32..171fc3b 100644 --- a/app/src/data/entities/query/Repository.ts +++ b/app/src/data/entities/query/Repository.ts @@ -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 implements EmitsEvents { - em: EntityManager; +export class Repository + implements EmitsEvents +{ + em: EntityManager; entity: Entity; static readonly Events = RepositoryEvents; emgr: EventManager; - constructor(em: EntityManager, entity: Entity, emgr?: EventManager) { + constructor(em: EntityManager, entity: Entity, emgr?: EventManager) { this.em = em; this.entity = entity; this.emgr = emgr ?? new EventManager(MutatorEvents); @@ -272,7 +274,7 @@ export class Repository implements EmitsEve async findId( id: PrimaryFieldType, _options?: Partial> - ): Promise> { + ): Promise> { const { qb, options } = this.buildQuery( { ..._options, @@ -288,7 +290,7 @@ export class Repository implements EmitsEve async findOne( where: RepoQuery["where"], _options?: Partial> - ): Promise> { + ): Promise> { const { qb, options } = this.buildQuery({ ..._options, where, @@ -298,7 +300,7 @@ export class Repository implements EmitsEve return this.single(qb, options) as any; } - async findMany(_options?: Partial): Promise> { + async findMany(_options?: Partial): Promise> { const { qb, options } = this.buildQuery(_options); //console.log("findMany:options", options); diff --git a/app/src/media/AppMedia.ts b/app/src/media/AppMedia.ts index 1e8e692..789dae9 100644 --- a/app/src/media/AppMedia.ts +++ b/app/src/media/AppMedia.ts @@ -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; -declare global { +declare module "core" { interface DB { - media: MediaFieldSchema; + media: { id: PrimaryFieldType } & MediaFieldSchema; } } @@ -112,14 +103,14 @@ export class AppMedia extends Module { return this.em.entity(entity_name); } - get em(): EntityManager { + 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 { 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); diff --git a/app/src/modules/Module.ts b/app/src/modules/Module.ts index 704e420..32f098c 100644 --- a/app/src/modules/Module.ts +++ b/app/src/modules/Module.ts @@ -5,10 +5,10 @@ import type { Static, TSchema } from "core/utils"; import type { Connection, EntityManager } from "data"; import type { Hono } from "hono"; -export type ModuleBuildContext = { +export type ModuleBuildContext = { connection: Connection; server: Hono; - em: EntityManager; + em: EntityManager; emgr: EventManager; guard: Guard; }; diff --git a/app/src/modules/ModuleManager.ts b/app/src/modules/ModuleManager.ts index e5b6c9c..566384c 100644 --- a/app/src/modules/ModuleManager.ts +++ b/app/src/modules/ModuleManager.ts @@ -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, + data: AppData, auth: AppAuth, media: AppMedia, flows: AppFlows @@ -112,9 +112,9 @@ const __bknd = entity(TABLE_NAME, { updated_at: datetime() }); type ConfigTable2 = Schema; -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; // ctx for modules - em!: EntityManager; + em!: EntityManager; server!: Hono; emgr!: EventManager; guard!: Guard; diff --git a/app/src/ui/client/api/use-data.ts b/app/src/ui/client/api/use-data.ts deleted file mode 100644 index cd4ff22..0000000 --- a/app/src/ui/client/api/use-data.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { DataApi } from "data/api/DataApi"; -import { useApi } from "ui/client"; - -type OmitFirstArg = F extends (x: any, ...args: infer P) => any - ? (...args: P) => ReturnType - : never; - -/** - * Maps all DataApi functions and omits - * the first argument "entity" for convenience - * @param entity - */ -export const useData = >(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]>; - } - ); -}; diff --git a/app/src/ui/client/api/use-entity.ts b/app/src/ui/client/api/use-entity.ts index 89ff6e0..4900972 100644 --- a/app/src/ui/client/api/use-entity.ts +++ b/app/src/ui/client/api/use-entity.ts @@ -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"; diff --git a/app/src/ui/client/index.ts b/app/src/ui/client/index.ts index 792f884..9367294 100644 --- a/app/src/ui/client/index.ts +++ b/app/src/ui/client/index.ts @@ -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"; diff --git a/examples/astro/src/pages/api/[...api].ts b/examples/astro/src/pages/api/[...api].ts index 87ac62a..37fe73c 100644 --- a/examples/astro/src/pages/api/[...api].ts +++ b/examples/astro/src/pages/api/[...api].ts @@ -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, diff --git a/examples/remix/app/routes/api.$.ts b/examples/remix/app/routes/api.$.ts index 96d577e..6bd0452 100644 --- a/examples/remix/app/routes/api.$.ts +++ b/examples/remix/app/routes/api.$.ts @@ -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, diff --git a/examples/remix/vite.config.ts b/examples/remix/vite.config.ts index e4e8cef..3fa98cd 100644 --- a/examples/remix/vite.config.ts +++ b/examples/remix/vite.config.ts @@ -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() + ] });