From 37c36e5d0705e24860b10bd96bc39d34c4fc98c9 Mon Sep 17 00:00:00 2001 From: dswbx Date: Fri, 24 Oct 2025 14:07:37 +0200 Subject: [PATCH 01/32] make non-fillable fields visible but disabled in UI --- .../modules/migrations/migrations.spec.ts | 16 +- .../modules/migrations/samples/v10.json | 270 ++++++++++++++++++ app/src/data/entities/Entity.ts | 22 +- app/src/data/entities/mutation/Mutator.ts | 6 +- app/src/data/fields/Field.ts | 22 +- app/src/data/fields/field-test-suite.ts | 7 +- app/src/data/schema/SchemaManager.ts | 2 +- app/src/modules/db/migrations.ts | 24 ++ .../ui/components/form/Formy/components.tsx | 11 +- .../ui/modules/data/components/EntityForm.tsx | 8 +- .../ui/modules/data/hooks/useEntityForm.tsx | 2 +- 11 files changed, 353 insertions(+), 37 deletions(-) create mode 100644 app/__test__/modules/migrations/samples/v10.json 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/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/schema/SchemaManager.ts b/app/src/data/schema/SchemaManager.ts index 78708d60..8a069572 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/ui/components/form/Formy/components.tsx b/app/src/ui/components/form/Formy/components.tsx index 502a8449..cd85aa43 100644 --- a/app/src/ui/components/form/Formy/components.tsx +++ b/app/src/ui/components/form/Formy/components.tsx @@ -29,7 +29,7 @@ export const Group = ({ > 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, @@ -153,7 +153,7 @@ export const Textarea = forwardRef @@ -213,7 +213,7 @@ export const BooleanInput = forwardRef { - console.log("setting", bool); props.onChange?.({ target: { value: bool } }); }} {...(props as any)} @@ -293,7 +292,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/modules/data/components/EntityForm.tsx b/app/src/ui/modules/data/components/EntityForm.tsx index ff778a17..9943aba1 100644 --- a/app/src/ui/modules/data/components/EntityForm.tsx +++ b/app/src/ui/modules/data/components/EntityForm.tsx @@ -44,7 +44,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 ( @@ -92,10 +92,6 @@ export function EntityForm({ ); } - if (!field.isFillable(action)) { - return; - } - const _key = `${entity.name}-${field.name}-${key}`; return ( @@ -127,7 +123,7 @@ export function EntityForm({ Date: Wed, 26 Nov 2025 10:08:30 +0100 Subject: [PATCH 02/32] feat: add sort plugin - Introduced a new sort plugin that allows sorting of entities based on a specified field. - Added API endpoints for reordering items and recalculating sort order. - Implemented tests to ensure correct functionality of sorting and reordering features. --- app/src/plugins/index.ts | 1 + app/src/plugins/sort.plugin.spec.ts | 859 ++++++++++++++++++++++++++++ app/src/plugins/sort.plugin.ts | 423 ++++++++++++++ 3 files changed, 1283 insertions(+) create mode 100644 app/src/plugins/sort.plugin.spec.ts create mode 100644 app/src/plugins/sort.plugin.ts diff --git a/app/src/plugins/index.ts b/app/src/plugins/index.ts index fcb23c15..4a0b68a3 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 "./sort.plugin"; diff --git a/app/src/plugins/sort.plugin.spec.ts b/app/src/plugins/sort.plugin.spec.ts new file mode 100644 index 00000000..6f769d03 --- /dev/null +++ b/app/src/plugins/sort.plugin.spec.ts @@ -0,0 +1,859 @@ +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 + const { data: task2 } = 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.only("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" }, + }); + console.dir(tasks, { depth: null }); + + 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/sort.plugin.ts b/app/src/plugins/sort.plugin.ts new file mode 100644 index 00000000..848ed7ac --- /dev/null +++ b/app/src/plugins/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", + }, + ); +} From 407d103a5fad2ac26519007bc128a1caf05012d7 Mon Sep 17 00:00:00 2001 From: dswbx Date: Wed, 26 Nov 2025 10:12:39 +0100 Subject: [PATCH 03/32] fix: remove test.only --- app/src/plugins/sort.plugin.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/plugins/sort.plugin.spec.ts b/app/src/plugins/sort.plugin.spec.ts index 6f769d03..8a868278 100644 --- a/app/src/plugins/sort.plugin.spec.ts +++ b/app/src/plugins/sort.plugin.spec.ts @@ -752,7 +752,7 @@ describe("sort plugin", () => { expect(tasks[1].position).toBe(1); }); - test.only("should not create duplicates when moving to an occupied position", async () => { + test("should not create duplicates when moving to an occupied position", async () => { const app = createApp({ config: { data: em({ From 5c3d3d763ec7467017eedffa740787ad52408c63 Mon Sep 17 00:00:00 2001 From: dswbx Date: Wed, 26 Nov 2025 10:26:58 +0100 Subject: [PATCH 04/32] move sort plugin to data plugins --- app/src/plugins/{ => data}/sort.plugin.spec.ts | 3 +-- app/src/plugins/{ => data}/sort.plugin.ts | 0 app/src/plugins/index.ts | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) rename app/src/plugins/{ => data}/sort.plugin.spec.ts (99%) rename app/src/plugins/{ => data}/sort.plugin.ts (100%) diff --git a/app/src/plugins/sort.plugin.spec.ts b/app/src/plugins/data/sort.plugin.spec.ts similarity index 99% rename from app/src/plugins/sort.plugin.spec.ts rename to app/src/plugins/data/sort.plugin.spec.ts index 8a868278..c455b3f7 100644 --- a/app/src/plugins/sort.plugin.spec.ts +++ b/app/src/plugins/data/sort.plugin.spec.ts @@ -731,7 +731,7 @@ describe("sort plugin", () => { .execute(); // create normal task - const { data: task2 } = await mutator.insertOne({ title: "Task 2" }); + await mutator.insertOne({ title: "Task 2" }); // update the null task to have a position const nullTask = await app.connection.kysely @@ -791,7 +791,6 @@ describe("sort plugin", () => { const { data: tasks } = await app.em.repo("tasks").findMany({ sort: { by: "position", dir: "asc" }, }); - console.dir(tasks, { depth: null }); expect(tasks.length).toBe(2); expect(tasks[0].id).toBe(task2.id); diff --git a/app/src/plugins/sort.plugin.ts b/app/src/plugins/data/sort.plugin.ts similarity index 100% rename from app/src/plugins/sort.plugin.ts rename to app/src/plugins/data/sort.plugin.ts diff --git a/app/src/plugins/index.ts b/app/src/plugins/index.ts index 4a0b68a3..5cd019a4 100644 --- a/app/src/plugins/index.ts +++ b/app/src/plugins/index.ts @@ -9,4 +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 "./sort.plugin"; +export { sort, type SortPluginOptions } from "./data/sort.plugin"; From 7751ee5db889a4ce00b8b0c40e36c18668a94c50 Mon Sep 17 00:00:00 2001 From: Shishant Biswas Date: Mon, 9 Mar 2026 19:05:31 +0530 Subject: [PATCH 05/32] init: nuxt adapter --- app/build.ts | 5 + app/e2e/inc/adapters.ts | 3 + app/package.json | 7 + app/src/adapter/nuxt/index.ts | 1 + app/src/adapter/nuxt/nuxt.adapter.spec.ts | 15 + app/src/adapter/nuxt/nuxt.adapter.ts | 30 ++ .../integration/(frameworks)/meta.json | 3 +- .../integration/(frameworks)/nuxt.mdx | 388 ++++++++++++++++++ .../integration/introduction.mdx | 6 + docs/content/docs/(documentation)/start.mdx | 6 + examples/nuxt/.gitignore | 27 ++ examples/nuxt/README.md | 64 +++ examples/nuxt/app/assets/css/main.css | 24 ++ examples/nuxt/app/components/Buttons.vue | 13 + examples/nuxt/app/components/Footer.vue | 29 ++ examples/nuxt/app/components/List.vue | 21 + .../nuxt/app/composables/useTodoActions.ts | 31 ++ examples/nuxt/app/composables/useUser.ts | 6 + examples/nuxt/app/pages/index.vue | 64 +++ examples/nuxt/app/pages/user.vue | 49 +++ examples/nuxt/bknd.config.ts | 58 +++ examples/nuxt/nuxt.config.ts | 11 + examples/nuxt/package.json | 21 + examples/nuxt/public/bknd.ico | Bin 0 -> 15086 bytes examples/nuxt/public/bknd.svg | 14 + examples/nuxt/public/favicon.ico | Bin 0 -> 4286 bytes examples/nuxt/public/file.svg | 1 + examples/nuxt/public/globe.svg | 1 + examples/nuxt/public/nuxt.svg | 3 + examples/nuxt/public/robots.txt | 2 + examples/nuxt/public/window.svg | 1 + examples/nuxt/server/middleware/bknd.ts | 15 + examples/nuxt/server/routes/todos.post.ts | 31 ++ examples/nuxt/server/utils/bknd.ts | 21 + examples/nuxt/tsconfig.json | 18 + 35 files changed, 988 insertions(+), 1 deletion(-) create mode 100644 app/src/adapter/nuxt/index.ts create mode 100644 app/src/adapter/nuxt/nuxt.adapter.spec.ts create mode 100644 app/src/adapter/nuxt/nuxt.adapter.ts create mode 100644 docs/content/docs/(documentation)/integration/(frameworks)/nuxt.mdx create mode 100644 examples/nuxt/.gitignore create mode 100644 examples/nuxt/README.md create mode 100644 examples/nuxt/app/assets/css/main.css create mode 100644 examples/nuxt/app/components/Buttons.vue create mode 100644 examples/nuxt/app/components/Footer.vue create mode 100644 examples/nuxt/app/components/List.vue create mode 100644 examples/nuxt/app/composables/useTodoActions.ts create mode 100644 examples/nuxt/app/composables/useUser.ts create mode 100644 examples/nuxt/app/pages/index.vue create mode 100644 examples/nuxt/app/pages/user.vue create mode 100644 examples/nuxt/bknd.config.ts create mode 100644 examples/nuxt/nuxt.config.ts create mode 100644 examples/nuxt/package.json create mode 100644 examples/nuxt/public/bknd.ico create mode 100644 examples/nuxt/public/bknd.svg create mode 100644 examples/nuxt/public/favicon.ico create mode 100644 examples/nuxt/public/file.svg create mode 100644 examples/nuxt/public/globe.svg create mode 100644 examples/nuxt/public/nuxt.svg create mode 100644 examples/nuxt/public/robots.txt create mode 100644 examples/nuxt/public/window.svg create mode 100644 examples/nuxt/server/middleware/bknd.ts create mode 100644 examples/nuxt/server/routes/todos.post.ts create mode 100644 examples/nuxt/server/utils/bknd.ts create mode 100644 examples/nuxt/tsconfig.json diff --git a/app/build.ts b/app/build.ts index 1ab90eaa..92f63356 100644 --- a/app/build.ts +++ b/app/build.ts @@ -335,6 +335,11 @@ async function buildAdapters() { platform: "node", }), + tsup.build({ + ...baseConfig("nuxt"), + platform: "node", + }), + tsup.build({ ...baseConfig("node"), platform: "node", diff --git a/app/e2e/inc/adapters.ts b/app/e2e/inc/adapters.ts index 30b647b0..66d691bb 100644 --- a/app/e2e/inc/adapters.ts +++ b/app/e2e/inc/adapters.ts @@ -15,6 +15,9 @@ const configs = { nextjs: { base_path: "/admin", }, + nuxt: { + base_path: "/admin", + }, astro: { base_path: "/admin", }, diff --git a/app/package.json b/app/package.json index ceb3771e..dd8d457d 100644 --- a/app/package.json +++ b/app/package.json @@ -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", @@ -287,6 +292,7 @@ "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"], @@ -318,6 +324,7 @@ "serverless", "cloudflare", "nextjs", + "nuxt", "remix", "react-router", "astro", 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/docs/content/docs/(documentation)/integration/(frameworks)/meta.json b/docs/content/docs/(documentation)/integration/(frameworks)/meta.json index 5cc79009..39b253cd 100644 --- a/docs/content/docs/(documentation)/integration/(frameworks)/meta.json +++ b/docs/content/docs/(documentation)/integration/(frameworks)/meta.json @@ -5,6 +5,7 @@ "astro", "sveltekit", "tanstack-start", - "vite" + "vite", + "nuxt" ] } 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..3cc715dd --- /dev/null +++ b/docs/content/docs/(documentation)/integration/(frameworks)/nuxt.mdx @@ -0,0 +1,388 @@ +--- +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" + + + +``` + +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/introduction.mdx b/docs/content/docs/(documentation)/integration/introduction.mdx index 6b943744..a04cb12c 100644 --- a/docs/content/docs/(documentation)/integration/introduction.mdx +++ b/docs/content/docs/(documentation)/integration/introduction.mdx @@ -39,6 +39,12 @@ bknd seamlessly integrates with popular frameworks, allowing you to use what you href="/integration/tanstack-start" /> +} + title="Nuxt" + href="/integration/nuxt" +/> + 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..d0644ae2 100644 --- a/docs/content/docs/(documentation)/start.mdx +++ b/docs/content/docs/(documentation)/start.mdx @@ -156,6 +156,12 @@ Pick your framework or runtime to get started. href="/integration/tanstack-start" /> +} + title="Nuxt" + href="/integration/nuxt" +/> + } title="AWS Lambda" 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..a88e69a7 --- /dev/null +++ b/examples/nuxt/app/assets/css/main.css @@ -0,0 +1,24 @@ +@import "tailwindcss"; + +:root { + --background: #ffffff; + --foreground: #171717; + font-weight: 400; +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +@theme { + --color-background: var(--background); + --color-foreground: var(--foreground); +} + +body { + @apply bg-background text-foreground; + font-family: Arial, Helvetica, sans-serif; +} 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..9795a168 --- /dev/null +++ b/examples/nuxt/app/pages/index.vue @@ -0,0 +1,64 @@ + + + diff --git a/examples/nuxt/app/pages/user.vue b/examples/nuxt/app/pages/user.vue new file mode 100644 index 00000000..5ac2e27a --- /dev/null +++ b/examples/nuxt/app/pages/user.vue @@ -0,0 +1,49 @@ + + + \ 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..7a0ba13a --- /dev/null +++ b/examples/nuxt/bknd.config.ts @@ -0,0 +1,58 @@ +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/", + }, +} satisfies NuxtBkndConfig; diff --git a/examples/nuxt/nuxt.config.ts b/examples/nuxt/nuxt.config.ts new file mode 100644 index 00000000..a179d24d --- /dev/null +++ b/examples/nuxt/nuxt.config.ts @@ -0,0 +1,11 @@ +// 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", + }, + }, +}); 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 0000000000000000000000000000000000000000..c1a946d533a71c15e51fc4cade87289ca467b57d GIT binary patch literal 15086 zcmeI3`*+mE6~H&yJT}=RyV=ci12qaFP%EgQVoRW{Xhk^@c}Z;%A4d^I@qsT68WgDr zC?W!C#R3W_pz@Gs1lrn`3M%~v`jh`ed)nUK&m@yhzWJ{E-E1D4bDDF8`F_7UGxs@n z?%aC`g$hF9(D?Bo?VCe;E)9jQ357z<&7JS@Q0VXaEv`MkFDeR!R_lP#I!3327HZ?W z`5W2Mx&OAG8y-G+zG;@W^VxYWGj{mhuUjJ>a%)h5nZk-jC2Op2g z&VvnxmVBoH+!ranvFbumduv)jK}bf8DV8^XS8HUFuQZ@9>))x7xP!Z;f&SW`MC8El zQ~fRn@Hq8thu&txE9U)Db;iZneXgi1l#TDJ40P>m)(twAy>|3$O5WL1C$GF!WBOyN z4}1@@2IJ6o7iagmxVS*pY_2i939?=`V}O0|4=I^XMJbJ@jQt#!oEm0Tl8=u!_Sg&TkDuODCdkUl&GvNst}2z6UQf!g zvmJOmwX)jaXYozfqo1QMFEMgt%@gN3&YR{D#XV?d4fX_M{BTY84O-D(KRd?vy(b#w zu0>UH(Iv%lo9f8JPsP*P-E?$Za%r)={6^C3QE*4kCjL0mwFl5yZ1#tT`~K{-vP56d zwc?WQ`z{^M?sw!+Syd<#Cq;~17$=`*9oCi%hW&u{R4^9Oa^ ze5Gvgwqx1LpPW)XQGWClzCqSP#%t6*aV}e2dehf8hGo|g-J_lMG#47+DK-*6jdO^1 z?|RUN4LYMbyGrdx*mGuPOG86bk?aQl9Qn`uje2%XyQSRd^dvoN+twwGZbAqAh8A=l zKlSd#Rptyx=Whh1V@x0D5BP1}RVP2tv(<%*eOspI{`Sx6W%+Ls#=qm79C2B(O#XS9 z-1AV!8Q{*z_IbM6eS8Y9oU?uBT=#>`ePG0>V!3>LiOiT2mF24wvg1I#9Q{km$b@su zZ(fjZrI7+zsWL_%v&GjPkIr8=IpT{)mHIL-tW6jj!5s9+?hE?upVrI8BmK|*Of*cL zR+g1-1A_zTmsX-S({M+W#Q@VGj46u|F!2`yY+TA=NeR5ZtF` zs!rbA8kISUGxwKm`|6D^9AsUKQ*ZiA`r@t$y{+@2g1oplfEzSnQ|MWX-re`^{K0$% zFKiR`hqI6O!NeyMYt=@ckx^Ml*KBZN0 zKcZ*tqtC_7eX*ppKq6%Y(loqCQrd_|VDopUbWvz*#yFaoyg~0{;8d-yf;|vWv6(%z1nG zPr>42y}=>ZIruhr-s>M@;O@ma3!S;b>96_Vw&!SrO#WF}x4UAg+M!!+k4j$K&dxmq z4a5kD5w^~a3chyR+NAMy@|pLpOu-YMrpv!kyE;^Gcj?HJ=b@YfCihMbK5uPC2HdE9 zpax<{)^8}@v)jK(Q9nZs$^~shp%wytr`BiI$*)u4GkSn(c~Pj1zJP!J|JR=!Xk&5Z z^BH>OJ9S#k+NNqNuytBm%jNx3d9`RetL@*~25P?p>nm-*rXMJ{UO3SGuR(uaht{WGtTuN~)l=h-6Z_xtVQw)*d>9)~ z_&$FIp10gk*5e-Vz|xqR-y1yv9x(y(ue4pCr_W;`Z(@U%cH7_gNX*nUcyWhcR#$9} z8anMbdU6@$PwiS0G#9#$oTvWgrH8f1do-lNW;}ThVz^UpEHk-M?m&z&bpkq{y!pKk zR~eoW8=o;dD$l=~F!5@}fy-;})=ETU@D@Jtcyx;2yzuS&7R}iaCw%0YxZxeSjXM@r znmjf+5aMyfaxI>AZF=&=Q#5A2^FV`H|M3;oQsU%}sj+x__xJ0Q#oh9fcVJQ%@Qc=R zA)iU!#f9sArlw-fuPbEc9Tlb)0zMLhTdFaZ-sV!MYuT!M0UEsU{NBO!bCnsgM&{(Y zEsuNC!w+JKYs!dEEy+gOXHAv~; zql3iSgTRO9OB9~F7Wp1@Xz>#z?khL!H1>!%sW&&w zT+R^kmqBT_`~U~+^*TrY>~FSb47EMzA^BeR13Ez+4)&KE5p{Oi!gkkBxBnOe?c~Lm zJRR@Kf3P?F&L6vGu065`C+Z2PPo#!%pyG+tTT&xN9v$0#qxwYTA$!Z;YiX*VO0|p)6*W=tf z@Ic?cp}VDR5Ti4|CQd%ynbgx^_{dl5N$@~@VE>GDmiVd>`?n@D~1w*B>{783dK zo#w*!J~NNIEj}tacQZb~Jl0_>b@jFm5c#?C^}_OdNADKg-%;C0eQCh?-Ru*(ZT` + + + \ 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 0000000000000000000000000000000000000000..18993ad91cfd43e03b074dd0b5cc3f37ab38e49c GIT binary patch literal 4286 zcmeHLOKuuL5PjK%MHWVi6lD zOGiREbCw`xmFozJ^aNatJY>w+g ze6a2@u~m#^BZm@8wco9#Crlli0uLb^3E$t2-WIc^#(?t)*@`UpuofJ(Uyh@F>b3Ph z$D^m8Xq~pTkGJ4Q`Q2)te3mgkWYZ^Ijq|hkiP^9`De={bQQ%heZC$QU2UpP(-tbl8 zPWD2abEew;oat@w`uP3J^YpsgT%~jT(Dk%oU}sa$7|n6hBjDj`+I;RX(>)%lm_7N{+B7Mu%H?422lE%MBJH!!YTN2oT7xr>>N-8OF$C&qU^ z>vLsa{$0X%q1fjOe3P1mCv#lN{xQ4_*HCSAZjTb1`}mlc+9rl8$B3OP%VT@mch_~G z7Y+4b{r>9e=M+7vSI;BgB?ryZDY4m>&wcHSn81VH1N~`0gvwH{ z8dv#hG|OK`>1;j7tM#B)Z7zDN?{6=dUal}$e \ 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" + } + ] +} From 06f9c3ee15002b5a00c2baac317c86c422d86ddc Mon Sep 17 00:00:00 2001 From: Shishant Biswas Date: Sat, 14 Mar 2026 22:57:05 +0530 Subject: [PATCH 06/32] refactor(docs): note for NuxtLink usage, added geist font in example, minor cleanups --- .../integration/(frameworks)/nuxt.mdx | 16 +++++++++++++- examples/nuxt/app/assets/css/main.css | 19 +++++++++++----- examples/nuxt/app/pages/index.vue | 18 ++++++++++----- examples/nuxt/app/pages/user.vue | 22 ++++++++++++------- examples/nuxt/bknd.config.ts | 1 + examples/nuxt/nuxt.config.ts | 1 + 6 files changed, 57 insertions(+), 20 deletions(-) diff --git a/docs/content/docs/(documentation)/integration/(frameworks)/nuxt.mdx b/docs/content/docs/(documentation)/integration/(frameworks)/nuxt.mdx index 3cc715dd..a96268a1 100644 --- a/docs/content/docs/(documentation)/integration/(frameworks)/nuxt.mdx +++ b/docs/content/docs/(documentation)/integration/(frameworks)/nuxt.mdx @@ -189,7 +189,7 @@ export default defineEventHandler(async (event) => { ``` - 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. + 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. @@ -385,4 +385,18 @@ onMounted(() => { ``` +## 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/examples/nuxt/app/assets/css/main.css b/examples/nuxt/app/assets/css/main.css index a88e69a7..b0042ae7 100644 --- a/examples/nuxt/app/assets/css/main.css +++ b/examples/nuxt/app/assets/css/main.css @@ -1,9 +1,15 @@ -@import "tailwindcss"; +@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-weight: 400; + font-family: "Geist Mono", monospace; } @media (prefers-color-scheme: dark) { @@ -14,11 +20,14 @@ } @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: Arial, Helvetica, sans-serif; -} + 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/pages/index.vue b/examples/nuxt/app/pages/index.vue index 9795a168..1c58fb1e 100644 --- a/examples/nuxt/app/pages/index.vue +++ b/examples/nuxt/app/pages/index.vue @@ -21,15 +21,15 @@ async function handleSubmit(event: Event) {
-
- Nuxt logo +
+ Nuxt logo
&
bknd logo
-
+

