diff --git a/.vscode/settings.json b/.vscode/settings.json index 7787afb..5c3e1c6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,7 +3,7 @@ "biome.enabled": true, "editor.defaultFormatter": "biomejs.biome", "editor.codeActionsOnSave": { - "source.organizeImports.biome": "explicit", + //"source.organizeImports.biome": "explicit", "source.fixAll.biome": "explicit" }, "typescript.preferences.importModuleSpecifier": "non-relative", diff --git a/app/__test__/app/modes.test.ts b/app/__test__/app/modes.test.ts new file mode 100644 index 0000000..034cf92 --- /dev/null +++ b/app/__test__/app/modes.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, test } from "bun:test"; +import { code, hybrid } from "modes"; + +describe("modes", () => { + describe("code", () => { + test("verify base configuration", async () => { + const c = code({}) as any; + const config = await c.app?.({} as any); + expect(Object.keys(config)).toEqual(["options"]); + expect(config.options.mode).toEqual("code"); + expect(config.options.plugins).toEqual([]); + expect(config.options.manager.skipValidation).toEqual(false); + expect(config.options.manager.onModulesBuilt).toBeDefined(); + }); + + test("keeps overrides", async () => { + const c = code({ + connection: { + url: ":memory:", + }, + }) as any; + const config = await c.app?.({} as any); + expect(config.connection.url).toEqual(":memory:"); + }); + }); + + describe("hybrid", () => { + test("fails if no reader is provided", () => { + // @ts-ignore + expect(hybrid({} as any).app?.({} as any)).rejects.toThrow(/reader/); + }); + test("verify base configuration", async () => { + const c = hybrid({ reader: async () => ({}) }) as any; + const config = await c.app?.({} as any); + expect(Object.keys(config)).toEqual(["reader", "beforeBuild", "config", "options"]); + expect(config.options.mode).toEqual("db"); + expect(config.options.plugins).toEqual([]); + expect(config.options.manager.skipValidation).toEqual(false); + expect(config.options.manager.onModulesBuilt).toBeDefined(); + }); + }); +}); diff --git a/app/src/adapter/bun/bun.adapter.ts b/app/src/adapter/bun/bun.adapter.ts index 51c9be3..c5d640d 100644 --- a/app/src/adapter/bun/bun.adapter.ts +++ b/app/src/adapter/bun/bun.adapter.ts @@ -43,6 +43,7 @@ export function createHandler( export function serve( { + app, distPath, connection, config: _config, @@ -62,6 +63,7 @@ export function serve( port, fetch: createHandler( { + app, connection, config: _config, options, diff --git a/app/src/adapter/cloudflare/proxy.ts b/app/src/adapter/cloudflare/proxy.ts index 9efd5c4..3476315 100644 --- a/app/src/adapter/cloudflare/proxy.ts +++ b/app/src/adapter/cloudflare/proxy.ts @@ -65,37 +65,31 @@ export function withPlatformProxy( } return { - ...config, - beforeBuild: async (app, registries) => { - if (!use_proxy) return; - const env = await getEnv(); - registerMedia(env, registries as any); - await config?.beforeBuild?.(app, registries); - }, - bindings: async (env) => { - return (await config?.bindings?.(await getEnv(env))) || {}; - }, // @ts-ignore app: async (_env) => { const env = await getEnv(_env); const binding = use_proxy ? getBinding(env, "D1Database") : undefined; + const appConfig = typeof config.app === "function" ? await config.app(env) : config; + const connection = + use_proxy && binding + ? d1Sqlite({ + binding: binding.value as any, + }) + : appConfig.connection; - if (config?.app === undefined && use_proxy && binding) { - return { - connection: d1Sqlite({ - binding: binding.value, - }), - }; - } else if (typeof config?.app === "function") { - const appConfig = await config?.app(env); - if (binding) { - appConfig.connection = d1Sqlite({ - binding: binding.value, - }) as any; - } - return appConfig; - } - return config?.app || {}; + return { + ...appConfig, + beforeBuild: async (app, registries) => { + if (!use_proxy) return; + const env = await getEnv(); + registerMedia(env, registries as any); + await config?.beforeBuild?.(app, registries); + }, + bindings: async (env) => { + return (await config?.bindings?.(await getEnv(env))) || {}; + }, + connection, + }; }, } satisfies CloudflareBkndConfig; } diff --git a/app/src/adapter/node/node.adapter.ts b/app/src/adapter/node/node.adapter.ts index 83feba8..d85d197 100644 --- a/app/src/adapter/node/node.adapter.ts +++ b/app/src/adapter/node/node.adapter.ts @@ -24,7 +24,7 @@ export async function createApp( path.resolve(distPath ?? relativeDistPath ?? "./node_modules/bknd/dist", "static"), ); if (relativeDistPath) { - console.warn("relativeDistPath is deprecated, please use distPath instead"); + $console.warn("relativeDistPath is deprecated, please use distPath instead"); } registerLocalMediaAdapter(); diff --git a/app/src/data/fields/JsonSchemaField.ts b/app/src/data/fields/JsonSchemaField.ts index fed47bf..1bc2bbc 100644 --- a/app/src/data/fields/JsonSchemaField.ts +++ b/app/src/data/fields/JsonSchemaField.ts @@ -26,7 +26,9 @@ export class JsonSchemaField< constructor(name: string, config: Partial) { super(name, config); - this.validator = new Validator({ ...this.getJsonSchema() }); + + // make sure to hand over clean json + this.validator = new Validator(JSON.parse(JSON.stringify(this.getJsonSchema()))); } protected getSchema() { diff --git a/app/src/modes/code.ts b/app/src/modes/code.ts index 30e4dc3..6c147a3 100644 --- a/app/src/modes/code.ts +++ b/app/src/modes/code.ts @@ -10,16 +10,19 @@ export type CodeMode = AdapterConfig extends B ? BkndModeConfig : never; -export function code(config: BkndCodeModeConfig): BkndConfig { +export function code< + Config extends BkndConfig, + Args = Config extends BkndConfig ? A : unknown, +>(codeConfig: CodeMode): BkndConfig { return { - ...config, + ...codeConfig, app: async (args) => { const { config: appConfig, plugins, isProd, syncSchemaOptions, - } = await makeModeConfig(config, args); + } = await makeModeConfig(codeConfig, args); if (appConfig?.options?.mode && appConfig?.options?.mode !== "code") { $console.warn("You should not set a different mode than `db` when using code mode"); diff --git a/app/src/modes/hybrid.ts b/app/src/modes/hybrid.ts index c7c1c37..ce72630 100644 --- a/app/src/modes/hybrid.ts +++ b/app/src/modes/hybrid.ts @@ -1,6 +1,6 @@ import type { BkndConfig } from "bknd/adapter"; import { makeModeConfig, type BkndModeConfig } from "./shared"; -import { getDefaultConfig, type MaybePromise, type ModuleConfigs, type Merge } from "bknd"; +import { getDefaultConfig, type MaybePromise, type Merge } from "bknd"; import type { DbModuleManager } from "modules/db/DbModuleManager"; import { invariant, $console } from "bknd/utils"; @@ -9,7 +9,7 @@ export type BkndHybridModeOptions = { * Reader function to read the configuration from the file system. * This is required for hybrid mode to work. */ - reader?: (path: string) => MaybePromise; + reader: (path: string) => MaybePromise; /** * Provided secrets to be merged into the configuration */ @@ -23,8 +23,12 @@ export type HybridMode = AdapterConfig extends ? BkndModeConfig> : never; -export function hybrid(hybridConfig: HybridBkndConfig): BkndConfig { +export function hybrid< + Config extends BkndConfig, + Args = Config extends BkndConfig ? A : unknown, +>(hybridConfig: HybridMode): BkndConfig { return { + ...hybridConfig, app: async (args) => { const { config: appConfig, @@ -40,16 +44,15 @@ export function hybrid(hybridConfig: HybridBkndConfig): BkndConfig = BkndConfig< Merge >; +function _isProd() { + try { + return process.env.NODE_ENV === "production"; + } catch (_e) { + return false; + } +} + export async function makeModeConfig< Args = any, Config extends BkndModeConfig = BkndModeConfig, @@ -69,25 +77,24 @@ export async function makeModeConfig< if (typeof config.isProduction !== "boolean") { $console.warn( - "You should set `isProduction` option when using managed modes to prevent accidental issues", + "You should set `isProduction` option when using managed modes to prevent accidental issues with writing plugins and syncing schema. As fallback, it is set to", + _isProd(), ); } - invariant( - typeof config.writer === "function", - "You must set the `writer` option when using managed modes", - ); + let needsWriter = false; const { typesFilePath, configFilePath, writer, syncSecrets: syncSecretsOptions } = config; - const isProd = config.isProduction; + const isProd = config.isProduction ?? _isProd(); const plugins = appConfig?.options?.plugins ?? ([] as AppPlugin[]); + const syncFallback = typeof config.syncSchema === "boolean" ? config.syncSchema : !isProd; const syncSchemaOptions = typeof config.syncSchema === "object" ? config.syncSchema : { - force: config.syncSchema !== false, - drop: true, + force: syncFallback, + drop: syncFallback, }; if (!isProd) { @@ -95,6 +102,7 @@ export async function makeModeConfig< if (plugins.some((p) => p.name === "bknd-sync-types")) { throw new Error("You have to unregister the `syncTypes` plugin"); } + needsWriter = true; plugins.push( syncTypes({ enabled: true, @@ -114,6 +122,7 @@ export async function makeModeConfig< if (plugins.some((p) => p.name === "bknd-sync-config")) { throw new Error("You have to unregister the `syncConfig` plugin"); } + needsWriter = true; plugins.push( syncConfig({ enabled: true, @@ -142,6 +151,7 @@ export async function makeModeConfig< .join("."); } + needsWriter = true; plugins.push( syncSecrets({ enabled: true, @@ -174,6 +184,10 @@ export async function makeModeConfig< } } + if (needsWriter && typeof config.writer !== "function") { + $console.warn("You must set a `writer` function, attempts to write will fail"); + } + return { config, isProd, diff --git a/app/src/modules/ModuleManager.ts b/app/src/modules/ModuleManager.ts index 8406eaa..706a8fd 100644 --- a/app/src/modules/ModuleManager.ts +++ b/app/src/modules/ModuleManager.ts @@ -223,7 +223,7 @@ export class ModuleManager { } extractSecrets() { - const moduleConfigs = structuredClone(this.configs()); + const moduleConfigs = JSON.parse(JSON.stringify(this.configs())); const secrets = { ...this.options?.secrets }; const extractedKeys: string[] = []; diff --git a/docs/content/docs/(documentation)/usage/introduction.mdx b/docs/content/docs/(documentation)/usage/introduction.mdx index adf6810..c290a12 100644 --- a/docs/content/docs/(documentation)/usage/introduction.mdx +++ b/docs/content/docs/(documentation)/usage/introduction.mdx @@ -213,9 +213,9 @@ To use it, you have to wrap your configuration in a mode helper, e.g. for `code` import { code, type CodeMode } from "bknd/modes"; import { type BunBkndConfig, writer } from "bknd/adapter/bun"; -const config = { +export default code({ // some normal bun bknd config - connection: { url: "file:test.db" }, + connection: { url: "file:data.db" }, // ... // a writer is required, to sync the types writer, @@ -227,9 +227,7 @@ const config = { force: true, drop: true, } -} satisfies CodeMode; - -export default code(config); +}); ``` Similarily, for `hybrid` mode: @@ -238,9 +236,9 @@ Similarily, for `hybrid` mode: import { hybrid, type HybridMode } from "bknd/modes"; import { type BunBkndConfig, writer, reader } from "bknd/adapter/bun"; -const config = { +export default hybrid({ // some normal bun bknd config - connection: { url: "file:test.db" }, + connection: { url: "file:data.db" }, // ... // reader/writer are required, to sync the types and config writer, @@ -262,7 +260,5 @@ const config = { force: true, drop: true, }, -} satisfies HybridMode; - -export default hybrid(config); +}); ``` \ No newline at end of file