diff --git a/app/__test__/api/Api.spec.ts b/app/__test__/api/Api.spec.ts index 4384928d..7da7fbc0 100644 --- a/app/__test__/api/Api.spec.ts +++ b/app/__test__/api/Api.spec.ts @@ -70,4 +70,10 @@ describe("Api", async () => { expect(params.token_transport).toBe("header"); expect(params.host).toBe("http://another.com"); }); + + it("should extract tokens case insensitive", async () => { + const token = await sign({ sub: "test" }, "1234"); + expect(new Api({ headers: new Headers({ Authorization: `Bearer ${token}` }) }).getAuthState().token).toBe(token); + expect(new Api({ headers: new Headers({ Authorization: `bearer ${token}` }) }).getAuthState().token).toBe(token); + }) }); diff --git a/app/__test__/auth/Authenticator.spec.ts b/app/__test__/auth/Authenticator.spec.ts index fcf8e5cb..171614dc 100644 --- a/app/__test__/auth/Authenticator.spec.ts +++ b/app/__test__/auth/Authenticator.spec.ts @@ -38,4 +38,27 @@ describe("Authenticator", async () => { expect(cookie).toStartWith("auth="); expect(cookie).toEndWith("HttpOnly; Secure; SameSite=Strict"); }); + + test("bearer token is case insensitive", async () => { + const auth = new Authenticator({}, null as any, { + jwt: { + secret: "secret", + fields: ["sub"], + }, + cookie: { + sameSite: "strict", + }, + }); + const token = await auth.jwt({ sub: "test" }); + + const res = await auth.resolveAuthFromRequest(new Headers({ + Authorization: `Bearer ${token}`, + })); + expect((res as any).sub).toBe("test") + + const res2 = await auth.resolveAuthFromRequest(new Headers({ + Authorization: `bearer ${token}`, + })); + expect((res2 as any).sub).toBe("test") + }) }); diff --git a/app/__test__/data/postgres.test.ts b/app/__test__/data/postgres.test.ts index 1560204d..58aa05bd 100644 --- a/app/__test__/data/postgres.test.ts +++ b/app/__test__/data/postgres.test.ts @@ -41,12 +41,12 @@ describe("postgres", () => { beforeAll(async () => { if (!(await isPostgresRunning())) { await $`docker run --rm --name bknd-test-postgres -d -e POSTGRES_PASSWORD=${credentials.password} -e POSTGRES_USER=${credentials.user} -e POSTGRES_DB=${credentials.database} -p ${credentials.port}:5432 postgres:17`; - await $waitUntil("Postgres is running", isPostgresRunning); + await $waitUntil("Postgres is running", isPostgresRunning, 500, 20); await new Promise((resolve) => setTimeout(resolve, 500)); } disableConsoleLog(); - }); + }, 30000); afterAll(async () => { if (await isPostgresRunning()) { try { diff --git a/app/__test__/modules/AppAuth.spec.ts b/app/__test__/modules/AppAuth.spec.ts index 59b0b1c4..15128c14 100644 --- a/app/__test__/modules/AppAuth.spec.ts +++ b/app/__test__/modules/AppAuth.spec.ts @@ -149,7 +149,7 @@ describe("AppAuth", () => { }); await app.build(); - app.registerAdminController(); + app.registerAdminController({ forceDev: true }); const spy = spyOn(app.module.auth.authenticator, "requestCookieRefresh"); // register custom route diff --git a/app/__test__/modules/migrations/migrations.spec.ts b/app/__test__/modules/migrations/migrations.spec.ts index 12667460..f68b462b 100644 --- a/app/__test__/modules/migrations/migrations.spec.ts +++ b/app/__test__/modules/migrations/migrations.spec.ts @@ -7,7 +7,9 @@ import v7 from "./samples/v7.json"; import v8 from "./samples/v8.json"; import v8_2 from "./samples/v8-2.json"; import v9 from "./samples/v9.json"; +import v10 from "./samples/v10.json"; import { disableConsoleLog, enableConsoleLog } from "core/utils/test"; +import { CURRENT_VERSION } from "modules/db/migrations"; beforeAll(() => disableConsoleLog()); afterAll(enableConsoleLog); @@ -61,7 +63,7 @@ async function getRawConfig( return await db .selectFrom("__bknd") .selectAll() - .$if(!!opts?.version, (qb) => qb.where("version", "=", opts?.version)) + .where("version", "=", opts?.version ?? CURRENT_VERSION) .$if((opts?.types?.length ?? 0) > 0, (qb) => qb.where("type", "in", opts?.types)) .execute(); } @@ -115,7 +117,6 @@ describe("Migrations", () => { "^^s3.secret_access_key^^", ); const [config, secrets] = (await getRawConfig(app, { - version: 10, types: ["config", "secrets"], })) as any; @@ -129,4 +130,15 @@ describe("Migrations", () => { "^^s3.secret_access_key^^", ); }); + + test("migration from 10 to 11", async () => { + expect(v10.version).toBe(10); + expect(v10.data.entities.test.fields.title.config.fillable).toEqual(["read", "update"]); + + const app = await createVersionedApp(v10); + + expect(app.version()).toBeGreaterThan(10); + const [config] = (await getRawConfig(app, { types: ["config"] })) as any; + expect(config.json.data.entities.test.fields.title.config.fillable).toEqual(true); + }); }); diff --git a/app/__test__/modules/migrations/samples/v10.json b/app/__test__/modules/migrations/samples/v10.json new file mode 100644 index 00000000..022ef2f1 --- /dev/null +++ b/app/__test__/modules/migrations/samples/v10.json @@ -0,0 +1,270 @@ +{ + "version": 10, + "server": { + "cors": { + "origin": "*", + "allow_methods": ["GET", "POST", "PATCH", "PUT", "DELETE"], + "allow_headers": [ + "Content-Type", + "Content-Length", + "Authorization", + "Accept" + ], + "allow_credentials": true + }, + "mcp": { + "enabled": true, + "path": "/api/system/mcp", + "logLevel": "warning" + } + }, + "data": { + "basepath": "/api/data", + "default_primary_format": "integer", + "entities": { + "test": { + "type": "regular", + "fields": { + "id": { + "type": "primary", + "config": { + "format": "integer", + "fillable": false, + "required": false + } + }, + "title": { + "type": "text", + "config": { + "required": false, + "fillable": ["read", "update"] + } + }, + "status": { + "type": "enum", + "config": { + "default_value": "INACTIVE", + "options": { + "type": "strings", + "values": ["INACTIVE", "SUBSCRIBED", "UNSUBSCRIBED"] + }, + "required": true, + "fillable": true + } + }, + "created_at": { + "type": "date", + "config": { + "type": "datetime", + "required": true, + "fillable": true + } + }, + "schema": { + "type": "jsonschema", + "config": { + "default_from_schema": true, + "schema": { + "type": "object", + "properties": { + "one": { + "type": "number", + "default": 1 + } + } + }, + "required": true, + "fillable": true + } + }, + "text": { + "type": "text", + "config": { + "required": false, + "fillable": true + } + } + }, + "config": { + "sort_field": "id", + "sort_dir": "asc" + } + }, + "items": { + "type": "regular", + "fields": { + "id": { + "type": "primary", + "config": { + "format": "integer", + "fillable": false, + "required": false + } + }, + "title": { + "type": "text", + "config": { + "required": false, + "fillable": true + } + } + }, + "config": { + "sort_field": "id", + "sort_dir": "asc" + } + }, + "media": { + "type": "system", + "fields": { + "id": { + "type": "primary", + "config": { + "format": "integer", + "fillable": false, + "required": false + } + }, + "path": { + "type": "text", + "config": { + "required": true, + "fillable": true + } + }, + "folder": { + "type": "boolean", + "config": { + "default_value": false, + "hidden": true, + "fillable": ["create"], + "required": false + } + }, + "mime_type": { + "type": "text", + "config": { + "required": false, + "fillable": true + } + }, + "size": { + "type": "number", + "config": { + "required": false, + "fillable": true + } + }, + "scope": { + "type": "text", + "config": { + "hidden": true, + "fillable": ["create"], + "required": false + } + }, + "etag": { + "type": "text", + "config": { + "required": false, + "fillable": true + } + }, + "modified_at": { + "type": "date", + "config": { + "type": "datetime", + "required": false, + "fillable": true + } + }, + "reference": { + "type": "text", + "config": { + "required": false, + "fillable": true + } + }, + "entity_id": { + "type": "text", + "config": { + "required": false, + "fillable": true + } + }, + "metadata": { + "type": "json", + "config": { + "required": false, + "fillable": true + } + } + }, + "config": { + "sort_field": "id", + "sort_dir": "asc" + } + } + }, + "relations": {}, + "indices": { + "idx_unique_media_path": { + "entity": "media", + "fields": ["path"], + "unique": true + }, + "idx_media_reference": { + "entity": "media", + "fields": ["reference"], + "unique": false + }, + "idx_media_entity_id": { + "entity": "media", + "fields": ["entity_id"], + "unique": false + } + } + }, + "auth": { + "enabled": false, + "basepath": "/api/auth", + "entity_name": "users", + "allow_register": true, + "jwt": { + "secret": "", + "alg": "HS256", + "fields": ["id", "email", "role"] + }, + "cookie": { + "path": "/", + "sameSite": "lax", + "secure": true, + "httpOnly": true, + "expires": 604800, + "partitioned": false, + "renew": true, + "pathSuccess": "/", + "pathLoggedOut": "/" + }, + "strategies": { + "password": { + "type": "password", + "enabled": true, + "config": { + "hashing": "sha256" + } + } + }, + "guard": { + "enabled": false + }, + "roles": {} + }, + "media": { + "enabled": false + }, + "flows": { + "basepath": "/api/flows", + "flows": {} + } +} diff --git a/app/__test__/ui/client/utils/AppReduced.spec.ts b/app/__test__/ui/client/utils/AppReduced.spec.ts index 58a7b3f8..f0471fc9 100644 --- a/app/__test__/ui/client/utils/AppReduced.spec.ts +++ b/app/__test__/ui/client/utils/AppReduced.spec.ts @@ -206,8 +206,7 @@ describe("AppReduced", () => { describe("withBasePath - double slash fix (admin_basepath with trailing slash)", () => { it("should not produce double slashes when admin_basepath has trailing slash", () => { const options: BkndAdminProps["config"] = { - basepath: "/", - admin_basepath: "/admin/", + basepath: "/admin", logo_return_path: "/", }; @@ -220,8 +219,7 @@ describe("AppReduced", () => { it("should work correctly when admin_basepath has no trailing slash", () => { const options: BkndAdminProps["config"] = { - basepath: "/", - admin_basepath: "/admin", + basepath: "/admin", logo_return_path: "/", }; @@ -233,8 +231,7 @@ describe("AppReduced", () => { it("should handle absolute paths with admin_basepath trailing slash", () => { const options: BkndAdminProps["config"] = { - basepath: "/", - admin_basepath: "/admin/", + basepath: "/admin", logo_return_path: "/", }; @@ -247,8 +244,7 @@ describe("AppReduced", () => { it("should handle settings path with admin_basepath trailing slash", () => { const options: BkndAdminProps["config"] = { - basepath: "/", - admin_basepath: "/admin/", + basepath: "/admin", logo_return_path: "/", }; diff --git a/app/build.ts b/app/build.ts index 1ab90eaa..23986b9d 100644 --- a/app/build.ts +++ b/app/build.ts @@ -335,11 +335,21 @@ async function buildAdapters() { platform: "node", }), + tsup.build({ + ...baseConfig("nuxt"), + platform: "node", + }), + tsup.build({ ...baseConfig("node"), platform: "node", }), + tsup.build({ + ...baseConfig("solid-start"), + platform: "node", + }), + tsup.build({ ...baseConfig("sqlite/edge"), entry: ["src/adapter/sqlite/edge.ts"], diff --git a/app/e2e/adapters.ts b/app/e2e/adapters.ts index 0dd009b6..5518e893 100644 --- a/app/e2e/adapters.ts +++ b/app/e2e/adapters.ts @@ -3,10 +3,11 @@ import path from "node:path"; import c from "picocolors"; const basePath = new URL(import.meta.resolve("../../")).pathname.slice(0, -1); +type RunOptions = Omit, "stdout" | "stderr">; async function run( cmd: string[] | string, - opts: Bun.SpawnOptions.OptionsObject & {}, + opts: RunOptions, onChunk: (chunk: string, resolve: (data: any) => void, reject: (err: Error) => void) => void, ): Promise<{ proc: Bun.Subprocess; data: any }> { return new Promise((resolve, reject) => { @@ -17,7 +18,7 @@ async function run( }); // Read from stdout - const reader = (proc.stdout as ReadableStream).getReader(); + const reader = proc.stdout.getReader(); const decoder = new TextDecoder(); // Function to read chunks diff --git a/app/e2e/inc/adapters.ts b/app/e2e/inc/adapters.ts index 30b647b0..c113b69f 100644 --- a/app/e2e/inc/adapters.ts +++ b/app/e2e/inc/adapters.ts @@ -15,6 +15,12 @@ const configs = { nextjs: { base_path: "/admin", }, + nuxt: { + base_path: "/admin", + }, + "solid-start": { + base_path: "/admin", + }, astro: { base_path: "/admin", }, diff --git a/app/package.json b/app/package.json index ceb3771e..aedc873a 100644 --- a/app/package.json +++ b/app/package.json @@ -3,7 +3,7 @@ "type": "module", "sideEffects": false, "bin": "./dist/cli/index.js", - "version": "0.20.0", + "version": "0.21.0-rc.1", "description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.", "homepage": "https://bknd.io", "repository": { @@ -60,7 +60,7 @@ "@xyflow/react": "^12.9.2", "aws4fetch": "^1.0.20", "bcryptjs": "^3.0.3", - "dayjs": "^1.11.19", + "dayjs": "^1.11.20", "fast-xml-parser": "^5.3.1", "hono": "4.10.4", "json-schema-library": "10.0.0-rc7", @@ -233,6 +233,11 @@ "import": "./dist/adapter/nextjs/index.js", "require": "./dist/adapter/nextjs/index.js" }, + "./adapter/nuxt": { + "types": "./dist/types/adapter/nuxt/index.d.ts", + "import": "./dist/adapter/nuxt/index.js", + "require": "./dist/adapter/nuxt/index.js" + }, "./adapter/react-router": { "types": "./dist/types/adapter/react-router/index.d.ts", "import": "./dist/adapter/react-router/index.js", @@ -258,6 +263,11 @@ "import": "./dist/adapter/sveltekit/index.js", "require": "./dist/adapter/sveltekit/index.js" }, + "./adapter/solid-start": { + "types": "./dist/types/adapter/solid-start/index.d.ts", + "import": "./dist/adapter/solid-start/index.js", + "require": "./dist/adapter/solid-start/index.js" + }, "./adapter/aws": { "types": "./dist/types/adapter/aws/index.d.ts", "import": "./dist/adapter/aws/index.js", @@ -287,11 +297,13 @@ "adapter/cloudflare": ["./dist/types/adapter/cloudflare/index.d.ts"], "adapter/vite": ["./dist/types/adapter/vite/index.d.ts"], "adapter/nextjs": ["./dist/types/adapter/nextjs/index.d.ts"], + "adapter/nuxt": ["./dist/types/adapter/nuxt/index.d.ts"], "adapter/react-router": ["./dist/types/adapter/react-router/index.d.ts"], "adapter/bun": ["./dist/types/adapter/bun/index.d.ts"], "adapter/node": ["./dist/types/adapter/node/index.d.ts"], "adapter/sveltekit": ["./dist/types/adapter/sveltekit/index.d.ts"], "adapter/tanstack-start": ["./dist/types/adapter/tanstack-start/index.d.ts"], + "adapter/solid-start": ["./dist/types/adapter/solid-start/index.d.ts"], "adapter/sqlite": ["./dist/types/adapter/sqlite/edge.d.ts"] } }, @@ -318,12 +330,14 @@ "serverless", "cloudflare", "nextjs", + "nuxt", "remix", "react-router", "astro", "sveltekit", "svelte", "bun", - "node" + "node", + "solid-start" ] } diff --git a/app/src/Api.ts b/app/src/Api.ts index adf69e6e..515d5ace 100644 --- a/app/src/Api.ts +++ b/app/src/Api.ts @@ -121,7 +121,7 @@ export class Api { } // try authorization header - const headerToken = this.options.headers.get("authorization")?.replace("Bearer ", ""); + const headerToken = this.options.headers.get("authorization")?.replace(/^Bearer\s+/i, ""); if (headerToken) { this.token_transport = "header"; this.updateToken(headerToken); diff --git a/app/src/adapter/nuxt/index.ts b/app/src/adapter/nuxt/index.ts new file mode 100644 index 00000000..c6920bbf --- /dev/null +++ b/app/src/adapter/nuxt/index.ts @@ -0,0 +1 @@ +export * from "./nuxt.adapter"; diff --git a/app/src/adapter/nuxt/nuxt.adapter.spec.ts b/app/src/adapter/nuxt/nuxt.adapter.spec.ts new file mode 100644 index 00000000..409771de --- /dev/null +++ b/app/src/adapter/nuxt/nuxt.adapter.spec.ts @@ -0,0 +1,15 @@ +import { afterAll, beforeAll, describe } from "bun:test"; +import * as nuxt from "./nuxt.adapter"; +import { disableConsoleLog, enableConsoleLog } from "core/utils"; +import { adapterTestSuite } from "adapter/adapter-test-suite"; +import { bunTestRunner } from "adapter/bun/test"; + +beforeAll(disableConsoleLog); +afterAll(enableConsoleLog); + +describe("nuxt adapter", () => { + adapterTestSuite(bunTestRunner, { + makeApp: nuxt.getApp, + makeHandler: nuxt.serve, + }); +}); diff --git a/app/src/adapter/nuxt/nuxt.adapter.ts b/app/src/adapter/nuxt/nuxt.adapter.ts new file mode 100644 index 00000000..426c735b --- /dev/null +++ b/app/src/adapter/nuxt/nuxt.adapter.ts @@ -0,0 +1,30 @@ +import { createRuntimeApp, type RuntimeBkndConfig } from "bknd/adapter"; + +export type NuxtEnv = NodeJS.ProcessEnv; +export type NuxtBkndConfig = RuntimeBkndConfig; + +/** + * Get bknd app instance + * @param config - bknd configuration + * @param args - environment variables + */ +export async function getApp( + config: NuxtBkndConfig = {} as NuxtBkndConfig, + args: Env, +) { + return await createRuntimeApp(config, args); +} + +/** + * Create middleware handler for Nuxt + * @param config - bknd configuration + * @param args - environment variables + */ +export function serve( + config: NuxtBkndConfig = {} as NuxtBkndConfig, + args: Env, +) { + return async (request: Request) => { + return (await getApp(config, args)).fetch(request); + }; +} diff --git a/app/src/adapter/solid-start/index.ts b/app/src/adapter/solid-start/index.ts new file mode 100644 index 00000000..62fe56f0 --- /dev/null +++ b/app/src/adapter/solid-start/index.ts @@ -0,0 +1 @@ +export * from "./solid-start.adapter"; diff --git a/app/src/adapter/solid-start/solid-start.adapter.spec.ts b/app/src/adapter/solid-start/solid-start.adapter.spec.ts new file mode 100644 index 00000000..0149a258 --- /dev/null +++ b/app/src/adapter/solid-start/solid-start.adapter.spec.ts @@ -0,0 +1,16 @@ +import { afterAll, beforeAll, describe } from "bun:test"; +import * as solidStart from "./solid-start.adapter"; +import { disableConsoleLog, enableConsoleLog } from "core/utils"; +import { adapterTestSuite } from "adapter/adapter-test-suite"; +import { bunTestRunner } from "adapter/bun/test"; +import type { SolidStartBkndConfig } from "./solid-start.adapter"; + +beforeAll(disableConsoleLog); +afterAll(enableConsoleLog); + +describe("solid-start adapter", () => { + adapterTestSuite(bunTestRunner, { + makeApp: solidStart.getApp, + makeHandler: solidStart.serve, + }); +}); diff --git a/app/src/adapter/solid-start/solid-start.adapter.ts b/app/src/adapter/solid-start/solid-start.adapter.ts new file mode 100644 index 00000000..8ada7f62 --- /dev/null +++ b/app/src/adapter/solid-start/solid-start.adapter.ts @@ -0,0 +1,31 @@ +import { createRuntimeApp, type RuntimeBkndConfig } from "bknd/adapter"; + +export type SolidStartEnv = NodeJS.ProcessEnv; +export type SolidStartBkndConfig = RuntimeBkndConfig; + +/** + * Get bknd app instance + * @param config - bknd configuration + * @param args - environment variables + */ +export async function getApp( + config: SolidStartBkndConfig, + args: Env = process.env as Env, +) { + return await createRuntimeApp(config, args); +} + +/** + * Create middleware handler for Solid Start + * @param config - bknd configuration + * @param args - environment variables + */ +export function serve( + config: SolidStartBkndConfig = {}, + args: Env = process.env as Env, +) { + return async (req: Request) => { + const app = await getApp(config, args); + return app.fetch(req); + }; +} diff --git a/app/src/auth/authenticate/Authenticator.ts b/app/src/auth/authenticate/Authenticator.ts index 03f13e9e..d0cb083f 100644 --- a/app/src/auth/authenticate/Authenticator.ts +++ b/app/src/auth/authenticate/Authenticator.ts @@ -430,7 +430,7 @@ export class Authenticator< let token: string | undefined; if (headers.has("Authorization")) { const bearerHeader = String(headers.get("Authorization")); - token = bearerHeader.replace("Bearer ", ""); + token = bearerHeader.replace(/^Bearer\s+/i, ""); } else { const context = is_context ? (c as Context) : ({ req: { raw: { headers } } } as Context); token = await this.getAuthCookie(context); diff --git a/app/src/data/entities/Entity.ts b/app/src/data/entities/Entity.ts index fcbe0929..db7e6f4e 100644 --- a/app/src/data/entities/Entity.ts +++ b/app/src/data/entities/Entity.ts @@ -136,8 +136,10 @@ export class Entity< .map((field) => (alias ? `${alias}.${field.name} as ${field.name}` : field.name)); } - getFillableFields(context?: TActionContext, include_virtual?: boolean): Field[] { - return this.getFields(include_virtual).filter((field) => field.isFillable(context)); + getFillableFields(context?: "create" | "update", include_virtual?: boolean): Field[] { + return this.getFields({ virtual: include_virtual }).filter((field) => + field.isFillable(context), + ); } getRequiredFields(): Field[] { @@ -189,9 +191,15 @@ export class Entity< return this.fields.findIndex((field) => field.name === name) !== -1; } - getFields(include_virtual: boolean = false): Field[] { - if (include_virtual) return this.fields; - return this.fields.filter((f) => !f.isVirtual()); + getFields({ + virtual = false, + primary = true, + }: { virtual?: boolean; primary?: boolean } = {}): Field[] { + return this.fields.filter((f) => { + if (!virtual && f.isVirtual()) return false; + if (!primary && f instanceof PrimaryField) return false; + return true; + }); } addField(field: Field) { @@ -231,7 +239,7 @@ export class Entity< } } - const fields = this.getFillableFields(context, false); + const fields = this.getFillableFields(context as any, false); if (options?.ignoreUnknown !== true) { const field_names = fields.map((f) => f.name); @@ -275,7 +283,7 @@ export class Entity< fields = this.getFillableFields(options.context); break; default: - fields = this.getFields(true); + fields = this.getFields({ virtual: true }); } const _fields = Object.fromEntries(fields.map((field) => [field.name, field])); diff --git a/app/src/data/entities/mutation/Mutator.ts b/app/src/data/entities/mutation/Mutator.ts index 84389c3d..e4bb6f0e 100644 --- a/app/src/data/entities/mutation/Mutator.ts +++ b/app/src/data/entities/mutation/Mutator.ts @@ -83,8 +83,10 @@ export class Mutator< } // we should never get here, but just to be sure (why?) - if (!field.isFillable(context)) { - throw new Error(`Field "${key}" is not fillable on entity "${entity.name}"`); + if (!field.isFillable(context as any)) { + throw new Error( + `Field "${key}" of entity "${entity.name}" is not fillable on context "${context}"`, + ); } // transform from field diff --git a/app/src/data/fields/Field.ts b/app/src/data/fields/Field.ts index 98a1f45d..8451d1df 100644 --- a/app/src/data/fields/Field.ts +++ b/app/src/data/fields/Field.ts @@ -26,11 +26,19 @@ export const baseFieldConfigSchema = s .strictObject({ label: s.string(), description: s.string(), - required: s.boolean({ default: false }), - fillable: s.anyOf([ - s.boolean({ title: "Boolean" }), - s.array(s.string({ enum: ActionContext }), { title: "Context", uniqueItems: true }), - ]), + required: s.boolean({ default: DEFAULT_REQUIRED }), + fillable: s.anyOf( + [ + s.boolean({ title: "Boolean" }), + s.array(s.string({ enum: ["create", "update"] }), { + title: "Context", + uniqueItems: true, + }), + ], + { + default: DEFAULT_FILLABLE, + }, + ), hidden: s.anyOf([ s.boolean({ title: "Boolean" }), // @todo: tmp workaround @@ -103,7 +111,7 @@ export abstract class Field< return this.config?.default_value; } - isFillable(context?: TActionContext): boolean { + isFillable(context?: "create" | "update"): boolean { if (Array.isArray(this.config.fillable)) { return context ? this.config.fillable.includes(context) : DEFAULT_FILLABLE; } @@ -165,7 +173,7 @@ export abstract class Field< // @todo: add field level validation isValid(value: any, context: TActionContext): boolean { if (typeof value !== "undefined") { - return this.isFillable(context); + return this.isFillable(context as any); } else if (context === "create") { return !this.isRequired(); } diff --git a/app/src/data/fields/field-test-suite.ts b/app/src/data/fields/field-test-suite.ts index 369d41ba..bf81fbd3 100644 --- a/app/src/data/fields/field-test-suite.ts +++ b/app/src/data/fields/field-test-suite.ts @@ -99,6 +99,7 @@ export function fieldTestSuite( const _config = { ..._requiredConfig, required: false, + fillable: true, }; function fieldJson(field: Field) { @@ -116,10 +117,7 @@ export function fieldTestSuite( expect(fieldJson(fillable)).toEqual({ type: noConfigField.type, - config: { - ..._config, - fillable: true, - }, + config: _config, }); expect(fieldJson(required)).toEqual({ @@ -150,7 +148,6 @@ export function fieldTestSuite( type: requiredAndDefault.type, config: { ..._config, - fillable: true, required: true, default_value: config.defaultValue, }, diff --git a/app/src/data/helper.ts b/app/src/data/helper.ts index 12120d83..0acc652a 100644 --- a/app/src/data/helper.ts +++ b/app/src/data/helper.ts @@ -24,7 +24,8 @@ export function getChangeSet( (acc, _value, key) => { const field = fields.find((f) => f.name === key); // @todo: filtering virtual here, need to check (because of media) - if (!field || field.isVirtual()) return; + // @note: adding non-fillable here as it'll fail validaiton on backend + if (!field || field.isVirtual() || !field.isFillable()) return; const value = _value === "" ? null : _value; // normalize to null if undefined diff --git a/app/src/data/schema/SchemaManager.ts b/app/src/data/schema/SchemaManager.ts index 8d6135a4..e10bc7cd 100644 --- a/app/src/data/schema/SchemaManager.ts +++ b/app/src/data/schema/SchemaManager.ts @@ -77,7 +77,7 @@ export class SchemaManager { } getIntrospectionFromEntity(entity: Entity): IntrospectedTable { - const fields = entity.getFields(false); + const fields = entity.getFields({ virtual: false }); const indices = this.em.getIndicesOf(entity); // this is intentionally setting values to defaults, like "nullable" and "default" diff --git a/app/src/modules/db/migrations.ts b/app/src/modules/db/migrations.ts index 13f39eea..e97071b4 100644 --- a/app/src/modules/db/migrations.ts +++ b/app/src/modules/db/migrations.ts @@ -1,6 +1,7 @@ import { transformObject } from "bknd/utils"; import type { Kysely } from "kysely"; import { set } from "lodash-es"; +import type { InitialModuleConfigs } from "modules/ModuleManager"; export type MigrationContext = { db: Kysely; @@ -107,6 +108,29 @@ export const migrations: Migration[] = [ return config; }, }, + { + // change field.config.fillable to only "create" and "update" + version: 11, + up: async (config: InitialModuleConfigs) => { + const { data, ...rest } = config; + return { + ...rest, + data: { + ...data, + entities: transformObject(data?.entities ?? {}, (entity) => { + return { + ...entity, + fields: transformObject(entity?.fields ?? {}, (field) => { + const fillable = field!.config?.fillable; + if (!fillable || typeof fillable === "boolean") return field; + return { ...field, config: { ...field!.config, fillable: true } }; + }), + }; + }), + }, + }; + }, + }, ]; export const CURRENT_VERSION = migrations[migrations.length - 1]?.version ?? 0; diff --git a/app/src/modules/server/AdminController.tsx b/app/src/modules/server/AdminController.tsx index d7592c58..8c12d398 100644 --- a/app/src/modules/server/AdminController.tsx +++ b/app/src/modules/server/AdminController.tsx @@ -210,8 +210,9 @@ export class AdminController extends Controller { }, }).then((res) => res.json()); } else { + const manifestPath = "bknd/dist/manifest.json"; // @ts-ignore - manifest = await import("bknd/dist/manifest.json", { + manifest = await import(/* @vite-ignore */ manifestPath, { with: { type: "json" }, }).then((res) => res.default); } diff --git a/app/src/plugins/data/sort.plugin.spec.ts b/app/src/plugins/data/sort.plugin.spec.ts new file mode 100644 index 00000000..c455b3f7 --- /dev/null +++ b/app/src/plugins/data/sort.plugin.spec.ts @@ -0,0 +1,858 @@ +import { describe, test, expect, beforeAll, afterAll } from "bun:test"; +import { sort } from "./sort.plugin"; +import { em, entity, text, number } from "bknd"; +import { createApp } from "core/test/utils"; +import { disableConsoleLog, enableConsoleLog } from "core/utils/test"; + +beforeAll(() => disableConsoleLog()); +afterAll(enableConsoleLog); + +describe("sort plugin", () => { + test("should add sort field to configured entities", async () => { + const app = createApp({ + config: { + data: em({ + tasks: entity("tasks", { + title: text(), + }), + }).toJSON(), + }, + options: { + plugins: [ + sort({ + entities: { + tasks: { + field: "position", + }, + }, + }), + ], + }, + }); + await app.build(); + + const taskEntity = app.em.entity("tasks"); + expect(taskEntity).toBeDefined(); + expect(taskEntity?.fields.map((f) => f.name)).toContain("position"); + expect(taskEntity?.field("position")?.type).toBe("number"); + }); + + test("should auto-assign sort values on insert (starting from 0)", async () => { + const app = createApp({ + config: { + data: em({ + tasks: entity("tasks", { + title: text(), + }), + }).toJSON(), + }, + options: { + plugins: [ + sort({ + entities: { + tasks: { + field: "position", + }, + }, + }), + ], + }, + }); + await app.build(); + + const mutator = app.em.mutator("tasks"); + + // insert first item + const { data: task1 } = await mutator.insertOne({ title: "Task 1" }); + expect(task1.position).toBe(0); + + // insert second item + const { data: task2 } = await mutator.insertOne({ title: "Task 2" }); + expect(task2.position).toBe(1); + + // insert third item + const { data: task3 } = await mutator.insertOne({ title: "Task 3" }); + expect(task3.position).toBe(2); + }); + + test("should preserve manually set sort values on insert", async () => { + const app = createApp({ + config: { + data: em({ + tasks: entity("tasks", { + title: text(), + }), + }).toJSON(), + }, + options: { + plugins: [ + sort({ + entities: { + tasks: { + field: "position", + }, + }, + }), + ], + }, + }); + await app.build(); + + const mutator = app.em.mutator("tasks"); + + // insert with explicit position + const { data: task } = await mutator.insertOne({ title: "Task 1", position: 10 }); + expect(task.position).toBe(10); + }); + + test("should reorder items via API endpoint", async () => { + const app = createApp({ + config: { + data: em({ + tasks: entity("tasks", { + title: text(), + }), + }).toJSON(), + }, + options: { + plugins: [ + sort({ + entities: { + tasks: { + field: "position", + }, + }, + }), + ], + }, + }); + await app.build(); + + const mutator = app.em.mutator("tasks"); + + // create tasks at positions 0, 1, 2 + const { data: task1 } = await mutator.insertOne({ title: "Task 1" }); + const { data: task2 } = await mutator.insertOne({ title: "Task 2" }); + const { data: task3 } = await mutator.insertOne({ title: "Task 3" }); + + // move task3 (position 2) to position 0 + const res = await app.server.request("/api/sort/tasks/reorder", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ id: task3.id, position: 0 }), + }); + + expect(res.status).toBe(200); + + // verify positions + const repo = app.em.repo("tasks"); + const { data: updatedTask1 } = await repo.findOne({ id: task1.id }); + const { data: updatedTask2 } = await repo.findOne({ id: task2.id }); + const { data: updatedTask3 } = await repo.findOne({ id: task3.id }); + + expect(updatedTask3.position).toBe(0); // moved to position 0 + expect(updatedTask1.position).toBe(1); // shifted down + expect(updatedTask2.position).toBe(2); // shifted down + }); + + test("should automatically reorder when updating sort field directly", async () => { + const app = createApp({ + config: { + data: em({ + tasks: entity("tasks", { + title: text(), + }), + }).toJSON(), + }, + options: { + plugins: [ + sort({ + entities: { + tasks: { + field: "position", + }, + }, + }), + ], + }, + }); + await app.build(); + + const mutator = app.em.mutator("tasks"); + + // create tasks at positions 0, 1, 2, 3 + const { data: task1 } = await mutator.insertOne({ title: "Task 1" }); + const { data: task2 } = await mutator.insertOne({ title: "Task 2" }); + const { data: task3 } = await mutator.insertOne({ title: "Task 3" }); + const { data: task4 } = await mutator.insertOne({ title: "Task 4" }); + + // move task4 (position 3) to position 1 by updating directly + await mutator.updateOne(task4.id, { position: 1 }); + + // verify positions + const repo = app.em.repo("tasks"); + const { data: updatedTask1 } = await repo.findOne({ id: task1.id }); + const { data: updatedTask2 } = await repo.findOne({ id: task2.id }); + const { data: updatedTask3 } = await repo.findOne({ id: task3.id }); + const { data: updatedTask4 } = await repo.findOne({ id: task4.id }); + + expect(updatedTask1.position).toBe(0); // unchanged + expect(updatedTask2.position).toBe(2); // shifted down + expect(updatedTask3.position).toBe(3); // shifted down + expect(updatedTask4.position).toBe(1); // moved to position 1 + }); + + test("should automatically reorder when moving items up", async () => { + const app = createApp({ + config: { + data: em({ + tasks: entity("tasks", { + title: text(), + }), + }).toJSON(), + }, + options: { + plugins: [ + sort({ + entities: { + tasks: { + field: "position", + }, + }, + }), + ], + }, + }); + await app.build(); + + const mutator = app.em.mutator("tasks"); + + // create tasks at positions 0, 1, 2, 3 + const { data: task1 } = await mutator.insertOne({ title: "Task 1" }); + const { data: task2 } = await mutator.insertOne({ title: "Task 2" }); + const { data: task3 } = await mutator.insertOne({ title: "Task 3" }); + const { data: task4 } = await mutator.insertOne({ title: "Task 4" }); + + // move task1 (position 0) to position 2 by updating directly + await mutator.updateOne(task1.id, { position: 2 }); + + // verify positions + const repo = app.em.repo("tasks"); + const { data: updatedTask1 } = await repo.findOne({ id: task1.id }); + const { data: updatedTask2 } = await repo.findOne({ id: task2.id }); + const { data: updatedTask3 } = await repo.findOne({ id: task3.id }); + const { data: updatedTask4 } = await repo.findOne({ id: task4.id }); + + expect(updatedTask1.position).toBe(2); // moved to position 2 + expect(updatedTask2.position).toBe(0); // shifted up + expect(updatedTask3.position).toBe(1); // shifted up + expect(updatedTask4.position).toBe(3); // unchanged + }); + + test("should support scoped sorting", async () => { + const app = createApp({ + config: { + data: em({ + tasks: entity("tasks", { + title: text(), + project_id: number(), + }), + }).toJSON(), + }, + options: { + plugins: [ + sort({ + entities: { + tasks: { + field: "position", + scope: "project_id", + }, + }, + }), + ], + }, + }); + await app.build(); + + const mutator = app.em.mutator("tasks"); + + // create tasks in project 1 + const { data: p1t1 } = await mutator.insertOne({ + title: "P1 Task 1", + project_id: 1, + }); + const { data: p1t2 } = await mutator.insertOne({ + title: "P1 Task 2", + project_id: 1, + }); + + // create tasks in project 2 + const { data: p2t1 } = await mutator.insertOne({ + title: "P2 Task 1", + project_id: 2, + }); + const { data: p2t2 } = await mutator.insertOne({ + title: "P2 Task 2", + project_id: 2, + }); + + // positions should be scoped per project + expect(p1t1.position).toBe(0); + expect(p1t2.position).toBe(1); + expect(p2t1.position).toBe(0); // resets for new scope + expect(p2t2.position).toBe(1); + }); + + test("should reorder only within scope", async () => { + const app = createApp({ + config: { + data: em({ + tasks: entity("tasks", { + title: text(), + project_id: number(), + }), + }).toJSON(), + }, + options: { + plugins: [ + sort({ + entities: { + tasks: { + field: "position", + scope: "project_id", + }, + }, + }), + ], + }, + }); + await app.build(); + + const mutator = app.em.mutator("tasks"); + + // create tasks in project 1 + const { data: p1t1 } = await mutator.insertOne({ + title: "P1 Task 1", + project_id: 1, + }); + const { data: p1t2 } = await mutator.insertOne({ + title: "P1 Task 2", + project_id: 1, + }); + const { data: p1t3 } = await mutator.insertOne({ + title: "P1 Task 3", + project_id: 1, + }); + + // create tasks in project 2 + const { data: p2t1 } = await mutator.insertOne({ + title: "P2 Task 1", + project_id: 2, + }); + const { data: p2t2 } = await mutator.insertOne({ + title: "P2 Task 2", + project_id: 2, + }); + + // move p1t3 to position 0 (should only affect project 1) + await app.server.request("/api/sort/tasks/reorder", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ id: p1t3.id, position: 0 }), + }); + + // verify project 1 tasks + const repo = app.em.repo("tasks"); + const { data: updatedP1t1 } = await repo.findOne({ id: p1t1.id }); + const { data: updatedP1t2 } = await repo.findOne({ id: p1t2.id }); + const { data: updatedP1t3 } = await repo.findOne({ id: p1t3.id }); + + expect(updatedP1t3.position).toBe(0); // moved to position 0 + expect(updatedP1t1.position).toBe(1); // shifted + expect(updatedP1t2.position).toBe(2); // shifted + + // verify project 2 tasks are unchanged + const { data: updatedP2t1 } = await repo.findOne({ id: p2t1.id }); + const { data: updatedP2t2 } = await repo.findOne({ id: p2t2.id }); + + expect(updatedP2t1.position).toBe(0); // unchanged + expect(updatedP2t2.position).toBe(1); // unchanged + }); + + test("should recalculate all positions", async () => { + const app = createApp({ + config: { + data: em({ + tasks: entity("tasks", { + title: text(), + }), + }).toJSON(), + }, + options: { + plugins: [ + sort({ + entities: { + tasks: { + field: "position", + }, + }, + }), + ], + }, + }); + await app.build(); + + const mutator = app.em.mutator("tasks"); + + // create tasks with irregular positions + await mutator.insertOne({ title: "Task 1", position: 5 }); + await mutator.insertOne({ title: "Task 2", position: 10 }); + await mutator.insertOne({ title: "Task 3", position: 15 }); + await mutator.insertOne({ title: "Task 4", position: 100 }); + + // recalculate + const res = await app.server.request("/api/sort/tasks/recalculate", { + method: "POST", + body: JSON.stringify({}), + headers: { + "Content-Type": "application/json", + }, + }); + + expect(res.status).toBe(200); + + // verify positions are now 0, 1, 2, 3 + const { data: tasks } = await app.em.repo("tasks").findMany({ + orderBy: [{ position: "asc" }], + }); + + expect(tasks.length).toBe(4); + expect(tasks[0].position).toBe(0); + expect(tasks[1].position).toBe(1); + expect(tasks[2].position).toBe(2); + expect(tasks[3].position).toBe(3); + }); + + test("should recalculate positions within scope only", async () => { + const app = createApp({ + config: { + data: em({ + tasks: entity("tasks", { + title: text(), + project_id: number(), + }), + }).toJSON(), + }, + options: { + plugins: [ + sort({ + entities: { + tasks: { + field: "position", + scope: "project_id", + }, + }, + }), + ], + }, + }); + await app.build(); + + const mutator = app.em.mutator("tasks"); + + // create tasks in project 1 with irregular positions + await mutator.insertOne({ title: "P1 Task 1", project_id: 1, position: 5 }); + await mutator.insertOne({ title: "P1 Task 2", project_id: 1, position: 15 }); + + // create tasks in project 2 with irregular positions + await mutator.insertOne({ title: "P2 Task 1", project_id: 2, position: 10 }); + await mutator.insertOne({ title: "P2 Task 2", project_id: 2, position: 20 }); + + // recalculate only project 1 + const res = await app.server.request("/api/sort/tasks/recalculate", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ scope: 1 }), + }); + + expect(res.status).toBe(200); + + // verify project 1 tasks are recalculated + const { data: p1Tasks } = await app.em.repo("tasks").findMany({ + where: { project_id: 1 }, + orderBy: [{ position: "asc" }], + }); + + expect(p1Tasks.length).toBe(2); + expect(p1Tasks[0].position).toBe(0); + expect(p1Tasks[1].position).toBe(1); + + // verify project 2 tasks are unchanged + const { data: p2Tasks } = await app.em.repo("tasks").findMany({ + where: { project_id: 2 }, + orderBy: [{ position: "asc" }], + }); + + expect(p2Tasks.length).toBe(2); + expect(p2Tasks[0].position).toBe(10); // unchanged + expect(p2Tasks[1].position).toBe(20); // unchanged + }); + + test("should handle moving items to the end", async () => { + const app = createApp({ + config: { + data: em({ + tasks: entity("tasks", { + title: text(), + }), + }).toJSON(), + }, + options: { + plugins: [ + sort({ + entities: { + tasks: { + field: "position", + }, + }, + }), + ], + }, + }); + await app.build(); + + const mutator = app.em.mutator("tasks"); + + // create tasks at positions 0, 1, 2, 3 + const { data: task1 } = await mutator.insertOne({ title: "Task 1" }); + const { data: task2 } = await mutator.insertOne({ title: "Task 2" }); + const { data: task3 } = await mutator.insertOne({ title: "Task 3" }); + const { data: task4 } = await mutator.insertOne({ title: "Task 4" }); + + // move task1 to the end (position 3) + await app.server.request("/api/sort/tasks/reorder", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ id: task1.id, position: 3 }), + }); + + // verify positions + const repo = app.em.repo("tasks"); + const { data: updatedTask1 } = await repo.findOne({ id: task1.id }); + const { data: updatedTask2 } = await repo.findOne({ id: task2.id }); + const { data: updatedTask3 } = await repo.findOne({ id: task3.id }); + const { data: updatedTask4 } = await repo.findOne({ id: task4.id }); + + expect(updatedTask1.position).toBe(3); // moved to end + expect(updatedTask2.position).toBe(0); // shifted up + expect(updatedTask3.position).toBe(1); // shifted up + expect(updatedTask4.position).toBe(2); // shifted up + }); + + test("should return 400 for invalid item id", async () => { + const app = createApp({ + config: { + data: em({ + tasks: entity("tasks", { + title: text(), + }), + }).toJSON(), + }, + options: { + plugins: [ + sort({ + entities: { + tasks: { + field: "position", + }, + }, + }), + ], + }, + }); + await app.build(); + + const res = await app.server.request("/api/sort/tasks/reorder", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ id: 999999, position: 0 }), + }); + + expect(res.status).toBe(400); + }); + + test("should handle multiple entities with different configurations", async () => { + const app = createApp({ + config: { + data: em({ + tasks: entity("tasks", { + title: text(), + }), + categories: entity("categories", { + name: text(), + }), + }).toJSON(), + }, + options: { + plugins: [ + sort({ + entities: { + tasks: { + field: "position", + }, + categories: { + field: "order", + }, + }, + }), + ], + }, + }); + await app.build(); + + // verify both entities have their sort fields + const taskEntity = app.em.entity("tasks"); + const categoryEntity = app.em.entity("categories"); + + expect(taskEntity?.fields.map((f) => f.name)).toContain("position"); + expect(categoryEntity?.fields.map((f) => f.name)).toContain("order"); + + // create items in both entities + const { data: task } = await app.em.mutator("tasks").insertOne({ title: "Task 1" }); + const { data: category } = await app.em.mutator("categories").insertOne({ name: "Cat 1" }); + + expect(task.position).toBe(0); + expect(category.order).toBe(0); + + // verify both endpoints exist + const taskRes = await app.server.request("/api/sort/tasks/recalculate", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({}), + }); + const catRes = await app.server.request("/api/sort/categories/recalculate", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({}), + }); + + expect(taskRes.status).toBe(200); + expect(catRes.status).toBe(200); + }); + + test("should not trigger reorder when updating other fields", async () => { + const app = createApp({ + config: { + data: em({ + tasks: entity("tasks", { + title: text(), + }), + }).toJSON(), + }, + options: { + plugins: [ + sort({ + entities: { + tasks: { + field: "position", + }, + }, + }), + ], + }, + }); + await app.build(); + + const mutator = app.em.mutator("tasks"); + + // create tasks + const { data: task1 } = await mutator.insertOne({ title: "Task 1" }); + const { data: task2 } = await mutator.insertOne({ title: "Task 2" }); + const { data: task3 } = await mutator.insertOne({ title: "Task 3" }); + + // update title only (should not trigger reordering) + await mutator.updateOne(task2.id, { title: "Task 2 Updated" }); + + // verify positions are unchanged + const repo = app.em.repo("tasks"); + const { data: updatedTask1 } = await repo.findOne({ id: task1.id }); + const { data: updatedTask2 } = await repo.findOne({ id: task2.id }); + const { data: updatedTask3 } = await repo.findOne({ id: task3.id }); + + expect(updatedTask1.position).toBe(0); + expect(updatedTask2.position).toBe(1); + expect(updatedTask2.title).toBe("Task 2 Updated"); + expect(updatedTask3.position).toBe(2); + }); + + test("should handle null sort values", async () => { + const app = createApp({ + config: { + data: em({ + tasks: entity("tasks", { + title: text(), + }), + }).toJSON(), + }, + options: { + plugins: [ + sort({ + entities: { + tasks: { + field: "position", + }, + }, + }), + ], + }, + }); + await app.build(); + + const mutator = app.em.mutator("tasks"); + + // create task with null position (bypass listener by using kysely directly) + await app.connection.kysely + .insertInto("tasks") + .values({ title: "Task with null", position: null }) + .execute(); + + // create normal task + await mutator.insertOne({ title: "Task 2" }); + + // update the null task to have a position + const nullTask = await app.connection.kysely + .selectFrom("tasks") + .selectAll() + .where("title", "=", "Task with null") + .executeTakeFirst(); + + await mutator.updateOne(nullTask!.id, { position: 0 }); + + // verify both tasks have proper positions + const { data: tasks } = await app.em.repo("tasks").findMany({ + sort: { by: "position", dir: "asc" }, + }); + + expect(tasks.length).toBe(2); + expect(tasks[0].position).toBe(0); + expect(tasks[1].position).toBe(1); + }); + + test("should not create duplicates when moving to an occupied position", async () => { + const app = createApp({ + config: { + data: em({ + tasks: entity("tasks", { + title: text(), + }), + }).toJSON(), + }, + options: { + plugins: [ + sort({ + entities: { + tasks: { + field: "position", + }, + }, + }), + ], + }, + }); + await app.build(); + + const mutator = app.em.mutator("tasks"); + + // create two tasks at positions 0 and 1 + const { data: task1 } = await mutator.insertOne({ title: "Task 1" }); + const { data: task2 } = await mutator.insertOne({ title: "Task 2" }); + + expect(task1.position).toBe(0); + expect(task2.position).toBe(1); + + // move task2 (at position 1) to position 0 + await mutator.updateOne(task2.id, { position: 0 }); + + // verify no duplicates + const { data: tasks } = await app.em.repo("tasks").findMany({ + sort: { by: "position", dir: "asc" }, + }); + + expect(tasks.length).toBe(2); + expect(tasks[0].id).toBe(task2.id); + expect(tasks[0].position).toBe(0); + expect(tasks[1].id).toBe(task1.id); + expect(tasks[1].position).toBe(1); + + // verify no tasks have the same position + const positions = tasks.map((t) => t.position); + const uniquePositions = new Set(positions); + expect(uniquePositions.size).toBe(positions.length); + }); + + test("should preserve order when recalculating", async () => { + const app = createApp({ + config: { + data: em({ + tasks: entity("tasks", { + title: text(), + }), + }).toJSON(), + }, + options: { + plugins: [ + sort({ + entities: { + tasks: { + field: "position", + }, + }, + }), + ], + }, + }); + await app.build(); + + const mutator = app.em.mutator("tasks"); + + // create tasks with specific positions + const { data: taskA } = await mutator.insertOne({ title: "Task A", position: 5 }); + const { data: taskB } = await mutator.insertOne({ title: "Task B", position: 3 }); + const { data: taskC } = await mutator.insertOne({ title: "Task C", position: 10 }); + + // recalculate + await app.server.request("/api/sort/tasks/recalculate", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({}), + }); + + // verify order is preserved (B, A, C based on original positions) + const { data: tasks } = await app.em.repo("tasks").findMany({ + sort: { by: "position", dir: "asc" }, + }); + + expect(tasks[0].id).toBe(taskB.id); // was at 3, now at 0 + expect(tasks[0].position).toBe(0); + expect(tasks[1].id).toBe(taskA.id); // was at 5, now at 1 + expect(tasks[1].position).toBe(1); + expect(tasks[2].id).toBe(taskC.id); // was at 10, now at 2 + expect(tasks[2].position).toBe(2); + }); +}); diff --git a/app/src/plugins/data/sort.plugin.ts b/app/src/plugins/data/sort.plugin.ts new file mode 100644 index 00000000..848ed7ac --- /dev/null +++ b/app/src/plugins/data/sort.plugin.ts @@ -0,0 +1,423 @@ +import { Exception, type App, type AppPlugin, DatabaseEvents, em, entity, number } from "bknd"; +import { invariant, HttpStatus, jsc, s, $console } from "bknd/utils"; +import { Hono } from "hono"; +import { sql } from "kysely"; + +const DEFAULT_BATCH_SIZE = 1000; + +export type SortPluginOptions = { + /** + * The base path for the API endpoints. + * @default "/api/sort" + */ + apiBasePath?: string; + + /** + * Configuration for entities that should have sorting enabled. + * Key is the entity name, value is the configuration. + */ + entities: Record< + string, + { + /** + * The name of the sort property (must be a number field). + */ + field: string; + + /** + * Optional scope field name. If provided, sorting will only happen within the same scope. + * For example, if scope is "category_id", items will only be sorted within items + * that have the same category_id value. + */ + scope?: string; + } + >; + + /** + * The batch size for recalculating sort order. + * @default 1000 + */ + recalculateBatchSize?: number; +}; + +class SortError extends Exception { + override name = "SortError"; + override code = HttpStatus.BAD_REQUEST; +} + +export function sort({ + apiBasePath = "/api/sort", + entities, + recalculateBatchSize = DEFAULT_BATCH_SIZE, +}: SortPluginOptions): AppPlugin { + return (app: App) => { + return { + name: "sort", + schema: () => { + return em( + Object.fromEntries( + Object.entries(entities).map(([entityName, config]) => [ + entityName, + entity(entityName, { + [config.field]: number({ default_value: 0 }), + }), + ]), + ), + ({ index }, schema) => { + for (const [entityName, config] of Object.entries(entities)) { + const indexed = app.em.getIndexedFields(entityName); + if (!indexed.some((f) => f.name === config.field)) { + index(schema[entityName]!).on([config.field]); + } + } + }, + ); + }, + onBuilt: async () => { + invariant( + entities && Object.keys(entities).length > 0, + "At least one entity must be configured", + ); + + // validate entities exist and have the configured fields + for (const [entityName, config] of Object.entries(entities)) { + const entity = app.em.entity(entityName); + invariant(entity, `Entity "${entityName}" not found in schema`); + + const sortFieldSchema = entity.field(config.field)!; + invariant( + sortFieldSchema, + `Sort field "${config.field}" not found in entity "${entityName}"`, + ); + invariant( + sortFieldSchema.type === "number", + `Sort field "${config.field}" in entity "${entityName}" must be a number field`, + ); + + if (config.scope) { + const scopeFieldSchema = entity.field(config.scope); + invariant( + scopeFieldSchema, + `Scope field "${config.scope}" not found in entity "${entityName}"`, + ); + } + } + + const hono = new Hono(); + + // register recalculate endpoints for each entity + for (const [entityName, config] of Object.entries(entities)) { + hono.post( + `/${entityName}/recalculate`, + jsc( + "json", + s + .object({ + scope: s.any().optional(), + }) + .optional(), + ), + async (c) => { + const body = c.req.valid("json"); + const scope = body?.scope; + + await recalculateSortOrder( + app, + entityName, + config, + recalculateBatchSize, + scope, + ); + + return c.json({ success: true, message: "Sort order recalculated" }); + }, + ); + + hono.post( + `/${entityName}/reorder`, + jsc( + "json", + s.object({ + id: s.any(), + position: s.number(), + }), + ), + async (c) => { + const { id, position } = c.req.valid("json"); + + await reorderItem(app, entityName, config, id, position); + + return c.json({ success: true, message: "Item reordered" }); + }, + ); + } + + app.server.route(apiBasePath, hono); + + // register listeners for automatic reordering + registerListeners(app, entities); + }, + }; + }; +} + +async function reorderItem( + app: App, + entityName: string, + config: SortPluginOptions["entities"][string], + id: any, + newPosition: number, +) { + const { field: sortField, scope: scopeField } = config; + const em = app.em.fork(); + const kysely = em.connection.kysely; + + // get the item + const { data: item } = await em.repo(entityName).findOne({ id }); + if (!item) { + throw new SortError(`Item with id "${id}" not found in entity "${entityName}"`); + } + + const oldPosition = item[sortField] as number | null | undefined; + const scopeValue = scopeField ? item[scopeField] : undefined; + + // update the item with new position (using forked em, so no listeners are triggered) + await em.mutator(entityName).updateOne(id, { [sortField]: newPosition }); + + // shift other items using kysely + if (oldPosition !== undefined && oldPosition !== null && oldPosition !== newPosition) { + if (newPosition < oldPosition) { + // moving up: increment items between newPosition and oldPosition + let query = kysely + .updateTable(entityName) + .set({ [sortField]: sql`${sql.ref(sortField)} + 1` } as any) + .where(sortField as any, ">=", newPosition) + .where(sortField as any, "<", oldPosition) + .where("id", "!=", id); + + if (scopeField && scopeValue !== undefined) { + query = query.where(scopeField as any, "=", scopeValue); + } + + await query.execute(); + } else { + // moving down: decrement items between oldPosition and newPosition + let query = kysely + .updateTable(entityName) + .set({ [sortField]: sql`${sql.ref(sortField)} - 1` } as any) + .where(sortField as any, ">", oldPosition) + .where(sortField as any, "<=", newPosition) + .where("id", "!=", id); + + if (scopeField && scopeValue !== undefined) { + query = query.where(scopeField as any, "=", scopeValue); + } + + await query.execute(); + } + } else if (oldPosition === undefined || oldPosition === null) { + // new item, shift everything at or after this position + let query = kysely + .updateTable(entityName) + .set({ [sortField]: sql`${sql.ref(sortField)} + 1` } as any) + .where(sortField as any, ">=", newPosition) + .where("id", "!=", id); + + if (scopeField && scopeValue !== undefined) { + query = query.where(scopeField as any, "=", scopeValue); + } + + await query.execute(); + } +} + +async function recalculateSortOrder( + app: App, + entityName: string, + config: SortPluginOptions["entities"][string], + batchSize: number, + scope?: any, +) { + const { field: sortField, scope: scopeField } = config; + const db = app.connection.kysely; + + const { count } = (await db + .selectFrom(entityName) + .select((eb) => eb.fn.count("id").as("count")) + .$if(Boolean(scopeField && scope !== undefined), (eb) => + eb.where(scopeField as any, "=", scope), + ) + .$castTo<{ count: number }>() + .executeTakeFirst()) ?? { count: 0 }; + + const batches = Math.ceil(count / batchSize); + for (let i = 0; i < batches; i++) { + // get all items in scope, ordered by current sort value + const items = await db + .selectFrom(entityName) + .select(["id", sortField]) + .$if(Boolean(scopeField && scope !== undefined), (eb) => + eb.where(scopeField as any, "=", scope), + ) + .orderBy(sortField, "asc") + .limit(batchSize) + .offset(i * batchSize) + .execute(); + + const newQbs = items.map((item, index) => + db + .updateTable(entityName) + .set({ [sortField]: index }) + .where("id", "=", item.id), + ); + + await app.connection.executeQueries(...newQbs); + $console.log( + `[Sort Plugin] Recalculated sort order for ${items.length} items in entity "${entityName}"${scopeField && scope !== undefined ? ` (scope: ${scope})` : ""} [batch ${i + 1}/${batches}]`, + ); + } +} + +function registerListeners(app: App, entities: SortPluginOptions["entities"]) { + const kysely = app.connection.kysely; + + // handle insert events + app.emgr.onEvent( + DatabaseEvents.MutatorInsertBefore, + async (e) => { + const entityName = e.params.entity.name; + const config = entities[entityName]; + if (!config) return e.params.data; + + const { field: sortField, scope: scopeField } = config; + const data = e.params.data; + const scopeValue = scopeField ? data[scopeField] : undefined; + + // if no position provided, set to max + 1 + if (data[sortField] === undefined || data[sortField] === null) { + const query = kysely + .selectFrom(entityName) + .select((eb) => [eb.fn.max(eb.ref(sortField)).as("max")]) + // add scope filter if needed + .$if(Boolean(scopeField && scopeValue), (eb) => + eb.where(scopeField as any, "=", scopeValue as any), + ); + + const result = await query.executeTakeFirst(); + const max = result?.max ?? -1; + + return { + ...data, + [sortField]: max + 1, + }; + } + + // if position is provided, shift other items at or after that position + const newPosition = data[sortField] as number; + + let shiftQuery = kysely + .updateTable(entityName) + .set({ [sortField]: sql`${sql.ref(sortField)} + 1` } as any) + .where(sortField as any, ">=", newPosition); + + if (scopeField && scopeValue !== undefined) { + shiftQuery = shiftQuery.where(scopeField as any, "=", scopeValue); + } + + await shiftQuery.execute(); + + return data; + }, + { + mode: "sync", + id: "bknd-sort-insert", + }, + ); + + // handle update events + app.emgr.onEvent( + DatabaseEvents.MutatorUpdateBefore, + async (e) => { + const entityName = e.params.entity.name; + const config = entities[entityName]; + if (!config) return e.params.data; + + const { field: sortField, scope: scopeField } = config; + const data = e.params.data; + + // only handle if sort field is being updated + if (!(sortField in data)) return e.params.data; + + const newPosition = data[sortField] as number; + const id = e.params.entityId; + + // get the current item to know its old position and scope + const item = await kysely + .selectFrom(entityName) + .selectAll() + .where("id" as any, "=", id) + .executeTakeFirst(); + + if (!item) return data; + + const oldPosition = item[sortField] as number | null | undefined; + const scopeValue = scopeField ? item[scopeField] : undefined; + + // if oldPosition is null or undefined, treat as inserting at newPosition + if (oldPosition === null || oldPosition === undefined) { + // shift items at or after the new position + let query = kysely + .updateTable(entityName) + .set({ [sortField]: sql`${sql.ref(sortField)} + 1` } as any) + .where(sortField as any, ">=", newPosition) + .where("id" as any, "!=", id); + + if (scopeField && scopeValue !== undefined) { + query = query.where(scopeField as any, "=", scopeValue); + } + + await query.execute(); + return data; + } + + // shift other items using kysely + if (oldPosition !== newPosition) { + if (newPosition < oldPosition) { + // moving up: increment items between newPosition and oldPosition + let query = kysely + .updateTable(entityName) + .set({ [sortField]: sql`${sql.ref(sortField)} + 1` } as any) + .where(sortField as any, ">=", newPosition) + .where(sortField as any, "<", oldPosition) + .where("id" as any, "!=", id); + + if (scopeField && scopeValue !== undefined) { + query = query.where(scopeField as any, "=", scopeValue); + } + + await query.execute(); + } else { + // moving down: decrement items between oldPosition and newPosition + let query = kysely + .updateTable(entityName) + .set({ [sortField]: sql`${sql.ref(sortField)} - 1` } as any) + .where(sortField as any, ">", oldPosition) + .where(sortField as any, "<=", newPosition) + .where("id" as any, "!=", id); + + if (scopeField && scopeValue !== undefined) { + query = query.where(scopeField as any, "=", scopeValue); + } + + await query.execute(); + } + } + + return data; + }, + { + mode: "sync", + id: "bknd-sort-update", + }, + ); +} diff --git a/app/src/plugins/index.ts b/app/src/plugins/index.ts index fcb23c15..5cd019a4 100644 --- a/app/src/plugins/index.ts +++ b/app/src/plugins/index.ts @@ -9,3 +9,4 @@ export { syncTypes, type SyncTypesOptions } from "./dev/sync-types.plugin"; export { syncSecrets, type SyncSecretsOptions } from "./dev/sync-secrets.plugin"; export { timestamps, type TimestampsPluginOptions } from "./data/timestamps.plugin"; export { emailOTP, type EmailOTPPluginOptions } from "./auth/email-otp.plugin"; +export { sort, type SortPluginOptions } from "./data/sort.plugin"; diff --git a/app/src/ui/Admin.tsx b/app/src/ui/Admin.tsx index 766b56a8..c62eedce 100644 --- a/app/src/ui/Admin.tsx +++ b/app/src/ui/Admin.tsx @@ -16,6 +16,11 @@ export type BkndAdminConfig = { * @default `/` */ basepath?: string; + /** + * Sub-path for the Admin UI within the base path + * @default `` + */ + admin_basepath?: string; /** * Path to return to when clicking the logo * @default `/` diff --git a/app/src/ui/client/utils/AppReduced.ts b/app/src/ui/client/utils/AppReduced.ts index 353faa40..3b493ce7 100644 --- a/app/src/ui/client/utils/AppReduced.ts +++ b/app/src/ui/client/utils/AppReduced.ts @@ -77,7 +77,7 @@ export class AppReduced { withBasePath(path: string | string[], absolute = false): string { const paths = Array.isArray(path) ? path : [path]; - return [absolute ? "~" : null, this.options.basepath, this.options.admin_basepath, ...paths] + return [absolute ? "~" : null, this.options.basepath, ...paths] .filter(Boolean) .join("/") .replace(/\/+/g, "/") diff --git a/app/src/ui/components/form/Formy/components.tsx b/app/src/ui/components/form/Formy/components.tsx index 83a69b52..8b71c784 100644 --- a/app/src/ui/components/form/Formy/components.tsx +++ b/app/src/ui/components/form/Formy/components.tsx @@ -30,7 +30,7 @@ export const Group = ({ {...props} data-role="group" className={twMerge( - "flex flex-col gap-1.5 w-full", + "flex flex-col gap-1.5 has-disabled:cursor-not-allowed w-full", as === "fieldset" && "border border-primary/10 p-3 rounded-md", as === "fieldset" && error && "border-red-500", error && "text-red-500", @@ -97,7 +97,7 @@ export const Input = forwardRef> ref={ref} className={twMerge( "bg-muted/40 h-11 rounded-md py-2.5 px-4 outline-none w-full disabled:cursor-not-allowed", - disabledOrReadonly && "bg-muted/50 text-primary/50", + disabledOrReadonly && "bg-muted/50 text-primary/50 cursor-not-allowed", !disabledOrReadonly && "focus:bg-muted focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all", props.className, @@ -154,7 +154,7 @@ export const Textarea = forwardRef @@ -214,7 +214,7 @@ export const BooleanInput = forwardRef { - console.log("setting", bool); props.onChange?.({ target: { value: bool } }); }} {...(props as any)} @@ -294,7 +293,7 @@ export const Select = forwardRef< {...props} ref={ref} className={twMerge( - "bg-muted/40 focus:bg-muted rounded-md py-2.5 px-4 outline-none focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all disabled:bg-muted/50 disabled:text-primary/50", + "bg-muted/40 focus:bg-muted rounded-md py-2.5 px-4 outline-none focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all disabled:bg-muted/50 disabled:text-primary/50 disabled:cursor-not-allowed", "appearance-none h-11 w-full", !props.multiple && "border-r-8 border-r-transparent", props.className, diff --git a/app/src/ui/components/wouter/Link.tsx b/app/src/ui/components/wouter/Link.tsx index 4c5238d6..051364dc 100644 --- a/app/src/ui/components/wouter/Link.tsx +++ b/app/src/ui/components/wouter/Link.tsx @@ -1,5 +1,6 @@ import { type LinkProps, Link as WouterLink, useRouter } from "wouter"; import { useEvent } from "../../hooks/use-event"; +import { useBkndOptions } from "ui/client/BkndProvider"; /* * Transforms `path` into its relative `base` version @@ -70,11 +71,13 @@ export function Link({ return false; } - + const bkndOptions = useBkndOptions(); + const adminBasePath = bkndOptions?.admin_basepath ?? ""; const _href = props.href ?? props.to; + const href = router .hrefs( - _href[0] === "~" ? _href.slice(1) : router.base + _href, + _href[0] === "~" ? adminBasePath + _href.slice(1) : router.base + _href, router, // pass router as a second argument for convinience ) .replace("//", "/"); @@ -82,8 +85,9 @@ export function Link({ const active = href.replace(router.base, "").length <= 1 ? href === absPath : isActive(absPath, href); + // console.log({ adminBasePath, _href, href, bkndOptions }) if (native) { - return ; + return ; } const wouterOnClick = (e: any) => { @@ -99,10 +103,11 @@ export function Link({ }; return ( + // @ts-expect-error className is not typed on WouterLink ); diff --git a/app/src/ui/lib/routes.ts b/app/src/ui/lib/routes.ts index 46ed4fbe..a2b5bcc6 100644 --- a/app/src/ui/lib/routes.ts +++ b/app/src/ui/lib/routes.ts @@ -101,7 +101,7 @@ export function useNavigate() { } } - const _url = options?.absolute ? `~/${basepath}${url}`.replace(/\/+/g, "/") : url; + const _url = options?.absolute ? `~/${app.options.basepath}/${app.options.admin_basepath}${url}`.replace(/\/+/g, "/") : url; const state = { ...options?.state, referrer: location, diff --git a/app/src/ui/main.tsx b/app/src/ui/main.tsx index ea561e45..b59d42ac 100644 --- a/app/src/ui/main.tsx +++ b/app/src/ui/main.tsx @@ -1,7 +1,6 @@ import * as React from "react"; import * as ReactDOM from "react-dom/client"; import Admin from "./Admin"; -//import "./main.css"; import "./styles.css"; function render() { diff --git a/app/src/ui/modules/data/components/EntityForm.tsx b/app/src/ui/modules/data/components/EntityForm.tsx index 4d606b56..b1d741d2 100644 --- a/app/src/ui/modules/data/components/EntityForm.tsx +++ b/app/src/ui/modules/data/components/EntityForm.tsx @@ -45,7 +45,7 @@ export function EntityForm({ className, action, }: EntityFormProps) { - const fields = entity.getFillableFields(action, true); + const fields = entity.getFields({ virtual: true, primary: false }); const options = useEntityAdminOptions(entity, action); return ( @@ -93,10 +93,6 @@ export function EntityForm({ ); } - if (!field.isFillable(action)) { - return; - } - const _key = `${entity.name}-${field.name}-${key}`; return ( @@ -128,7 +124,7 @@ export function EntityForm({ =16.8.0" } }, "sha512-DF86rstDK2zs/aTbPOUTWwXiVmFG2GDxXYvztIfXUNtuCvNDJKAZK2thYaULv9bGTRitPKWlwcz0zAMjYRHavQ=="], @@ -1214,7 +1216,7 @@ "@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="], - "@tsconfig/strictest": ["@tsconfig/strictest@2.0.7", "", {}, "sha512-lG1bXQloBVvQXasPBZSBaWbs7GNW4txZMYs7XMUVzM0s4seCbJACYBPFIRpGNVD3gC8gndK00AZON1uBrJt4Fw=="], + "@tsconfig/strictest": ["@tsconfig/strictest@2.0.8", "", {}, "sha512-XnQ7vNz5HRN0r88GYf1J9JJjqtZPiHt2woGJOo2dYqyHGGcd6OLGqSlBB6p1j9mpzja6Oe5BoPqWmeDx6X9rLw=="], "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], @@ -1226,7 +1228,7 @@ "@types/babel__traverse": ["@types/babel__traverse@7.20.6", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg=="], - "@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="], + "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], @@ -1322,41 +1324,41 @@ "@ungap/with-resolvers": ["@ungap/with-resolvers@0.1.0", "", {}, "sha512-g7f0IkJdPW2xhY7H4iE72DAsIyfuwEFc6JWc2tYFwKDMWWAF699vGjrM348cwQuOXgHpe1gWFe+Eiyjx/ewvvw=="], - "@verdaccio/auth": ["@verdaccio/auth@8.0.0-next-8.24", "", { "dependencies": { "@verdaccio/config": "8.0.0-next-8.24", "@verdaccio/core": "8.0.0-next-8.24", "@verdaccio/loaders": "8.0.0-next-8.14", "@verdaccio/signature": "8.0.0-next-8.16", "debug": "4.4.3", "lodash": "4.17.21", "verdaccio-htpasswd": "13.0.0-next-8.24" } }, "sha512-stRp0DdTTx3p6dnh2cKOPJZOhu6sZOf8evV2fpYtADYW0UyhhZwELBXukpa5WGQ3H3rWzsXSaccra+D7tB1LgA=="], + "@verdaccio/auth": ["@verdaccio/auth@8.0.0-next-8.33", "", { "dependencies": { "@verdaccio/config": "8.0.0-next-8.33", "@verdaccio/core": "8.0.0-next-8.33", "@verdaccio/loaders": "8.0.0-next-8.23", "@verdaccio/signature": "8.0.0-next-8.25", "debug": "4.4.3", "lodash": "4.17.23", "verdaccio-htpasswd": "13.0.0-next-8.33" } }, "sha512-HRIqEj9J7t95ZG3pCVpaCJoXCNVPB0R2DpkEXfG19TXsapf/mv5vMnBYG6O1C/ShNagHMA7z+Z6NwFZXHUzm9g=="], - "@verdaccio/config": ["@verdaccio/config@8.0.0-next-8.24", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.24", "debug": "4.4.3", "js-yaml": "4.1.0", "lodash": "4.17.21", "minimatch": "7.4.6" } }, "sha512-TRTVY6g2bH5V/1MQOXmdwOIuiT8i/tAtRX4T7FHwCutWTMfdFLgwy4e1VvTH8d1MwGRNyVin4O8PHVu5Xh4ouA=="], + "@verdaccio/config": ["@verdaccio/config@8.0.0-next-8.33", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.33", "debug": "4.4.3", "js-yaml": "4.1.1", "lodash": "4.17.23" } }, "sha512-6/n/qcVMbNTK8oFY8l6vlJMeG6zor/aOFcR2Wd6yCoKcehITigmLn8MFziZPriYivzxYJJRjlaKO9+HuuQSKCA=="], - "@verdaccio/core": ["@verdaccio/core@8.0.0-next-8.24", "", { "dependencies": { "ajv": "8.17.1", "http-errors": "2.0.0", "http-status-codes": "2.3.0", "minimatch": "7.4.6", "process-warning": "1.0.0", "semver": "7.7.3" } }, "sha512-58Et0Mj562ergUd7galslWNsTseOHBCDkCIHokmoeWGX4+P46Aoa9+G0laFHyZxxfkuGkZXhO1WHysc9LzkfiA=="], + "@verdaccio/core": ["@verdaccio/core@8.0.0-next-8.33", "", { "dependencies": { "ajv": "8.18.0", "http-errors": "2.0.1", "http-status-codes": "2.3.0", "minimatch": "7.4.9", "process-warning": "1.0.0", "semver": "7.7.4" } }, "sha512-ndPAfZVyN677y/EZX+USkL0/aOcU5rvnzS99nRCIHarZB44WMno9jl6FdX5Ax3b3exGo9GsxhEdbYHgOYaRlLQ=="], "@verdaccio/file-locking": ["@verdaccio/file-locking@10.3.1", "", { "dependencies": { "lockfile": "1.0.4" } }, "sha512-oqYLfv3Yg3mAgw9qhASBpjD50osj2AX4IwbkUtyuhhKGyoFU9eZdrbeW6tpnqUnj6yBMtAPm2eGD4BwQuX400g=="], - "@verdaccio/hooks": ["@verdaccio/hooks@8.0.0-next-8.24", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.24", "@verdaccio/logger": "8.0.0-next-8.24", "debug": "4.4.3", "got-cjs": "12.5.4", "handlebars": "4.7.8" } }, "sha512-jrBHk51rsANI47YbHCFKprBPelLDklwKhkMINEYnFOQwuB3HEcupd6hGNDaj64sRnZNoK2VuJH0cAWn0iqVErg=="], + "@verdaccio/hooks": ["@verdaccio/hooks@8.0.0-next-8.33", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.33", "@verdaccio/logger": "8.0.0-next-8.33", "debug": "4.4.3", "got-cjs": "12.5.4", "handlebars": "4.7.8" } }, "sha512-8xP6kVOCufjaZWriFtaxP3HEmvYTIBhcxWldui4eCG7dzBdZzPlCkRbYKWP8LMnOEIxbJNbeFkokOaI1nho1jg=="], - "@verdaccio/loaders": ["@verdaccio/loaders@8.0.0-next-8.14", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.24", "debug": "4.4.3", "lodash": "4.17.21" } }, "sha512-cWrTTJ7HWjrzhXIVVXPHFUFTdzbRgvU5Xwte8O/JPMtrEAxtbjg18kCIdQwAcomB1S+BkffnnQJ8TPLymnuCrg=="], + "@verdaccio/loaders": ["@verdaccio/loaders@8.0.0-next-8.23", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.33", "debug": "4.4.3", "lodash": "4.17.23" } }, "sha512-UruK7pFz7aRKkR41/Hg/cPFSqX5Oxbx9rKGWK5ql3mvg2+xaWleRwknmnNjbVod7QK6cWsYA6cKnttRBTQuPkQ=="], "@verdaccio/local-storage-legacy": ["@verdaccio/local-storage-legacy@11.1.1", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.21", "@verdaccio/file-locking": "10.3.1", "@verdaccio/streams": "10.2.1", "async": "3.2.6", "debug": "4.4.1", "lodash": "4.17.21", "lowdb": "1.0.0", "mkdirp": "1.0.4" } }, "sha512-P6ahH2W6/KqfJFKP+Eid7P134FHDLNvHa+i8KVgRVBeo2/IXb6FEANpM1mCVNvPANu0LCAmNJBOXweoUKViaoA=="], - "@verdaccio/logger": ["@verdaccio/logger@8.0.0-next-8.24", "", { "dependencies": { "@verdaccio/logger-commons": "8.0.0-next-8.24", "pino": "9.13.1" } }, "sha512-7/arkwQy2zI5W5z9zMf5wyWiZx18xbLultteNPWHkQv9CtoFtuK+TSY8rH7ITtCGoNCcB94jvvTYRylxAdaHEw=="], + "@verdaccio/logger": ["@verdaccio/logger@8.0.0-next-8.33", "", { "dependencies": { "@verdaccio/logger-commons": "8.0.0-next-8.33", "pino": "9.14.0" } }, "sha512-8nR3hreOcINIbMIOohhxZNUL3l0uvgGYQecl8WJvR4XizYTp1vPHv4qz1CPyuaI5bKj1QFRqHkKvvtEnD1A0rw=="], - "@verdaccio/logger-commons": ["@verdaccio/logger-commons@8.0.0-next-8.24", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.24", "@verdaccio/logger-prettify": "8.0.0-next-8.4", "colorette": "2.0.20", "debug": "4.4.3" } }, "sha512-gEBUajG1m93xG+FJ+9+Mxg3FC9tSnP0RHyrhb4gPZPqft7JpRkwjbqpjxASGzPyxdTJZwiAefr7aafXv0xMpJA=="], + "@verdaccio/logger-commons": ["@verdaccio/logger-commons@8.0.0-next-8.33", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.33", "@verdaccio/logger-prettify": "8.0.0-next-8.4", "colorette": "2.0.20", "debug": "4.4.3" } }, "sha512-WmMq/6HyRliKWus3sQR9Pgodopzbp84dl/h/E0tnxuOmzc/eDwYCEiQMfFhIBaOlpVJdsdLYqNAM9+YkoSDK0g=="], "@verdaccio/logger-prettify": ["@verdaccio/logger-prettify@8.0.0-next-8.4", "", { "dependencies": { "colorette": "2.0.20", "dayjs": "1.11.13", "lodash": "4.17.21", "on-exit-leak-free": "2.1.2", "pino-abstract-transport": "1.2.0", "sonic-boom": "3.8.1" } }, "sha512-gjI/JW29fyalutn/X1PQ0iNuGvzeVWKXRmnLa7gXVKhdi4p37l/j7YZ7n44XVbbiLIKAK0pbavEg9Yr66QrYaA=="], - "@verdaccio/middleware": ["@verdaccio/middleware@8.0.0-next-8.24", "", { "dependencies": { "@verdaccio/config": "8.0.0-next-8.24", "@verdaccio/core": "8.0.0-next-8.24", "@verdaccio/url": "13.0.0-next-8.24", "debug": "4.4.3", "express": "4.21.2", "express-rate-limit": "5.5.1", "lodash": "4.17.21", "lru-cache": "7.18.3", "mime": "2.6.0" } }, "sha512-LuFralbC8bxl2yQx9prKEMrNbxj8BkojcUDIa+jZZRnghaZrMm1yHqf5eLHCrBBIOM3m2thJ6Ux2UfBCQP4UKA=="], + "@verdaccio/middleware": ["@verdaccio/middleware@8.0.0-next-8.33", "", { "dependencies": { "@verdaccio/config": "8.0.0-next-8.33", "@verdaccio/core": "8.0.0-next-8.33", "@verdaccio/url": "13.0.0-next-8.33", "debug": "4.4.3", "express": "4.22.1", "express-rate-limit": "5.5.1", "lodash": "4.17.23", "lru-cache": "7.18.3" } }, "sha512-iMzE3stUp/qocHzFX2+xkzUe6L89vWZ/J4a4v7IxPu6KqBH39XCg4gI8Qu0xXRIFeYSTpZNzRUO+FTkeZRhJ1A=="], "@verdaccio/search-indexer": ["@verdaccio/search-indexer@8.0.0-next-8.5", "", {}, "sha512-0GC2tJKstbPg/W2PZl2yE+hoAxffD2ZWilEnEYSEo2e9UQpNIy2zg7KE/uMUq2P72Vf5EVfVzb8jdaH4KV4QeA=="], - "@verdaccio/signature": ["@verdaccio/signature@8.0.0-next-8.16", "", { "dependencies": { "@verdaccio/config": "8.0.0-next-8.24", "@verdaccio/core": "8.0.0-next-8.24", "debug": "4.4.3", "jsonwebtoken": "9.0.2" } }, "sha512-JBIpoYJQFjo3ITTRjum1IjXxNrSYcPoBLZTPlt9OLL5Brd7s1fJkTBgs9tbcCRZPrek/XBIJ/cLFZZn8zkc/bQ=="], + "@verdaccio/signature": ["@verdaccio/signature@8.0.0-next-8.25", "", { "dependencies": { "@verdaccio/config": "8.0.0-next-8.33", "@verdaccio/core": "8.0.0-next-8.33", "debug": "4.4.3", "jsonwebtoken": "9.0.3" } }, "sha512-3qMHZ9uXgNmRQDw7Bz3coCbXJAfI3q+bWRXQ0/fKg03LgSV6pbMD0HinGKcIBn0Kni+e8IsXxBfrs5ga4FF+Rg=="], "@verdaccio/streams": ["@verdaccio/streams@10.2.1", "", {}, "sha512-OojIG/f7UYKxC4dYX8x5ax8QhRx1b8OYUAMz82rUottCuzrssX/4nn5QE7Ank0DUSX3C9l/HPthc4d9uKRJqJQ=="], - "@verdaccio/tarball": ["@verdaccio/tarball@13.0.0-next-8.24", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.24", "@verdaccio/url": "13.0.0-next-8.24", "debug": "4.4.3", "gunzip-maybe": "1.4.2", "tar-stream": "3.1.7" } }, "sha512-rDz8gWukO7dcaWzMTr7wMvKPUsRHclVzZljyTERplpIX9NWGHRsMRO7NjoTIUWtDS0euMlRVDnR8QVnYDWl2uA=="], + "@verdaccio/tarball": ["@verdaccio/tarball@13.0.0-next-8.33", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.33", "@verdaccio/url": "13.0.0-next-8.33", "debug": "4.4.3", "gunzip-maybe": "1.4.2", "tar-stream": "3.1.7" } }, "sha512-VQZ8I5Fs6INxO1cY6Lpk4Wx3sD0Uy7YIzTY9qWrZNRPGDxGcZu2fyFMUfoNc1+ygJP0r5TTqdVAY74z0iQnsWg=="], - "@verdaccio/ui-theme": ["@verdaccio/ui-theme@8.0.0-next-8.24", "", {}, "sha512-A8lMenzJmC0EioBjQ9+L2e8tv/iEB/jLJKH4WJjPYa8B1xlm/LLUuk2aBg7pYD1fPWuazvJiH/NAnkrA09541g=="], + "@verdaccio/ui-theme": ["@verdaccio/ui-theme@8.0.0-next-8.30", "", {}, "sha512-MSvFnYeocTWp6hqtIqtwp7Zm920UNhO3zQkKTPT6qGNjarkhxCCWRcSOAz7oJZhpbyLXc85zuxOLkTezCl4KUg=="], - "@verdaccio/url": ["@verdaccio/url@13.0.0-next-8.24", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.24", "debug": "4.4.3", "lodash": "4.17.21", "validator": "13.15.15" } }, "sha512-zNHR9qgiDTXp+IuOtz925tzGteXGj18IZphvxo2apgz3cAeUpL/nnmp4ZScczpxWxE8sT/88xVYgJm+Ec8fNCA=="], + "@verdaccio/url": ["@verdaccio/url@13.0.0-next-8.33", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.33", "debug": "4.4.3", "validator": "13.15.26" } }, "sha512-26LzQTCuUFFcNasAdzayBqsYWBheQy+D4tSWnVtmj4kKQnVhufBvHLjpEjK1gqphOMx6/Mju0IQe0LsjRc1zdg=="], - "@verdaccio/utils": ["@verdaccio/utils@8.1.0-next-8.24", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.24", "lodash": "4.17.21", "minimatch": "7.4.6" } }, "sha512-2e54Z1J1+OPM0LCxjkJHgwFm8jESsCYaX6ARs3+29hjoI75uiSphxFI3Hrhr+67ho/7Mtul0oyakK6l18MN/Dg=="], + "@verdaccio/utils": ["@verdaccio/utils@8.1.0-next-8.33", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.33", "lodash": "4.17.23", "minimatch": "7.4.9" } }, "sha512-WLE8qTBgzuZPa+gq/nKPWtF4MTMayaeOD3Qy6yfChxNvFXY+6yPFkS0QocXL7CdB2A+w+ylgTOOzKr0tWvyTpQ=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.0", "", { "dependencies": { "@babel/core": "^7.28.4", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.43", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-4LuWrg7EKWgQaMJfnN+wcmbAW+VSsCmqGohftWjuct47bv8uE4n/nPpq4XjJPsxgq00GGG5J8dvBczp8uxScew=="], @@ -1416,7 +1418,7 @@ "agent-base": ["agent-base@7.1.3", "", {}, "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="], - "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], @@ -1608,7 +1610,7 @@ "builtin-modules": ["builtin-modules@3.3.0", "", {}, "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw=="], - "bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="], + "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], @@ -1746,7 +1748,7 @@ "core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="], - "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], "cosmiconfig": ["cosmiconfig@6.0.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.1.0", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.7.2" } }, "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg=="], @@ -1930,7 +1932,7 @@ "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], - "envinfo": ["envinfo@7.15.0", "", { "bin": { "envinfo": "dist/cli.js" } }, "sha512-chR+t7exF6y59kelhXw5I3849nTy7KIRO+ePdLMhCD+JRP/JvmkenDWP7QSFGlsHX+kxGxdDutOPrmj5j1HR6g=="], + "envinfo": ["envinfo@7.21.0", "", { "bin": { "envinfo": "dist/cli.js" } }, "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow=="], "error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g=="], @@ -2026,7 +2028,7 @@ "expect-type": ["expect-type@1.2.0", "", {}, "sha512-80F22aiJ3GLyVnS/B3HzgR6RelZVumzj9jkL0Rhz4h0xYbNW9PjlQz5h3J/SShErbXBc295vseR4/MIbVmUbeA=="], - "express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="], + "express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="], "express-rate-limit": ["express-rate-limit@5.5.1", "", {}, "sha512-MTjE2eIbHv5DyfuFz4zLYWxpqVhEhkTiwFGuB74Q9CSou2WHO52nlE5y3Zlg6SIsiYUIPj6ifFxnkPz6O3sIUg=="], @@ -2232,7 +2234,7 @@ "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], - "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], @@ -2474,7 +2476,7 @@ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], "jsbn": ["jsbn@0.1.1", "", {}, "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg=="], @@ -2514,7 +2516,7 @@ "jsonv-ts": ["jsonv-ts@0.10.1", "", { "optionalDependencies": { "hono": "*" }, "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-IfuXZigNjLQzW4X7dLRTpwd1pD1lk86SoXBWmLdF+VE6SE4PcXevWs8c/bPl7qVrZXhh8lYwbTF7TFtgO2/jXg=="], - "jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="], + "jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="], "jsprim": ["jsprim@2.0.2", "", { "dependencies": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", "json-schema": "0.4.0", "verror": "1.10.0" } }, "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ=="], @@ -2522,9 +2524,9 @@ "jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="], - "jwa": ["jwa@1.4.1", "", { "dependencies": { "buffer-equal-constant-time": "1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA=="], + "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], - "jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="], + "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], @@ -2592,7 +2594,7 @@ "lockfile": ["lockfile@1.0.4", "", { "dependencies": { "signal-exit": "^3.0.2" } }, "sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA=="], - "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], "lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], @@ -2688,7 +2690,7 @@ "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], - "miniflare": ["miniflare@3.20250718.2", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "stoppable": "1.1.0", "undici": "^5.28.5", "workerd": "1.20250718.0", "ws": "8.18.0", "youch": "3.3.4", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-cW/NQPBKc+fb0FwcEu+z/v93DZd+/6q/AF0iR0VFELtNPOsCvLalq6ndO743A7wfZtFxMxvuDQUXNx3aKQhOwA=="], + "miniflare": ["miniflare@3.20250718.3", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "stoppable": "1.1.0", "undici": "^5.28.5", "workerd": "1.20250718.0", "ws": "8.18.0", "youch": "3.3.4", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-JuPrDJhwLrNLEJiNLWO7ZzJrv/Vv9kZuwMYCfv0LskQDM6Eonw4OvywO3CH/wCGjgHzha/qyjUh8JQ068TjDgQ=="], "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -2906,7 +2908,7 @@ "pify": ["pify@3.0.0", "", {}, "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg=="], - "pino": ["pino@9.13.1", "", { "dependencies": { "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "slow-redact": "^0.3.0", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-Szuj+ViDTjKPQYiKumGmEn3frdl+ZPSdosHyt9SnUevFosOkMY2b7ipxlEctNKPmMD/VibeBI+ZcZCJK+4DPuw=="], + "pino": ["pino@9.14.0", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w=="], "pino-abstract-transport": ["pino-abstract-transport@2.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="], @@ -3002,7 +3004,7 @@ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], + "qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="], "query-selector-shadow-dom": ["query-selector-shadow-dom@1.0.1", "", {}, "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw=="], @@ -3194,7 +3196,7 @@ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], - "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="], @@ -3252,8 +3254,6 @@ "slice-ansi": ["slice-ansi@2.1.0", "", { "dependencies": { "ansi-styles": "^3.2.0", "astral-regex": "^1.0.0", "is-fullwidth-code-point": "^2.0.0" } }, "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ=="], - "slow-redact": ["slow-redact@0.3.2", "", {}, "sha512-MseHyi2+E/hBRqdOi5COy6wZ7j7DxXRz9NkseavNYSvvWC06D8a5cidVZX3tcG5eCW3NIyVU4zT63hw0Q486jw=="], - "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], "smtp-address-parser": ["smtp-address-parser@1.0.10", "", { "dependencies": { "nearley": "^2.20.1" } }, "sha512-Osg9LmvGeAG/hyao4mldbflLOkkr3a+h4m1lwKCK5U8M6ZAr7tdXEz/+/vr752TSGE4MNUlUl9cIK2cB8cgzXg=="], @@ -3512,7 +3512,7 @@ "typedarray-to-buffer": ["typedarray-to-buffer@3.1.5", "", { "dependencies": { "is-typedarray": "^1.0.0" } }, "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q=="], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="], "ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="], @@ -3596,15 +3596,15 @@ "validate.io-number": ["validate.io-number@1.0.3", "", {}, "sha512-kRAyotcbNaSYoDnXvb4MHg/0a1egJdLwS6oJ38TJY7aw9n93Fl/3blIXdyYvPOp55CNxywooG/3BcrwNrBpcSg=="], - "validator": ["validator@13.15.15", "", {}, "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A=="], + "validator": ["validator@13.15.26", "", {}, "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA=="], "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - "verdaccio": ["verdaccio@6.2.1", "", { "dependencies": { "@cypress/request": "3.0.9", "@verdaccio/auth": "8.0.0-next-8.24", "@verdaccio/config": "8.0.0-next-8.24", "@verdaccio/core": "8.0.0-next-8.24", "@verdaccio/hooks": "8.0.0-next-8.24", "@verdaccio/loaders": "8.0.0-next-8.14", "@verdaccio/local-storage-legacy": "11.1.1", "@verdaccio/logger": "8.0.0-next-8.24", "@verdaccio/middleware": "8.0.0-next-8.24", "@verdaccio/search-indexer": "8.0.0-next-8.5", "@verdaccio/signature": "8.0.0-next-8.16", "@verdaccio/streams": "10.2.1", "@verdaccio/tarball": "13.0.0-next-8.24", "@verdaccio/ui-theme": "8.0.0-next-8.24", "@verdaccio/url": "13.0.0-next-8.24", "@verdaccio/utils": "8.1.0-next-8.24", "JSONStream": "1.3.5", "async": "3.2.6", "clipanion": "4.0.0-rc.4", "compression": "1.8.1", "cors": "2.8.5", "debug": "4.4.3", "envinfo": "7.15.0", "express": "4.21.2", "lodash": "4.17.21", "lru-cache": "7.18.3", "mime": "3.0.0", "semver": "7.7.2", "verdaccio-audit": "13.0.0-next-8.24", "verdaccio-htpasswd": "13.0.0-next-8.24" }, "bin": { "verdaccio": "bin/verdaccio" } }, "sha512-b7EjPyVKvO/7J2BtLaybQqDd8dh4uUsuQL1zQMVLsw3aYqBsHCAOa6T1zb6gpCg68cNUHluw7IjLs2hha72TZA=="], + "verdaccio": ["verdaccio@6.3.2", "", { "dependencies": { "@cypress/request": "3.0.10", "@verdaccio/auth": "8.0.0-next-8.33", "@verdaccio/config": "8.0.0-next-8.33", "@verdaccio/core": "8.0.0-next-8.33", "@verdaccio/hooks": "8.0.0-next-8.33", "@verdaccio/loaders": "8.0.0-next-8.23", "@verdaccio/local-storage-legacy": "11.1.1", "@verdaccio/logger": "8.0.0-next-8.33", "@verdaccio/middleware": "8.0.0-next-8.33", "@verdaccio/search-indexer": "8.0.0-next-8.5", "@verdaccio/signature": "8.0.0-next-8.25", "@verdaccio/streams": "10.2.1", "@verdaccio/tarball": "13.0.0-next-8.33", "@verdaccio/ui-theme": "8.0.0-next-8.30", "@verdaccio/url": "13.0.0-next-8.33", "@verdaccio/utils": "8.1.0-next-8.33", "JSONStream": "1.3.5", "async": "3.2.6", "clipanion": "4.0.0-rc.4", "compression": "1.8.1", "cors": "2.8.6", "debug": "4.4.3", "envinfo": "7.21.0", "express": "4.22.1", "lodash": "4.17.23", "lru-cache": "7.18.3", "mime": "3.0.0", "semver": "7.7.4", "verdaccio-audit": "13.0.0-next-8.33", "verdaccio-htpasswd": "13.0.0-next-8.33" }, "bin": { "verdaccio": "bin/verdaccio" } }, "sha512-9BmfrGlakdAW1QNBrD2GgO8hOhwIp6ogbAhaaDgtDsK3/94qXwS6n2PM1/gG2V/zFC5JH1rWbLia390i0xbodA=="], - "verdaccio-audit": ["verdaccio-audit@13.0.0-next-8.24", "", { "dependencies": { "@verdaccio/config": "8.0.0-next-8.24", "@verdaccio/core": "8.0.0-next-8.24", "express": "4.21.2", "https-proxy-agent": "5.0.1", "node-fetch": "cjs" } }, "sha512-dXqsnhTGqOuIsZq/MrW05YKwhuKg94VtL0tcYI4kcT+J+fN3gKiZ1Q3wDPaVzCMc081stBlKhi+SL66gIidHdA=="], + "verdaccio-audit": ["verdaccio-audit@13.0.0-next-8.33", "", { "dependencies": { "@verdaccio/config": "8.0.0-next-8.33", "@verdaccio/core": "8.0.0-next-8.33", "express": "4.22.1", "https-proxy-agent": "5.0.1", "node-fetch": "cjs" } }, "sha512-qBMN0b4cbSbo6RbOke0QtVy/HuSzmxgRXIjnXM3C86ZhjN6DlhdeAoQYcZUfbXM8BklRXtObAnMoTlgeJ7BJLA=="], - "verdaccio-htpasswd": ["verdaccio-htpasswd@13.0.0-next-8.24", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.24", "@verdaccio/file-locking": "13.0.0-next-8.6", "apache-md5": "1.1.8", "bcryptjs": "2.4.3", "debug": "4.4.3", "http-errors": "2.0.0", "unix-crypt-td-js": "1.1.4" } }, "sha512-nZ+V/szt3lXQRIyqvOpJlW0MceLM3tUyTGwqy4y0uwq7w9KGD/VqytnCpiQbkjVmmfScimXwRW7PHjHyRXLCVw=="], + "verdaccio-htpasswd": ["verdaccio-htpasswd@13.0.0-next-8.33", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.33", "@verdaccio/file-locking": "13.0.0-next-8.6", "apache-md5": "1.1.8", "bcryptjs": "2.4.3", "debug": "4.4.3", "http-errors": "2.0.1", "unix-crypt-td-js": "1.1.4" } }, "sha512-ZYqvF/EuA4W4idfv2O1ZN8YhSI2xreFbGdrcWgOd+QBedDin7NzMhgypbafutm9mPhkI6Xv/+3syee1u1cEL8g=="], "verror": ["verror@1.10.0", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw=="], @@ -3846,7 +3846,7 @@ "@babel/traverse/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], - "@bknd/plasmic/@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], + "@bknd/plasmic/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "@bundled-es-modules/tough-cookie/tough-cookie": ["tough-cookie@4.1.4", "", { "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", "universalify": "^0.2.0", "url-parse": "^1.5.3" } }, "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag=="], @@ -3924,6 +3924,10 @@ "@puppeteer/browsers/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], + "@rjsf/core/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + + "@rjsf/utils/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "@rollup/plugin-babel/@rollup/pluginutils": ["@rollup/pluginutils@3.1.0", "", { "dependencies": { "@types/estree": "0.0.39", "estree-walker": "^1.0.1", "picomatch": "^2.2.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0" } }, "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg=="], "@rollup/plugin-commonjs/@rollup/pluginutils": ["@rollup/pluginutils@3.1.0", "", { "dependencies": { "@types/estree": "0.0.39", "estree-walker": "^1.0.1", "picomatch": "^2.2.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0" } }, "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg=="], @@ -4090,27 +4094,27 @@ "@typescript-eslint/typescript-estree/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "@typescript-eslint/typescript-estree/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], - "@verdaccio/config/minimatch": ["minimatch@7.4.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw=="], - - "@verdaccio/core/minimatch": ["minimatch@7.4.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw=="], - - "@verdaccio/core/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "@verdaccio/core/minimatch": ["minimatch@7.4.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-Brg/fp/iAVDOQoHxkuN5bEYhyQlZhxddI78yWsCbeEwTHXQjlNLtiJDUsp1GIptVqMI7/gkJMz4vVAc01mpoBw=="], "@verdaccio/local-storage-legacy/@verdaccio/core": ["@verdaccio/core@8.0.0-next-8.21", "", { "dependencies": { "ajv": "8.17.1", "http-errors": "2.0.0", "http-status-codes": "2.3.0", "minimatch": "7.4.6", "process-warning": "1.0.0", "semver": "7.7.2" } }, "sha512-n3Y8cqf84cwXxUUdTTfEJc8fV55PONPKijCt2YaC0jilb5qp1ieB3d4brqTOdCdXuwkmnG2uLCiGpUd/RuSW0Q=="], "@verdaccio/local-storage-legacy/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + "@verdaccio/local-storage-legacy/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "@verdaccio/logger-prettify/dayjs": ["dayjs@1.11.13", "", {}, "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="], + "@verdaccio/logger-prettify/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "@verdaccio/logger-prettify/pino-abstract-transport": ["pino-abstract-transport@1.2.0", "", { "dependencies": { "readable-stream": "^4.0.0", "split2": "^4.0.0" } }, "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q=="], "@verdaccio/logger-prettify/sonic-boom": ["sonic-boom@3.8.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg=="], - "@verdaccio/middleware/mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="], - - "@verdaccio/utils/minimatch": ["minimatch@7.4.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw=="], + "@verdaccio/utils/minimatch": ["minimatch@7.4.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-Brg/fp/iAVDOQoHxkuN5bEYhyQlZhxddI78yWsCbeEwTHXQjlNLtiJDUsp1GIptVqMI7/gkJMz4vVAc01mpoBw=="], "@vitejs/plugin-react/@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="], @@ -4158,6 +4162,8 @@ "archiver-utils/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], + "archiver-utils/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "archiver-utils/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], "aria-hidden/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -4186,14 +4192,14 @@ "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "body-parser/http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], + "body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], "body-parser/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], "browser-resolve/resolve": ["resolve@1.1.7", "", {}, "sha512-9znBF0vBcaSN3W2j7wKvdERPwqTxSpCq+if5C0WoTCyV9n24rua28jeuQ2pL/HOf+yUe/Mef+H/5p60K0Id3bg=="], - "bun-types/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], - "cheerio/undici": ["undici@6.21.1", "", {}, "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ=="], "class-utils/define-property": ["define-property@0.2.5", "", { "dependencies": { "is-descriptor": "^0.1.0" } }, "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA=="], @@ -4244,6 +4250,8 @@ "eslint/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], + "eslint/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "eslint/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "eslint/mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], @@ -4258,6 +4266,8 @@ "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + "eslint-plugin-flowtype/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], "eslint-plugin-import/doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], @@ -4288,13 +4298,13 @@ "expand-brackets/extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="], - "express/cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="], - "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "express/http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], + "express/path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="], - "express/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], + "express/qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], "external-editor/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], @@ -4346,12 +4356,16 @@ "has-values/kind-of": ["kind-of@4.0.0", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw=="], + "http-errors/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + "http-proxy-agent/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], "https-proxy-agent/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + "inquirer/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "ip-address/jsbn": ["jsbn@1.1.0", "", {}, "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="], "ip-address/sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], @@ -4424,7 +4438,9 @@ "jsdom/ws": ["ws@8.18.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w=="], - "jsonwebtoken/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], + "json-schema-compare/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + + "json-schema-merge-allof/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], "jszip/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], @@ -4438,6 +4454,8 @@ "log-update/cli-cursor": ["cli-cursor@2.1.0", "", { "dependencies": { "restore-cursor": "^2.0.0" } }, "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw=="], + "lowdb/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "lower-case/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "make-dir/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], @@ -4498,6 +4516,8 @@ "pumpify/pump": ["pump@2.0.1", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA=="], + "raw-body/http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], + "raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], "react-redux/use-sync-external-store": ["use-sync-external-store@1.4.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw=="], @@ -4532,6 +4552,8 @@ "request/uuid": ["uuid@3.4.0", "", { "bin": { "uuid": "./bin/uuid" } }, "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="], + "request-promise-core/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "request-promise-native/tough-cookie": ["tough-cookie@2.5.0", "", { "dependencies": { "psl": "^1.1.28", "punycode": "^2.1.1" } }, "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g=="], "resq/fast-deep-equal": ["fast-deep-equal@2.0.1", "", {}, "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w=="], @@ -4562,6 +4584,8 @@ "send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], + "send/http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], + "send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], "serialize-error/type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], @@ -4626,6 +4650,8 @@ "table/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + "table/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "table/string-width": ["string-width@3.1.0", "", { "dependencies": { "emoji-regex": "^7.0.1", "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^5.1.0" } }, "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w=="], "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], @@ -4750,8 +4776,6 @@ "@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.3", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg=="], - "@bknd/plasmic/@types/bun/bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], - "@bundled-es-modules/tough-cookie/tough-cookie/universalify": ["universalify@0.2.0", "", {}, "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg=="], "@cloudflare/vitest-pool-workers/miniflare/undici": ["undici@7.14.0", "", {}, "sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ=="], @@ -4834,10 +4858,20 @@ "@types/yauzl/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + "@verdaccio/core/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "@verdaccio/local-storage-legacy/@verdaccio/core/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + + "@verdaccio/local-storage-legacy/@verdaccio/core/http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], + "@verdaccio/local-storage-legacy/@verdaccio/core/minimatch": ["minimatch@7.4.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw=="], + "@verdaccio/local-storage-legacy/@verdaccio/core/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + "@verdaccio/logger-prettify/pino-abstract-transport/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], + "@verdaccio/utils/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "@vitejs/plugin-react/@babel/core/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], "@vitejs/plugin-react/@babel/core/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], @@ -4918,8 +4952,6 @@ "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "bun-types/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], - "class-utils/define-property/is-descriptor": ["is-descriptor@0.1.7", "", { "dependencies": { "is-accessor-descriptor": "^1.0.1", "is-data-descriptor": "^1.0.1" } }, "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg=="], "compress-commons/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], @@ -5078,6 +5110,8 @@ "next/sharp/detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="], + "next/sharp/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + "node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], "node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], diff --git a/docs/content/docs/(documentation)/extending/plugins.mdx b/docs/content/docs/(documentation)/extending/plugins.mdx index df203270..072891e1 100644 --- a/docs/content/docs/(documentation)/extending/plugins.mdx +++ b/docs/content/docs/(documentation)/extending/plugins.mdx @@ -335,3 +335,55 @@ export default { ``` + + +### `sort` + +A plugin that maintains sequential numeric positions for one or more entities. Positions are updated automatically when items are inserted or moved. You can optionally configure a scope so that positions are maintained independently within groups, such as tasks within a project. + +```typescript title="bknd.config.ts" +import { sort } from "bknd/plugins"; + +export default { + options: { + plugins: [ + sort({ + entities: { + tasks: { + field: "position", + scope: "project_id", + }, + }, + }), + ], + }, +} satisfies BkndConfig; +``` + +The configured `field` is used for the numeric position. If the field does not exist, the plugin adds it as a number field with a default value of `0`; if it already exists, it must be a number field. The plugin also adds an index for the field when one is not already present. The optional `scope` field limits reordering to records with the same scope value. + +The plugin registers the following endpoints. The default base path is `/api/sort` and can be changed with `apiBasePath`: + +- `POST /api/sort/:entity/reorder` moves an item to a position. The request body must include the item `id` and its new numeric `position`. +- `POST /api/sort/:entity/recalculate` recalculates positions in ascending order of the configured field. To recalculate only one scope, pass its value in the optional `scope` property. + +```http title="Move a task" +POST /api/sort/tasks/reorder +Content-Type: application/json + +{ + "id": "task_123", + "position": 2 +} +``` + +```http title="Recalculate a project's task positions" +POST /api/sort/tasks/recalculate +Content-Type: application/json + +{ + "scope": "project_456" +} +``` + + diff --git a/docs/content/docs/(documentation)/integration/(frameworks)/meta.json b/docs/content/docs/(documentation)/integration/(frameworks)/meta.json index 5cc79009..cc263d11 100644 --- a/docs/content/docs/(documentation)/integration/(frameworks)/meta.json +++ b/docs/content/docs/(documentation)/integration/(frameworks)/meta.json @@ -5,6 +5,8 @@ "astro", "sveltekit", "tanstack-start", - "vite" + "vite", + "nuxt", + "solid-start" ] } diff --git a/docs/content/docs/(documentation)/integration/(frameworks)/nuxt.mdx b/docs/content/docs/(documentation)/integration/(frameworks)/nuxt.mdx new file mode 100644 index 00000000..a96268a1 --- /dev/null +++ b/docs/content/docs/(documentation)/integration/(frameworks)/nuxt.mdx @@ -0,0 +1,402 @@ +--- +title: "Nuxt" +description: "Run bknd inside Nuxt" +tags: ["documentation"] +--- + +## Installation + +To get started with Nuxt and bknd, create a new Nuxt project by following the [official guide](https://nuxt.com/docs/4.x/getting-started/installation), and then install bknd as a dependency: + + + +```bash tab="npm" +npm install bknd +``` + +```bash tab="pnpm" +pnpm install bknd +``` + +```bash tab="yarn" +yarn add bknd +``` + +```bash tab="bun" +bun add bknd +``` + + + +## Configuration + + + When run with Node.js, a version of 22 (LTS) or higher is required. Please + verify your version by running `node -v`, and + [upgrade](https://nodejs.org/en/download/) if necessary. + + +Now create a `bknd.config.ts` file in the root of your project: + +```typescript title="bknd.config.ts" +import type NuxtBkndConfig from "bknd/adapter/nuxt"; +import { em, entity, text, boolean } from "bknd"; +import { secureRandomString } from "bknd/utils"; + +const schema = em({ + todos: entity("todos", { + title: text(), + done: boolean(), + }), +}); + +export default { + connection: { + url: "file:data.db", + }, + config: { + data: schema.toJSON(), + auth: { + enabled: true, + jwt: { + secret: secureRandomString(32), + }, + }, + }, + options: { + // the seed option is only executed if the database was empty + seed: async (ctx) => { + // create some entries + await ctx.em.mutator("todos").insertMany([ + { title: "Learn bknd", done: true }, + { title: "Build something cool", done: false }, + ]); + + // and create a user + await ctx.app.module.auth.createUser({ + email: "test@bknd.io", + password: "12345678", + }); + }, + }, +} satisfies NuxtBkndConfig; +``` + +For more information about the connection object, refer to the [Database](/usage/database) guide. + + +See [bknd.config.ts](/extending/config) for more information on how to configure bknd. The `NuxtBkndConfig` type extends the base config type with the following properties: + +```typescript +export type NuxtBkndConfig = FrameworkBkndConfig; +``` + +## Serve the API and Admin UI + +The Nuxt adapter uses Nuxt middleware to handle API requests and serve the Admin UI. Create a `/server/middleware/bknd.ts` file: + +```typescript title="/server/middleware/bknd.ts" +import { serve } from "bknd/adapter/nuxt"; +import config from "../../bknd.config"; + +const handler = serve(config, process.env); + +export default defineEventHandler(async (event) => { + const pathname = event.path; + const request = toWebRequest(event); + + if (pathname.startsWith("/api") || pathname !== "/") { + const res = await handle(request); + + if (res && res.status !== 404) { + return res; + } + } +}); +``` + + + You can visit https://localhost:3000/admin to see the admin UI. Additionally you can create more todos as you explore the admin UI. + + +Create a helper file to instantiate the bknd instance and retrieve the API, importing the configuration from the `bknd.config.ts` file: + +```ts title="server/utils/bknd.ts" +import { type NuxtBkndConfig, getApp as getNuxtApp } from "bknd/adapter/nuxt"; +import bkndConfig from "../../bknd.config"; + +export async function getApp( + config: NuxtBkndConfig, + args: Env = process.env as Env, +) { + return await getNuxtApp(config, args); +} + +export async function getApi({ headers, verify }: { verify?: boolean; headers?: Headers }) { + const app = await getApp(bkndConfig, process.env); + + if (verify) { + const api = app.getApi({ headers }); + await api.verifyAuth(); + return api; + } + + return app.getApi(); +}; +``` + + + The adapter uses `process.env` to access environment variables, this works because Nuxt uses Nitro underneath and it will use polyfills for `process.env` making it platform/runtime agnostic. + + + +## Example usage of the API + +You can use the `getApp` function to access the bknd API in your app to expose endpoints, +Here are some examples: + +```typescript title="server/routes/todos.post.ts" +export default defineEventHandler(async (event) => { + const body = await readBody(event); + const { data, action } = body; + + const api = await getApi({}); + + switch (action) { + case 'get': { + const limit = 5; + const todos = await api.data.readMany("todos", { limit, sort: "-id" }); + return { total: todos.body.meta.total, todos, limit }; + } + + case 'create': { + return await api.data.createOne("todos", { title: data.title }); + } + + case 'delete': { + return await api.data.deleteOne("todos", data.id); + } + + case 'toggle': { + return await api.data.updateOne("todos", data.id, { done: !data.done }); + } + + default: { + return { path: action }; + } + } +}); +``` + + + This can't be done in the `server/api` directory as it will collide with the API endpoints created by the middleware. We will use [defineEventHandler](https://nuxt.com/docs/4.x/directory-structure/server) in the [server/routes](https://nuxt.com/docs/4.x/directory-structure/server) directory to create endpoints and use them in conjunction with composables to access the API safely. + + + +### Using the API through composables + +To use the API in your frontend components/pages, you can create a composable that uses the enpoints created in previous steps: + +```typescript title="app/composables/useTodoActions.ts" +import type { DB } from "bknd"; + +type Todo = DB["todos"]; + +export const useTodoActions = () => { + const fetchTodos = () => + $fetch<{ limit: number; todos: Array; total: number }>("/todos", { + method: "POST", + body: { action: "get" }, + }); + + const createTodo = (title: string) => + $fetch("/todos", { + method: "POST", + body: { action: "create", data: { title } }, + }); + + const deleteTodo = (todo: Todo) => + $fetch("/todos", { + method: "POST", + body: { action: "delete", data: { id: todo.id } }, + }); + + const toggleTodo = (todo: Todo) => + $fetch("/todos", { + method: "POST", + body: { action: "toggle", data: todo }, + }); + + return { fetchTodos, createTodo, deleteTodo, toggleTodo }; +}; +``` + +### Usage in a page/component + +Make a composable to fetch the todos: + +```ts title="app/composables/useTodoActions.ts" +import type { DB } from "bknd"; + +type Todo = DB["todos"]; + +export const useTodoActions = () => { + const fetchTodos = () => + $fetch<{ limit: number; todos: Array; total: number }>("/todos", { + method: "POST", + body: { action: "get" }, + }); + + const createTodo = (title: string) => + $fetch("/todos", { + method: "POST", + body: { action: "create", data: { title } }, + }); + + const deleteTodo = (todo: Todo) => + $fetch("/todos", { + method: "POST", + body: { action: "delete", data: { id: todo.id } }, + }); + + const toggleTodo = (todo: Todo) => + $fetch("/todos", { + method: "POST", + body: { action: "toggle", data: todo }, + }); + + return { fetchTodos, createTodo, deleteTodo, toggleTodo }; +}; +``` +Then use the `useTodoActions` composable in a page: + +```vue title="app/pages/todos.vue" + + + +``` + + + You can visit https://localhost:3000/todos to see all the todos. + + +### Using authentication + +Make a composable to fetch the user: + +```ts title="app/composables/useUser.ts" +import type { User } from "bknd"; + +export const useUser = () => { + const getUser = () => $fetch("/api/auth/me") as Promise<{ user: User }>; + return { getUser }; +}; +``` + +Then use the `useUser` composable in a page: + +```vue title="app/pages/user.vue" + + + +``` + +## Important Note + +Use `external` attribute on Nuxt links, anytime you are traversing to an external route (like `/api/*` which are handled by bknd's middleware) to prevent vue router from intercepting the link. + +```vue + + Admin + +``` + + + If you don't use the `external` attribute, vue router will intercept the link and try to navigate to it, which will fail and result in a 404 error. + + +Check the [Nuxt repository example](https://github.com/bknd-io/bknd/tree/main/examples/nuxt) for more implementation details. \ No newline at end of file diff --git a/docs/content/docs/(documentation)/integration/(frameworks)/solid-start.mdx b/docs/content/docs/(documentation)/integration/(frameworks)/solid-start.mdx new file mode 100644 index 00000000..5174f2fa --- /dev/null +++ b/docs/content/docs/(documentation)/integration/(frameworks)/solid-start.mdx @@ -0,0 +1,334 @@ +--- +title: "Solid Start" +description: "Run bknd inside Solid Start" +tags: ["documentation"] +--- + +## Installation + +To get started with Solid Start and bknd you can either install the package manually, and follow the descriptions below, or use the CLI starter. + +Create a new Solid Start project by following the [official guide](https://docs.solidjs.com/solid-start/getting-started), and then install bknd as a dependency: + + + +```bash tab="npm" +npm install bknd +``` + +```bash tab="pnpm" +pnpm install bknd +``` + +```bash tab="yarn" +yarn add bknd +``` + +```bash tab="bun" +bun add bknd +``` + + + +## Configuration + + + When run with Node.js, a version of 22 (LTS) or higher is required. Please + verify your version by running `node -v`, and + [upgrade](https://nodejs.org/en/download/) if necessary. + + +Now create a `bknd.config.ts` file in the root of your project. If you created the project using the CLI starter, this file is already created for you. + +```typescript title="bknd.config.ts" +import type { SolidStartBkndConfig } from "bknd/adapter/solid-start"; +import { em, entity, text, boolean } from "bknd"; +import { secureRandomString } from "bknd/utils"; + +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" { + interface DB extends Database {} +} + +export default { + connection: { + url: "file:data.db", + }, + options: { + // the seed option is only executed if the database was empty + seed: async (ctx) => { + // create some entries + await ctx.em.mutator("todos").insertMany([ + { title: "Learn bknd", done: true }, + { title: "Build something cool", done: false }, + ]); + + // and create a user + await ctx.app.module.auth.createUser({ + email: "test@bknd.io", + password: "12345678", + }); + }, + }, + config: { + data: schema.toJSON(), + auth: { + enabled: true, + jwt: { + secret: secureRandomString(32), + }, + }, + }, + // please run `bun bknd copy-assets --out public/admin` to copy the admin assets into the public directory + // alternatively add this as a postinstall script in your package.json like this: + // "postinstall": "bknd copy-assets --out public/admin" + adminOptions: { + adminBasepath: "/admin", + assetsPath: "/admin/", // trailing slash is important + }, +} satisfies SolidStartBkndConfig; +``` + +See [bknd.config.ts](/extending/config) for more information on how to configure bknd. The `SolidStartBkndConfig` type extends the `BkndConfig` type with the following additional properties: + +```typescript +export type SolidStartBkndConfig = RuntimeBkndConfig; +``` + +## Serve the API and Admin UI + +The Solid Start adapter uses middleware to handle API requests and serve the Admin UI. Create a `src/middleware/index.ts` file: + +```ts title="src/middleware/index.ts" +import { createMiddleware } from "@solidjs/start/middleware"; +import config from "../../bknd.config"; +import { serve } from "bknd/adapter/solid-start"; + +const handler = serve(config); + +export default createMiddleware({ + onRequest: async (event) => { + const url = new URL(event.request.url); + const pathname = url.pathname; + + if (pathname.startsWith("/api") || pathname !== "/") { + const res = await handler(event.request); + + if (res && res.status !== 404) { + return res; + } + } + }, +}); +``` + +### Update the app config (Solid Start v1 based on vinxi) + +Add the following to your `app.config.ts` file: + +```ts title="app.config.ts" +import { defineConfig } from "@solidjs/start/config"; + +const app = defineConfig({ + // ... your existing config + middleware: "src/middleware/index.ts", + // ... your existing config +}); + +export default app; +``` + +### Update the vite config (Solid Start v2) + +Add the following to your `vite.config.ts` file: + +```ts title="vite.config.ts" +import { defineConfig } from "vite"; +import solid from "vite-plugin-solid"; +import { solidStart } from "@solidjs/start/vite"; + +export default defineConfig({ + plugins: [ + solid(), + solidStart({ + middleware: "./src/middleware/index.ts", + }), + ], +}); +``` + + + + You can visit https://localhost:3000/admin to see the admin UI. Additionally you can create more todos as you explore the admin UI. + + +Create a helper file to instantiate the bknd instance and retrieve the API, importing the configuration from the `bknd.config.ts` file: + + +```ts title="src/lib/bknd.ts" +import { getApp as getBkndApp } from "bknd/adapter/solid-start"; +import bkndConfig from "../../bknd.config"; +import type { App } from "bknd"; + +export const getApp = async () => { + return await getBkndApp(bkndConfig); +}; + +export async function getApi({ + headers, + verify, +}: { + verify?: boolean; + headers?: Headers; +}) { + const app = await getApp(); + + if (verify) { + const api = app.getApi({ headers }); + await api.verifyAuth(); + return api; + } + + return app.getApi(); +}; +``` +## Example usage of the API + +You can use the `getApi` helper function we've already set up to fetch and mutate in static pages and server components: + +```tsx title="src/routes/index.tsx" +import { getApi } from "~/lib/bknd"; +import { query, createAsync } from "@solidjs/router"; + +export const getTodo = async () => { + "use server" + const api = await getApi({}); + const limit = 5; + const todos = await api.data.readMany("todos"); + const total = todos.body.meta.total as number; + return { total, todos: todos as unknown as Todo[], limit }; +}; + +const getTodosFromServer = query(async () => await getTodo(), "getTodosFromServer"); + +export default function Home() { + const data = createAsync(() => getTodosFromServer()); + + return ( +
    + + {(todo) =>
  • {todo.title}
  • } +
    +
+ ); +} +``` + +## Using authentication + +When using authentication, you'll need to pass the headers to the `getApi` function to verify the user is authenticated. + +```tsx title="src/routes/auth.tsx" +import { createResource, Suspense } from "solid-js"; +import { A } from "@solidjs/router"; +import { getRequestEvent } from "solid-js/web"; +import { getApi } from "~/lib/bknd"; + +export const getUser = async () => { + "use server" + const request = getRequestEvent()?.request; + const api = await getApi({ verify: true, headers: request?.headers }); + return api.getUser(); +} + +export default function Auth() { + const [data] = createResource(async () => { + const user = await getUser() + return { user }; + }, { + initialValue: { + user: null + } + }); + + return ( +
+
+
+ Solid logo +
&
+ bknd logo +
+ + Loading...

}> +
+ {data()?.user ? ( + <> + Logged in as {data()?.user?.email}. + + Logout + + + ) : ( +
+

+ Not logged in. + + Login + +

+

+ Sign in with: + + test@bknd.io + + / + + 12345678 + +

+
+ )} +
+ +
+
+ ); +} +``` + + +## Tips + +Use `target="_self"` on links, anytime your traversing to an external route (like `/api/auth/logout` which are handled by bknd's middleware) to prevent solid router from intercepting the link. +```jsx + + Logout + +``` \ No newline at end of file diff --git a/docs/content/docs/(documentation)/integration/introduction.mdx b/docs/content/docs/(documentation)/integration/introduction.mdx index 6b943744..8a854f53 100644 --- a/docs/content/docs/(documentation)/integration/introduction.mdx +++ b/docs/content/docs/(documentation)/integration/introduction.mdx @@ -39,6 +39,18 @@ bknd seamlessly integrates with popular frameworks, allowing you to use what you href="/integration/tanstack-start" /> +} + title="Nuxt" + href="/integration/nuxt" +/> + +} + title="Solid Start" + href="/integration/solid-start" +/> + Create a new issue to request a guide for your framework. diff --git a/docs/content/docs/(documentation)/start.mdx b/docs/content/docs/(documentation)/start.mdx index 744588d6..6edc0276 100644 --- a/docs/content/docs/(documentation)/start.mdx +++ b/docs/content/docs/(documentation)/start.mdx @@ -156,6 +156,18 @@ Pick your framework or runtime to get started. href="/integration/tanstack-start" /> +} + title="Nuxt" + href="/integration/nuxt" +/> + +} + title="Solid Start" + href="/integration/solid-start" +/> + } title="AWS Lambda" diff --git a/docs/tsconfig.json b/docs/tsconfig.json index 17aaff78..524dcd37 100644 --- a/docs/tsconfig.json +++ b/docs/tsconfig.json @@ -1,7 +1,6 @@ { "compilerOptions": { - "baseUrl": ".", - "target": "ESNext", + "target": "es2023", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, diff --git a/examples/bun/index.ts b/examples/bun/index.ts index c8f1b79e..c08a6254 100644 --- a/examples/bun/index.ts +++ b/examples/bun/index.ts @@ -6,7 +6,7 @@ import { type BunBkndConfig, serve } from "bknd/adapter/bun"; // this is optional, if omitted, it uses an in-memory database const config: BunBkndConfig = { connection: { - url: "file:data.db", + url: "data.db", }, config: { media: { diff --git a/examples/bun/tsconfig.json b/examples/bun/tsconfig.json index 6f144048..d2110229 100644 --- a/examples/bun/tsconfig.json +++ b/examples/bun/tsconfig.json @@ -17,7 +17,6 @@ "noUnusedLocals": false, "noUnusedParameters": false, "noPropertyAccessFromIndexSignature": false, - "baseUrl": "." }, "include": ["index.ts"] } diff --git a/examples/node/index.js b/examples/node/index.js index 46e319f0..935d7e0f 100644 --- a/examples/node/index.js +++ b/examples/node/index.js @@ -7,7 +7,7 @@ import { serve } from "bknd/adapter/node"; /** @type {import("bknd/adapter/node").NodeBkndConfig} */ const config = { connection: { - url: "file:data.db", + url: "data.db", }, config: { media: { diff --git a/examples/nuxt/.gitignore b/examples/nuxt/.gitignore new file mode 100644 index 00000000..8cd13a3a --- /dev/null +++ b/examples/nuxt/.gitignore @@ -0,0 +1,27 @@ +# Nuxt dev/build outputs +.output +.data +.nuxt +.nitro +.cache +dist + +# Node dependencies +node_modules + +# Logs +logs +*.log + +# Misc +.DS_Store +.fleet +.idea + +# Local env files +.env +.env.* +!.env.example + +public/admin +data.db \ No newline at end of file diff --git a/examples/nuxt/README.md b/examples/nuxt/README.md new file mode 100644 index 00000000..25990b9b --- /dev/null +++ b/examples/nuxt/README.md @@ -0,0 +1,64 @@ +# bknd starter: Nuxt +A minimal example of a Nuxt project with bknd integration. + +## Project Structure + +Inside of your Nuxt project, you'll see the following folders and files: + +```text +. +├── app +│ ├── assets +│ │ └── css +│ ├── components +│ │ ├── Buttons.vue +│ │ ├── Footer.vue +│ │ └── List.vue +│ ├── composables +│ │ ├── useTodoActions.ts +│ │ └── useUser.ts +│ └── pages +│ ├── index.vue +│ └── user.vue +├── bknd.config.ts +├── bun.lock +├── nuxt.config.ts +├── package.json +├── public +│ ├── admin # generated by bknd and contains the admin UI +│ ├── bknd.ico +│ ├── bknd.svg +│ ├── favicon.ico +│ ├── file.svg +│ ├── globe.svg +│ ├── nuxt.svg +│ ├── robots.txt +│ └── window.svg +├── README.md +├── server +│ ├── middleware +│ │ └── bknd.ts # intercepts api and admin ui requests +│ ├── routes +│ │ └── todos.post.ts +│ └── utils +│ └── bknd.ts # initializes bknd instance +└── tsconfig.json +``` + +Here is a quick overview about how to adjust the behavior of `bknd`: +* Initialization of the `bknd` config with helper functions are located at `src/server/utils/bknd.ts` +* Admin UI is rendered at `src/server/middleware/bknd.ts` + +## Commands + +All commands are run from the root of the project, from a terminal: + +| Command | Action | +|:--------------------------|:-------------------------------------------------| +| `npm install` | Installs dependencies | +| `npm run dev` | Starts local dev server at `localhost:3000` | +| `npm run build` | Build your production site | + +## Want to learn more? + +Feel free to check [our documentation](https://docs.bknd.io/integration/nuxt) or jump into our [Discord server](https://discord.gg/952SFk8Tb8). \ No newline at end of file diff --git a/examples/nuxt/app/assets/css/main.css b/examples/nuxt/app/assets/css/main.css new file mode 100644 index 00000000..b0042ae7 --- /dev/null +++ b/examples/nuxt/app/assets/css/main.css @@ -0,0 +1,33 @@ +@import url("https://fonts.googleapis.com/css2?family=Geist+Mono:wght@100..900&display=swap"); + +.geist-mono-100 { + font-optical-sizing: auto; + font-weight: 100; + font-style: normal; +} + +:root { + --background: #ffffff; + --foreground: #171717; + font-family: "Geist Mono", monospace; +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +@theme { + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); + --color-background: var(--background); + --color-foreground: var(--foreground); +} + +body { + background-color: var(--background); + color: var(--foreground); + font-family: var(--font-geist-mono-100); +} \ No newline at end of file diff --git a/examples/nuxt/app/components/Buttons.vue b/examples/nuxt/app/components/Buttons.vue new file mode 100644 index 00000000..864dc3a2 --- /dev/null +++ b/examples/nuxt/app/components/Buttons.vue @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/examples/nuxt/app/components/Footer.vue b/examples/nuxt/app/components/Footer.vue new file mode 100644 index 00000000..be698bf8 --- /dev/null +++ b/examples/nuxt/app/components/Footer.vue @@ -0,0 +1,29 @@ + + + diff --git a/examples/nuxt/app/components/List.vue b/examples/nuxt/app/components/List.vue new file mode 100644 index 00000000..a3fddc22 --- /dev/null +++ b/examples/nuxt/app/components/List.vue @@ -0,0 +1,21 @@ + + + \ No newline at end of file diff --git a/examples/nuxt/app/composables/useTodoActions.ts b/examples/nuxt/app/composables/useTodoActions.ts new file mode 100644 index 00000000..138a9d21 --- /dev/null +++ b/examples/nuxt/app/composables/useTodoActions.ts @@ -0,0 +1,31 @@ +import type { DB } from "bknd"; + +type Todo = DB["todos"]; + +export const useTodoActions = () => { + const fetchTodos = () => + $fetch<{ limit: number; todos: Array; total: number }>("/todos", { + method: "POST", + body: { action: "get" }, + }); + + const createTodo = (title: string) => + $fetch("/todos", { + method: "POST", + body: { action: "create", data: { title } }, + }); + + const deleteTodo = (todo: Todo) => + $fetch("/todos", { + method: "POST", + body: { action: "delete", data: { id: todo.id } }, + }); + + const toggleTodo = (todo: Todo) => + $fetch("/todos", { + method: "POST", + body: { action: "toggle", data: todo }, + }); + + return { fetchTodos, createTodo, deleteTodo, toggleTodo }; +}; diff --git a/examples/nuxt/app/composables/useUser.ts b/examples/nuxt/app/composables/useUser.ts new file mode 100644 index 00000000..1dc371a7 --- /dev/null +++ b/examples/nuxt/app/composables/useUser.ts @@ -0,0 +1,6 @@ +import type { User } from "bknd"; + +export const useUser = () => { + const getUser = () => $fetch("/api/auth/me") as Promise<{ user: User }>; + return { getUser }; +}; diff --git a/examples/nuxt/app/pages/index.vue b/examples/nuxt/app/pages/index.vue new file mode 100644 index 00000000..1c58fb1e --- /dev/null +++ b/examples/nuxt/app/pages/index.vue @@ -0,0 +1,70 @@ + + + diff --git a/examples/nuxt/app/pages/user.vue b/examples/nuxt/app/pages/user.vue new file mode 100644 index 00000000..f2775220 --- /dev/null +++ b/examples/nuxt/app/pages/user.vue @@ -0,0 +1,55 @@ + + + \ No newline at end of file diff --git a/examples/nuxt/bknd.config.ts b/examples/nuxt/bknd.config.ts new file mode 100644 index 00000000..3c144c03 --- /dev/null +++ b/examples/nuxt/bknd.config.ts @@ -0,0 +1,59 @@ +import { em, entity, text, boolean, } from "bknd"; +import { secureRandomString } from "bknd/utils"; +import type { NuxtBkndConfig } from "bknd/adapter/nuxt"; +import { registerLocalMediaAdapter } from "bknd/adapter/node"; + +const local = 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" { + interface DB extends Database { } +} + +export default { + connection: { url: "file:data.db" }, + options: { + // the seed option is only executed if the database was empty + seed: async (ctx) => { + // create some entries + await ctx.em.mutator("todos").insertMany([ + { title: "Learn bknd", done: true }, + { title: "Build something cool", done: false }, + ]); + + // and create a user + await ctx.app.module.auth.createUser({ + email: "test@bknd.io", + password: "12345678", + }); + }, + }, + config: { + data: schema.toJSON(), + auth: { + enabled: true, + jwt: { + secret: secureRandomString(32), + }, + }, + media: { + enabled: true, + adapter: local({ + path: "./public/uploads", + }), + }, + }, + adminOptions: { + adminBasepath: "/admin", + assetsPath: "/admin/", + logoReturnPath: "../..", + }, +} satisfies NuxtBkndConfig; diff --git a/examples/nuxt/nuxt.config.ts b/examples/nuxt/nuxt.config.ts new file mode 100644 index 00000000..da89e1f7 --- /dev/null +++ b/examples/nuxt/nuxt.config.ts @@ -0,0 +1,12 @@ +// https://nuxt.com/docs/api/configuration/nuxt-config +export default defineNuxtConfig({ + compatibilityDate: "2025-07-15", + devtools: { enabled: false }, + modules: ["@nuxtjs/tailwindcss"], + app: { + head: { + title: "Nuxt 🤝 Bknd.io", + }, + }, + css: ["assets/css/main.css"], +}); diff --git a/examples/nuxt/package.json b/examples/nuxt/package.json new file mode 100644 index 00000000..139736d1 --- /dev/null +++ b/examples/nuxt/package.json @@ -0,0 +1,21 @@ +{ + "name": "nuxbknd", + "type": "module", + "private": true, + "scripts": { + "build": "nuxt build", + "dev": "nuxt dev", + "generate": "nuxt generate", + "preview": "nuxt preview", + "typegen": "bunx tsx node_modules/.bin/bknd types --outfile bknd-types.d.ts", + "postinstall": "nuxt prepare && bun run bknd copy-assets --out public/admin" + }, + "dependencies": { + "@nuxtjs/tailwindcss": "6.14.0", + "@types/node": "^25.2.3", + "bknd": "file:../../app", + "nuxt": "^4.3.1", + "vue": "^3.5.28", + "vue-router": "^4.6.4" + } +} diff --git a/examples/nuxt/public/bknd.ico b/examples/nuxt/public/bknd.ico new file mode 100644 index 00000000..c1a946d5 Binary files /dev/null and b/examples/nuxt/public/bknd.ico differ diff --git a/examples/nuxt/public/bknd.svg b/examples/nuxt/public/bknd.svg new file mode 100644 index 00000000..182ef92d --- /dev/null +++ b/examples/nuxt/public/bknd.svg @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/examples/nuxt/public/favicon.ico b/examples/nuxt/public/favicon.ico new file mode 100644 index 00000000..18993ad9 Binary files /dev/null and b/examples/nuxt/public/favicon.ico differ diff --git a/examples/nuxt/public/file.svg b/examples/nuxt/public/file.svg new file mode 100644 index 00000000..004145cd --- /dev/null +++ b/examples/nuxt/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/nuxt/public/globe.svg b/examples/nuxt/public/globe.svg new file mode 100644 index 00000000..567f17b0 --- /dev/null +++ b/examples/nuxt/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/nuxt/public/nuxt.svg b/examples/nuxt/public/nuxt.svg new file mode 100644 index 00000000..5ad1e9dc --- /dev/null +++ b/examples/nuxt/public/nuxt.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/nuxt/public/robots.txt b/examples/nuxt/public/robots.txt new file mode 100644 index 00000000..0ad279c7 --- /dev/null +++ b/examples/nuxt/public/robots.txt @@ -0,0 +1,2 @@ +User-Agent: * +Disallow: diff --git a/examples/nuxt/public/window.svg b/examples/nuxt/public/window.svg new file mode 100644 index 00000000..b2b2a44f --- /dev/null +++ b/examples/nuxt/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/nuxt/server/middleware/bknd.ts b/examples/nuxt/server/middleware/bknd.ts new file mode 100644 index 00000000..d298768d --- /dev/null +++ b/examples/nuxt/server/middleware/bknd.ts @@ -0,0 +1,15 @@ +import { serve } from "bknd/adapter/nuxt"; +import config from "../../bknd.config"; + +export default defineEventHandler(async (event) => { + const pathname = event.path + const request = toWebRequest(event); + + if (pathname.startsWith("/api") || pathname !== "/") { + const res = await serve(config, process.env)(request); + + if (res && res.status !== 404) { + return res; + } + } +}); diff --git a/examples/nuxt/server/routes/todos.post.ts b/examples/nuxt/server/routes/todos.post.ts new file mode 100644 index 00000000..314e0ff3 --- /dev/null +++ b/examples/nuxt/server/routes/todos.post.ts @@ -0,0 +1,31 @@ +export default defineEventHandler(async (event) => { + const body = await readBody(event); + const { data, action } = body; + + const api = await getApi({}); + + switch (action) { + case 'get': { + const limit = 5; + const todos = await api.data.readMany("todos", { limit, sort: "-id" }); + return { total: todos.body.meta.total, todos, limit }; + } + + case 'create': { + return await api.data.createOne("todos", { title: data.title }); + } + + case 'delete': { + return await api.data.deleteOne("todos", data.id); + } + + case 'toggle': { + return await api.data.updateOne("todos", data.id, { done: !data.done }); + } + + default: { + return { path: action }; + } + } + +}); diff --git a/examples/nuxt/server/utils/bknd.ts b/examples/nuxt/server/utils/bknd.ts new file mode 100644 index 00000000..6b8d02a1 --- /dev/null +++ b/examples/nuxt/server/utils/bknd.ts @@ -0,0 +1,21 @@ +import { type NuxtBkndConfig, getApp as getNuxtApp } from "bknd/adapter/nuxt"; +import bkndConfig from "../../bknd.config"; + +export async function getApp( + config: NuxtBkndConfig, + args: Env = process.env as Env, +) { + return await getNuxtApp(config, args); +} + +export async function getApi({ headers, verify }: { verify?: boolean; headers?: Headers }) { + const app = await getApp(bkndConfig, process.env); + + if (verify) { + const api = app.getApi({ headers }); + await api.verifyAuth(); + return api; + } + + return app.getApi(); +} diff --git a/examples/nuxt/tsconfig.json b/examples/nuxt/tsconfig.json new file mode 100644 index 00000000..307b2134 --- /dev/null +++ b/examples/nuxt/tsconfig.json @@ -0,0 +1,18 @@ +{ + // https://nuxt.com/docs/guide/concepts/typescript + "files": [], + "references": [ + { + "path": "./.nuxt/tsconfig.app.json" + }, + { + "path": "./.nuxt/tsconfig.server.json" + }, + { + "path": "./.nuxt/tsconfig.shared.json" + }, + { + "path": "./.nuxt/tsconfig.node.json" + } + ] +} diff --git a/examples/postgres/tsconfig.json b/examples/postgres/tsconfig.json index 7fb1ec56..af3606b0 100644 --- a/examples/postgres/tsconfig.json +++ b/examples/postgres/tsconfig.json @@ -23,7 +23,6 @@ "isolatedModules": true, "esModuleInterop": true, "skipLibCheck": true, - "baseUrl": ".", "paths": { "bknd": ["../app/src/index.ts"], "bknd/*": ["../app/src/*"] diff --git a/examples/react-router/tsconfig.json b/examples/react-router/tsconfig.json index dc391a45..cbe49c7a 100644 --- a/examples/react-router/tsconfig.json +++ b/examples/react-router/tsconfig.json @@ -13,7 +13,6 @@ "moduleResolution": "bundler", "jsx": "react-jsx", "rootDirs": [".", "./.react-router/types"], - "baseUrl": ".", "paths": { "~/*": ["./app/*"] }, diff --git a/examples/solid/.gitignore b/examples/solid/.gitignore new file mode 100644 index 00000000..4dcc6201 --- /dev/null +++ b/examples/solid/.gitignore @@ -0,0 +1,31 @@ +dist +.wrangler +.output +.vercel +.netlify +.vinxi +app.config.timestamp_*.js + +# Environment +.env +.env*.local + +# dependencies +/node_modules + +# IDEs and editors +/.idea +.project +.classpath +*.launch +.settings/ + +# Temp +gitignore + +# System Files +.DS_Store +Thumbs.db + +public/admin +data.db diff --git a/examples/solid/README.md b/examples/solid/README.md new file mode 100644 index 00000000..c9b636ec --- /dev/null +++ b/examples/solid/README.md @@ -0,0 +1,59 @@ +# bknd starter: Solid Start +A minimal example of a Solid Start project with bknd integration. + +## Project Structure + +Inside of your Solid Start project, you'll see the following folders and files: + +```text +. +├── bknd.config.ts +├── bun.lock +├── package.json +├── public +│ ├── admin # generated by bknd and contains the admin UI +│ ├── bknd.ico +│ ├── bknd.svg +│ ├── file.svg +│ ├── globe.svg +│ ├── solid.svg +│ └── window.svg +├── README.md # you're here +├── src +│ ├── app.css +│ ├── app.tsx +│ ├── components +│ │ ├── Footer.tsx +│ │ └── List.tsx +│ ├── entry-client.tsx +│ ├── entry-server.tsx +│ ├── global.d.ts +│ ├── lib +│ │ └── bknd.ts # initializes bknd instance +│ ├── middleware +│ │ └── index.ts # mounts bknd middleware +│ └── routes +│ ├── [...404].tsx +│ ├── index.tsx +│ └── user.tsx +├── tsconfig.json +└── vite.config.ts +``` + +Here is a quick overview about how to adjust the behavior of `bknd`: +* Initialization of the `bknd` config with helper functions are located at `src/lib/bknd.ts` +* Admin UI is rendered using `src/middleware/index.ts` + +## Commands + +All commands are run from the root of the project, from a terminal: + +| Command | Action | +|:--------------------------|:-------------------------------------------------| +| `npm install` | Installs dependencies | +| `npm run dev` | Starts local dev server at `localhost:3000` | +| `npm run build` | Build your production site | + +## Want to learn more? + +Feel free to check [our documentation](https://docs.bknd.io/integration/solid-start) or jump into our [Discord server](https://discord.gg/952SFk8Tb8). \ No newline at end of file diff --git a/examples/solid/bknd.config.ts b/examples/solid/bknd.config.ts new file mode 100644 index 00000000..d4c3fda0 --- /dev/null +++ b/examples/solid/bknd.config.ts @@ -0,0 +1,51 @@ +import { em, entity, text, boolean } from "bknd"; +import { secureRandomString } from "bknd/utils"; +import type { SolidStartBkndConfig } from "bknd/adapter/solid-start"; + +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" { + interface DB extends Database { } +} + +export default { + connection: { url: "file:data.db" }, + options: { + // the seed option is only executed if the database was empty + seed: async (ctx) => { + // create some entries + await ctx.em.mutator("todos").insertMany([ + { title: "Learn bknd", done: true }, + { title: "Build something cool", done: false }, + ]); + + // and create a user + await ctx.app.module.auth.createUser({ + email: "test@bknd.io", + password: "12345678", + }); + }, + }, + config: { + data: schema.toJSON(), + auth: { + enabled: true, + jwt: { + secret: secureRandomString(32), + }, + }, + }, + adminOptions: { + adminBasepath: "/admin", + assetsPath: "/admin/", + logoReturnPath: "../..", + }, +} satisfies SolidStartBkndConfig; + diff --git a/examples/solid/package.json b/examples/solid/package.json new file mode 100644 index 00000000..080b310f --- /dev/null +++ b/examples/solid/package.json @@ -0,0 +1,28 @@ +{ + "name": "example-basic", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "start": "vite start", + "preview": "vite preview", + "typegen": "bunx tsx node_modules/.bin/bknd types --outfile bknd-types.d.ts", + "postinstall": "bun run bknd copy-assets --out public/admin" + }, + "dependencies": { + "@solidjs/meta": "^0.29.4", + "@solidjs/router": "^1.0.0", + "@solidjs/start": "2.0.5", + "bknd": "file:../../app", + "nitro": "3.0.260903-beta", + "solid-js": "^1.9.5", + "vite": "^8.3.0" + }, + "engines": { + "node": ">=22" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.18", + "tailwindcss": "^4.1.18" + } +} \ No newline at end of file diff --git a/examples/solid/public/bknd.ico b/examples/solid/public/bknd.ico new file mode 100644 index 00000000..c1a946d5 Binary files /dev/null and b/examples/solid/public/bknd.ico differ diff --git a/examples/solid/public/bknd.svg b/examples/solid/public/bknd.svg new file mode 100644 index 00000000..182ef92d --- /dev/null +++ b/examples/solid/public/bknd.svg @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/examples/solid/public/file.svg b/examples/solid/public/file.svg new file mode 100644 index 00000000..004145cd --- /dev/null +++ b/examples/solid/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/solid/public/globe.svg b/examples/solid/public/globe.svg new file mode 100644 index 00000000..567f17b0 --- /dev/null +++ b/examples/solid/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/solid/public/solid.svg b/examples/solid/public/solid.svg new file mode 100644 index 00000000..25df77ff --- /dev/null +++ b/examples/solid/public/solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/solid/public/window.svg b/examples/solid/public/window.svg new file mode 100644 index 00000000..b2b2a44f --- /dev/null +++ b/examples/solid/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/solid/src/app.css b/examples/solid/src/app.css new file mode 100644 index 00000000..4a737b8c --- /dev/null +++ b/examples/solid/src/app.css @@ -0,0 +1,33 @@ +@import url('https://fonts.googleapis.com/css2?family=Geist+Mono:wght@100..900&display=swap'); +@import "tailwindcss"; + +.geist-mono-100 { + font-optical-sizing: auto; + font-weight: 100; + font-style: normal; +} + +:root { + --background: #ffffff; + --foreground: #171717; + font-family: "Geist Mono", monospace; +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +@theme { + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); + --color-background: var(--background); + --color-foreground: var(--foreground); +} + +body { + @apply bg-background text-foreground; + font-family: var(--font-geist-mono-100) +} \ No newline at end of file diff --git a/examples/solid/src/app.tsx b/examples/solid/src/app.tsx new file mode 100644 index 00000000..ce077acb --- /dev/null +++ b/examples/solid/src/app.tsx @@ -0,0 +1,20 @@ +import { MetaProvider, Title } from "@solidjs/meta"; +import { Router } from "@solidjs/router"; +import { FileRoutes } from "@solidjs/start/router"; +import { Suspense } from "solid-js"; +import "./app.css"; + +export default function App() { + return ( + ( + + Solid Start 🤝 Bknd.io + {props.children} + + )} + > + + + ); +} diff --git a/examples/solid/src/components/Footer.tsx b/examples/solid/src/components/Footer.tsx new file mode 100644 index 00000000..a6cdd70d --- /dev/null +++ b/examples/solid/src/components/Footer.tsx @@ -0,0 +1,53 @@ +import { A, useLocation } from "@solidjs/router"; + +export function Footer() { + const pathname = useLocation().pathname; + + return ( + + ); +} diff --git a/examples/solid/src/components/List.tsx b/examples/solid/src/components/List.tsx new file mode 100644 index 00000000..3ec1bb75 --- /dev/null +++ b/examples/solid/src/components/List.tsx @@ -0,0 +1,11 @@ +import { JSX } from "solid-js"; + +export const List = ({ items = [] }: { items: JSX.Element[] }) => ( +
    + {items.map((item, i) => ( +
  1. + {item} +
  2. + ))} +
+); diff --git a/examples/solid/src/entry-client.tsx b/examples/solid/src/entry-client.tsx new file mode 100644 index 00000000..0ca4e3c3 --- /dev/null +++ b/examples/solid/src/entry-client.tsx @@ -0,0 +1,4 @@ +// @refresh reload +import { mount, StartClient } from "@solidjs/start/client"; + +mount(() => , document.getElementById("app")!); diff --git a/examples/solid/src/entry-server.tsx b/examples/solid/src/entry-server.tsx new file mode 100644 index 00000000..38126d1f --- /dev/null +++ b/examples/solid/src/entry-server.tsx @@ -0,0 +1,21 @@ +// @refresh reload +import { createHandler, StartServer } from "@solidjs/start/server"; + +export default createHandler(() => ( + ( + + + + + + {assets} + + +
{children}
+ {scripts} + + + )} + /> +)); diff --git a/examples/solid/src/global.d.ts b/examples/solid/src/global.d.ts new file mode 100644 index 00000000..dc6f10c2 --- /dev/null +++ b/examples/solid/src/global.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/solid/src/lib/bknd.ts b/examples/solid/src/lib/bknd.ts new file mode 100644 index 00000000..170eadba --- /dev/null +++ b/examples/solid/src/lib/bknd.ts @@ -0,0 +1,30 @@ +import { getApp as getBkndApp } from "bknd/adapter/solid-start"; +import bkndConfig from "../../bknd.config"; +import type { App } from "bknd"; + +let client: App | null = null; + +export const getApp = async () => { + if (!client) { + client = await getBkndApp(bkndConfig); + } + return client; +}; + +export async function getApi({ + headers, + verify, +}: { + verify?: boolean; + headers?: Headers; +}) { + const app = await getApp(); + + if (verify) { + const api = app.getApi({ headers }); + await api.verifyAuth(); + return api; + } + + return app.getApi(); +} diff --git a/examples/solid/src/middleware/index.ts b/examples/solid/src/middleware/index.ts new file mode 100644 index 00000000..ebebed8f --- /dev/null +++ b/examples/solid/src/middleware/index.ts @@ -0,0 +1,20 @@ +import { createMiddleware } from "@solidjs/start/middleware"; +import config from "../../bknd.config"; +import { serve } from "bknd/adapter/solid-start"; + +const handler = serve(config); + +export default createMiddleware({ + onRequest: async (event) => { + const url = new URL(event.request.url); + const pathname = url.pathname; + + if (pathname.startsWith("/api") || pathname !== "/") { + const res = await handler(event.request); + + if (res && res.status !== 404) { + return res; + } + } + }, +}); diff --git a/examples/solid/src/routes/[...404].tsx b/examples/solid/src/routes/[...404].tsx new file mode 100644 index 00000000..4ea71ec7 --- /dev/null +++ b/examples/solid/src/routes/[...404].tsx @@ -0,0 +1,19 @@ +import { Title } from "@solidjs/meta"; +import { HttpStatusCode } from "@solidjs/start"; + +export default function NotFound() { + return ( +
+ Not Found + +

Page Not Found

+

+ Visit{" "} + + start.solidjs.com + {" "} + to learn how to build SolidStart apps. +

+
+ ); +} diff --git a/examples/solid/src/routes/index.tsx b/examples/solid/src/routes/index.tsx new file mode 100644 index 00000000..b83a9911 --- /dev/null +++ b/examples/solid/src/routes/index.tsx @@ -0,0 +1,150 @@ +import type { DB } from "bknd"; +import { For, Suspense } from "solid-js"; +import { Footer } from "~/components/Footer"; +import { List } from "~/components/List"; +import { getApi } from "~/lib/bknd"; +import { action, redirect, useAction, useSubmission } from "@solidjs/router"; +import { query, createAsync } from "@solidjs/router"; + +type Todo = DB['todos']; + +export const getTodo = async () => { + "use server" + const api = await getApi({}); + const limit = 5; + const todos = await api.data.readMany("todos"); + const total = todos.body.meta.total as number; + return { total, todos: todos as unknown as Todo[], limit }; +}; + +const getTodosFromServer = query(async () => await getTodo(), "getTodosFromServer"); + + +const createTodo = action(async (formData: FormData) => { + "use server" + const title = formData.get("title") as string; + const api = await getApi({}); + await api.data.createOne("todos", { title }); + throw redirect("/", { revalidate: getTodosFromServer.keyFor() }); +}, "createTodo"); + + + +const completeTodo = action(async (todo: Todo) => { + "use server" + const api = await getApi({}); + await api.data.updateOne("todos", todo.id, { + done: !todo.done, + }); + throw redirect("/", { revalidate: getTodosFromServer.keyFor() }); +}, "completeTodo"); + + +const deleteTodo = action(async (todo: Todo) => { + "use server" + const api = await getApi({}); + await api.data.deleteOne("todos", todo.id); + throw redirect("/", { revalidate: getTodosFromServer.keyFor() }); +}, "deleteTodo"); + +export default function Home() { + const data = createAsync(() => getTodosFromServer()); + + const submission = useSubmission(createTodo); + + const updateTodo = useAction(completeTodo); + const removeTodo = useAction(deleteTodo); + + return ( +
+
+
+ Solid Start logo +
&
+ bknd logo +
+ + +
+

+ What's next? +

+
+ {(data()?.total ?? 0) > (data()?.limit ?? 0) && ( +
+ {(data()?.total ?? 0) - (data()?.limit ?? 0)} more todo(s) hidden +
+ )} +
+ + {(todo) => ( +
+
+ { + await updateTodo(todo); + }} + /> +
+ {todo.title} +
+
+ +
+ )} +
+
+
+ + +
+
+
+
+
+
+
); +} + + + +const Description = () => ( + +); \ No newline at end of file diff --git a/examples/solid/src/routes/user.tsx b/examples/solid/src/routes/user.tsx new file mode 100644 index 00000000..4c0900e3 --- /dev/null +++ b/examples/solid/src/routes/user.tsx @@ -0,0 +1,139 @@ +import { A } from "@solidjs/router"; +import type { DB } from "bknd"; +import { createResource, Suspense } from "solid-js"; +import { getRequestEvent } from "solid-js/web"; +import { Footer } from "~/components/Footer"; +import { List } from "~/components/List"; +import { getApi } from "~/lib/bknd"; + + +const getUser = async () => { + "use server" + const request = getRequestEvent()?.request; + const api = await getApi({ verify: true, headers: request?.headers }); + return api.getUser(); +} + +type Todo = DB['todos']; +export const getTodo = async () => { + "use server" + const api = await getApi({}); + const limit = 5; + const todos = await api.data.readMany("todos"); + const total = todos.body.meta.total as number; + return { total, todos: todos as unknown as Todo[], limit }; +}; + +export default function Home() { + const [data] = createResource(async () => { + const todo = await getTodo(); + const user = await getUser() + return { todo, user }; + }, { + initialValue: { + todo: { + todos: [], + limit: 0, + total: 0 + }, + user: null + } + }); + + return ( +
+
+
+ Solid logo +
&
+ bknd logo +
+ todo.title)} /> + + + Loading...

}> +
+ {data()?.user ? ( + <> + Logged in as {data()?.user?.email}. + + Logout + + + ) : ( +
+

+ Not logged in. + + Login + +

+

+ Sign in with: + + test@bknd.io + + / + + 12345678 + +

+
+ )} +
+
+
+
+
+ ); +} + +function Buttons() { + return ( + + ); +} diff --git a/examples/solid/tsconfig.json b/examples/solid/tsconfig.json new file mode 100644 index 00000000..7692957f --- /dev/null +++ b/examples/solid/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowJs": true, + "strict": true, + "noEmit": true, + "types": ["vite/client"], + "isolatedModules": true, + "paths": { + "~/*": ["./src/*"] + } + } +} diff --git a/examples/solid/vite.config.ts b/examples/solid/vite.config.ts new file mode 100644 index 00000000..ff31018b --- /dev/null +++ b/examples/solid/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from "vite"; +import { nitro } from "nitro/vite"; + +import { solidStart } from "@solidjs/start/config"; +import tailwindcss from "@tailwindcss/vite"; + +export default defineConfig({ + plugins: [ + tailwindcss(), + solidStart({ + middleware: "./src/middleware/index.ts", + }), + nitro(), + ], +}); diff --git a/examples/tanstack-start/tsconfig.json b/examples/tanstack-start/tsconfig.json index 477479fb..582a8694 100644 --- a/examples/tanstack-start/tsconfig.json +++ b/examples/tanstack-start/tsconfig.json @@ -20,7 +20,6 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true, - "baseUrl": ".", "paths": { "@/*": ["./src/*"] } diff --git a/package.json b/package.json index 189c80d6..cc5cef21 100644 --- a/package.json +++ b/package.json @@ -17,11 +17,11 @@ "dependencies": {}, "devDependencies": { "@biomejs/biome": "2.3.3", - "@tsconfig/strictest": "^2.0.7", + "@tsconfig/strictest": "^2.0.8", "@types/bun": "^1.3.1", "@types/lodash-es": "^4.17.12", "miniflare": "^3.20250718.2", - "typescript": "^5.9.3", + "typescript": "^6.0.2", "verdaccio": "^6.2.1" }, "workspaces": ["app", "packages/*"] diff --git a/packages/plasmic/tsconfig.json b/packages/plasmic/tsconfig.json index d8a2e777..eb8496fe 100644 --- a/packages/plasmic/tsconfig.json +++ b/packages/plasmic/tsconfig.json @@ -10,17 +10,16 @@ "allowImportingTsExtensions": false, "verbatimModuleSyntax": true, "strict": true, - "outDir": "dist", - "declarationDir": "dist", + "outDir": "./dist", + "declarationDir": "./dist", "declaration": true, "skipLibCheck": true, "noFallthroughCasesInSwitch": true, "noImplicitAny": false, "noPropertyAccessFromIndexSignature": false, - "rootDir": "src", - "baseUrl": "src", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + "rootDir": "./src", + "tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo" }, - "include": ["src/**/*"], + "include": ["./src/**/*"], "exclude": ["bknd", "dist", "node_modules"] }