What's next?

-
{{ todo.title }}
@@ -52,8 +55,11 @@ async function handleSubmit(event: Event) {
- +
diff --git a/examples/nuxt/app/pages/user.vue b/examples/nuxt/app/pages/user.vue index 5ac2e27a..f2775220 100644 --- a/examples/nuxt/app/pages/user.vue +++ b/examples/nuxt/app/pages/user.vue @@ -12,25 +12,31 @@ onMounted(() => {
+ >
-
- Nuxt logo +
+ Nuxt logo
&
- bknd logo + bknd logo
Logged in as {{ data.user.email }}. - + Logout - +

Not logged in. - + Login - +

Sign in with: diff --git a/examples/nuxt/bknd.config.ts b/examples/nuxt/bknd.config.ts index 7a0ba13a..3c144c03 100644 --- a/examples/nuxt/bknd.config.ts +++ b/examples/nuxt/bknd.config.ts @@ -54,5 +54,6 @@ export default { adminOptions: { adminBasepath: "/admin", assetsPath: "/admin/", + logoReturnPath: "../..", }, } satisfies NuxtBkndConfig; diff --git a/examples/nuxt/nuxt.config.ts b/examples/nuxt/nuxt.config.ts index a179d24d..da89e1f7 100644 --- a/examples/nuxt/nuxt.config.ts +++ b/examples/nuxt/nuxt.config.ts @@ -8,4 +8,5 @@ export default defineNuxtConfig({ title: "Nuxt 🤝 Bknd.io", }, }, + css: ["assets/css/main.css"], }); From ff216ec4e57df531c41cbe03dee04e65695eba62 Mon Sep 17 00:00:00 2001 From: dswbx Date: Sat, 14 Mar 2026 13:55:26 +0100 Subject: [PATCH 07/32] chore: remove unused admin_basepath and bump version to 0.21.0-rc.0 --- app/package.json | 2 +- app/src/ui/client/utils/AppReduced.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/package.json b/app/package.json index ceb3771e..c76bde02 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.0", "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": { 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, "/") From 9628720f87a7a5d218cdd7b2d22de4ef2be35b92 Mon Sep 17 00:00:00 2001 From: dswbx Date: Sat, 14 Mar 2026 13:57:16 +0100 Subject: [PATCH 08/32] test(AppReduced): remove redundant `admin_base_path` --- app/__test__/ui/client/utils/AppReduced.spec.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) 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: "/", }; From 4121597fa2e4e0a6a61a179bdd54663b5273b02a Mon Sep 17 00:00:00 2001 From: Jonas Perusquia Morales Date: Tue, 24 Mar 2026 12:20:17 -0600 Subject: [PATCH 09/32] fix: Match Bearer case-insensitively and trim whitespace(s) --- app/src/Api.ts | 2 +- app/src/auth/authenticate/Authenticator.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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); From dc8aca6a970e72e72c7a3a4946482355fd22cccb Mon Sep 17 00:00:00 2001 From: Jonas Perusquia Morales Date: Tue, 24 Mar 2026 13:30:54 -0600 Subject: [PATCH 10/32] fix: update database URL to remove 'file:' prefix in examples --- examples/bun/index.ts | 2 +- examples/node/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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: { From 4afba215b9160bf6d83d48bbc045820cfde30cdc Mon Sep 17 00:00:00 2001 From: Jonas Perusquia Morales Date: Tue, 24 Mar 2026 13:48:42 -0600 Subject: [PATCH 11/32] chore: update devDependencies for improved compatibility - bump @tsconfig/strictest from 2.0.7 to 2.0.8 - upgrade typescript from 5.9.3 to 6.0.2 --- bun.lock | 152 +++++++++++++++++++++++++++++++-------------------- package.json | 4 +- 2 files changed, 95 insertions(+), 61 deletions(-) diff --git a/bun.lock b/bun.lock index f5b57ad5..a81add7b 100644 --- a/bun.lock +++ b/bun.lock @@ -6,11 +6,11 @@ "name": "bknd", "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", }, }, @@ -538,7 +538,7 @@ "@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.3", "", {}, "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw=="], - "@cypress/request": ["@cypress/request@3.0.9", "", { "dependencies": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", "caseless": "~0.12.0", "combined-stream": "~1.0.6", "extend": "~3.0.2", "forever-agent": "~0.6.1", "form-data": "~4.0.4", "http-signature": "~1.4.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "performance-now": "^2.1.0", "qs": "6.14.0", "safe-buffer": "^5.1.2", "tough-cookie": "^5.0.0", "tunnel-agent": "^0.6.0", "uuid": "^8.3.2" } }, "sha512-I3l7FdGRXluAS44/0NguwWlO83J18p0vlr2FYHrJkWdNYhgVoiYo61IXPqaOsL+vNxU1ZqMACzItGK3/KKDsdw=="], + "@cypress/request": ["@cypress/request@3.0.10", "", { "dependencies": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", "caseless": "~0.12.0", "combined-stream": "~1.0.6", "extend": "~3.0.2", "forever-agent": "~0.6.1", "form-data": "~4.0.4", "http-signature": "~1.4.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "performance-now": "^2.1.0", "qs": "~6.14.1", "safe-buffer": "^5.1.2", "tough-cookie": "^5.0.0", "tunnel-agent": "^0.6.0", "uuid": "^8.3.2" } }, "sha512-hauBrOdvu08vOsagkZ/Aju5XuiZx6ldsLfByg1htFeldhex+PeMrYauANzFsMJeAA0+dyPLbDoX2OYuvVoLDkQ=="], "@dagrejs/dagre": ["@dagrejs/dagre@1.1.8", "", { "dependencies": { "@dagrejs/graphlib": "2.2.4" } }, "sha512-5SEDlndt4W/LaVzPYJW+bSmSEZc9EzTf8rJ20WCKvjS5EAZAN0b+x0Yww7VMT4R3Wootkg+X9bUfUxazYw6Blw=="], @@ -798,6 +798,8 @@ "@open-draft/until": ["@open-draft/until@2.1.0", "", {}, "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="], + "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], "@plasmicapp/data-sources-context": ["@plasmicapp/data-sources-context@0.1.21", "", { "peerDependencies": { "react": ">=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/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/*"] From d03cd7f7bc1cf72caf00c31fd6b36396b037d02d Mon Sep 17 00:00:00 2001 From: Jonas Perusquia Morales Date: Tue, 24 Mar 2026 13:49:10 -0600 Subject: [PATCH 12/32] chore: standardize TypeScript module and target casing in configuration files --- app/tsconfig.build.json | 1 - app/tsconfig.json | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/tsconfig.build.json b/app/tsconfig.build.json index 4d813b10..7ca26901 100644 --- a/app/tsconfig.build.json +++ b/app/tsconfig.build.json @@ -3,7 +3,6 @@ "compilerOptions": { "outDir": "./dist/types", "rootDir": "./src", - "baseUrl": ".", "tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo" }, "include": ["./src/**/*.ts", "./src/**/*.tsx"], diff --git a/app/tsconfig.json b/app/tsconfig.json index d6060d98..ed133995 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -2,11 +2,11 @@ "compilerOptions": { "composite": false, "incremental": true, - "module": "ESNext", + "module": "esnext", "moduleResolution": "bundler", "jsx": "react-jsx", "allowImportingTsExtensions": false, - "target": "ES2022", + "target": "es2023", "noImplicitAny": false, "allowJs": true, "verbatimModuleSyntax": true, @@ -26,7 +26,6 @@ "esModuleInterop": true, "skipLibCheck": true, "rootDir": ".", - "baseUrl": ".", "outDir": "./dist/types", "paths": { "*": ["./src/*"], From d160af5ceccb819a716fc8fe559ab356a9c96fd5 Mon Sep 17 00:00:00 2001 From: Jonas Perusquia Morales Date: Tue, 24 Mar 2026 13:49:45 -0600 Subject: [PATCH 13/32] chore: add TypeScript declaration for CSS modules, fixes TS error --- app/src/ui/types.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 app/src/ui/types.ts diff --git a/app/src/ui/types.ts b/app/src/ui/types.ts new file mode 100644 index 00000000..3255cfe2 --- /dev/null +++ b/app/src/ui/types.ts @@ -0,0 +1 @@ +declare module '*.css'; \ No newline at end of file From d206c38d878848105889dea262efb9afe177fb34 Mon Sep 17 00:00:00 2001 From: Jonas Perusquia Morales Date: Tue, 24 Mar 2026 13:49:53 -0600 Subject: [PATCH 14/32] chore: remove unused import of main.css in main.tsx --- app/src/ui/main.tsx | 1 - 1 file changed, 1 deletion(-) 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() { From 237b42bb9c6bda72a72b114cf7eaac5ed6c87aa2 Mon Sep 17 00:00:00 2001 From: Jonas Perusquia Morales Date: Tue, 24 Mar 2026 13:59:05 -0600 Subject: [PATCH 15/32] chore: remove baseUrl from TypeScript configuration files --- docs/tsconfig.json | 3 +-- examples/bun/tsconfig.json | 1 - examples/postgres/tsconfig.json | 1 - examples/react-router/tsconfig.json | 1 - examples/tanstack-start/tsconfig.json | 1 - packages/plasmic/tsconfig.json | 11 +++++------ 6 files changed, 6 insertions(+), 12 deletions(-) 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/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/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/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/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"] } From ab1eddcb6a6d1174fbb1051a10c2f49c34496e7f Mon Sep 17 00:00:00 2001 From: Jonas Perusquia Morales Date: Fri, 27 Mar 2026 14:49:16 -0600 Subject: [PATCH 16/32] chore: Bump dayjs to 1.11.20 --- app/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/package.json b/app/package.json index ceb3771e..62aca469 100644 --- a/app/package.json +++ b/app/package.json @@ -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", From 2417833ed7ceeb34e603a7ba3723e6b9bdec3d2b Mon Sep 17 00:00:00 2001 From: Jonas Perusquia Morales Date: Sat, 28 Mar 2026 02:55:58 -0600 Subject: [PATCH 17/32] chore: Replace Bun's deprecated OptionsObject with SpawnOptions and remove stdout cast. Fix warning. --- app/e2e/adapters.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 From e437ce71208a4a28b8a93148735e5b57ea039984 Mon Sep 17 00:00:00 2001 From: Jonas Perusquia Morales Date: Sat, 28 Mar 2026 03:02:08 -0600 Subject: [PATCH 18/32] chore: Use dynamic import for manifest with Vite ignore. Fixes warning --- app/src/modules/server/AdminController.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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); } From 9219bf3b3c5192ac0708160464f7c2de984ce611 Mon Sep 17 00:00:00 2001 From: dswbx Date: Sat, 28 Mar 2026 10:33:24 +0100 Subject: [PATCH 19/32] test(auth, api): ensure bearer token is case-insensitive --- app/__test__/api/Api.spec.ts | 6 ++++++ app/__test__/auth/Authenticator.spec.ts | 23 +++++++++++++++++++++++ 2 files changed, 29 insertions(+) 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") + }) }); From a8e5c45a220dd7a612f1fe1e6bfb6b193c5df35f Mon Sep 17 00:00:00 2001 From: dswbx Date: Sat, 28 Mar 2026 10:50:14 +0100 Subject: [PATCH 20/32] bump 0.21.0-rc.1 --- app/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/package.json b/app/package.json index c76bde02..112e3c54 100644 --- a/app/package.json +++ b/app/package.json @@ -3,7 +3,7 @@ "type": "module", "sideEffects": false, "bin": "./dist/cli/index.js", - "version": "0.21.0-rc.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": { From 8d37f9df3f5d48aef7f19715e519acb88ab25bd3 Mon Sep 17 00:00:00 2001 From: Shishant Biswas Date: Sat, 14 Mar 2026 15:59:40 +0530 Subject: [PATCH 21/32] init: solid start adapter --- app/build.ts | 5 + app/e2e/inc/adapters.ts | 3 + app/package.json | 9 +- app/src/adapter/solid-start/index.ts | 1 + .../solid-start/solid-start.adapter.spec.ts | 16 + .../solid-start/solid-start.adapter.ts | 22 ++ .../integration/(frameworks)/meta.json | 3 +- .../integration/(frameworks)/solid-start.mdx | 335 ++++++++++++++++++ .../integration/introduction.mdx | 6 + docs/content/docs/(documentation)/start.mdx | 6 + examples/solid/.gitignore | 31 ++ examples/solid/README.md | 32 ++ examples/solid/bknd.config.ts | 51 +++ examples/solid/package.json | 28 ++ examples/solid/public/bknd.ico | Bin 0 -> 15086 bytes examples/solid/public/bknd.svg | 14 + examples/solid/public/file.svg | 1 + examples/solid/public/globe.svg | 1 + examples/solid/public/solid.svg | 1 + examples/solid/public/window.svg | 1 + examples/solid/src/app.css | 33 ++ examples/solid/src/app.tsx | 20 ++ examples/solid/src/components/Footer.tsx | 53 +++ examples/solid/src/components/List.tsx | 11 + examples/solid/src/entry-client.tsx | 4 + examples/solid/src/entry-server.tsx | 21 ++ examples/solid/src/global.d.ts | 1 + examples/solid/src/lib/bknd.ts | 30 ++ examples/solid/src/middleware/index.ts | 20 ++ examples/solid/src/routes/[...404].tsx | 19 + examples/solid/src/routes/index.tsx | 150 ++++++++ examples/solid/src/routes/user.tsx | 139 ++++++++ examples/solid/tsconfig.json | 19 + examples/solid/vite.config.ts | 15 + 34 files changed, 1099 insertions(+), 2 deletions(-) create mode 100644 app/src/adapter/solid-start/index.ts create mode 100644 app/src/adapter/solid-start/solid-start.adapter.spec.ts create mode 100644 app/src/adapter/solid-start/solid-start.adapter.ts create mode 100644 docs/content/docs/(documentation)/integration/(frameworks)/solid-start.mdx create mode 100644 examples/solid/.gitignore create mode 100644 examples/solid/README.md create mode 100644 examples/solid/bknd.config.ts create mode 100644 examples/solid/package.json create mode 100644 examples/solid/public/bknd.ico create mode 100644 examples/solid/public/bknd.svg create mode 100644 examples/solid/public/file.svg create mode 100644 examples/solid/public/globe.svg create mode 100644 examples/solid/public/solid.svg create mode 100644 examples/solid/public/window.svg create mode 100644 examples/solid/src/app.css create mode 100644 examples/solid/src/app.tsx create mode 100644 examples/solid/src/components/Footer.tsx create mode 100644 examples/solid/src/components/List.tsx create mode 100644 examples/solid/src/entry-client.tsx create mode 100644 examples/solid/src/entry-server.tsx create mode 100644 examples/solid/src/global.d.ts create mode 100644 examples/solid/src/lib/bknd.ts create mode 100644 examples/solid/src/middleware/index.ts create mode 100644 examples/solid/src/routes/[...404].tsx create mode 100644 examples/solid/src/routes/index.tsx create mode 100644 examples/solid/src/routes/user.tsx create mode 100644 examples/solid/tsconfig.json create mode 100644 examples/solid/vite.config.ts diff --git a/app/build.ts b/app/build.ts index 92f63356..23986b9d 100644 --- a/app/build.ts +++ b/app/build.ts @@ -345,6 +345,11 @@ async function buildAdapters() { 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/inc/adapters.ts b/app/e2e/inc/adapters.ts index 66d691bb..c113b69f 100644 --- a/app/e2e/inc/adapters.ts +++ b/app/e2e/inc/adapters.ts @@ -18,6 +18,9 @@ const configs = { nuxt: { base_path: "/admin", }, + "solid-start": { + base_path: "/admin", + }, astro: { base_path: "/admin", }, diff --git a/app/package.json b/app/package.json index e9428f06..aedc873a 100644 --- a/app/package.json +++ b/app/package.json @@ -263,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", @@ -298,6 +303,7 @@ "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"] } }, @@ -331,6 +337,7 @@ "sveltekit", "svelte", "bun", - "node" + "node", + "solid-start" ] } 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..37c6fe17 --- /dev/null +++ b/app/src/adapter/solid-start/solid-start.adapter.ts @@ -0,0 +1,22 @@ +import { createRuntimeApp, type RuntimeBkndConfig } from "bknd/adapter"; + +export type SolidStartEnv = NodeJS.ProcessEnv; +export type SolidStartBkndConfig = RuntimeBkndConfig; + +export async function getApp( + config: SolidStartBkndConfig, + args: Env = process.env as Env, +) { + return await createRuntimeApp(config, args); +} + + +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/docs/content/docs/(documentation)/integration/(frameworks)/meta.json b/docs/content/docs/(documentation)/integration/(frameworks)/meta.json index 39b253cd..cc263d11 100644 --- a/docs/content/docs/(documentation)/integration/(frameworks)/meta.json +++ b/docs/content/docs/(documentation)/integration/(frameworks)/meta.json @@ -6,6 +6,7 @@ "sveltekit", "tanstack-start", "vite", - "nuxt" + "nuxt", + "solid-start" ] } 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..ab88c016 --- /dev/null +++ b/docs/content/docs/(documentation)/integration/(frameworks)/solid-start.mdx @@ -0,0 +1,335 @@ +--- +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 async function Home() { + const api = await getApi(); + 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 Home() { + 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 a04cb12c..8a854f53 100644 --- a/docs/content/docs/(documentation)/integration/introduction.mdx +++ b/docs/content/docs/(documentation)/integration/introduction.mdx @@ -45,6 +45,12 @@ bknd seamlessly integrates with popular frameworks, allowing you to use what you 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 d0644ae2..6edc0276 100644 --- a/docs/content/docs/(documentation)/start.mdx +++ b/docs/content/docs/(documentation)/start.mdx @@ -162,6 +162,12 @@ Pick your framework or runtime to get started. href="/integration/nuxt" /> +} + title="Solid Start" + href="/integration/solid-start" +/> + } title="AWS Lambda" 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..9337430c --- /dev/null +++ b/examples/solid/README.md @@ -0,0 +1,32 @@ +# SolidStart + +Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com); + +## Creating a project + +```bash +# create a new project in the current directory +npm init solid@latest + +# create a new project in my-app +npm init solid@latest my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```bash +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +Solid apps are built with _presets_, which optimise your project for deployment to different environments. + +By default, `npm run build` will generate a Node app that you can run with `npm start`. To use a different preset, add it to the `devDependencies` in `package.json` and specify in your `app.config.js`. + +## This project was created with the [Solid CLI](https://github.com/solidjs-community/solid-cli) 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..3e777653 --- /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": "^0.15.0", + "@solidjs/start": "2.0.0-alpha.2", + "bknd": "file:../../app", + "nitro": "3.0.1-alpha.2", + "solid-js": "^1.9.5", + "vite": "^7.0.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 0000000000000000000000000000000000000000..c1a946d533a71c15e51fc4cade87289ca467b57d GIT binary patch literal 15086 zcmeI3`*+mE6~H&yJT}=RyV=ci12qaFP%EgQVoRW{Xhk^@c}Z;%A4d^I@qsT68WgDr zC?W!C#R3W_pz@Gs1lrn`3M%~v`jh`ed)nUK&m@yhzWJ{E-E1D4bDDF8`F_7UGxs@n z?%aC`g$hF9(D?Bo?VCe;E)9jQ357z<&7JS@Q0VXaEv`MkFDeR!R_lP#I!3327HZ?W z`5W2Mx&OAG8y-G+zG;@W^VxYWGj{mhuUjJ>a%)h5nZk-jC2Op2g z&VvnxmVBoH+!ranvFbumduv)jK}bf8DV8^XS8HUFuQZ@9>))x7xP!Z;f&SW`MC8El zQ~fRn@Hq8thu&txE9U)Db;iZneXgi1l#TDJ40P>m)(twAy>|3$O5WL1C$GF!WBOyN z4}1@@2IJ6o7iagmxVS*pY_2i939?=`V}O0|4=I^XMJbJ@jQt#!oEm0Tl8=u!_Sg&TkDuODCdkUl&GvNst}2z6UQf!g zvmJOmwX)jaXYozfqo1QMFEMgt%@gN3&YR{D#XV?d4fX_M{BTY84O-D(KRd?vy(b#w zu0>UH(Iv%lo9f8JPsP*P-E?$Za%r)={6^C3QE*4kCjL0mwFl5yZ1#tT`~K{-vP56d zwc?WQ`z{^M?sw!+Syd<#Cq;~17$=`*9oCi%hW&u{R4^9Oa^ ze5Gvgwqx1LpPW)XQGWClzCqSP#%t6*aV}e2dehf8hGo|g-J_lMG#47+DK-*6jdO^1 z?|RUN4LYMbyGrdx*mGuPOG86bk?aQl9Qn`uje2%XyQSRd^dvoN+twwGZbAqAh8A=l zKlSd#Rptyx=Whh1V@x0D5BP1}RVP2tv(<%*eOspI{`Sx6W%+Ls#=qm79C2B(O#XS9 z-1AV!8Q{*z_IbM6eS8Y9oU?uBT=#>`ePG0>V!3>LiOiT2mF24wvg1I#9Q{km$b@su zZ(fjZrI7+zsWL_%v&GjPkIr8=IpT{)mHIL-tW6jj!5s9+?hE?upVrI8BmK|*Of*cL zR+g1-1A_zTmsX-S({M+W#Q@VGj46u|F!2`yY+TA=NeR5ZtF` zs!rbA8kISUGxwKm`|6D^9AsUKQ*ZiA`r@t$y{+@2g1oplfEzSnQ|MWX-re`^{K0$% zFKiR`hqI6O!NeyMYt=@ckx^Ml*KBZN0 zKcZ*tqtC_7eX*ppKq6%Y(loqCQrd_|VDopUbWvz*#yFaoyg~0{;8d-yf;|vWv6(%z1nG zPr>42y}=>ZIruhr-s>M@;O@ma3!S;b>96_Vw&!SrO#WF}x4UAg+M!!+k4j$K&dxmq z4a5kD5w^~a3chyR+NAMy@|pLpOu-YMrpv!kyE;^Gcj?HJ=b@YfCihMbK5uPC2HdE9 zpax<{)^8}@v)jK(Q9nZs$^~shp%wytr`BiI$*)u4GkSn(c~Pj1zJP!J|JR=!Xk&5Z z^BH>OJ9S#k+NNqNuytBm%jNx3d9`RetL@*~25P?p>nm-*rXMJ{UO3SGuR(uaht{WGtTuN~)l=h-6Z_xtVQw)*d>9)~ z_&$FIp10gk*5e-Vz|xqR-y1yv9x(y(ue4pCr_W;`Z(@U%cH7_gNX*nUcyWhcR#$9} z8anMbdU6@$PwiS0G#9#$oTvWgrH8f1do-lNW;}ThVz^UpEHk-M?m&z&bpkq{y!pKk zR~eoW8=o;dD$l=~F!5@}fy-;})=ETU@D@Jtcyx;2yzuS&7R}iaCw%0YxZxeSjXM@r znmjf+5aMyfaxI>AZF=&=Q#5A2^FV`H|M3;oQsU%}sj+x__xJ0Q#oh9fcVJQ%@Qc=R zA)iU!#f9sArlw-fuPbEc9Tlb)0zMLhTdFaZ-sV!MYuT!M0UEsU{NBO!bCnsgM&{(Y zEsuNC!w+JKYs!dEEy+gOXHAv~; zql3iSgTRO9OB9~F7Wp1@Xz>#z?khL!H1>!%sW&&w zT+R^kmqBT_`~U~+^*TrY>~FSb47EMzA^BeR13Ez+4)&KE5p{Oi!gkkBxBnOe?c~Lm zJRR@Kf3P?F&L6vGu065`C+Z2PPo#!%pyG+tTT&xN9v$0#qxwYTA$!Z;YiX*VO0|p)6*W=tf z@Ic?cp}VDR5Ti4|CQd%ynbgx^_{dl5N$@~@VE>GDmiVd>`?n@D~1w*B>{783dK zo#w*!J~NNIEj}tacQZb~Jl0_>b@jFm5c#?C^}_OdNADKg-%;C0eQCh?-Ru*(ZT` + + + \ 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..3423bf01 --- /dev/null +++ b/examples/solid/src/routes/index.tsx @@ -0,0 +1,150 @@ +import type { DB } from "bknd"; +import { 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 +
+ )} +
+ {data()?.todos?. + splice(0, data()?.limit ?? 0) + .map((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..b5be6904 --- /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(), + ], +}); From fc83a593a07e8fbb17f993e70c43c1b0d69bbdd8 Mon Sep 17 00:00:00 2001 From: Shishant Biswas Date: Sun, 15 Mar 2026 00:23:19 +0530 Subject: [PATCH 22/32] docs(solid-start): add JSDoc comments to adapter functions --- .../adapter/solid-start/solid-start.adapter.ts | 11 ++++++++++- examples/solid/src/routes/index.tsx | 18 +++++++++--------- examples/solid/src/routes/user.tsx | 2 +- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/app/src/adapter/solid-start/solid-start.adapter.ts b/app/src/adapter/solid-start/solid-start.adapter.ts index 37c6fe17..8ada7f62 100644 --- a/app/src/adapter/solid-start/solid-start.adapter.ts +++ b/app/src/adapter/solid-start/solid-start.adapter.ts @@ -3,6 +3,11 @@ 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, @@ -10,7 +15,11 @@ export async function getApp( 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, diff --git a/examples/solid/src/routes/index.tsx b/examples/solid/src/routes/index.tsx index 3423bf01..b83a9911 100644 --- a/examples/solid/src/routes/index.tsx +++ b/examples/solid/src/routes/index.tsx @@ -1,5 +1,5 @@ import type { DB } from "bknd"; -import { Suspense } from "solid-js"; +import { For, Suspense } from "solid-js"; import { Footer } from "~/components/Footer"; import { List } from "~/components/List"; import { getApi } from "~/lib/bknd"; @@ -58,9 +58,9 @@ export default function Home() { return (
-
+
Solid Start logo @@ -86,15 +86,14 @@ export default function Home() {
)}
- {data()?.todos?. - splice(0, data()?.limit ?? 0) - .map((todo) => ( + + {(todo) => (
{ await updateTodo(todo); }} @@ -113,7 +112,8 @@ export default function Home() { ❌
- ))} + )} +