Compare commits

..
Author SHA1 Message Date
Shishant Biswas 4709fec45c Merge pull request #287 from bknd-io/feat/data-ui-fillable-visible
make non-fillable fields visible but disabled in UI
2026-09-24 17:22:42 +05:30
shishantbiswas da80e91074 fix(ui-form): skip non-fillable fields in form data because they fail validation in backend 2026-09-24 14:22:23 +05:30
Shishant Biswas 3ad9fd01e1 Merge pull request #309 from bknd-io/feat/sort-plugin
Add sort plugin for managing orderable entities
2026-09-24 12:02:13 +05:30
Shishant Biswas 89d7e4ea2b Merge pull request #405 from shishantbiswas/fix-sidebar-link-href
fix: sidebar links href to include `adminBasePath`
2026-09-23 17:04:56 +05:30
shishantbiswas 6e3b7515d5 docs(plugin): add sort plugin documentation 2026-09-23 12:44:23 +05:30
shishantbiswas 62d3a9c4f4 fix: data/schema toggle link in /data path to include adminBasePath 2026-09-22 21:47:15 +05:30
shishantbiswas 919d9f15d1 revert: <a> ts expect error 2026-09-22 21:07:37 +05:30
shishantbiswas e68d29760a fix: sidebar link href with 2026-09-18 19:29:31 +05:30
Shishant Biswas eba2f8a535 Merge pull request #365 from shishantbiswas/feature-solid-start-adapter
feat: Solid Start adapter
2026-09-18 10:13:49 +05:30
shishantbiswas 6cfdb77ba0 update solid, vite, and nitro versions 2026-09-18 10:08:48 +05:30
Cameron Pak 8a90b9356b Merge pull request #387 from bknd-io/cp/fix-auth-bug
fix(tests): resolve prepublishOnly failures in AppAuth and postgres tests
2026-04-03 08:34:33 -05:00
Cameron Pak b070fb591e fix(tests): resolve prepublishOnly failures in AppAuth and postgres tests 2026-04-03 08:30:47 -05:00
Cameron Pak 6489d08434 Merge pull request #374 from jonaspm/chore/upgrade-typescript
Upgrade TypeScript to latest v6
2026-04-03 08:10:05 -05:00
shishantbiswas b2315ed012 refactor: dark mode styling 2026-03-28 16:21:42 +05:30
shishantbiswas 9e5c77db79 fix(docs): remove solid from home component 2026-03-28 16:21:42 +05:30
shishantbiswas 5892b898bf add: update README in solid example 2026-03-28 16:21:42 +05:30
shishantbiswas fc83a593a0 docs(solid-start): add JSDoc comments to adapter functions 2026-03-28 16:21:42 +05:30
shishantbiswas 8d37f9df3f init: solid start adapter 2026-03-28 16:21:42 +05:30
dswbx 81bf41ce34 Merge pull request #379 from jonaspm/chore/fix-manifestpath-warning
chore: Use dynamic import for manifest with Vite ignore. Fixes warning
2026-03-28 11:28:11 +01:00
dswbx d9ab583a16 Merge pull request #378 from jonaspm/chore/replace-deprecated-OptionsObjects
Replace deprecated OptionsObject with SpawnOptions and fix warning
2026-03-28 11:27:16 +01:00
dswbx e68df5e606 Merge pull request #377 from jonaspm/chore/dayjs-v1.11.20
chore: Bump dayjs to 1.11.20
2026-03-28 11:26:38 +01:00
dswbx 6304ef81f6 Merge pull request #373 from jonaspm/fix/example-sqlite-paths
fix: update local database URL to remove 'file:' prefix in examples
2026-03-28 11:24:07 +01:00
dswbx 6c7e82781d Merge pull request #362 from shishantbiswas/feature-nuxt-adapter
feat: Nuxt adapter
2026-03-28 10:55:14 +01:00
dswbx a8e5c45a22 bump 0.21.0-rc.1 2026-03-28 10:50:14 +01:00
dswbx ee28c2ef0c Merge pull request #372 from jonaspm/fix/broader-bearer-token-resolution
fix: Match Bearer case-insensitively and trim whitespace(s)
2026-03-28 10:35:43 +01:00
dswbx 9219bf3b3c test(auth, api): ensure bearer token is case-insensitive 2026-03-28 10:33:24 +01:00
Jonas Perusquia Morales e437ce7120 chore: Use dynamic import for manifest with Vite ignore. Fixes warning 2026-03-28 03:02:08 -06:00
Jonas Perusquia Morales 2417833ed7 chore: Replace Bun's deprecated OptionsObject with SpawnOptions and
remove stdout cast. Fix warning.
2026-03-28 02:56:26 -06:00
Jonas Perusquia Morales ab1eddcb6a chore: Bump dayjs to 1.11.20 2026-03-27 14:49:16 -06:00
Jonas Perusquia Morales 237b42bb9c chore: remove baseUrl from TypeScript configuration files 2026-03-24 13:59:05 -06:00
Jonas Perusquia Morales d206c38d87 chore: remove unused import of main.css in main.tsx 2026-03-24 13:49:53 -06:00
Jonas Perusquia Morales d160af5cec chore: add TypeScript declaration for CSS modules, fixes TS error 2026-03-24 13:49:45 -06:00
Jonas Perusquia Morales d03cd7f7bc chore: standardize TypeScript module and target casing in configuration files 2026-03-24 13:49:10 -06:00
Jonas Perusquia Morales 4afba215b9 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
2026-03-24 13:48:42 -06:00
Jonas Perusquia Morales dc8aca6a97 fix: update database URL to remove 'file:' prefix in examples 2026-03-24 13:30:54 -06:00
Jonas Perusquia Morales 4121597fa2 fix: Match Bearer case-insensitively and trim whitespace(s) 2026-03-24 12:20:17 -06:00
dswbx 9628720f87 test(AppReduced): remove redundant admin_base_path 2026-03-17 16:31:58 -05:00
dswbx ff216ec4e5 chore: remove unused admin_basepath and bump version to 0.21.0-rc.0 2026-03-17 16:31:58 -05:00
shishantbiswas 06f9c3ee15 refactor(docs): note for NuxtLink usage, added geist font in example, minor cleanups 2026-03-14 22:57:05 +05:30
shishantbiswas 7751ee5db8 init: nuxt adapter 2026-03-14 19:27:31 +05:30
dswbx feb3911d46 Merge pull request #322 from bknd-io/fix/auth-strategy-respect-register-setting
fix(auth): `allow_register` was not respected in strategy controllers
2026-03-14 13:43:55 +01:00
dswbx ab545d3ca2 Merge pull request #347 from bknd-io/fix/with-builder-respect-alias
fix: `WithBuilder` produced embedding errored when `mappedBy` was used
2026-03-14 13:42:50 +01:00
dswbx 06c3a6f89b Merge remote-tracking branch 'origin/main' into fix/with-builder-respect-alias
# Conflicts:
#	bun.lock
2026-03-14 13:41:01 +01:00
dswbx 898a825f6e fix(WithBuilder test): updated snapshots 2026-03-14 13:40:35 +01:00
dswbx a5ddf6aa93 Merge pull request #363 from bknd-io/release/0.21
Release 0.21
2026-03-14 13:34:23 +01:00
dswbx 5aa286298c Merge remote-tracking branch 'origin/main' into release/0.21
# Conflicts:
#	bun.lock
2026-03-14 13:31:45 +01:00
dswbx 55cb2bacef chore: upgrade bun.lock 2026-03-14 13:29:47 +01:00
dswbx e6ea3f64d7 Merge pull request #351 from shishantbiswas/main
feat: add Tanstack Start adapter
2026-03-14 13:25:35 +01:00
dswbx a5f5ea2084 Merge pull request #339 from cameronapak/allow-seed
Allow CLI `sync` command to seed data properly when schema changes aren't present
2026-03-14 13:18:33 +01:00
dswbx 6fb8e9a0e7 Merge pull request #218 from cameronapak/cp/216-fix-users-link
Fix Sidebar Users Route Links
2026-03-14 13:12:53 +01:00
dswbx abc6de6ad3 Merge pull request #332 from bknd-io/feat/timestamps-index
feat(plugin/timestamps): add indexStrategy option for entity indexing
2026-03-14 13:07:49 +01:00
Shishant Biswas 71b9a0f546 Merge branch 'bknd-io:main' into main 2026-03-13 17:27:35 +05:30
Cameron Pak 038ec0d672 Merge pull request #357 from cameronapak/cp/add-contributing-md
Adding CONTRIBUTING.md doc
2026-03-08 14:16:10 -05:00
Cameron Pak 7832328a50 Merge pull request #359 from jonaspm/patch-1
Update client:only attribute in astro.mdx
2026-03-03 11:37:59 -06:00
Jonás Perusquía Morales e5bfc602e1 Update client:only attribute in astro.mdx
Astro requires client:only to specify the frontend library used
2026-03-03 11:27:59 -06:00
Cameron PakandAmp 0318e595f2 docs: add CONTRIBUTING.md with dev setup, project structure, and contribution guidelines
Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019ca575-cf6e-7388-9bd4-6ddd4c27609c
2026-02-28 12:40:44 -06:00
shishantbiswas 80b0716a4a Remove unrelated Next.js file changes 2026-02-28 23:32:20 +05:30
Cameron Pak 3894271b17 Merge pull request #353 from jonaspm/docs/auth-change-password
docs: Document changePassword method in auth module
2026-02-28 11:38:33 -06:00
shishantbiswas 8bf8b22e57 refactor: remove duplicate tsup build config and clean up 2026-02-28 23:05:38 +05:30
Cameron Pak 367d5878df Fixed test 2026-02-28 11:26:27 -06:00
Cameron PakandAmp fb429e57e2 fix: resolve undefined variable in MediaApi test and remove dead comment
Amp-Thread-ID: https://ampcode.com/threads/T-019ca53d-a225-706c-923e-28123c84d83e
Co-authored-by: Amp <amp@ampcode.com>
2026-02-28 11:21:39 -06:00
Cameron PakandAmp 9008f9c6a3 fix: normalize admin basepath to prevent double slashes in URLs
Strip trailing slashes from adminBasepath in AdminController window context
and wouter Router base to prevent paths like /admin//data when users
configure adminBasepath with a trailing slash.

Amp-Thread-ID: https://ampcode.com/threads/T-019ca537-4cc4-7174-bf9a-5325d782f097
Co-authored-by: Amp <amp@ampcode.com>
2026-02-28 11:11:44 -06:00
Cameron Pak 6321a9935a Merge branch 'main' into cp/216-fix-users-link 2026-02-28 11:03:54 -06:00
Cameron Pak eb22289460 Update @types/bun to 1.3.9
This commit updates the `@types/bun` package to version 1.3.9.
2026-02-28 11:00:37 -06:00
shishantbiswas c3198394de docs: add tanstack start integration guide 2026-02-26 09:40:42 +05:30
shishantbiswas 51237f5e1a docs(tanstack-start): add JSDoc comments to adapter functions 2026-02-26 09:40:42 +05:30
shishantbiswas 01483b912f add: example for tanstack start 2026-02-26 09:40:42 +05:30
shishantbiswas 6288faef33 Triggering CI 2026-02-26 09:40:42 +05:30
shishantbiswas 384e337bbd add: Tanstack Start adapter 2026-02-26 09:38:28 +05:30
Jonas Perusquia Morales cc1e1976f7 chore: Document error cases for password update method 2026-02-23 13:03:37 -06:00
Jonas Perusquia Morales 8805f017a1 Update auth docs to clarify server module usage 2026-02-18 11:12:14 -06:00
Jonas Perusquia Morales 0ef06375c0 docs: Document changePassword method in auth module 2026-02-18 11:03:58 -06:00
dswbx 1c114d9533 fix: WithBuilder produced embedding errored when mappedBy was used 2026-02-03 18:15:40 +01:00
cameronapak 11e3488917 Allow seeding without forcing sync 2026-01-19 11:46:48 -06:00
dswbx d9bce049ef feat(plugin/timestamps): add indexStrategy option for entity indexing
Introduced a new `indexStrategy` option in the timestamps plugin to configure index generation for `created_at` and `updated_at` fields. Supports "composite" and "individual" strategies for better control over database indexing. Added test coverage for different index strategies.
2026-01-09 16:04:54 +01:00
cameronapak d00e5fe68c Remove redundant basepath option 2025-12-30 07:34:00 -06:00
cameronapak 6faf0d421f Fix test for Blob to File conversion
The Response.blob() method in JavaScript returns a Blob, not a File.
This means that file-specific metadata like `name` and `lastModified` is
lost. The tests were updated to reflect this by manually constructing a
File object from the Blob, mimicking how client code would handle this
conversion. The MIME type is now correctly preserved from the response
headers when creating the File.
2025-12-30 07:33:55 -06:00
cameronapak 19e51fae63 Refactor AppReduced to use basepath config option
The `admin_basepath` configuration option has been renamed to `basepath`
for clarity and consistency. This change affects how the admin base path
is accessed and utilized within the `AppReduced` utility. Tests have
been updated to reflect this renaming.
2025-12-30 07:09:00 -06:00
cameronapak 7e01cc6101 Updated bun lockfile 2025-12-30 06:59:36 -06:00
cameronapak 103cbd11df Merge branch 'main' into cp/216-fix-users-link 2025-12-30 06:55:20 -06:00
dswbx 09b9a5e857 fix(auth): allow_register was not respected in strategy controllers 2025-12-06 20:51:09 +01:00
dswbx 5c3d3d763e move sort plugin to data plugins 2025-11-26 10:26:58 +01:00
dswbx 407d103a5f fix: remove test.only 2025-11-26 10:12:39 +01:00
dswbx 2fd8b9f9a0 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.
2025-11-26 10:08:30 +01:00
dswbx e81636ea95 Merge remote-tracking branch 'origin/main' into feat/data-ui-fillable-visible 2025-10-31 13:34:42 +01:00
dswbx 37c36e5d07 make non-fillable fields visible but disabled in UI 2025-10-24 14:09:07 +02:00
dswbx ac891a4b61 Merge remote-tracking branch 'origin/fix/release-0.16-examples' into fork/cameronapak/cp/216-fix-users-link 2025-08-03 13:21:05 +02:00
dswbx 412eab6653 examples: fixing imports due to 0.16 2025-08-03 13:15:01 +02:00
dswbx 2fb07698d5 fixing AppReduced.spec.ts 2025-08-03 13:10:49 +02:00
dswbx 64e400c4f1 ui AppReduced: fixing basepath handling in static and embedded modes 2025-08-03 13:08:04 +02:00
dswbx 86d6e891a8 Merge remote-tracking branch 'origin/main' into fork/cameronapak/cp/216-fix-users-link
# Conflicts:
#	bun.lock
2025-08-03 12:55:20 +02:00
cameronapak 7b86611f35 bun run format 2025-07-22 08:58:30 -05:00
cameronapak 9e316a92f6 refactor: use AppTheme type and handle empty admin_basepath
Replace literal theme union with AppTheme type in AdminController options and
make path helpers resilient when admin_basepath is empty.
2025-07-22 08:57:06 -05:00
cameronapak 12ef72d595 test: add comprehensive tests for AppReduced utility and path normalization 2025-07-22 08:52:32 -05:00
cameronapak 75c29ae5e2 Add .env.local to app/.gitignore 2025-07-22 08:37:31 -05:00
cameronapak f0551f50d1 fix: set default admin_basepath and logo_return_path in constructor
Move default option values into the constructor parameter initializer to
prevent them from being overwritten by the spread operator in
`get options()`.
2025-07-22 08:36:38 -05:00
cameronapak 03aac3437b refactor: unify theme type and admin options from AdminController 2025-07-22 08:33:42 -05:00
cameronapak f07f3d1007 refactor: rename basepath to admin_basepath and normalize admin paths 2025-07-22 08:28:23 -05:00
cameronapak c833b2b539 feat: add @clack/prompts because somehow it was missing? 2025-07-22 05:53:23 -05:00
cameronapak 6070394cca Updated bun.lock file 2025-07-22 05:50:44 -05:00
155 changed files with 6418 additions and 567 deletions
+267
View File
@@ -0,0 +1,267 @@
# Contributing to bknd
Thank you for your interest in contributing to bknd. This guide will help you get started, understand the codebase, and submit contributions that align with the project's direction.
## Table of Contents
- [Before You Start](#before-you-start)
- [Important Resources](#important-resources)
- [Development Setup](#development-setup)
- [Project Structure](#project-structure)
- [Understanding the Codebase](#understanding-the-codebase)
- [Running Tests](#running-tests)
- [Code Style](#code-style)
- [Submitting Changes](#submitting-changes)
- [AI-Generated Code](#ai-generated-code)
- [Contributing to Documentation](#contributing-to-documentation)
- [Reporting Bugs](#reporting-bugs)
- [Getting Help](#getting-help)
- [Contributors](#contributors)
- [Maintainers](#maintainers)
## Before You Start
**Open a GitHub Issue before writing code.** This is the preferred way to propose any change. It lets maintainers and the community align on the approach before time is spent on implementation. If you have discussed a contribution in the [Discord server](https://discord.com/invite/952SFk8Tb8) instead, include a link to the relevant Discord thread in your pull request. Pull requests submitted without a corresponding issue or Discord discussion may be closed.
**Unsolicited architectural changes will be closed.** The internal architecture of bknd is intentional. Refactors, restructuring, or changes to core patterns must be discussed and approved before any code is written.
Contributions that are generally welcome (after opening an issue):
- Bug fixes
- New adapters, strategies, or storage backends
- Documentation improvements
- New examples
- Test coverage improvements
**A note on versioning**: bknd is pre-1.0 and under active development. Full backward compatibility is not guaranteed before v1.0.0. Contributors should be aware that APIs and internal interfaces may change between releases.
## Important Resources
- **Documentation**: https://docs.bknd.io
- **Issue Tracker**: https://github.com/bknd-io/bknd/issues
- **Discord**: https://discord.com/invite/952SFk8Tb8
- **FAQ / Search**: https://www.answeroverflow.com/c/1308395750564302952
## Development Setup
### Prerequisites
- Node.js 22.13 or higher
- Bun 1.3.3
### Getting Started
1. Fork the repository and clone your fork.
2. Install dependencies from the repo root:
```bash
bun install
```
3. Navigate to the main app package:
```bash
cd app
```
4. Copy the example environment file:
```bash
cp .env.example .env
```
The default `.env.example` includes a file-based SQLite database (`file:.db/dev.db`). You can change `VITE_DB_URL` to `:memory:` for an in-memory database instead.
5. Start the dev server:
```bash
bun run dev
```
The dev server runs on `http://localhost:28623` using Vite with Hono.
## Project Structure
This is a Bun monorepo using workspaces. The vast majority of the code lives in `app/`.
```
bknd/
app/ # Main "bknd" npm package (this is where most work happens)
src/
adapter/ # Runtime/framework adapters (node, bun, cloudflare, nextjs, astro, etc.)
auth/ # Authentication module (strategies, sessions, user pool)
cli/ # CLI commands
core/ # Shared utilities, event system, drivers, server helpers
data/ # Data module (entities, fields, relations, queries, schema)
flows/ # Workflow engine and tasks
media/ # Media module (storage adapters, uploads)
modes/ # Configuration modes (code, hybrid, db)
modules/ # Module system, MCP server, permissions framework
plugins/ # Built-in plugins (auth, data, cloudflare, dev)
ui/ # All frontend code
client/ # TypeScript SDK and React hooks (exported as bknd/client)
elements/ # React components for auth/media (exported as bknd/elements)
(everything else) # Admin UI (exported as bknd/ui)
App.ts # Central application orchestrator
index.ts # Main package exports
__test__/ # Unit tests (mirrors src/ structure)
e2e/ # End-to-end tests (Playwright)
build.ts # Build script (tsup/esbuild)
build.cli.ts # CLI build script
packages/ # Small satellite packages
cli/ # Standalone CLI package (bknd-cli)
plasmic/ # Plasmic integration
postgres/ # Postgres helper
sqlocal/ # SQLocal helper
docs/ # Documentation site (Next.js + Fumadocs, deployed to docs.bknd.io)
examples/ # Example projects across runtimes and frameworks
docker/ # Docker configuration
```
## Understanding the Codebase
### Path Aliases
The project uses TypeScript path aliases defined in `app/tsconfig.json`. Imports like `core/utils`, `data/connection`, or `auth/authenticate` resolve to directories under `app/src/`. When reading source code, keep this in mind -- an import like `import { Connection } from "data/connection"` refers to `app/src/data/connection`, not an external package.
### Module System
bknd is built around four core modules, each with its own schema, API routes, and permissions:
- **Data** -- entity definitions, field types, relations, queries (backed by Kysely)
- **Auth** -- authentication strategies, sessions, user management
- **Media** -- file storage with pluggable adapters (S3, Cloudinary, local filesystem, etc.)
- **Flows** -- workflow automation and task execution
These modules are managed by the `ModuleManager` (in `app/src/modules/`), which handles configuration, building, and lifecycle.
### Adapter Pattern
Adapters in `app/src/adapter/` allow bknd to run on different runtimes and frameworks. Each adapter provides the glue between bknd's Hono-based server and a specific environment (Node, Bun, Cloudflare Workers, Next.js, Astro, etc.).
### Plugin System
Plugins (in `app/src/plugins/`) hook into the app lifecycle via callbacks like `beforeBuild`, `onBuilt`, `onServerInit`, and `schema`. They can add entities, register routes, and extend behavior.
### Build System
The build is handled by a custom `app/build.ts` script using tsup (esbuild under the hood). It builds four targets in parallel:
- **API** -- the core backend
- **UI** -- the admin interface
- **Elements** -- standalone React components
- **Adapters** -- all runtime/framework adapters
## Running Tests
All test commands run from the `app/` directory.
| Command | Runner | What it runs |
|---|---|---|
| `bun run test` | Bun test | Unit tests (`*.spec.ts` files) |
| `bun run test:node` | Vitest | Node-specific tests (`*.vi-test.ts`, `*.vitest.ts`) |
| `bun run test:e2e` | Playwright | End-to-end tests (`*.e2e-spec.ts` in `e2e/`) |
| `bun run types` | TypeScript | Type checking (no emit) |
CI runs both Bun and Node tests, with a Postgres 17 service for database integration tests.
## Code Style
The project uses **Biome** for formatting and linting.
- 3-space indentation for JavaScript/TypeScript
- 100-character line width
- Spaces (not tabs)
To format and lint, run from the repo root:
```bash
bunx biome format --write ./app
bunx biome lint --changed --write ./app
```
Run both before submitting a PR. CI will catch style issues, but it is better to fix them locally first.
## Submitting Changes
1. Open a GitHub Issue describing your proposed change, or link to a Discord thread where it was discussed. Wait for feedback before writing code.
2. Fork the repo and create a branch from `main`.
3. Keep changes focused and minimal. One PR per issue.
4. Add or update tests if applicable.
5. Run the test suite and linter before pushing.
6. Open a pull request against `main`. Reference the issue number in the PR description.
Expect a response from maintainers, but be patient -- this is an actively developed project and review may take some time.
## AI-Generated Code
If you use AI tools to write or assist with your code, you must:
- **Fully review all AI-generated code yourself** before submitting. You are responsible for understanding and standing behind every line in your PR.
- **Include the prompts used** in the PR description. This gives maintainers additional context for reviewing the code.
Pull requests with unreviewed AI-generated code will be closed.
## Contributing to Documentation
The documentation site lives in the `docs/` directory and is a **separate Next.js application** built with [Fumadocs](https://fumadocs.dev). It uses **npm** (not Bun) as its package manager.
### Docs Setup
```bash
cd docs
npm install
npm run dev
```
The docs dev server runs on `http://localhost:3000`.
### Where Documentation Lives
Documentation content is written in MDX and located in `docs/content/docs/`. The directory is organized into:
- `(documentation)/` -- core documentation pages
- `guide/` -- guides and tutorials
- `api-reference/` -- API reference (partially auto-generated from OpenAPI)
To add or edit a page, create or modify an `.mdx` file in the appropriate directory. Page metadata is defined via frontmatter at the top of each file. Navigation order is controlled by `meta.json` files in each directory.
### Building the Docs
```bash
npm run build
```
This generates the OpenAPI and MCP reference pages before building the Next.js site. Make sure the build succeeds locally before submitting a docs PR.
## Reporting Bugs
Open a GitHub Issue with:
- A clear title describing the problem.
- Steps to reproduce the bug.
- Expected behavior vs. actual behavior.
- Your environment (runtime, database, adapter, bknd version).
- Any relevant error messages or logs.
## Getting Help
- **Discord**: https://discord.com/invite/952SFk8Tb8
- **FAQ / Search**: https://www.answeroverflow.com/c/1308395750564302952
- **GitHub Issues**: https://github.com/bknd-io/bknd/issues
## Contributors
Thank you to everyone who has contributed to bknd.
<a href="https://github.com/bknd-io/bknd/graphs/contributors">
<img src="https://contrib.rocks/image?repo=bknd-io/bknd" />
</a>
Made with [contrib.rocks](https://contrib.rocks).
## Maintainers
- **Dennis** ([@dswbx](https://github.com/dswbx)) -- Creator and maintainer of bknd
- **Cameron Pak** ([@cameronapak](https://github.com/cameronapak)) -- Maintainer of bknd docs
+16
View File
@@ -165,3 +165,19 @@ npx bknd run
```bash ```bash
npm install bknd npm install bknd
``` ```
## Contributing
We welcome contributions! Please read our [CONTRIBUTING.md](CONTRIBUTING.md) before getting started. It covers dev setup, project structure, testing, code style, and how to submit changes.
Before writing code, please open a GitHub Issue or start a conversation in [Discord](https://discord.com/invite/952SFk8Tb8) so we can align on the approach.
## Contributors
Thank you to everyone who has contributed to bknd.
<a href="https://github.com/bknd-io/bknd/graphs/contributors">
<img src="https://contrib.rocks/image?repo=bknd-io/bknd" />
</a>
Made with [contrib.rocks](https://contrib.rocks).
+2 -1
View File
@@ -1,4 +1,5 @@
playwright-report playwright-report
test-results test-results
bknd.config.* bknd.config.*
__test__/helper.d.ts __test__/helper.d.ts
.env.local
+6
View File
@@ -70,4 +70,10 @@ describe("Api", async () => {
expect(params.token_transport).toBe("header"); expect(params.token_transport).toBe("header");
expect(params.host).toBe("http://another.com"); 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);
})
}); });
+4 -3
View File
@@ -110,10 +110,11 @@ describe("MediaApi", () => {
const api = new MediaApi({}, mockedBackend.request); const api = new MediaApi({}, mockedBackend.request);
const name = "image.png"; const name = "image.png";
const res = await api.getFileStream(name); const res = await api.getFile(name);
expect(isReadableStream(res)).toBe(true); const stream = await api.getFileStream(name);
expect(isReadableStream(stream)).toBe(true);
const blob = (await new Response(res).blob()) as File; const blob = (await res.res.blob()) as File;
expect(isFile(blob)).toBe(true); expect(isFile(blob)).toBe(true);
expect(blob.size).toBeGreaterThan(0); expect(blob.size).toBeGreaterThan(0);
expect(blob.type).toBe("image/png"); expect(blob.type).toBe("image/png");
+23
View File
@@ -38,4 +38,27 @@ describe("Authenticator", async () => {
expect(cookie).toStartWith("auth="); expect(cookie).toStartWith("auth=");
expect(cookie).toEndWith("HttpOnly; Secure; SameSite=Strict"); 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")
})
}); });
+2 -2
View File
@@ -41,12 +41,12 @@ describe("postgres", () => {
beforeAll(async () => { beforeAll(async () => {
if (!(await isPostgresRunning())) { if (!(await isPostgresRunning())) {
await $`docker run --rm --name bknd-test-postgres -d -e POSTGRES_PASSWORD=${credentials.password} -e POSTGRES_USER=${credentials.user} -e POSTGRES_DB=${credentials.database} -p ${credentials.port}:5432 postgres:17`; await $`docker run --rm --name bknd-test-postgres -d -e POSTGRES_PASSWORD=${credentials.password} -e POSTGRES_USER=${credentials.user} -e POSTGRES_DB=${credentials.database} -p ${credentials.port}:5432 postgres:17`;
await $waitUntil("Postgres is running", isPostgresRunning); await $waitUntil("Postgres is running", isPostgresRunning, 500, 20);
await new Promise((resolve) => setTimeout(resolve, 500)); await new Promise((resolve) => setTimeout(resolve, 500));
} }
disableConsoleLog(); disableConsoleLog();
}); }, 30000);
afterAll(async () => { afterAll(async () => {
if (await isPostgresRunning()) { if (await isPostgresRunning()) {
try { try {
+5 -13
View File
@@ -88,7 +88,7 @@ describe("[data] WithBuilder", async () => {
const res2 = qb2.compile(); const res2 = qb2.compile();
expect(res2.sql).toBe( expect(res2.sql).toBe(
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username") from (select "users"."id" as "id", "users"."username" as "username" from "users" as "author" where "author"."id" = "posts"."author_id" order by "users"."id" asc limit ?) as obj) as "author" from "posts"', 'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username") from (select "author"."id" as "id", "author"."username" as "username" from "users" as "author" where "author"."id" = "posts"."author_id" order by "author"."id" asc limit ?) as obj) as "author" from "posts"',
); );
expect(res2.parameters).toEqual([1]); expect(res2.parameters).toEqual([1]);
}); });
@@ -192,9 +192,7 @@ describe("[data] WithBuilder", async () => {
{ single: {} }, { single: {} },
); );
const res = qb.compile(); const res = qb.compile();
expect(res.sql).toBe( expect(res.sql).toMatchSnapshot();
'select (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "media"."id" asc limit ?) as obj) as "single" from "categories"',
);
expect(res.parameters).toEqual(["categories.single", 1]); expect(res.parameters).toEqual(["categories.single", 1]);
const qb2 = WithBuilder.addClause( const qb2 = WithBuilder.addClause(
@@ -204,9 +202,7 @@ describe("[data] WithBuilder", async () => {
{ multiple: {} }, { multiple: {} },
); );
const res2 = qb2.compile(); const res2 = qb2.compile();
expect(res2.sql).toBe( expect(res2.sql).toMatchSnapshot();
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'path\', "agg"."path")), \'[]\') from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as agg) as "multiple" from "categories"',
);
expect(res2.parameters).toEqual(["categories.multiple", 10, 0]); expect(res2.parameters).toEqual(["categories.multiple", 10, 0]);
}); });
@@ -271,9 +267,7 @@ describe("[data] WithBuilder", async () => {
); );
//prettyPrintQb(qb); //prettyPrintQb(qb);
expect(qb.compile().sql).toBe( expect(qb.compile().sql).toMatchSnapshot();
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username", \'avatar\', "obj"."avatar") from (select "users"."id" as "id", "users"."username" as "username", (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "users"."id" = "media"."entity_id" order by "media"."id" asc limit ?) as obj) as "avatar" from "users" as "users" where "users"."id" = "posts"."users_id" order by "users"."username" asc limit ?) as obj) as "users" from "posts"',
);
expect(qb.compile().parameters).toEqual(["users.avatar", 1, 1]); expect(qb.compile().parameters).toEqual(["users.avatar", 1, 1]);
}); });
@@ -313,9 +307,7 @@ describe("[data] WithBuilder", async () => {
}, },
); );
expect(qb.compile().sql).toBe( expect(qb.compile().sql).toMatchSnapshot();
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'posts_id\', "agg"."posts_id", \'users_id\', "agg"."users_id", \'users\', "agg"."users")), \'[]\') from (select "comments"."id" as "id", "comments"."posts_id" as "posts_id", "comments"."users_id" as "users_id", (select json_object(\'username\', "obj"."username") from (select "users"."username" as "username" from "users" as "users" where "users"."id" = "comments"."users_id" order by "users"."id" asc limit ?) as obj) as "users" from "comments" as "comments" where "comments"."posts_id" = "posts"."id" order by "comments"."id" asc limit ? offset ?) as agg) as "comments" from "posts"',
);
expect(qb.compile().parameters).toEqual([1, 12, 0]); expect(qb.compile().parameters).toEqual([1, 12, 0]);
}); });
@@ -0,0 +1,9 @@
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
exports[`[data] WithBuilder recursive compiles with singles 1`] = `"select (select json_object('id', "obj"."id", 'username', "obj"."username", 'avatar', "obj"."avatar") from (select "users"."id" as "id", "users"."username" as "username", (select json_object('id', "obj"."id", 'path', "obj"."path") from (select "avatar"."id" as "id", "avatar"."path" as "path" from "media" where "media"."reference" = ? and "users"."id" = "media"."entity_id" order by "avatar"."id" asc limit ?) as obj) as "avatar" from "users" as "users" where "users"."id" = "posts"."users_id" order by "users"."username" asc limit ?) as obj) as "users" from "posts""`;
exports[`[data] WithBuilder recursive compiles with many 1`] = `"select (select coalesce(json_group_array(json_object('id', "agg"."id", 'posts_id', "agg"."posts_id", 'users_id', "agg"."users_id", 'users', "agg"."users")), '[]') from (select "comments"."id" as "id", "comments"."posts_id" as "posts_id", "comments"."users_id" as "users_id", (select json_object('username', "obj"."username") from (select "users"."username" as "username" from "users" as "users" where "users"."id" = "comments"."users_id" order by "users"."id" asc limit ?) as obj) as "users" from "comments" as "comments" where "comments"."posts_id" = "posts"."id" order by "comments"."id" asc limit ? offset ?) as agg) as "comments" from "posts""`;
exports[`[data] WithBuilder polymorphic 1`] = `"select (select json_object('id', "obj"."id", 'path', "obj"."path") from (select "single"."id" as "id", "single"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "single"."id" asc limit ?) as obj) as "single" from "categories""`;
exports[`[data] WithBuilder polymorphic 2`] = `"select (select coalesce(json_group_array(json_object('id', "agg"."id", 'path', "agg"."path")), '[]') from (select "multiple"."id" as "id", "multiple"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "multiple"."id" asc limit ? offset ?) as agg) as "multiple" from "categories""`;
+1 -1
View File
@@ -149,7 +149,7 @@ describe("AppAuth", () => {
}); });
await app.build(); await app.build();
app.registerAdminController(); app.registerAdminController({ forceDev: true });
const spy = spyOn(app.module.auth.authenticator, "requestCookieRefresh"); const spy = spyOn(app.module.auth.authenticator, "requestCookieRefresh");
// register custom route // register custom route
@@ -7,7 +7,9 @@ import v7 from "./samples/v7.json";
import v8 from "./samples/v8.json"; import v8 from "./samples/v8.json";
import v8_2 from "./samples/v8-2.json"; import v8_2 from "./samples/v8-2.json";
import v9 from "./samples/v9.json"; import v9 from "./samples/v9.json";
import v10 from "./samples/v10.json";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test"; import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
import { CURRENT_VERSION } from "modules/db/migrations";
beforeAll(() => disableConsoleLog()); beforeAll(() => disableConsoleLog());
afterAll(enableConsoleLog); afterAll(enableConsoleLog);
@@ -61,7 +63,7 @@ async function getRawConfig(
return await db return await db
.selectFrom("__bknd") .selectFrom("__bknd")
.selectAll() .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)) .$if((opts?.types?.length ?? 0) > 0, (qb) => qb.where("type", "in", opts?.types))
.execute(); .execute();
} }
@@ -115,7 +117,6 @@ describe("Migrations", () => {
"^^s3.secret_access_key^^", "^^s3.secret_access_key^^",
); );
const [config, secrets] = (await getRawConfig(app, { const [config, secrets] = (await getRawConfig(app, {
version: 10,
types: ["config", "secrets"], types: ["config", "secrets"],
})) as any; })) as any;
@@ -129,4 +130,15 @@ describe("Migrations", () => {
"^^s3.secret_access_key^^", "^^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);
});
}); });
@@ -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": {}
}
}
@@ -0,0 +1,284 @@
import { describe, it, expect, beforeEach } from "vitest";
import { AppReduced, type AppType } from "ui/client/utils/AppReduced";
import type { BkndAdminProps } from "ui/Admin";
// Import the normalizeAdminPath function for testing
// Note: This assumes the function is exported or we need to test it indirectly through public methods
describe("AppReduced", () => {
let mockAppJson: AppType;
let appReduced: AppReduced;
beforeEach(() => {
mockAppJson = {
data: {
entities: {},
relations: {},
},
flows: {
flows: {},
},
auth: {},
} as AppType;
});
describe("getSettingsPath", () => {
it("should return settings path with basepath", () => {
const options: BkndAdminProps["config"] = {
basepath: "/admin",
logo_return_path: "/",
};
appReduced = new AppReduced(mockAppJson, options);
const result = appReduced.getSettingsPath();
expect(result).toBe("~/admin/settings");
});
it("should return settings path with empty basepath", () => {
const options: BkndAdminProps["config"] = {
basepath: "",
logo_return_path: "/",
};
appReduced = new AppReduced(mockAppJson, options);
const result = appReduced.getSettingsPath();
expect(result).toBe("~/settings");
});
it("should append additional path segments", () => {
const options: BkndAdminProps["config"] = {
basepath: "/admin",
logo_return_path: "/",
};
appReduced = new AppReduced(mockAppJson, options);
const result = appReduced.getSettingsPath(["user", "profile"]);
expect(result).toBe("~/admin/settings/user/profile");
});
it("should normalize multiple slashes", () => {
const options: BkndAdminProps["config"] = {
basepath: "//admin//",
logo_return_path: "/",
};
appReduced = new AppReduced(mockAppJson, options);
const result = appReduced.getSettingsPath(["//user//"]);
expect(result).toBe("~/admin/settings/user");
});
it("should handle basepath without leading slash", () => {
const options: BkndAdminProps["config"] = {
basepath: "admin",
logo_return_path: "/",
};
appReduced = new AppReduced(mockAppJson, options);
const result = appReduced.getSettingsPath();
expect(result).toBe("~/admin/settings");
});
});
describe("getAbsolutePath", () => {
it("should return absolute path with basepath", () => {
const options: BkndAdminProps["config"] = {
basepath: "/admin",
logo_return_path: "/",
};
appReduced = new AppReduced(mockAppJson, options);
const result = appReduced.getAbsolutePath("dashboard");
expect(result).toBe("~/admin/dashboard");
});
it("should return base path when no path provided", () => {
const options: BkndAdminProps["config"] = {
basepath: "/admin",
logo_return_path: "/",
};
appReduced = new AppReduced(mockAppJson, options);
const result = appReduced.getAbsolutePath();
expect(result).toBe("~/admin");
});
it("should normalize paths correctly", () => {
const options: BkndAdminProps["config"] = {
basepath: "//admin//",
logo_return_path: "/",
};
appReduced = new AppReduced(mockAppJson, options);
const result = appReduced.getAbsolutePath("//dashboard//");
expect(result).toBe("~/admin/dashboard");
});
});
describe("options getter", () => {
it("should return merged options with defaults", () => {
const customOptions: BkndAdminProps["config"] = {
basepath: "/custom-admin",
logo_return_path: "/custom-home",
};
appReduced = new AppReduced(mockAppJson, customOptions);
const options = appReduced.options;
expect(options).toEqual({
logo_return_path: "/custom-home",
basepath: "/custom-admin",
});
});
it("should use default logo_return_path when not provided", () => {
const customOptions: BkndAdminProps["config"] = {
basepath: "/admin",
};
appReduced = new AppReduced(mockAppJson, customOptions);
const options = appReduced.options;
expect(options.logo_return_path).toBe("/");
expect(options.basepath).toBe("/admin");
});
});
describe("path normalization behavior", () => {
it("should normalize duplicate slashes in settings path", () => {
const options: BkndAdminProps["config"] = {
basepath: "/admin",
logo_return_path: "/",
};
appReduced = new AppReduced(mockAppJson, options);
const result = appReduced.getSettingsPath(["//nested//path//"]);
expect(result).toBe("~/admin/settings/nested/path");
});
it("should handle root path normalization", () => {
const options: BkndAdminProps["config"] = {
basepath: "/",
logo_return_path: "/",
};
appReduced = new AppReduced(mockAppJson, options);
const result = appReduced.getAbsolutePath();
// The normalizeAdminPath function removes trailing slashes except for root "/"
// When basepath is "/", the result is "~/" which becomes "~" after normalization
expect(result).toBe("~");
});
it("should preserve entity paths ending with slash", () => {
const options: BkndAdminProps["config"] = {
basepath: "/admin",
logo_return_path: "/",
};
appReduced = new AppReduced(mockAppJson, options);
const result = appReduced.getAbsolutePath("entity/");
expect(result).toBe("~/admin/entity");
});
it("should remove trailing slashes from non-entity paths", () => {
const options: BkndAdminProps["config"] = {
basepath: "/admin",
logo_return_path: "/",
};
appReduced = new AppReduced(mockAppJson, options);
const result = appReduced.getAbsolutePath("dashboard/");
expect(result).toBe("~/admin/dashboard");
});
});
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",
logo_return_path: "/",
};
appReduced = new AppReduced(mockAppJson, options);
const result = appReduced.withBasePath("/data");
expect(result).toBe("/admin/data");
expect(result).not.toContain("//");
});
it("should work correctly when admin_basepath has no trailing slash", () => {
const options: BkndAdminProps["config"] = {
basepath: "/admin",
logo_return_path: "/",
};
appReduced = new AppReduced(mockAppJson, options);
const result = appReduced.withBasePath("/data");
expect(result).toBe("/admin/data");
});
it("should handle absolute paths with admin_basepath trailing slash", () => {
const options: BkndAdminProps["config"] = {
basepath: "/admin",
logo_return_path: "/",
};
appReduced = new AppReduced(mockAppJson, options);
const result = appReduced.getAbsolutePath("data");
expect(result).toBe("~/admin/data");
expect(result).not.toContain("//");
});
it("should handle settings path with admin_basepath trailing slash", () => {
const options: BkndAdminProps["config"] = {
basepath: "/admin",
logo_return_path: "/",
};
appReduced = new AppReduced(mockAppJson, options);
const result = appReduced.getSettingsPath(["general"]);
expect(result).toBe("~/admin/settings/general");
expect(result).not.toContain("//");
});
});
describe("edge cases", () => {
it("should handle undefined basepath", () => {
const options: BkndAdminProps["config"] = {
logo_return_path: "/",
};
appReduced = new AppReduced(mockAppJson, options);
const result = appReduced.getSettingsPath();
// When basepath is undefined, it defaults to empty string
expect(result).toBe("~/settings");
});
it("should handle null path segments", () => {
const options: BkndAdminProps["config"] = {
basepath: "/admin",
logo_return_path: "/",
};
appReduced = new AppReduced(mockAppJson, options);
const result = appReduced.getSettingsPath(["", "valid", ""]);
expect(result).toBe("~/admin/settings/valid");
});
});
});
+320 -285
View File
@@ -14,15 +14,15 @@ const clean = args.includes("--clean");
// silence tsup // silence tsup
const oldConsole = { const oldConsole = {
log: console.log, log: console.log,
warn: console.warn, warn: console.warn,
}; };
console.log = () => {}; console.log = () => {};
console.warn = () => {}; console.warn = () => {};
const define = { const define = {
__isDev: "0", __isDev: "0",
__version: JSON.stringify(pkg.version), __version: JSON.stringify(pkg.version),
}; };
if (clean) { if (clean) {
@@ -43,146 +43,149 @@ if (clean) {
let types_running = false; let types_running = false;
function buildTypes() { function buildTypes() {
if (types_running || !types) return; if (types_running || !types) return;
types_running = true; types_running = true;
Bun.spawn(["bun", "build:types"], { Bun.spawn(["bun", "build:types"], {
stdout: "inherit", stdout: "inherit",
onExit: () => { onExit: () => {
oldConsole.log(c.cyan("[Types]"), c.green("built")); oldConsole.log(c.cyan("[Types]"), c.green("built"));
Bun.spawn(["bun", "tsc-alias"], { Bun.spawn(["bun", "tsc-alias"], {
stdout: "inherit", stdout: "inherit",
onExit: () => { onExit: () => {
oldConsole.log(c.cyan("[Types]"), c.green("aliased")); oldConsole.log(c.cyan("[Types]"), c.green("aliased"));
types_running = false; types_running = false;
}, },
}); });
}, },
}); });
} }
if (types && !watch) { if (types && !watch) {
buildTypes(); buildTypes();
} }
let watcher_timeout: any; let watcher_timeout: any;
function delayTypes() { function delayTypes() {
if (!watch || !types) return; if (!watch || !types) return;
if (watcher_timeout) { if (watcher_timeout) {
clearTimeout(watcher_timeout); clearTimeout(watcher_timeout);
} }
watcher_timeout = setTimeout(buildTypes, 1000); watcher_timeout = setTimeout(buildTypes, 1000);
} }
const dependencies = Object.keys(pkg.dependencies); const dependencies = Object.keys(pkg.dependencies);
// collection of always-external packages // collection of always-external packages
const external = [ const external = [
...dependencies, ...dependencies,
"bun:test", "bun:test",
"node:test", "node:test",
"node:assert/strict", "node:assert/strict",
"@libsql/client", "@libsql/client",
"bknd", "bknd",
/^bknd\/.*/, /^bknd\/.*/,
"jsonv-ts", "jsonv-ts",
/^jsonv-ts\/.*/, /^jsonv-ts\/.*/,
] as const; ] as const;
/** /**
* Building backend and general API * Building backend and general API
*/ */
async function buildApi() { async function buildApi() {
await tsup.build({ await tsup.build({
minify, minify,
sourcemap, sourcemap,
// don't use tsup's broken watch, we'll handle it ourselves // don't use tsup's broken watch, we'll handle it ourselves
watch: false, watch: false,
define, define,
entry: [ entry: [
"src/index.ts", "src/index.ts",
"src/core/utils/index.ts", "src/core/utils/index.ts",
"src/plugins/index.ts", "src/plugins/index.ts",
"src/modes/index.ts", "src/modes/index.ts",
], ],
outDir: "dist", outDir: "dist",
external: [...external], external: [...external],
metafile: true, metafile: true,
target: "esnext", target: "esnext",
platform: "browser", platform: "browser",
removeNodeProtocol: false, removeNodeProtocol: false,
format: ["esm"], format: ["esm"],
splitting: false, splitting: false,
loader: { loader: {
".svg": "dataurl", ".svg": "dataurl",
}, },
onSuccess: async () => { onSuccess: async () => {
delayTypes(); delayTypes();
oldConsole.log(c.cyan("[API]"), c.green("built")); oldConsole.log(c.cyan("[API]"), c.green("built"));
}, },
}); });
} }
async function rewriteClient(path: string) { async function rewriteClient(path: string) {
const bundle = await Bun.file(path).text(); const bundle = await Bun.file(path).text();
await Bun.write(path, '"use client";\n' + bundle.replaceAll("ui/client", "bknd/client")); await Bun.write(
path,
'"use client";\n' + bundle.replaceAll("ui/client", "bknd/client"),
);
} }
/** /**
* Building UI for direct imports * Building UI for direct imports
*/ */
async function buildUi() { async function buildUi() {
const base = { const base = {
minify, minify,
sourcemap, sourcemap,
watch: false, watch: false,
define, define,
external: [ external: [
...external, ...external,
"react", "react",
"react-dom", "react-dom",
"react/jsx-runtime", "react/jsx-runtime",
"react/jsx-dev-runtime", "react/jsx-dev-runtime",
"use-sync-external-store", "use-sync-external-store",
/codemirror/, /codemirror/,
"@xyflow/react", "@xyflow/react",
"@mantine/core", "@mantine/core",
], ],
metafile: true, metafile: true,
platform: "browser", platform: "browser",
format: ["esm"], format: ["esm"],
splitting: false, splitting: false,
bundle: true, bundle: true,
treeshake: true, treeshake: true,
loader: { loader: {
".svg": "dataurl", ".svg": "dataurl",
}, },
esbuildOptions: (options) => { esbuildOptions: (options) => {
options.logLevel = "silent"; options.logLevel = "silent";
}, },
} satisfies tsup.Options; } satisfies tsup.Options;
await tsup.build({ await tsup.build({
...base, ...base,
entry: ["src/ui/index.ts", "src/ui/main.css", "src/ui/styles.css"], entry: ["src/ui/index.ts", "src/ui/main.css", "src/ui/styles.css"],
outDir: "dist/ui", outDir: "dist/ui",
onSuccess: async () => { onSuccess: async () => {
await rewriteClient("./dist/ui/index.js"); await rewriteClient("./dist/ui/index.js");
delayTypes(); delayTypes();
oldConsole.log(c.cyan("[UI]"), c.green("built")); oldConsole.log(c.cyan("[UI]"), c.green("built"));
}, },
}); });
await tsup.build({ await tsup.build({
...base, ...base,
entry: ["src/ui/client/index.ts"], entry: ["src/ui/client/index.ts"],
outDir: "dist/ui/client", outDir: "dist/ui/client",
onSuccess: async () => { onSuccess: async () => {
await rewriteClient("./dist/ui/client/index.js"); await rewriteClient("./dist/ui/client/index.js");
delayTypes(); delayTypes();
oldConsole.log(c.cyan("[UI]"), "Client", c.green("built")); oldConsole.log(c.cyan("[UI]"), "Client", c.green("built"));
}, },
}); });
} }
/** /**
@@ -191,171 +194,195 @@ async function buildUi() {
* - ui/client is external, and after built replaced with "bknd/client" * - ui/client is external, and after built replaced with "bknd/client"
*/ */
async function buildUiElements() { async function buildUiElements() {
await tsup.build({ await tsup.build({
minify, minify,
sourcemap, sourcemap,
watch: false, watch: false,
define, define,
entry: ["src/ui/elements/index.ts"], entry: ["src/ui/elements/index.ts"],
outDir: "dist/ui/elements", outDir: "dist/ui/elements",
external: [ external: [
"ui/client", "ui/client",
"bknd", "bknd",
/^bknd\/.*/, /^bknd\/.*/,
"wouter", "wouter",
"react", "react",
"react-dom", "react-dom",
"react/jsx-runtime", "react/jsx-runtime",
"react/jsx-dev-runtime", "react/jsx-dev-runtime",
"use-sync-external-store", "use-sync-external-store",
], ],
metafile: true, metafile: true,
platform: "browser", platform: "browser",
format: ["esm"], format: ["esm"],
splitting: false, splitting: false,
bundle: true, bundle: true,
treeshake: true, treeshake: true,
loader: { loader: {
".svg": "dataurl", ".svg": "dataurl",
}, },
esbuildOptions: (options) => { esbuildOptions: (options) => {
options.alias = { options.alias = {
// not important for elements, mock to reduce bundle // not important for elements, mock to reduce bundle
"tailwind-merge": "./src/ui/elements/mocks/tailwind-merge.ts", "tailwind-merge": "./src/ui/elements/mocks/tailwind-merge.ts",
}; };
}, },
onSuccess: async () => { onSuccess: async () => {
await rewriteClient("./dist/ui/elements/index.js"); await rewriteClient("./dist/ui/elements/index.js");
delayTypes(); delayTypes();
oldConsole.log(c.cyan("[UI]"), "Elements", c.green("built")); oldConsole.log(c.cyan("[UI]"), "Elements", c.green("built"));
}, },
}); });
} }
/** /**
* Building adapters * Building adapters
*/ */
function baseConfig(adapter: string, overrides: Partial<tsup.Options> = {}): tsup.Options { function baseConfig(
return { adapter: string,
minify, overrides: Partial<tsup.Options> = {},
sourcemap, ): tsup.Options {
watch: false, return {
entry: [`src/adapter/${adapter}/index.ts`], minify,
format: ["esm"], sourcemap,
platform: "neutral", watch: false,
outDir: `dist/adapter/${adapter}`, entry: [`src/adapter/${adapter}/index.ts`],
metafile: true, format: ["esm"],
splitting: false, platform: "neutral",
removeNodeProtocol: false, outDir: `dist/adapter/${adapter}`,
onSuccess: async () => { metafile: true,
delayTypes(); splitting: false,
oldConsole.log(c.cyan("[Adapter]"), adapter || "base", c.green("built")); removeNodeProtocol: false,
}, onSuccess: async () => {
...overrides, delayTypes();
define: { oldConsole.log(c.cyan("[Adapter]"), adapter || "base", c.green("built"));
...define, },
...overrides.define, ...overrides,
}, define: {
external: [ ...define,
/^cloudflare*/, ...overrides.define,
/^@?hono.*?/, },
/^(bknd|react|next|node).*?/, external: [
/.*\.(html)$/, /^cloudflare*/,
...external, /^@?hono.*?/,
...(Array.isArray(overrides.external) ? overrides.external : []), /^(bknd|react|next|node).*?/,
], /.*\.(html)$/,
}; ...external,
...(Array.isArray(overrides.external) ? overrides.external : []),
],
};
} }
async function buildAdapters() { async function buildAdapters() {
await Promise.all([ await Promise.all([
// base adapter handles // base adapter handles
tsup.build({ tsup.build({
...baseConfig(""), ...baseConfig(""),
target: "esnext", target: "esnext",
platform: "neutral", platform: "neutral",
entry: ["src/adapter/index.ts"], entry: ["src/adapter/index.ts"],
outDir: "dist/adapter", outDir: "dist/adapter",
// only way to keep @vite-ignore comments // only way to keep @vite-ignore comments
minify: false, minify: false,
}), }),
// specific adatpers // specific adatpers
tsup.build(baseConfig("react-router")), tsup.build(baseConfig("react-router")),
tsup.build( tsup.build(
baseConfig("browser", { baseConfig("browser", {
external: [/^sqlocal\/?.*?/, "wouter"], external: [/^sqlocal\/?.*?/, "wouter"],
}),
),
tsup.build(
baseConfig("bun", {
external: [/^bun\:.*/],
}),
),
tsup.build(baseConfig("astro")),
tsup.build(baseConfig("aws")),
tsup.build(
baseConfig("cloudflare", {
external: ["wrangler", "node:process"],
}),
),
tsup.build(
baseConfig("cloudflare/proxy", {
target: "esnext",
entry: ["src/adapter/cloudflare/proxy.ts"],
outDir: "dist/adapter/cloudflare",
metafile: false,
external: [/bknd/, "wrangler", "node:process"],
}),
),
tsup.build({
...baseConfig("vite"),
platform: "node",
}), }),
),
tsup.build({ tsup.build(
...baseConfig("nextjs"), baseConfig("bun", {
platform: "node", external: [/^bun\:.*/],
}), }),
),
tsup.build({ tsup.build(baseConfig("astro")),
...baseConfig("sveltekit"), tsup.build(baseConfig("aws")),
platform: "node", tsup.build(
baseConfig("cloudflare", {
external: ["wrangler", "node:process"],
}), }),
),
tsup.build({ tsup.build(
...baseConfig("node"), baseConfig("cloudflare/proxy", {
platform: "node", target: "esnext",
entry: ["src/adapter/cloudflare/proxy.ts"],
outDir: "dist/adapter/cloudflare",
metafile: false,
external: [/bknd/, "wrangler", "node:process"],
}), }),
),
tsup.build({ tsup.build({
...baseConfig("sqlite/edge"), ...baseConfig("vite"),
entry: ["src/adapter/sqlite/edge.ts"], platform: "node",
outDir: "dist/adapter/sqlite", }),
metafile: false,
}),
tsup.build({ tsup.build({
...baseConfig("sqlite/node"), ...baseConfig("nextjs"),
entry: ["src/adapter/sqlite/node.ts"], platform: "node",
outDir: "dist/adapter/sqlite", }),
platform: "node",
metafile: false,
}),
tsup.build({ tsup.build({
...baseConfig("sqlite/bun"), ...baseConfig("tanstack-start"),
entry: ["src/adapter/sqlite/bun.ts"], platform: "node",
outDir: "dist/adapter/sqlite", }),
metafile: false,
external: [/^bun\:.*/], tsup.build({
}), ...baseConfig("sveltekit"),
]); platform: "node",
}),
tsup.build({
...baseConfig("nuxt"),
platform: "node",
}),
tsup.build({
...baseConfig("node"),
platform: "node",
}),
tsup.build({
...baseConfig("solid-start"),
platform: "node",
}),
tsup.build({
...baseConfig("sqlite/edge"),
entry: ["src/adapter/sqlite/edge.ts"],
outDir: "dist/adapter/sqlite",
metafile: false,
}),
tsup.build({
...baseConfig("sqlite/node"),
entry: ["src/adapter/sqlite/node.ts"],
outDir: "dist/adapter/sqlite",
platform: "node",
metafile: false,
}),
tsup.build({
...baseConfig("sqlite/bun"),
entry: ["src/adapter/sqlite/bun.ts"],
outDir: "dist/adapter/sqlite",
metafile: false,
external: [/^bun\:.*/],
}),
]);
} }
async function buildAll() { async function buildAll() {
await Promise.all([buildApi(), buildUi(), buildUiElements(), buildAdapters()]); await Promise.all([
buildApi(),
buildUi(),
buildUiElements(),
buildAdapters(),
]);
} }
// initial build // initial build
@@ -363,39 +390,47 @@ await buildAll();
// custom watcher since tsup's watch is broken in 8.3.5+ // custom watcher since tsup's watch is broken in 8.3.5+
if (watch) { if (watch) {
oldConsole.log(c.cyan("[Watch]"), "watching for changes in src/..."); oldConsole.log(c.cyan("[Watch]"), "watching for changes in src/...");
let debounceTimer: ReturnType<typeof setTimeout> | null = null; let debounceTimer: ReturnType<typeof setTimeout> | null = null;
let isBuilding = false; let isBuilding = false;
const rebuild = async () => { const rebuild = async () => {
if (isBuilding) return; if (isBuilding) return;
isBuilding = true; isBuilding = true;
oldConsole.log(c.cyan("[Watch]"), "rebuilding..."); oldConsole.log(c.cyan("[Watch]"), "rebuilding...");
try { try {
await buildAll(); await buildAll();
oldConsole.log(c.cyan("[Watch]"), c.green("done")); oldConsole.log(c.cyan("[Watch]"), c.green("done"));
} catch (e) { } catch (e) {
oldConsole.warn(c.cyan("[Watch]"), c.red("build failed"), e); oldConsole.warn(c.cyan("[Watch]"), c.red("build failed"), e);
} }
isBuilding = false; isBuilding = false;
}; };
const debouncedRebuild = () => { const debouncedRebuild = () => {
if (debounceTimer) clearTimeout(debounceTimer); if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(rebuild, 100); debounceTimer = setTimeout(rebuild, 100);
}; };
// watch src directory recursively // watch src directory recursively
fsWatch(join(import.meta.dir, "src"), { recursive: true }, (event, filename) => { fsWatch(
join(import.meta.dir, "src"),
{ recursive: true },
(event, filename) => {
if (!filename) return; if (!filename) return;
// ignore non-source files // ignore non-source files
if (!filename.endsWith(".ts") && !filename.endsWith(".tsx") && !filename.endsWith(".css")) if (
return; !filename.endsWith(".ts") &&
!filename.endsWith(".tsx") &&
!filename.endsWith(".css")
)
return;
oldConsole.log(c.cyan("[Watch]"), c.dim(`${event}: ${filename}`)); oldConsole.log(c.cyan("[Watch]"), c.dim(`${event}: ${filename}`));
debouncedRebuild(); debouncedRebuild();
}); },
);
// keep process alive // keep process alive
await new Promise(() => {}); await new Promise(() => {});
} }
+3 -2
View File
@@ -3,10 +3,11 @@ import path from "node:path";
import c from "picocolors"; import c from "picocolors";
const basePath = new URL(import.meta.resolve("../../")).pathname.slice(0, -1); const basePath = new URL(import.meta.resolve("../../")).pathname.slice(0, -1);
type RunOptions = Omit<Bun.SpawnOptions.SpawnOptions<"ignore", "pipe", "pipe">, "stdout" | "stderr">;
async function run( async function run(
cmd: string[] | string, cmd: string[] | string,
opts: Bun.SpawnOptions.OptionsObject & {}, opts: RunOptions,
onChunk: (chunk: string, resolve: (data: any) => void, reject: (err: Error) => void) => void, onChunk: (chunk: string, resolve: (data: any) => void, reject: (err: Error) => void) => void,
): Promise<{ proc: Bun.Subprocess; data: any }> { ): Promise<{ proc: Bun.Subprocess; data: any }> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -17,7 +18,7 @@ async function run(
}); });
// Read from stdout // Read from stdout
const reader = (proc.stdout as ReadableStream).getReader(); const reader = proc.stdout.getReader();
const decoder = new TextDecoder(); const decoder = new TextDecoder();
// Function to read chunks // Function to read chunks
+40 -31
View File
@@ -1,44 +1,53 @@
const adapter = process.env.TEST_ADAPTER; const adapter = process.env.TEST_ADAPTER;
const default_config = { const default_config = {
media_adapter: "local", media_adapter: "local",
base_path: "", base_path: "",
} as const; } as const;
const configs = { const configs = {
cloudflare: { cloudflare: {
media_adapter: "r2", media_adapter: "r2",
}, },
"react-router": { "react-router": {
base_path: "/admin",
},
nextjs: {
base_path: "/admin",
},
nuxt: {
base_path: "/admin",
},
"solid-start": {
base_path: "/admin", base_path: "/admin",
}, },
nextjs: { astro: {
base_path: "/admin", base_path: "/admin",
}, },
astro: { node: {
base_path: "/admin", base_path: "",
}, },
node: { bun: {
base_path: "", base_path: "",
}, },
bun: { "tanstack-start": {
base_path: "", base_path: "/admin",
}, },
}; };
export function getAdapterConfig(): typeof default_config { export function getAdapterConfig(): typeof default_config {
if (adapter) { if (adapter) {
if (!configs[adapter]) { if (!configs[adapter]) {
console.warn( console.warn(
`Adapter "${adapter}" not found. Available adapters: ${Object.keys(configs).join(", ")}`, `Adapter "${adapter}" not found. Available adapters: ${Object.keys(configs).join(", ")}`,
); );
} else { } else {
return { return {
...default_config, ...default_config,
...configs[adapter], ...configs[adapter],
}; };
} }
} }
return default_config; return default_config;
} }
+23 -3
View File
@@ -3,7 +3,7 @@
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,
"bin": "./dist/cli/index.js", "bin": "./dist/cli/index.js",
"version": "0.20.0", "version": "0.21.0-rc.1",
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.", "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", "homepage": "https://bknd.io",
"repository": { "repository": {
@@ -60,7 +60,7 @@
"@xyflow/react": "^12.9.2", "@xyflow/react": "^12.9.2",
"aws4fetch": "^1.0.20", "aws4fetch": "^1.0.20",
"bcryptjs": "^3.0.3", "bcryptjs": "^3.0.3",
"dayjs": "^1.11.19", "dayjs": "^1.11.20",
"fast-xml-parser": "^5.3.1", "fast-xml-parser": "^5.3.1",
"hono": "4.10.4", "hono": "4.10.4",
"json-schema-library": "10.0.0-rc7", "json-schema-library": "10.0.0-rc7",
@@ -233,6 +233,11 @@
"import": "./dist/adapter/nextjs/index.js", "import": "./dist/adapter/nextjs/index.js",
"require": "./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": { "./adapter/react-router": {
"types": "./dist/types/adapter/react-router/index.d.ts", "types": "./dist/types/adapter/react-router/index.d.ts",
"import": "./dist/adapter/react-router/index.js", "import": "./dist/adapter/react-router/index.js",
@@ -258,6 +263,11 @@
"import": "./dist/adapter/sveltekit/index.js", "import": "./dist/adapter/sveltekit/index.js",
"require": "./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": { "./adapter/aws": {
"types": "./dist/types/adapter/aws/index.d.ts", "types": "./dist/types/adapter/aws/index.d.ts",
"import": "./dist/adapter/aws/index.js", "import": "./dist/adapter/aws/index.js",
@@ -268,6 +278,11 @@
"import": "./dist/adapter/browser/index.js", "import": "./dist/adapter/browser/index.js",
"require": "./dist/adapter/browser/index.js" "require": "./dist/adapter/browser/index.js"
}, },
"./adapter/tanstack-start": {
"types": "./dist/types/adapter/tanstack-start/index.d.ts",
"import": "./dist/adapter/tanstack-start/index.js",
"require": "./dist/adapter/tanstack-start/index.js"
},
"./dist/main.css": "./dist/ui/main.css", "./dist/main.css": "./dist/ui/main.css",
"./dist/styles.css": "./dist/ui/styles.css", "./dist/styles.css": "./dist/ui/styles.css",
"./dist/manifest.json": "./dist/static/.vite/manifest.json", "./dist/manifest.json": "./dist/static/.vite/manifest.json",
@@ -282,10 +297,13 @@
"adapter/cloudflare": ["./dist/types/adapter/cloudflare/index.d.ts"], "adapter/cloudflare": ["./dist/types/adapter/cloudflare/index.d.ts"],
"adapter/vite": ["./dist/types/adapter/vite/index.d.ts"], "adapter/vite": ["./dist/types/adapter/vite/index.d.ts"],
"adapter/nextjs": ["./dist/types/adapter/nextjs/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/react-router": ["./dist/types/adapter/react-router/index.d.ts"],
"adapter/bun": ["./dist/types/adapter/bun/index.d.ts"], "adapter/bun": ["./dist/types/adapter/bun/index.d.ts"],
"adapter/node": ["./dist/types/adapter/node/index.d.ts"], "adapter/node": ["./dist/types/adapter/node/index.d.ts"],
"adapter/sveltekit": ["./dist/types/adapter/sveltekit/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"] "adapter/sqlite": ["./dist/types/adapter/sqlite/edge.d.ts"]
} }
}, },
@@ -312,12 +330,14 @@
"serverless", "serverless",
"cloudflare", "cloudflare",
"nextjs", "nextjs",
"nuxt",
"remix", "remix",
"react-router", "react-router",
"astro", "astro",
"sveltekit", "sveltekit",
"svelte", "svelte",
"bun", "bun",
"node" "node",
"solid-start"
] ]
} }
+1 -1
View File
@@ -121,7 +121,7 @@ export class Api {
} }
// try authorization header // 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) { if (headerToken) {
this.token_transport = "header"; this.token_transport = "header";
this.updateToken(headerToken); this.updateToken(headerToken);
+1
View File
@@ -0,0 +1 @@
export * from "./nuxt.adapter";
+15
View File
@@ -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,
});
});
+30
View File
@@ -0,0 +1,30 @@
import { createRuntimeApp, type RuntimeBkndConfig } from "bknd/adapter";
export type NuxtEnv = NodeJS.ProcessEnv;
export type NuxtBkndConfig<Env = NuxtEnv> = RuntimeBkndConfig<Env>;
/**
* Get bknd app instance
* @param config - bknd configuration
* @param args - environment variables
*/
export async function getApp<Env>(
config: NuxtBkndConfig<Env> = {} as NuxtBkndConfig<Env>,
args: Env,
) {
return await createRuntimeApp(config, args);
}
/**
* Create middleware handler for Nuxt
* @param config - bknd configuration
* @param args - environment variables
*/
export function serve<Env>(
config: NuxtBkndConfig<Env> = {} as NuxtBkndConfig<Env>,
args: Env,
) {
return async (request: Request) => {
return (await getApp(config, args)).fetch(request);
};
}
+1
View File
@@ -0,0 +1 @@
export * from "./solid-start.adapter";
@@ -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<SolidStartBkndConfig>(bunTestRunner, {
makeApp: solidStart.getApp,
makeHandler: solidStart.serve,
});
});
@@ -0,0 +1,31 @@
import { createRuntimeApp, type RuntimeBkndConfig } from "bknd/adapter";
export type SolidStartEnv = NodeJS.ProcessEnv;
export type SolidStartBkndConfig<Env = SolidStartEnv> = RuntimeBkndConfig<Env>;
/**
* Get bknd app instance
* @param config - bknd configuration
* @param args - environment variables
*/
export async function getApp<Env = SolidStartEnv>(
config: SolidStartBkndConfig<Env>,
args: Env = process.env as Env,
) {
return await createRuntimeApp(config, args);
}
/**
* Create middleware handler for Solid Start
* @param config - bknd configuration
* @param args - environment variables
*/
export function serve<Env = SolidStartEnv>(
config: SolidStartBkndConfig<Env> = {},
args: Env = process.env as Env,
) {
return async (req: Request) => {
const app = await getApp(config, args);
return app.fetch(req);
};
}
+1
View File
@@ -0,0 +1 @@
export * from "./tanstack-start.adapter";
@@ -0,0 +1,16 @@
import { afterAll, beforeAll, describe } from "bun:test";
import * as tanstackStart from "./tanstack-start.adapter";
import { disableConsoleLog, enableConsoleLog } from "core/utils";
import { adapterTestSuite } from "adapter/adapter-test-suite";
import { bunTestRunner } from "adapter/bun/test";
import type { TanstackStartConfig } from "./tanstack-start.adapter";
beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
describe("tanstack start adapter", () => {
adapterTestSuite<TanstackStartConfig>(bunTestRunner, {
makeApp: tanstackStart.getApp,
makeHandler: tanstackStart.serve,
});
});
@@ -0,0 +1,33 @@
import { createFrameworkApp, type FrameworkBkndConfig } from "bknd/adapter";
export type TanstackStartEnv = NodeJS.ProcessEnv;
export type TanstackStartConfig<Env = TanstackStartEnv> =
FrameworkBkndConfig<Env>;
/**
* Get bknd app instance
* @param config - bknd configuration
* @param args - environment variables
*/
export async function getApp<Env = TanstackStartEnv>(
config: TanstackStartConfig<Env> = {},
args: Env = process.env as Env,
) {
return await createFrameworkApp(config, args);
}
/**
* Create request handler for src/routes/api.$.ts
* @param config - bknd configuration
* @param args - environment variables
*/
export function serve<Env = TanstackStartEnv>(
config: TanstackStartConfig<Env> = {},
args: Env = process.env as Env,
) {
return async (request: Request) => {
const app = await getApp(config, args);
return app.fetch(request);
};
}
+9 -2
View File
@@ -199,7 +199,12 @@ export class AuthController extends Controller {
for (const [name, strategy] of Object.entries(strategies)) { for (const [name, strategy] of Object.entries(strategies)) {
if (!this.auth.isStrategyEnabled(strategy)) continue; if (!this.auth.isStrategyEnabled(strategy)) continue;
hono.route(`/${name}`, strategy.getController(this.auth.authenticator)); hono.route(
`/${name}`,
strategy.getController(this.auth.authenticator, {
allow_register: this.auth.config.allow_register,
}),
);
this.registerStrategyActions(strategy, hono); this.registerStrategyActions(strategy, hono);
} }
@@ -305,7 +310,9 @@ export class AuthController extends Controller {
await c.context.ctx().helper.granted(c, AuthPermissions.testPassword); await c.context.ctx().helper.granted(c, AuthPermissions.testPassword);
const pw = this.auth.authenticator.strategy("password") as PasswordStrategy; const pw = this.auth.authenticator.strategy("password") as PasswordStrategy;
const controller = pw.getController(this.auth.authenticator); const controller = pw.getController(this.auth.authenticator, {
allow_register: this.auth.config.allow_register,
});
const res = await controller.request( const res = await controller.request(
new Request("https://localhost/login", { new Request("https://localhost/login", {
+1 -1
View File
@@ -430,7 +430,7 @@ export class Authenticator<
let token: string | undefined; let token: string | undefined;
if (headers.has("Authorization")) { if (headers.has("Authorization")) {
const bearerHeader = String(headers.get("Authorization")); const bearerHeader = String(headers.get("Authorization"));
token = bearerHeader.replace("Bearer ", ""); token = bearerHeader.replace(/^Bearer\s+/i, "");
} else { } else {
const context = is_context ? (c as Context) : ({ req: { raw: { headers } } } as Context); const context = is_context ? (c as Context) : ({ req: { raw: { headers } } } as Context);
token = await this.getAuthCookie(context); token = await this.getAuthCookie(context);
@@ -87,7 +87,7 @@ export class PasswordStrategy extends AuthStrategy<typeof schema> {
}; };
} }
getController(authenticator: Authenticator): Hono<any> { getController(authenticator: Authenticator, opts: { allow_register?: boolean }): Hono<any> {
const hono = new Hono(); const hono = new Hono();
const redirectQuerySchema = s.object({ const redirectQuerySchema = s.object({
redirect: s.string().optional(), redirect: s.string().optional(),
@@ -120,41 +120,43 @@ export class PasswordStrategy extends AuthStrategy<typeof schema> {
}, },
); );
hono.post( if (opts.allow_register) {
"/register", hono.post(
describeRoute({ "/register",
summary: "Register a new user with email and password", describeRoute({
tags: ["auth"], summary: "Register a new user with email and password",
}), tags: ["auth"],
jsc("query", redirectQuerySchema), }),
async (c) => { jsc("query", redirectQuerySchema),
try { async (c) => {
const { redirect } = c.req.valid("query"); try {
const { password, email, ...body } = parse( const { redirect } = c.req.valid("query");
payloadSchema, const { password, email, ...body } = parse(
await authenticator.getBody(c), payloadSchema,
{ await authenticator.getBody(c),
onError: (errors) => { {
$console.error("Invalid register payload", [...errors]); onError: (errors) => {
new InvalidCredentialsException(); $console.error("Invalid register payload", [...errors]);
new InvalidCredentialsException();
},
}, },
}, );
);
const profile = { const profile = {
...body, ...body,
email, email,
strategy_value: await this.hash(password), strategy_value: await this.hash(password),
}; };
return await authenticator.resolveRegister(c, this, profile, async () => void 0, { return await authenticator.resolveRegister(c, this, profile, async () => void 0, {
redirect, redirect,
}); });
} catch (e) { } catch (e) {
return authenticator.respondWithError(c, e as any); return authenticator.respondWithError(c, e as any);
} }
}, },
); );
}
return hono; return hono;
} }
@@ -36,7 +36,7 @@ export abstract class AuthStrategy<Schema extends s.Schema = s.Schema> {
protected abstract getSchema(): Schema; protected abstract getSchema(): Schema;
abstract getController(auth: Authenticator): Hono; abstract getController(auth: Authenticator, opts: { allow_register?: boolean }): Hono;
getType(): string { getType(): string {
return this.type; return this.type;
@@ -284,7 +284,7 @@ export class OAuthStrategy extends AuthStrategy<typeof schemaProvided> {
} }
} }
getController(auth: Authenticator): Hono<any> { getController(auth: Authenticator, opts: { allow_register?: boolean }): Hono<any> {
const hono = new Hono(); const hono = new Hono();
const secret = "secret"; const secret = "secret";
const cookie_name = "_challenge"; const cookie_name = "_challenge";
@@ -379,6 +379,10 @@ export class OAuthStrategy extends AuthStrategy<typeof schemaProvided> {
return c.notFound(); return c.notFound();
} }
if (action === "register" && !opts.allow_register) {
return c.notFound();
}
const url = new URL(c.req.url); const url = new URL(c.req.url);
const path = url.pathname.replace(`/${action}`, ""); const path = url.pathname.replace(`/${action}`, "");
const redirect_uri = url.origin + path + "/callback"; const redirect_uri = url.origin + path + "/callback";
+33 -32
View File
@@ -21,46 +21,47 @@ export const sync: CliCommand = (program) => {
console.info(""); console.info("");
if (stmts.length === 0) { if (stmts.length === 0) {
console.info(c.yellow("No changes to sync")); console.info(c.yellow("No changes to sync"));
process.exit(0); } else {
} // @todo: currently assuming parameters aren't used
// @todo: currently assuming parameters aren't used const sql = stmts.map((d) => d.sql).join(";\n") + ";";
const sql = stmts.map((d) => d.sql).join(";\n") + ";";
if (options.force) { if (options.force) {
console.info(c.dim("Executing:") + "\n" + c.cyan(sql)); console.info(c.dim("Executing:") + "\n" + c.cyan(sql));
await schema.sync({ force: true, drop: options.drop }); await schema.sync({ force: true, drop: options.drop });
console.info(`\n${c.dim(`Executed ${c.cyan(stmts.length)} statement(s)`)}`); console.info(`\n${c.dim(`Executed ${c.cyan(stmts.length)} statement(s)`)}`);
console.info(`${c.green("Database synced")}`); console.info(`${c.green("Database synced")}`);
} else {
if (options.seed) { if (options.out) {
console.info(c.dim("\nExecuting seed...")); const output = options.sql ? sql : JSON.stringify(stmts, null, 2);
const seed = app.options?.seed; await writeFile(options.out, output);
if (seed) { console.info(`SQL written to ${c.cyan(options.out)}`);
await app.options?.seed?.({
...app.modules.ctx(),
app: app,
});
console.info(c.green("Seed executed"));
} else { } else {
console.info(c.yellow("No seed function provided")); console.info(c.dim("DDL to execute:") + "\n" + c.cyan(sql));
console.info(
c.yellow(
"\nNo statements have been executed. Use --force to perform database syncing operations",
),
);
} }
} }
} else { }
if (options.out) {
const output = options.sql ? sql : JSON.stringify(stmts, null, 2);
await writeFile(options.out, output);
console.info(`SQL written to ${c.cyan(options.out)}`);
} else {
console.info(c.dim("DDL to execute:") + "\n" + c.cyan(sql));
console.info( if (options.seed) {
c.yellow( console.info(c.dim("\nExecuting seed..."));
"\nNo statements have been executed. Use --force to perform database syncing operations", const seed = app.options?.seed;
), if (seed) {
); await seed({
...app.modules.ctx(),
app: app,
});
console.info(c.green("Seed executed"));
} else {
console.info(c.yellow("No seed function provided"));
} }
} }
process.exit(0); process.exit(0);
}); });
}; };
+15 -7
View File
@@ -136,8 +136,10 @@ export class Entity<
.map((field) => (alias ? `${alias}.${field.name} as ${field.name}` : field.name)); .map((field) => (alias ? `${alias}.${field.name} as ${field.name}` : field.name));
} }
getFillableFields(context?: TActionContext, include_virtual?: boolean): Field[] { getFillableFields(context?: "create" | "update", include_virtual?: boolean): Field[] {
return this.getFields(include_virtual).filter((field) => field.isFillable(context)); return this.getFields({ virtual: include_virtual }).filter((field) =>
field.isFillable(context),
);
} }
getRequiredFields(): Field[] { getRequiredFields(): Field[] {
@@ -189,9 +191,15 @@ export class Entity<
return this.fields.findIndex((field) => field.name === name) !== -1; return this.fields.findIndex((field) => field.name === name) !== -1;
} }
getFields(include_virtual: boolean = false): Field[] { getFields({
if (include_virtual) return this.fields; virtual = false,
return this.fields.filter((f) => !f.isVirtual()); 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) { 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) { if (options?.ignoreUnknown !== true) {
const field_names = fields.map((f) => f.name); const field_names = fields.map((f) => f.name);
@@ -275,7 +283,7 @@ export class Entity<
fields = this.getFillableFields(options.context); fields = this.getFillableFields(options.context);
break; break;
default: default:
fields = this.getFields(true); fields = this.getFields({ virtual: true });
} }
const _fields = Object.fromEntries(fields.map((field) => [field.name, field])); const _fields = Object.fromEntries(fields.map((field) => [field.name, field]));
+4 -2
View File
@@ -83,8 +83,10 @@ export class Mutator<
} }
// we should never get here, but just to be sure (why?) // we should never get here, but just to be sure (why?)
if (!field.isFillable(context)) { if (!field.isFillable(context as any)) {
throw new Error(`Field "${key}" is not fillable on entity "${entity.name}"`); throw new Error(
`Field "${key}" of entity "${entity.name}" is not fillable on context "${context}"`,
);
} }
// transform from field // transform from field
@@ -39,6 +39,7 @@ export class WithBuilder {
if (query) { if (query) {
subQuery = em.repo(other.entity).addOptionsToQueryBuilder(subQuery, query as any, { subQuery = em.repo(other.entity).addOptionsToQueryBuilder(subQuery, query as any, {
ignore: ["with", cardinality === 1 ? "limit" : undefined].filter(Boolean) as any, ignore: ["with", cardinality === 1 ? "limit" : undefined].filter(Boolean) as any,
alias: other.reference,
}); });
} }
+15 -7
View File
@@ -26,11 +26,19 @@ export const baseFieldConfigSchema = s
.strictObject({ .strictObject({
label: s.string(), label: s.string(),
description: s.string(), description: s.string(),
required: s.boolean({ default: false }), required: s.boolean({ default: DEFAULT_REQUIRED }),
fillable: s.anyOf([ fillable: s.anyOf(
s.boolean({ title: "Boolean" }), [
s.array(s.string({ enum: ActionContext }), { title: "Context", uniqueItems: true }), s.boolean({ title: "Boolean" }),
]), s.array(s.string({ enum: ["create", "update"] }), {
title: "Context",
uniqueItems: true,
}),
],
{
default: DEFAULT_FILLABLE,
},
),
hidden: s.anyOf([ hidden: s.anyOf([
s.boolean({ title: "Boolean" }), s.boolean({ title: "Boolean" }),
// @todo: tmp workaround // @todo: tmp workaround
@@ -103,7 +111,7 @@ export abstract class Field<
return this.config?.default_value; return this.config?.default_value;
} }
isFillable(context?: TActionContext): boolean { isFillable(context?: "create" | "update"): boolean {
if (Array.isArray(this.config.fillable)) { if (Array.isArray(this.config.fillable)) {
return context ? this.config.fillable.includes(context) : DEFAULT_FILLABLE; return context ? this.config.fillable.includes(context) : DEFAULT_FILLABLE;
} }
@@ -165,7 +173,7 @@ export abstract class Field<
// @todo: add field level validation // @todo: add field level validation
isValid(value: any, context: TActionContext): boolean { isValid(value: any, context: TActionContext): boolean {
if (typeof value !== "undefined") { if (typeof value !== "undefined") {
return this.isFillable(context); return this.isFillable(context as any);
} else if (context === "create") { } else if (context === "create") {
return !this.isRequired(); return !this.isRequired();
} }
+2 -5
View File
@@ -99,6 +99,7 @@ export function fieldTestSuite(
const _config = { const _config = {
..._requiredConfig, ..._requiredConfig,
required: false, required: false,
fillable: true,
}; };
function fieldJson(field: Field) { function fieldJson(field: Field) {
@@ -116,10 +117,7 @@ export function fieldTestSuite(
expect(fieldJson(fillable)).toEqual({ expect(fieldJson(fillable)).toEqual({
type: noConfigField.type, type: noConfigField.type,
config: { config: _config,
..._config,
fillable: true,
},
}); });
expect(fieldJson(required)).toEqual({ expect(fieldJson(required)).toEqual({
@@ -150,7 +148,6 @@ export function fieldTestSuite(
type: requiredAndDefault.type, type: requiredAndDefault.type,
config: { config: {
..._config, ..._config,
fillable: true,
required: true, required: true,
default_value: config.defaultValue, default_value: config.defaultValue,
}, },
+2 -1
View File
@@ -24,7 +24,8 @@ export function getChangeSet(
(acc, _value, key) => { (acc, _value, key) => {
const field = fields.find((f) => f.name === key); const field = fields.find((f) => f.name === key);
// @todo: filtering virtual here, need to check (because of media) // @todo: filtering virtual here, need to check (because of media)
if (!field || field.isVirtual()) return; // @note: adding non-fillable here as it'll fail validaiton on backend
if (!field || field.isVirtual() || !field.isFillable()) return;
const value = _value === "" ? null : _value; const value = _value === "" ? null : _value;
// normalize to null if undefined // normalize to null if undefined
+1 -1
View File
@@ -77,7 +77,7 @@ export class SchemaManager {
} }
getIntrospectionFromEntity(entity: Entity): IntrospectedTable { getIntrospectionFromEntity(entity: Entity): IntrospectedTable {
const fields = entity.getFields(false); const fields = entity.getFields({ virtual: false });
const indices = this.em.getIndicesOf(entity); const indices = this.em.getIndicesOf(entity);
// this is intentionally setting values to defaults, like "nullable" and "default" // this is intentionally setting values to defaults, like "nullable" and "default"
+24
View File
@@ -1,6 +1,7 @@
import { transformObject } from "bknd/utils"; import { transformObject } from "bknd/utils";
import type { Kysely } from "kysely"; import type { Kysely } from "kysely";
import { set } from "lodash-es"; import { set } from "lodash-es";
import type { InitialModuleConfigs } from "modules/ModuleManager";
export type MigrationContext = { export type MigrationContext = {
db: Kysely<any>; db: Kysely<any>;
@@ -107,6 +108,29 @@ export const migrations: Migration[] = [
return config; 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; export const CURRENT_VERSION = migrations[migrations.length - 1]?.version ?? 0;
+7 -5
View File
@@ -11,6 +11,7 @@ import { css, Style } from "hono/css";
import { Controller } from "modules/Controller"; import { Controller } from "modules/Controller";
import * as SystemPermissions from "modules/permissions"; import * as SystemPermissions from "modules/permissions";
import type { TApiUser } from "Api"; import type { TApiUser } from "Api";
import type { AppTheme } from "ui/client/use-theme";
import type { Manifest } from "vite"; import type { Manifest } from "vite";
const htmlBkndContextReplace = "<!-- BKND_CONTEXT -->"; const htmlBkndContextReplace = "<!-- BKND_CONTEXT -->";
@@ -20,7 +21,7 @@ export type AdminBkndWindowContext = {
logout_route: string; logout_route: string;
admin_basepath: string; admin_basepath: string;
logo_return_path?: string; logo_return_path?: string;
theme?: "dark" | "light" | "system"; theme?: AppTheme;
}; };
// @todo: add migration to remove admin path from config // @todo: add migration to remove admin path from config
@@ -31,7 +32,7 @@ export type AdminControllerOptions = {
html?: string; html?: string;
forceDev?: boolean | { mainPath: string }; forceDev?: boolean | { mainPath: string };
debugRerenders?: boolean; debugRerenders?: boolean;
theme?: "dark" | "light" | "system"; theme?: AppTheme;
logoReturnPath?: string; logoReturnPath?: string;
manifest?: Manifest; manifest?: Manifest;
}; };
@@ -122,7 +123,7 @@ export class AdminController extends Controller {
const obj: AdminBkndWindowContext = { const obj: AdminBkndWindowContext = {
user: c.get("auth")?.user, user: c.get("auth")?.user,
logout_route: authRoutes.logout, logout_route: authRoutes.logout,
admin_basepath: this.options.adminBasepath, admin_basepath: this.options.adminBasepath.replace(/\/+$/, ""),
theme: this.options.theme, theme: this.options.theme,
logo_return_path: this.options.logoReturnPath, logo_return_path: this.options.logoReturnPath,
}; };
@@ -209,8 +210,9 @@ export class AdminController extends Controller {
}, },
}).then((res) => res.json()); }).then((res) => res.json());
} else { } else {
const manifestPath = "bknd/dist/manifest.json";
// @ts-ignore // @ts-ignore
manifest = await import("bknd/dist/manifest.json", { manifest = await import(/* @vite-ignore */ manifestPath, {
with: { type: "json" }, with: { type: "json" },
}).then((res) => res.default); }).then((res) => res.default);
} }
@@ -304,7 +306,7 @@ const wrapperStyle = css`
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
color: rgb(9,9,11); color: rgb(9,9,11);
background-color: rgb(250,250,250); background-color: rgb(250,250,250);
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
color: rgb(250,250,250); color: rgb(250,250,250);
background-color: rgb(30,31,34); background-color: rgb(30,31,34);
+858
View File
@@ -0,0 +1,858 @@
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
import { sort } from "./sort.plugin";
import { em, entity, text, number } from "bknd";
import { createApp } from "core/test/utils";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
beforeAll(() => disableConsoleLog());
afterAll(enableConsoleLog);
describe("sort plugin", () => {
test("should add sort field to configured entities", async () => {
const app = createApp({
config: {
data: em({
tasks: entity("tasks", {
title: text(),
}),
}).toJSON(),
},
options: {
plugins: [
sort({
entities: {
tasks: {
field: "position",
},
},
}),
],
},
});
await app.build();
const taskEntity = app.em.entity("tasks");
expect(taskEntity).toBeDefined();
expect(taskEntity?.fields.map((f) => f.name)).toContain("position");
expect(taskEntity?.field("position")?.type).toBe("number");
});
test("should auto-assign sort values on insert (starting from 0)", async () => {
const app = createApp({
config: {
data: em({
tasks: entity("tasks", {
title: text(),
}),
}).toJSON(),
},
options: {
plugins: [
sort({
entities: {
tasks: {
field: "position",
},
},
}),
],
},
});
await app.build();
const mutator = app.em.mutator("tasks");
// insert first item
const { data: task1 } = await mutator.insertOne({ title: "Task 1" });
expect(task1.position).toBe(0);
// insert second item
const { data: task2 } = await mutator.insertOne({ title: "Task 2" });
expect(task2.position).toBe(1);
// insert third item
const { data: task3 } = await mutator.insertOne({ title: "Task 3" });
expect(task3.position).toBe(2);
});
test("should preserve manually set sort values on insert", async () => {
const app = createApp({
config: {
data: em({
tasks: entity("tasks", {
title: text(),
}),
}).toJSON(),
},
options: {
plugins: [
sort({
entities: {
tasks: {
field: "position",
},
},
}),
],
},
});
await app.build();
const mutator = app.em.mutator("tasks");
// insert with explicit position
const { data: task } = await mutator.insertOne({ title: "Task 1", position: 10 });
expect(task.position).toBe(10);
});
test("should reorder items via API endpoint", async () => {
const app = createApp({
config: {
data: em({
tasks: entity("tasks", {
title: text(),
}),
}).toJSON(),
},
options: {
plugins: [
sort({
entities: {
tasks: {
field: "position",
},
},
}),
],
},
});
await app.build();
const mutator = app.em.mutator("tasks");
// create tasks at positions 0, 1, 2
const { data: task1 } = await mutator.insertOne({ title: "Task 1" });
const { data: task2 } = await mutator.insertOne({ title: "Task 2" });
const { data: task3 } = await mutator.insertOne({ title: "Task 3" });
// move task3 (position 2) to position 0
const res = await app.server.request("/api/sort/tasks/reorder", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ id: task3.id, position: 0 }),
});
expect(res.status).toBe(200);
// verify positions
const repo = app.em.repo("tasks");
const { data: updatedTask1 } = await repo.findOne({ id: task1.id });
const { data: updatedTask2 } = await repo.findOne({ id: task2.id });
const { data: updatedTask3 } = await repo.findOne({ id: task3.id });
expect(updatedTask3.position).toBe(0); // moved to position 0
expect(updatedTask1.position).toBe(1); // shifted down
expect(updatedTask2.position).toBe(2); // shifted down
});
test("should automatically reorder when updating sort field directly", async () => {
const app = createApp({
config: {
data: em({
tasks: entity("tasks", {
title: text(),
}),
}).toJSON(),
},
options: {
plugins: [
sort({
entities: {
tasks: {
field: "position",
},
},
}),
],
},
});
await app.build();
const mutator = app.em.mutator("tasks");
// create tasks at positions 0, 1, 2, 3
const { data: task1 } = await mutator.insertOne({ title: "Task 1" });
const { data: task2 } = await mutator.insertOne({ title: "Task 2" });
const { data: task3 } = await mutator.insertOne({ title: "Task 3" });
const { data: task4 } = await mutator.insertOne({ title: "Task 4" });
// move task4 (position 3) to position 1 by updating directly
await mutator.updateOne(task4.id, { position: 1 });
// verify positions
const repo = app.em.repo("tasks");
const { data: updatedTask1 } = await repo.findOne({ id: task1.id });
const { data: updatedTask2 } = await repo.findOne({ id: task2.id });
const { data: updatedTask3 } = await repo.findOne({ id: task3.id });
const { data: updatedTask4 } = await repo.findOne({ id: task4.id });
expect(updatedTask1.position).toBe(0); // unchanged
expect(updatedTask2.position).toBe(2); // shifted down
expect(updatedTask3.position).toBe(3); // shifted down
expect(updatedTask4.position).toBe(1); // moved to position 1
});
test("should automatically reorder when moving items up", async () => {
const app = createApp({
config: {
data: em({
tasks: entity("tasks", {
title: text(),
}),
}).toJSON(),
},
options: {
plugins: [
sort({
entities: {
tasks: {
field: "position",
},
},
}),
],
},
});
await app.build();
const mutator = app.em.mutator("tasks");
// create tasks at positions 0, 1, 2, 3
const { data: task1 } = await mutator.insertOne({ title: "Task 1" });
const { data: task2 } = await mutator.insertOne({ title: "Task 2" });
const { data: task3 } = await mutator.insertOne({ title: "Task 3" });
const { data: task4 } = await mutator.insertOne({ title: "Task 4" });
// move task1 (position 0) to position 2 by updating directly
await mutator.updateOne(task1.id, { position: 2 });
// verify positions
const repo = app.em.repo("tasks");
const { data: updatedTask1 } = await repo.findOne({ id: task1.id });
const { data: updatedTask2 } = await repo.findOne({ id: task2.id });
const { data: updatedTask3 } = await repo.findOne({ id: task3.id });
const { data: updatedTask4 } = await repo.findOne({ id: task4.id });
expect(updatedTask1.position).toBe(2); // moved to position 2
expect(updatedTask2.position).toBe(0); // shifted up
expect(updatedTask3.position).toBe(1); // shifted up
expect(updatedTask4.position).toBe(3); // unchanged
});
test("should support scoped sorting", async () => {
const app = createApp({
config: {
data: em({
tasks: entity("tasks", {
title: text(),
project_id: number(),
}),
}).toJSON(),
},
options: {
plugins: [
sort({
entities: {
tasks: {
field: "position",
scope: "project_id",
},
},
}),
],
},
});
await app.build();
const mutator = app.em.mutator("tasks");
// create tasks in project 1
const { data: p1t1 } = await mutator.insertOne({
title: "P1 Task 1",
project_id: 1,
});
const { data: p1t2 } = await mutator.insertOne({
title: "P1 Task 2",
project_id: 1,
});
// create tasks in project 2
const { data: p2t1 } = await mutator.insertOne({
title: "P2 Task 1",
project_id: 2,
});
const { data: p2t2 } = await mutator.insertOne({
title: "P2 Task 2",
project_id: 2,
});
// positions should be scoped per project
expect(p1t1.position).toBe(0);
expect(p1t2.position).toBe(1);
expect(p2t1.position).toBe(0); // resets for new scope
expect(p2t2.position).toBe(1);
});
test("should reorder only within scope", async () => {
const app = createApp({
config: {
data: em({
tasks: entity("tasks", {
title: text(),
project_id: number(),
}),
}).toJSON(),
},
options: {
plugins: [
sort({
entities: {
tasks: {
field: "position",
scope: "project_id",
},
},
}),
],
},
});
await app.build();
const mutator = app.em.mutator("tasks");
// create tasks in project 1
const { data: p1t1 } = await mutator.insertOne({
title: "P1 Task 1",
project_id: 1,
});
const { data: p1t2 } = await mutator.insertOne({
title: "P1 Task 2",
project_id: 1,
});
const { data: p1t3 } = await mutator.insertOne({
title: "P1 Task 3",
project_id: 1,
});
// create tasks in project 2
const { data: p2t1 } = await mutator.insertOne({
title: "P2 Task 1",
project_id: 2,
});
const { data: p2t2 } = await mutator.insertOne({
title: "P2 Task 2",
project_id: 2,
});
// move p1t3 to position 0 (should only affect project 1)
await app.server.request("/api/sort/tasks/reorder", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ id: p1t3.id, position: 0 }),
});
// verify project 1 tasks
const repo = app.em.repo("tasks");
const { data: updatedP1t1 } = await repo.findOne({ id: p1t1.id });
const { data: updatedP1t2 } = await repo.findOne({ id: p1t2.id });
const { data: updatedP1t3 } = await repo.findOne({ id: p1t3.id });
expect(updatedP1t3.position).toBe(0); // moved to position 0
expect(updatedP1t1.position).toBe(1); // shifted
expect(updatedP1t2.position).toBe(2); // shifted
// verify project 2 tasks are unchanged
const { data: updatedP2t1 } = await repo.findOne({ id: p2t1.id });
const { data: updatedP2t2 } = await repo.findOne({ id: p2t2.id });
expect(updatedP2t1.position).toBe(0); // unchanged
expect(updatedP2t2.position).toBe(1); // unchanged
});
test("should recalculate all positions", async () => {
const app = createApp({
config: {
data: em({
tasks: entity("tasks", {
title: text(),
}),
}).toJSON(),
},
options: {
plugins: [
sort({
entities: {
tasks: {
field: "position",
},
},
}),
],
},
});
await app.build();
const mutator = app.em.mutator("tasks");
// create tasks with irregular positions
await mutator.insertOne({ title: "Task 1", position: 5 });
await mutator.insertOne({ title: "Task 2", position: 10 });
await mutator.insertOne({ title: "Task 3", position: 15 });
await mutator.insertOne({ title: "Task 4", position: 100 });
// recalculate
const res = await app.server.request("/api/sort/tasks/recalculate", {
method: "POST",
body: JSON.stringify({}),
headers: {
"Content-Type": "application/json",
},
});
expect(res.status).toBe(200);
// verify positions are now 0, 1, 2, 3
const { data: tasks } = await app.em.repo("tasks").findMany({
orderBy: [{ position: "asc" }],
});
expect(tasks.length).toBe(4);
expect(tasks[0].position).toBe(0);
expect(tasks[1].position).toBe(1);
expect(tasks[2].position).toBe(2);
expect(tasks[3].position).toBe(3);
});
test("should recalculate positions within scope only", async () => {
const app = createApp({
config: {
data: em({
tasks: entity("tasks", {
title: text(),
project_id: number(),
}),
}).toJSON(),
},
options: {
plugins: [
sort({
entities: {
tasks: {
field: "position",
scope: "project_id",
},
},
}),
],
},
});
await app.build();
const mutator = app.em.mutator("tasks");
// create tasks in project 1 with irregular positions
await mutator.insertOne({ title: "P1 Task 1", project_id: 1, position: 5 });
await mutator.insertOne({ title: "P1 Task 2", project_id: 1, position: 15 });
// create tasks in project 2 with irregular positions
await mutator.insertOne({ title: "P2 Task 1", project_id: 2, position: 10 });
await mutator.insertOne({ title: "P2 Task 2", project_id: 2, position: 20 });
// recalculate only project 1
const res = await app.server.request("/api/sort/tasks/recalculate", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ scope: 1 }),
});
expect(res.status).toBe(200);
// verify project 1 tasks are recalculated
const { data: p1Tasks } = await app.em.repo("tasks").findMany({
where: { project_id: 1 },
orderBy: [{ position: "asc" }],
});
expect(p1Tasks.length).toBe(2);
expect(p1Tasks[0].position).toBe(0);
expect(p1Tasks[1].position).toBe(1);
// verify project 2 tasks are unchanged
const { data: p2Tasks } = await app.em.repo("tasks").findMany({
where: { project_id: 2 },
orderBy: [{ position: "asc" }],
});
expect(p2Tasks.length).toBe(2);
expect(p2Tasks[0].position).toBe(10); // unchanged
expect(p2Tasks[1].position).toBe(20); // unchanged
});
test("should handle moving items to the end", async () => {
const app = createApp({
config: {
data: em({
tasks: entity("tasks", {
title: text(),
}),
}).toJSON(),
},
options: {
plugins: [
sort({
entities: {
tasks: {
field: "position",
},
},
}),
],
},
});
await app.build();
const mutator = app.em.mutator("tasks");
// create tasks at positions 0, 1, 2, 3
const { data: task1 } = await mutator.insertOne({ title: "Task 1" });
const { data: task2 } = await mutator.insertOne({ title: "Task 2" });
const { data: task3 } = await mutator.insertOne({ title: "Task 3" });
const { data: task4 } = await mutator.insertOne({ title: "Task 4" });
// move task1 to the end (position 3)
await app.server.request("/api/sort/tasks/reorder", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ id: task1.id, position: 3 }),
});
// verify positions
const repo = app.em.repo("tasks");
const { data: updatedTask1 } = await repo.findOne({ id: task1.id });
const { data: updatedTask2 } = await repo.findOne({ id: task2.id });
const { data: updatedTask3 } = await repo.findOne({ id: task3.id });
const { data: updatedTask4 } = await repo.findOne({ id: task4.id });
expect(updatedTask1.position).toBe(3); // moved to end
expect(updatedTask2.position).toBe(0); // shifted up
expect(updatedTask3.position).toBe(1); // shifted up
expect(updatedTask4.position).toBe(2); // shifted up
});
test("should return 400 for invalid item id", async () => {
const app = createApp({
config: {
data: em({
tasks: entity("tasks", {
title: text(),
}),
}).toJSON(),
},
options: {
plugins: [
sort({
entities: {
tasks: {
field: "position",
},
},
}),
],
},
});
await app.build();
const res = await app.server.request("/api/sort/tasks/reorder", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ id: 999999, position: 0 }),
});
expect(res.status).toBe(400);
});
test("should handle multiple entities with different configurations", async () => {
const app = createApp({
config: {
data: em({
tasks: entity("tasks", {
title: text(),
}),
categories: entity("categories", {
name: text(),
}),
}).toJSON(),
},
options: {
plugins: [
sort({
entities: {
tasks: {
field: "position",
},
categories: {
field: "order",
},
},
}),
],
},
});
await app.build();
// verify both entities have their sort fields
const taskEntity = app.em.entity("tasks");
const categoryEntity = app.em.entity("categories");
expect(taskEntity?.fields.map((f) => f.name)).toContain("position");
expect(categoryEntity?.fields.map((f) => f.name)).toContain("order");
// create items in both entities
const { data: task } = await app.em.mutator("tasks").insertOne({ title: "Task 1" });
const { data: category } = await app.em.mutator("categories").insertOne({ name: "Cat 1" });
expect(task.position).toBe(0);
expect(category.order).toBe(0);
// verify both endpoints exist
const taskRes = await app.server.request("/api/sort/tasks/recalculate", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
const catRes = await app.server.request("/api/sort/categories/recalculate", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
expect(taskRes.status).toBe(200);
expect(catRes.status).toBe(200);
});
test("should not trigger reorder when updating other fields", async () => {
const app = createApp({
config: {
data: em({
tasks: entity("tasks", {
title: text(),
}),
}).toJSON(),
},
options: {
plugins: [
sort({
entities: {
tasks: {
field: "position",
},
},
}),
],
},
});
await app.build();
const mutator = app.em.mutator("tasks");
// create tasks
const { data: task1 } = await mutator.insertOne({ title: "Task 1" });
const { data: task2 } = await mutator.insertOne({ title: "Task 2" });
const { data: task3 } = await mutator.insertOne({ title: "Task 3" });
// update title only (should not trigger reordering)
await mutator.updateOne(task2.id, { title: "Task 2 Updated" });
// verify positions are unchanged
const repo = app.em.repo("tasks");
const { data: updatedTask1 } = await repo.findOne({ id: task1.id });
const { data: updatedTask2 } = await repo.findOne({ id: task2.id });
const { data: updatedTask3 } = await repo.findOne({ id: task3.id });
expect(updatedTask1.position).toBe(0);
expect(updatedTask2.position).toBe(1);
expect(updatedTask2.title).toBe("Task 2 Updated");
expect(updatedTask3.position).toBe(2);
});
test("should handle null sort values", async () => {
const app = createApp({
config: {
data: em({
tasks: entity("tasks", {
title: text(),
}),
}).toJSON(),
},
options: {
plugins: [
sort({
entities: {
tasks: {
field: "position",
},
},
}),
],
},
});
await app.build();
const mutator = app.em.mutator("tasks");
// create task with null position (bypass listener by using kysely directly)
await app.connection.kysely
.insertInto("tasks")
.values({ title: "Task with null", position: null })
.execute();
// create normal task
await mutator.insertOne({ title: "Task 2" });
// update the null task to have a position
const nullTask = await app.connection.kysely
.selectFrom("tasks")
.selectAll()
.where("title", "=", "Task with null")
.executeTakeFirst();
await mutator.updateOne(nullTask!.id, { position: 0 });
// verify both tasks have proper positions
const { data: tasks } = await app.em.repo("tasks").findMany({
sort: { by: "position", dir: "asc" },
});
expect(tasks.length).toBe(2);
expect(tasks[0].position).toBe(0);
expect(tasks[1].position).toBe(1);
});
test("should not create duplicates when moving to an occupied position", async () => {
const app = createApp({
config: {
data: em({
tasks: entity("tasks", {
title: text(),
}),
}).toJSON(),
},
options: {
plugins: [
sort({
entities: {
tasks: {
field: "position",
},
},
}),
],
},
});
await app.build();
const mutator = app.em.mutator("tasks");
// create two tasks at positions 0 and 1
const { data: task1 } = await mutator.insertOne({ title: "Task 1" });
const { data: task2 } = await mutator.insertOne({ title: "Task 2" });
expect(task1.position).toBe(0);
expect(task2.position).toBe(1);
// move task2 (at position 1) to position 0
await mutator.updateOne(task2.id, { position: 0 });
// verify no duplicates
const { data: tasks } = await app.em.repo("tasks").findMany({
sort: { by: "position", dir: "asc" },
});
expect(tasks.length).toBe(2);
expect(tasks[0].id).toBe(task2.id);
expect(tasks[0].position).toBe(0);
expect(tasks[1].id).toBe(task1.id);
expect(tasks[1].position).toBe(1);
// verify no tasks have the same position
const positions = tasks.map((t) => t.position);
const uniquePositions = new Set(positions);
expect(uniquePositions.size).toBe(positions.length);
});
test("should preserve order when recalculating", async () => {
const app = createApp({
config: {
data: em({
tasks: entity("tasks", {
title: text(),
}),
}).toJSON(),
},
options: {
plugins: [
sort({
entities: {
tasks: {
field: "position",
},
},
}),
],
},
});
await app.build();
const mutator = app.em.mutator("tasks");
// create tasks with specific positions
const { data: taskA } = await mutator.insertOne({ title: "Task A", position: 5 });
const { data: taskB } = await mutator.insertOne({ title: "Task B", position: 3 });
const { data: taskC } = await mutator.insertOne({ title: "Task C", position: 10 });
// recalculate
await app.server.request("/api/sort/tasks/recalculate", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
// verify order is preserved (B, A, C based on original positions)
const { data: tasks } = await app.em.repo("tasks").findMany({
sort: { by: "position", dir: "asc" },
});
expect(tasks[0].id).toBe(taskB.id); // was at 3, now at 0
expect(tasks[0].position).toBe(0);
expect(tasks[1].id).toBe(taskA.id); // was at 5, now at 1
expect(tasks[1].position).toBe(1);
expect(tasks[2].id).toBe(taskC.id); // was at 10, now at 2
expect(tasks[2].position).toBe(2);
});
});
+423
View File
@@ -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<number>("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<number>(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",
},
);
}
@@ -71,4 +71,54 @@ describe("timestamps plugin", () => {
expect(updatedData.updated_at).toBeDefined(); expect(updatedData.updated_at).toBeDefined();
expect(updatedData.updated_at).toBeInstanceOf(Date); expect(updatedData.updated_at).toBeInstanceOf(Date);
}); });
test("index strategy", async () => {
const app = createApp({
config: {
data: em({
posts: entity("posts", {
title: text(),
}),
}).toJSON(),
},
options: {
plugins: [timestamps({ entities: ["posts"] })],
},
});
await app.build();
expect(app.em.indices.length).toBe(0);
{
const app = createApp({
config: {
data: em({
posts: entity("posts", {
title: text(),
}),
}).toJSON(),
},
options: {
plugins: [timestamps({ entities: ["posts"], indexStrategy: "composite" })],
},
});
await app.build();
expect(app.em.indices.length).toBe(1);
}
{
const app = createApp({
config: {
data: em({
posts: entity("posts", {
title: text(),
}),
}).toJSON(),
},
options: {
plugins: [timestamps({ entities: ["posts"], indexStrategy: "individual" })],
},
});
await app.build();
expect(app.em.indices.length).toBe(2);
}
});
}); });
+27 -9
View File
@@ -4,6 +4,7 @@ import { $console } from "bknd/utils";
export type TimestampsPluginOptions = { export type TimestampsPluginOptions = {
entities: string[]; entities: string[];
setUpdatedOnCreate?: boolean; setUpdatedOnCreate?: boolean;
indexStrategy?: "composite" | "individual";
}; };
/** /**
@@ -19,6 +20,7 @@ export type TimestampsPluginOptions = {
export function timestamps({ export function timestamps({
entities = [], entities = [],
setUpdatedOnCreate = true, setUpdatedOnCreate = true,
indexStrategy,
}: TimestampsPluginOptions): AppPlugin { }: TimestampsPluginOptions): AppPlugin {
return (app: App) => ({ return (app: App) => ({
name: "timestamps", name: "timestamps",
@@ -29,19 +31,35 @@ export function timestamps({
} }
const appEntities = app.em.entities.map((e) => e.name); const appEntities = app.em.entities.map((e) => e.name);
const actualEntities = entities.filter((e) => appEntities.includes(e));
return em( return em(
Object.fromEntries( Object.fromEntries(
entities actualEntities.map((e) => [
.filter((e) => appEntities.includes(e)) e,
.map((e) => [ entity(e, {
e, created_at: datetime(),
entity(e, { updated_at: datetime(),
created_at: datetime(), }),
updated_at: datetime(), ]),
}),
]),
), ),
(fns, schema) => {
if (indexStrategy) {
for (const entity of actualEntities) {
if (entity in schema) {
switch (indexStrategy) {
case "composite":
fns.index(schema[entity]!).on(["created_at", "updated_at"]);
break;
case "individual":
fns.index(schema[entity]!).on(["created_at"]);
fns.index(schema[entity]!).on(["updated_at"]);
break;
}
}
}
}
},
); );
}, },
onBuilt: async () => { onBuilt: async () => {
+1
View File
@@ -9,3 +9,4 @@ export { syncTypes, type SyncTypesOptions } from "./dev/sync-types.plugin";
export { syncSecrets, type SyncSecretsOptions } from "./dev/sync-secrets.plugin"; export { syncSecrets, type SyncSecretsOptions } from "./dev/sync-secrets.plugin";
export { timestamps, type TimestampsPluginOptions } from "./data/timestamps.plugin"; export { timestamps, type TimestampsPluginOptions } from "./data/timestamps.plugin";
export { emailOTP, type EmailOTPPluginOptions } from "./auth/email-otp.plugin"; export { emailOTP, type EmailOTPPluginOptions } from "./auth/email-otp.plugin";
export { sort, type SortPluginOptions } from "./data/sort.plugin";
+5
View File
@@ -16,6 +16,11 @@ export type BkndAdminConfig = {
* @default `/` * @default `/`
*/ */
basepath?: string; basepath?: string;
/**
* Sub-path for the Admin UI within the base path
* @default ``
*/
admin_basepath?: string;
/** /**
* Path to return to when clicking the logo * Path to return to when clicking the logo
* @default `/` * @default `/`
+12 -6
View File
@@ -22,7 +22,6 @@ export class AppReduced {
protected appJson: AppType, protected appJson: AppType,
protected _options: BkndAdminProps["config"] = {}, protected _options: BkndAdminProps["config"] = {},
) { ) {
//console.log("received appjson", appJson);
this._entities = Object.entries(this.appJson.data.entities ?? {}).map(([name, entity]) => { this._entities = Object.entries(this.appJson.data.entities ?? {}).map(([name, entity]) => {
return constructEntity(name, entity); return constructEntity(name, entity);
@@ -70,20 +69,27 @@ export class AppReduced {
get options() { get options() {
return { return {
basepath: "", basepath: "/",
logo_return_path: "/", logo_return_path: "/",
...this._options, ...this._options,
}; };
} }
withBasePath(path: string | string[], absolute = false): string {
const paths = Array.isArray(path) ? path : [path];
return [absolute ? "~" : null, this.options.basepath, ...paths]
.filter(Boolean)
.join("/")
.replace(/\/+/g, "/")
.replace(/\/$/, "");
}
getSettingsPath(path: string[] = []): string { getSettingsPath(path: string[] = []): string {
const base = `~/${this.options.basepath}/settings`.replace(/\/+/g, "/"); return this.withBasePath(["settings", ...path], true);
return [base, ...path].join("/");
} }
getAbsolutePath(path?: string): string { getAbsolutePath(path?: string): string {
const { basepath } = this.options; return this.withBasePath(path ?? [], true);
return (path ? `~/${basepath}/${path}` : `~/${basepath}`).replace(/\/+/g, "/");
} }
getAuthConfig() { getAuthConfig() {
@@ -30,7 +30,7 @@ export const Group = <E extends ElementType = "div">({
{...props} {...props}
data-role="group" data-role="group"
className={twMerge( className={twMerge(
"flex flex-col gap-1.5 w-full", "flex flex-col gap-1.5 has-disabled:cursor-not-allowed w-full",
as === "fieldset" && "border border-primary/10 p-3 rounded-md", as === "fieldset" && "border border-primary/10 p-3 rounded-md",
as === "fieldset" && error && "border-red-500", as === "fieldset" && error && "border-red-500",
error && "text-red-500", error && "text-red-500",
@@ -97,7 +97,7 @@ export const Input = forwardRef<HTMLInputElement, React.ComponentProps<"input">>
ref={ref} ref={ref}
className={twMerge( className={twMerge(
"bg-muted/40 h-11 rounded-md py-2.5 px-4 outline-none w-full disabled:cursor-not-allowed", "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 && !disabledOrReadonly &&
"focus:bg-muted focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all", "focus:bg-muted focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all",
props.className, props.className,
@@ -154,7 +154,7 @@ export const Textarea = forwardRef<HTMLTextAreaElement, React.ComponentProps<"te
{...props} {...props}
ref={ref} ref={ref}
className={twMerge( className={twMerge(
"bg-muted/40 min-h-11 rounded-md py-2.5 px-4 focus:bg-muted 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 min-h-11 rounded-md py-2.5 px-4 focus:bg-muted 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",
props.className, props.className,
)} )}
/> />
@@ -214,7 +214,7 @@ export const BooleanInput = forwardRef<HTMLInputElement, React.ComponentProps<"i
{...props} {...props}
type="checkbox" type="checkbox"
ref={ref} ref={ref}
className="outline-none focus:outline-none focus:ring-2 focus:ring-zinc-500 transition-all disabled:opacity-70 scale-150 ml-1" className="outline-none focus:outline-none focus:ring-2 focus:ring-zinc-500 transition-all disabled:opacity-70 disabled:cursor-not-allowed scale-150 ml-1"
checked={checked} checked={checked}
onChange={handleCheck} onChange={handleCheck}
disabled={props.disabled} disabled={props.disabled}
@@ -260,7 +260,6 @@ export const Switch = forwardRef<
props.disabled && "opacity-50 !cursor-not-allowed", props.disabled && "opacity-50 !cursor-not-allowed",
)} )}
onCheckedChange={(bool) => { onCheckedChange={(bool) => {
console.log("setting", bool);
props.onChange?.({ target: { value: bool } }); props.onChange?.({ target: { value: bool } });
}} }}
{...(props as any)} {...(props as any)}
@@ -294,7 +293,7 @@ export const Select = forwardRef<
{...props} {...props}
ref={ref} ref={ref}
className={twMerge( 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", "appearance-none h-11 w-full",
!props.multiple && "border-r-8 border-r-transparent", !props.multiple && "border-r-8 border-r-transparent",
props.className, props.className,
+10 -5
View File
@@ -1,5 +1,6 @@
import { type LinkProps, Link as WouterLink, useRouter } from "wouter"; import { type LinkProps, Link as WouterLink, useRouter } from "wouter";
import { useEvent } from "../../hooks/use-event"; import { useEvent } from "../../hooks/use-event";
import { useBkndOptions } from "ui/client/BkndProvider";
/* /*
* Transforms `path` into its relative `base` version * Transforms `path` into its relative `base` version
@@ -70,11 +71,13 @@ export function Link({
return false; return false;
} }
const bkndOptions = useBkndOptions();
const adminBasePath = bkndOptions?.admin_basepath ?? "";
const _href = props.href ?? props.to; const _href = props.href ?? props.to;
const href = router const href = router
.hrefs( .hrefs(
_href[0] === "~" ? _href.slice(1) : router.base + _href, _href[0] === "~" ? adminBasePath + _href.slice(1) : router.base + _href,
router, // pass router as a second argument for convinience router, // pass router as a second argument for convinience
) )
.replace("//", "/"); .replace("//", "/");
@@ -82,8 +85,9 @@ export function Link({
const active = const active =
href.replace(router.base, "").length <= 1 ? href === absPath : isActive(absPath, href); href.replace(router.base, "").length <= 1 ? href === absPath : isActive(absPath, href);
// console.log({ adminBasePath, _href, href, bkndOptions })
if (native) { if (native) {
return <a className={`${active ? "active " : ""}${className}`} {...props} />; return <a className={`${active ? "active " : ""}${className}`} {...props} href={href} />;
} }
const wouterOnClick = (e: any) => { const wouterOnClick = (e: any) => {
@@ -99,10 +103,11 @@ export function Link({
}; };
return ( return (
// @ts-expect-error className is not typed on WouterLink
<WouterLink <WouterLink
// @ts-expect-error className is not typed on WouterLink
className={`${active ? "active " : ""}${className}`}
{...props} {...props}
className={`${active ? "active " : ""}${className}`}
href={`~${href}`}
onClick={wouterOnClick} onClick={wouterOnClick}
/> />
); );
+1 -1
View File
@@ -101,7 +101,7 @@ export function useNavigate() {
} }
} }
const _url = options?.absolute ? `~/${basepath}${url}`.replace(/\/+/g, "/") : url; const _url = options?.absolute ? `~/${app.options.basepath}/${app.options.admin_basepath}${url}`.replace(/\/+/g, "/") : url;
const state = { const state = {
...options?.state, ...options?.state,
referrer: location, referrer: location,
-1
View File
@@ -1,7 +1,6 @@
import * as React from "react"; import * as React from "react";
import * as ReactDOM from "react-dom/client"; import * as ReactDOM from "react-dom/client";
import Admin from "./Admin"; import Admin from "./Admin";
//import "./main.css";
import "./styles.css"; import "./styles.css";
function render() { function render() {
@@ -45,7 +45,7 @@ export function EntityForm({
className, className,
action, action,
}: EntityFormProps) { }: EntityFormProps) {
const fields = entity.getFillableFields(action, true); const fields = entity.getFields({ virtual: true, primary: false });
const options = useEntityAdminOptions(entity, action); const options = useEntityAdminOptions(entity, action);
return ( return (
@@ -93,10 +93,6 @@ export function EntityForm({
); );
} }
if (!field.isFillable(action)) {
return;
}
const _key = `${entity.name}-${field.name}-${key}`; const _key = `${entity.name}-${field.name}-${key}`;
return ( return (
@@ -128,7 +124,7 @@ export function EntityForm({
<EntityFormField <EntityFormField
field={field} field={field}
fieldApi={props} fieldApi={props}
disabled={fieldsDisabled} disabled={fieldsDisabled || !field.isFillable(action)}
tabIndex={key + 1} tabIndex={key + 1}
action={action} action={action}
data={data} data={data}
@@ -17,7 +17,7 @@ export function useEntityForm({
}: EntityFormProps) { }: EntityFormProps) {
const data = initialData ?? {}; const data = initialData ?? {};
// @todo: check if virtual must be filtered // @todo: check if virtual must be filtered
const fields = entity.getFillableFields(action, true); const fields = entity.getFields({ virtual: true, primary: false });
// filter defaultValues to only contain fillable fields // filter defaultValues to only contain fillable fields
const defaultValues = getDefaultValues(fields, data); const defaultValues = getDefaultValues(fields, data);
+1 -1
View File
@@ -33,7 +33,7 @@ export function Routes({
}) { }) {
const { theme } = useTheme(); const { theme } = useTheme();
const ctx = useBkndWindowContext(); const ctx = useBkndWindowContext();
const actualBasePath = basePath || ctx.admin_basepath; const actualBasePath = (basePath || ctx.admin_basepath).replace(/\/+$/, "");
return ( return (
<div id="bknd-admin" className={theme + " antialiased"}> <div id="bknd-admin" className={theme + " antialiased"}>
+1
View File
@@ -0,0 +1 @@
declare module '*.css';
+8 -9
View File
@@ -1,11 +1,10 @@
{ {
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "./dist/types", "outDir": "./dist/types",
"rootDir": "./src", "rootDir": "./src",
"baseUrl": ".", "tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo" },
}, "include": ["./src/**/*.ts", "./src/**/*.tsx"],
"include": ["./src/**/*.ts", "./src/**/*.tsx"], "exclude": ["./node_modules", "./__test__", "./e2e"]
"exclude": ["./node_modules", "./__test__", "./e2e"]
} }
+2 -3
View File
@@ -2,11 +2,11 @@
"compilerOptions": { "compilerOptions": {
"composite": false, "composite": false,
"incremental": true, "incremental": true,
"module": "ESNext", "module": "esnext",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"jsx": "react-jsx", "jsx": "react-jsx",
"allowImportingTsExtensions": false, "allowImportingTsExtensions": false,
"target": "ES2022", "target": "es2023",
"noImplicitAny": false, "noImplicitAny": false,
"allowJs": true, "allowJs": true,
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true,
@@ -26,7 +26,6 @@
"esModuleInterop": true, "esModuleInterop": true,
"skipLibCheck": true, "skipLibCheck": true,
"rootDir": ".", "rootDir": ".",
"baseUrl": ".",
"outDir": "./dist/types", "outDir": "./dist/types",
"paths": { "paths": {
"*": ["./src/*"], "*": ["./src/*"],
+94 -60
View File
@@ -1,16 +1,16 @@
{ {
"lockfileVersion": 1, "lockfileVersion": 1,
"configVersion": 0, "configVersion": 1,
"workspaces": { "workspaces": {
"": { "": {
"name": "bknd", "name": "bknd",
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.3.3", "@biomejs/biome": "2.3.3",
"@tsconfig/strictest": "^2.0.7", "@tsconfig/strictest": "^2.0.8",
"@types/bun": "^1.3.1", "@types/bun": "^1.3.1",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"miniflare": "^3.20250718.2", "miniflare": "^3.20250718.2",
"typescript": "^5.9.3", "typescript": "^6.0.2",
"verdaccio": "^6.2.1", "verdaccio": "^6.2.1",
}, },
}, },
@@ -538,7 +538,7 @@
"@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.3", "", {}, "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw=="], "@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=="], "@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=="], "@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=="], "@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=="], "@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=="], "@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=="], "@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/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=="], "@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=="], "@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/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/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/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/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/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=="], "@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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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-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=="], "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-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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "@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.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], "@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=="], "@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=="], "@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-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=="], "@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/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=="], "@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.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-Brg/fp/iAVDOQoHxkuN5bEYhyQlZhxddI78yWsCbeEwTHXQjlNLtiJDUsp1GIptVqMI7/gkJMz4vVAc01mpoBw=="],
"@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/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/@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/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/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/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/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.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-Brg/fp/iAVDOQoHxkuN5bEYhyQlZhxddI78yWsCbeEwTHXQjlNLtiJDUsp1GIptVqMI7/gkJMz4vVAc01mpoBw=="],
"@verdaccio/utils/minimatch": ["minimatch@7.4.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw=="],
"@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=="], "@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/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=="], "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=="], "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/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/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=="], "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=="], "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=="], "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=="], "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/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/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=="], "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-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/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=="], "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=="], "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/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/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=="], "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=="], "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=="], "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=="], "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=="], "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/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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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/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=="], "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=="], "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/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=="], "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=="], "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/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=="], "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=="], "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=="], "@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.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
"@bundled-es-modules/tough-cookie/tough-cookie/universalify": ["universalify@0.2.0", "", {}, "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg=="], "@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=="], "@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=="], "@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/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/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/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=="], "@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=="], "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=="], "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=="], "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/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/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
"node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], "node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
@@ -335,3 +335,55 @@ export default {
``` ```
<AutoTypeTable path="../app/src/plugins/auth/email-otp.plugin.ts" name="EmailOTPPluginOptions" /> <AutoTypeTable path="../app/src/plugins/auth/email-otp.plugin.ts" name="EmailOTPPluginOptions" />
### `sort`
A plugin that maintains sequential numeric positions for one or more entities. Positions are updated automatically when items are inserted or moved. You can optionally configure a scope so that positions are maintained independently within groups, such as tasks within a project.
```typescript title="bknd.config.ts"
import { sort } from "bknd/plugins";
export default {
options: {
plugins: [
sort({
entities: {
tasks: {
field: "position",
scope: "project_id",
},
},
}),
],
},
} satisfies BkndConfig;
```
The configured `field` is used for the numeric position. If the field does not exist, the plugin adds it as a number field with a default value of `0`; if it already exists, it must be a number field. The plugin also adds an index for the field when one is not already present. The optional `scope` field limits reordering to records with the same scope value.
The plugin registers the following endpoints. The default base path is `/api/sort` and can be changed with `apiBasePath`:
- `POST /api/sort/:entity/reorder` moves an item to a position. The request body must include the item `id` and its new numeric `position`.
- `POST /api/sort/:entity/recalculate` recalculates positions in ascending order of the configured field. To recalculate only one scope, pass its value in the optional `scope` property.
```http title="Move a task"
POST /api/sort/tasks/reorder
Content-Type: application/json
{
"id": "task_123",
"position": 2
}
```
```http title="Recalculate a project's task positions"
POST /api/sort/tasks/recalculate
Content-Type: application/json
{
"scope": "project_456"
}
```
<AutoTypeTable path="../app/src/plugins/data/sort.plugin.ts" name="SortPluginOptions" />
@@ -170,7 +170,7 @@ export const prerender = false;
theme: "dark", theme: "dark",
logo_return_path: "/../" logo_return_path: "/../"
}} }}
client:only client:only="react"
/> />
</body> </body>
</html> </html>
@@ -1,3 +1,12 @@
{ {
"pages": ["nextjs", "react-router", "astro", "sveltekit", "vite"] "pages": [
"nextjs",
"react-router",
"astro",
"sveltekit",
"tanstack-start",
"vite",
"nuxt",
"solid-start"
]
} }
@@ -0,0 +1,402 @@
---
title: "Nuxt"
description: "Run bknd inside Nuxt"
tags: ["documentation"]
---
## Installation
To get started with Nuxt and bknd, create a new Nuxt project by following the [official guide](https://nuxt.com/docs/4.x/getting-started/installation), and then install bknd as a dependency:
<Tabs groupId='package-manager' persist items={[ 'npm', 'pnpm', 'yarn', 'bun']}>
```bash tab="npm"
npm install bknd
```
```bash tab="pnpm"
pnpm install bknd
```
```bash tab="yarn"
yarn add bknd
```
```bash tab="bun"
bun add bknd
```
</Tabs>
## Configuration
<Callout type="warning">
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.
</Callout>
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<Env = NuxtEnv> = FrameworkBkndConfig<Env>;
```
## 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;
}
}
});
```
<Callout type="success">
You can visit https://localhost:3000/admin to see the admin UI. Additionally you can create more todos as you explore the admin UI.
</Callout>
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<Env = NodeJS.ProcessEnv>(
config: NuxtBkndConfig<Env>,
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();
};
```
<Callout type="info">
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.
</Callout>
## 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 };
}
}
});
```
<Callout type="warning">
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.
</Callout>
### 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<Todo>; 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<Todo>; 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"
<script lang="ts" setup>
const { fetchTodos, createTodo, deleteTodo, toggleTodo } = useTodoActions();
const { data:todos, execute } = await useAsyncData("todos", () => fetchTodos());
onMounted(() => {
execute();
});
</script>
<template>
<div
v-if="todos"
className="flex flex-col items-center justify-center min-h-screen p-8 pb-20 gap-16 sm:p-20"
>
<main className="flex flex-col gap-8 row-start-2 justify-center items-center sm:items-start">
<div class="flex flex-row items-center ">
<img class="dark:invert size-24" src="/nuxt.svg" alt="Nuxt logo" />
<div class="ml-3.5 mr-2 font-mono opacity-70">&amp;</div>
<img class="dark:invert" src="/bknd.svg" alt="bknd logo" width="183" height="59" />
</div>
<div v-if="data?.todos">
<ul>
<li v-for="todo in data.todos" :key="todo.id">
{{ todo.title }}
<button @click="toggleTodo(todo)">Toggle</button>
<button @click="deleteTodo(todo)">Delete</button>
</li>
</ul>
<form @submit.prevent="createTodo('New Todo')">
<input type="text" placeholder="New Todo" />
<button type="submit">Add</button>
</form>
</div>
<div v-else className="flex flex-col gap-1">
<p>
No todos found.
</p>
</div>
</main>
</div>
</template>
```
<Callout type="success">
You can visit https://localhost:3000/todos to see all the todos.
</Callout>
### 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"
<script lang="ts" setup>
const { getUser } = useUser();
const { data, status: userStatus, execute } = await useAsyncData("user", () => getUser());
onMounted(() => {
execute();
});
</script>
<template>
<div
v-if="userStatus !== 'pending'"
className="flex flex-col items-center justify-center min-h-screen p-8 pb-20 gap-16 sm:p-20"
>
<main className="flex flex-col gap-8 row-start-2 justify-center items-center sm:items-start">
<div class="flex flex-row items-center ">
<img class="dark:invert size-24" src="/nuxt.svg" alt="Nuxt logo" />
<div class="ml-3.5 mr-2 font-mono opacity-70">&amp;</div>
<img class="dark:invert" src="/bknd.svg" alt="bknd logo" width="183" height="59" />
</div>
<div v-if="data?.user">
Logged in as {{ data.user.email }}.
<a className="font-medium underline" href='/api/auth/logout'>
Logout
</a>
</div>
<div v-else className="flex flex-col gap-1">
<p>
Not logged in.
<a className="font-medium underline" href="/admin/auth/login">
Login
</a>
</p>
<p className="text-xs opacity-50">
Sign in with:
<b>
<code>test@bknd.io</code>
</b>
/
<b>
<code>12345678</code>
</b>
</p>
</div>
</main>
<Footer />
</div>
</template>
```
## 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
<NuxtLink external href="/admin">
Admin
</NuxtLink>
```
<Callout type="error">
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.
</Callout>
Check the [Nuxt repository example](https://github.com/bknd-io/bknd/tree/main/examples/nuxt) for more implementation details.
@@ -0,0 +1,334 @@
---
title: "Solid Start"
description: "Run bknd inside Solid Start"
tags: ["documentation"]
---
## Installation
To get started with Solid Start and bknd you can either install the package manually, and follow the descriptions below, or use the CLI starter.
Create a new Solid Start project by following the [official guide](https://docs.solidjs.com/solid-start/getting-started), and then install bknd as a dependency:
<Tabs groupId='package-manager' persist items={[ 'npm', 'pnpm', 'yarn', 'bun']}>
```bash tab="npm"
npm install bknd
```
```bash tab="pnpm"
pnpm install bknd
```
```bash tab="yarn"
yarn add bknd
```
```bash tab="bun"
bun add bknd
```
</Tabs>
## Configuration
<Callout type="warning">
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.
</Callout>
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<Env = SolidStartEnv> = RuntimeBkndConfig<Env>;
```
## 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",
}),
],
});
```
<Callout type="success">
You can visit https://localhost:3000/admin to see the admin UI. Additionally you can create more todos as you explore the admin UI.
</Callout>
Create a helper file to instantiate the bknd instance and retrieve the API, importing the configuration from the `bknd.config.ts` file:
```ts title="src/lib/bknd.ts"
import { getApp as getBkndApp } from "bknd/adapter/solid-start";
import bkndConfig from "../../bknd.config";
import type { App } from "bknd";
export const getApp = async () => {
return await getBkndApp(bkndConfig);
};
export async function getApi({
headers,
verify,
}: {
verify?: boolean;
headers?: Headers;
}) {
const app = await getApp();
if (verify) {
const api = app.getApi({ headers });
await api.verifyAuth();
return api;
}
return app.getApi();
};
```
## Example usage of the API
You can use the `getApi` helper function we've already set up to fetch and mutate in static pages and server components:
```tsx title="src/routes/index.tsx"
import { getApi } from "~/lib/bknd";
import { query, createAsync } from "@solidjs/router";
export const getTodo = async () => {
"use server"
const api = await getApi({});
const limit = 5;
const todos = await api.data.readMany("todos");
const total = todos.body.meta.total as number;
return { total, todos: todos as unknown as Todo[], limit };
};
const getTodosFromServer = query(async () => await getTodo(), "getTodosFromServer");
export default function Home() {
const data = createAsync(() => getTodosFromServer());
return (
<ul>
<For each={data()}>
{(todo) => <li>{todo.title}</li>}
</For>
</ul>
);
}
```
## Using authentication
When using authentication, you'll need to pass the headers to the `getApi` function to verify the user is authenticated.
```tsx title="src/routes/auth.tsx"
import { createResource, Suspense } from "solid-js";
import { A } from "@solidjs/router";
import { getRequestEvent } from "solid-js/web";
import { getApi } from "~/lib/bknd";
export const getUser = async () => {
"use server"
const request = getRequestEvent()?.request;
const api = await getApi({ verify: true, headers: request?.headers });
return api.getUser();
}
export default function Auth() {
const [data] = createResource(async () => {
const user = await getUser()
return { user };
}, {
initialValue: {
user: null
}
});
return (
<div class="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
<main class="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<div class="flex flex-row items-center ">
<img
class="dark:invert size-18"
src="/solid.svg"
alt="Solid logo"
/>
<div class="ml-3.5 mr-2 opacity-70">&amp;</div>
<img
class="dark:invert"
src="/bknd.svg"
alt="bknd logo"
width={183}
height={59}
/>
</div>
<Suspense fallback={<p>Loading...</p>}>
<div>
{data()?.user ? (
<>
Logged in as {data()?.user?.email}.
<A
class="underline"
target="_self"
href={"/api/auth/logout"}
>
Logout
</A>
</>
) : (
<div class="flex flex-col gap-1">
<p>
Not logged in.
<A
class="underline"
target="_self"
href={"/admin/auth/login"}
>
Login
</A>
</p>
<p class="text-xs opacity-50">
Sign in with:
<b>
<code>test@bknd.io</code>
</b>
/
<b>
<code>12345678</code>
</b>
</p>
</div>
)}
</div>
</Suspense>
</main>
</div>
);
}
```
## 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
<A href="/api/auth/logout" target="_self">
Logout
</A>
```
@@ -0,0 +1,291 @@
---
title: "Tanstack Start"
description: "Run bknd inside Tanstack Start"
tags: ["documentation"]
---
## Installation
To get started with Tanstack Start and bknd, create a new Tanstack Start project by following the [official guide](https://tanstack.com/start/latest/docs/framework/react/getting-started#start-a-new-project-from-scratch), and then install bknd as a dependency:
<Tabs groupId='package-manager' persist items={[ 'npm', 'pnpm', 'yarn', 'bun']}>
```bash tab="npm"
npm install bknd
```
```bash tab="pnpm"
pnpm install bknd
```
```bash tab="yarn"
yarn add bknd
```
```bash tab="bun"
bun add bknd
```
</Tabs>
## Configuration
<Callout type="warning">
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.
</Callout>
Now create a `bknd.config.ts` file in the root of your project:
```typescript title="bknd.config.ts"
import { type TanstackStartConfig } from "bknd/adapter/tanstack-start";
import { em, entity, text, boolean } from "bknd";
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: "random_gibberish_please_change_this",
// use something like `openssl rand -hex 32` for production
},
},
},
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 TanstackStartConfig;
```
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 `TanstackStartConfig` type extends the base config type with the following properties:
```typescript
export type TanstackStartConfig<Env = TanstackStartEnv> = FrameworkBkndConfig<Env>;
```
## Serve the API
The Tanstack Start adapter uses Tanstack Start's hooks mechanism to handle API requests. Create a `/src/routes/api.$.ts` file:
```typescript title="/src/routes/api.$.ts"
import { createFileRoute } from "@tanstack/react-router";
import config from "../../bknd.config";
import { serve } from "bknd/adapter/tanstack-start";
const handler = serve(config);
export const Route = createFileRoute("/api/$")({
server: {
handlers: {
ANY: async ({ request }) => await handler(request),
},
},
});
```
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/bknd.ts"
import config from "../bknd.config";
import { getApp } from "bknd/adapter/tanstack-start";
export async function getApi({
headers,
verify,
}: {
verify?: boolean;
headers?: Headers;
}) {
const app = await getApp(config, process.env);
if (verify) {
const api = app.getApi({ headers });
await api.verifyAuth();
return api;
}
return app.getApi();
};
```
<Callout type="info">
The adapter uses `process.env` to access environment variables, this works because Tanstack Start uses Nitro underneath and it will use polyfills for `process.env` making it platform/runtime agnostic.
</Callout>
## Enabling the Admin UI
Create a page at /src/routes/admin.$.tsx:
```typescript title="/src/routes/admin.$.tsx"
import { createFileRoute } from "@tanstack/react-router";
import { useAuth } from "bknd/client";
import "bknd/dist/styles.css";
import { Admin } from "bknd/ui";
export const Route = createFileRoute("/admin/$")({
ssr: false, // [!code highlight] "data-only" works too
component: RouteComponent,
});
function RouteComponent() {
const { user } = useAuth();
return (
<Admin
withProvider={{ user: user }}
config={{
basepath: "/admin",
logo_return_path: "/../",
theme: "system",
}}
baseUrl={import.meta.env.APP_URL}
/>
);
};
```
<Callout type="info">
Admin routes are expected to run on the client not using `ssr: false` will cause errors like `✘ [ERROR] No matching export in "node_modules/json-schema-library/dist/index.mjs" for import "Draft2019"` and production build might fail because of this
</Callout>
## Example usage of the API
You can use the `getApp` function to access the bknd API in your app:
These are a few examples how you can validate user and handle server-side requests using `createServerFn`.
```typescript title="src/routes/index.tsx"
import { createFileRoute } from "@tanstack/react-router";
import { getApi } from "@/bknd";
import { createServerFn } from "@tanstack/react-start";
export const getTodo = createServerFn()
.handler(async () => {
const api = await getApi({});
const limit = 5;
const todos = await api.data.readMany("todos", { limit, sort: "-id" });
return { todos };
});
export const Route = createFileRoute("/")({
ssr: false,
component: App,
loader: async () => {
return await getTodo();
},
});
function App() {
const { todos } = Route.useLoaderData();
return (
<div>
<h1>Todos</h1>
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</div>
);
}
```
### Using authentication
To use authentication in your app, pass the request headers to the API:
```typescript title="src/routes/user.tsx"
import { getApi } from "@/bknd";
import { createServerFn } from "@tanstack/react-start";
import { Link } from "@tanstack/react-router";
import { createFileRoute } from "@tanstack/react-router";
import { getRequest } from "@tanstack/react-start/server";
export const getUser = createServerFn()
.handler(async () => {
const request = getRequest();
const api = await getApi({ verify: true, headers: request.headers });
const user = api.getUser();
return { user };
});
export const Route = createFileRoute("/user")({
component: RouteComponent,
loader: async () => {
return { user: await getUser() };
},
});
function RouteComponent() {
const { user } = Route.useLoaderData();
return (
<div>
{user ? (
<>
Logged in as {user.email}.{" "}
<Link
className="font-medium underline"
to={"/api/auth/logout" as string}
>
Logout
</Link>
</>
) : (
<div className="flex flex-col gap-1">
<p>
Not logged in.
<Link
className="font-medium underline"
to={"/admin/auth/login" as string}
>
Login
</Link>
</p>
<p className="text-xs opacity-50">
Sign in with:
<b>
<code>test@bknd.io</code>
</b>
/
<b>
<code>12345678</code>
</b>
</p>
</div>
)}
</div>
)
}
```
Check the [Tanstack Start repository example](https://github.com/bknd-io/bknd/tree/main/examples/tanstack-start) for more implementation details.
@@ -33,6 +33,24 @@ bknd seamlessly integrates with popular frameworks, allowing you to use what you
href="/integration/sveltekit" href="/integration/sveltekit"
/> />
<Card
icon={<Icon icon="simple-icons:tanstack" className="text-fd-primary !size-6" />}
title="Tanstack Start"
href="/integration/tanstack-start"
/>
<Card
icon={<Icon icon="simple-icons:nuxt" className="text-fd-primary !size-6" />}
title="Nuxt"
href="/integration/nuxt"
/>
<Card
icon={<Icon icon="simple-icons:solid" className="text-fd-primary !size-6" />}
title="Solid Start"
href="/integration/solid-start"
/>
<Card title="Yours missing?" href="https://github.com/bknd-io/bknd/issues/new"> <Card title="Yours missing?" href="https://github.com/bknd-io/bknd/issues/new">
Create a new issue to request a guide for your framework. Create a new issue to request a guide for your framework.
</Card> </Card>
@@ -30,6 +30,25 @@ With a focus on flexibility and ease of integration, bknd's authentication syste
--- ---
## Server Module: `module.auth`
### `auth.changePassword([userId], [password])`
To change a user's password, use the `changePassword` method:
```ts
await app.module.auth.changePassword(user.id, password);
```
This method updates the password for the specified user.
- `userId`: The ID of the user whose password should be changed.
- `password`: The new password value.
This method throws an error if:
- The user with the given `userId` is not found.
- The user's authentication strategy is not `"password"` (e.g. the user registered via an OAuth provider).
## Securing Your Admin Portal ## Securing Your Admin Portal
<Callout type="warn" title="Do not enable the Guard without an admin user"> <Callout type="warn" title="Do not enable the Guard without an admin user">
@@ -150,6 +150,24 @@ Pick your framework or runtime to get started.
href="/integration/sveltekit" href="/integration/sveltekit"
/> />
<Card
icon={<Icon icon="simple-icons:tanstack" className="text-fd-primary !size-6" />}
title="Tanstack Start"
href="/integration/tanstack-start"
/>
<Card
icon={<Icon icon="simple-icons:nuxt" className="text-fd-primary !size-6" />}
title="Nuxt"
href="/integration/nuxt"
/>
<Card
icon={<Icon icon="simple-icons:solid" className="text-fd-primary !size-6" />}
title="Solid Start"
href="/integration/solid-start"
/>
<Card <Card
icon={<Icon icon="tabler:lambda" className="text-fd-primary !size-6" />} icon={<Icon icon="tabler:lambda" className="text-fd-primary !size-6" />}
title="AWS Lambda" title="AWS Lambda"
+1 -2
View File
@@ -1,7 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "target": "es2023",
"target": "ESNext",
"lib": ["dom", "dom.iterable", "esnext"], "lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
+1 -1
View File
@@ -6,7 +6,7 @@ import { type BunBkndConfig, serve } from "bknd/adapter/bun";
// this is optional, if omitted, it uses an in-memory database // this is optional, if omitted, it uses an in-memory database
const config: BunBkndConfig = { const config: BunBkndConfig = {
connection: { connection: {
url: "file:data.db", url: "data.db",
}, },
config: { config: {
media: { media: {
-1
View File
@@ -17,7 +17,6 @@
"noUnusedLocals": false, "noUnusedLocals": false,
"noUnusedParameters": false, "noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false, "noPropertyAccessFromIndexSignature": false,
"baseUrl": "."
}, },
"include": ["index.ts"] "include": ["index.ts"]
} }
+1 -1
View File
@@ -7,7 +7,7 @@ import { serve } from "bknd/adapter/node";
/** @type {import("bknd/adapter/node").NodeBkndConfig} */ /** @type {import("bknd/adapter/node").NodeBkndConfig} */
const config = { const config = {
connection: { connection: {
url: "file:data.db", url: "data.db",
}, },
config: { config: {
media: { media: {
+27
View File
@@ -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
+64
View File
@@ -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).
+33
View File
@@ -0,0 +1,33 @@
@import url("https://fonts.googleapis.com/css2?family=Geist+Mono:wght@100..900&display=swap");
.geist-mono-100 {
font-optical-sizing: auto;
font-weight: 100;
font-style: normal;
}
:root {
--background: #ffffff;
--foreground: #171717;
font-family: "Geist Mono", monospace;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
@theme {
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-background: var(--background);
--color-foreground: var(--foreground);
}
body {
background-color: var(--background);
color: var(--foreground);
font-family: var(--font-geist-mono-100);
}
+13
View File
@@ -0,0 +1,13 @@
<template>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground gap-2 text-white hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
href="https://bknd.io/" target="_blank" rel="noopener noreferrer">
<img className="grayscale" src="/bknd.ico" alt="bknd logomark" width={20} height={20} />
Go To Bknd.io
</a>
<a className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
href="https://docs.bknd.io/integration/nextjs" target="_blank" rel="noopener noreferrer">
Read our docs
</a>
</div>
</template>
+29
View File
@@ -0,0 +1,29 @@
<script lang="ts" setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const pathname = computed(() => route.path)
</script>
<template>
<footer class="row-start-3 flex gap-6 flex-wrap items-center justify-center">
<NuxtLink class="flex items-center gap-2 hover:underline hover:underline-offset-4"
:to="pathname === '/' ? '/user' : '/'">
<img aria-hidden src="/file.svg" alt="File icon" width="16" height="16" />
{{ pathname === '/' ? 'User' : 'Home' }}
</NuxtLink>
<!-- external is attribute required to hit the trigger middleware -->
<NuxtLink external class="flex items-center gap-2 hover:underline hover:underline-offset-4" href="/admin/data">
<img aria-hidden src="/window.svg" alt="Window icon" width="16" height="16" />
Admin
</NuxtLink>
<a class="flex items-center gap-2 hover:underline hover:underline-offset-4" href="https://bknd.io" target="_blank"
rel="noopener noreferrer">
<img aria-hidden src="/globe.svg" alt="Globe icon" width="16" height="16" />
Go to bknd.io →
</a>
</footer>
</template>
+21
View File
@@ -0,0 +1,21 @@
<script lang="ts" setup>
const props = withDefaults(defineProps<{ items?: any[] }>(), { items: () => [] })
function isPrimitive(val: unknown): boolean {
const t = typeof val
return t === 'string' || t === 'number' || t === 'boolean'
}
</script>
<template>
<ol class="list-inside list-decimal text-sm text-center sm:text-left w-full text-center">
<li
v-for="(item, i) in props.items"
:key="i"
:class="{ 'mb-2': i < props.items.length - 1 }"
>
<span v-if="isPrimitive(item)">{{ item }}</span>
<component v-else :is="item" />
</li>
</ol>
</template>
@@ -0,0 +1,31 @@
import type { DB } from "bknd";
type Todo = DB["todos"];
export const useTodoActions = () => {
const fetchTodos = () =>
$fetch<{ limit: number; todos: Array<Todo>; 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 };
};
+6
View File
@@ -0,0 +1,6 @@
import type { User } from "bknd";
export const useUser = () => {
const getUser = () => $fetch("/api/auth/me") as Promise<{ user: User }>;
return { getUser };
};
+70
View File
@@ -0,0 +1,70 @@
<script lang="ts" setup>
const { fetchTodos, toggleTodo, createTodo, deleteTodo } = useTodoActions();
const { data: todos, refresh } = await useAsyncData('todos', () => fetchTodos());
async function handleSubmit(event: Event) {
event.preventDefault();
const form = event.currentTarget as HTMLFormElement;
if (!form) return;
const formData = new FormData(form);
const title = formData.get("title");
await createTodo(title as string);
refresh();
};
</script>
<template>
<div v-if="todos !== undefined"
class="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
<main class="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<div class="flex flex-row items-center justify-evenly min-w-full">
<img class="size-24" src="/nuxt.svg" alt="Nuxt logo" />
<div class="ml-3.5 mr-2 font-mono opacity-70">&amp;</div>
<img class="dark:invert" src="/bknd.svg" alt="bknd logo" width="183" height="59" />
</div>
<List :items="['Get started with a full backend.', 'Focus on what matters instead of repetition.']" />
<div class="flex flex-col border border-black/15 dark:border-white/15 w-full py-4 px-5 gap-2">
<h2 class="font-mono mb-1 opacity-70"><code>What's next?</code></h2>
<div class="flex flex-col w-full gap-2">
<div v-if="todos.total > todos.limit"
class="bg-foreground/10 flex justify-center p-1 text-xs rounded text-foreground/40">
{{ todos.total - todos.limit }} more todo(s) hidden
</div>
<div class="flex flex-col gap-3">
<div v-for="todo in todos.todos" :key="String(todo.id)" class="flex flex-row">
<div class="flex flex-row flex-grow items-center gap-3 ml-1">
<input
type="checkbox"
class="flex-shrink-0 cursor-pointer"
:checked="Boolean(todo.done)"
@change="() => { toggleTodo(todo); refresh() }" />
<div class="text-foreground/90 leading-none">{{ todo.title }}</div>
</div>
<button type="button" class="cursor-pointer grayscale transition-all hover:grayscale-0 text-xs"
@click="async () => { await deleteTodo(todo); refresh() }">
❌
</button>
</div>
</div>
<form class="flex flex-row w-full gap-3 mt-2" :key="todos.todos.map(t => t.id).join()" @submit="handleSubmit">
<input
type="text"
name="title"
placeholder="New todo"
class="py-2 px-4 flex flex-grow rounded-sm bg-black/5 focus:bg-black/10 dark:bg-white/5 dark:focus:bg-white/10 transition-colors outline-none" />
<button type="submit" class="cursor-pointer">Add</button>
</form>
</div>
</div>
</main>
<Footer />
</div>
</template>
+55
View File
@@ -0,0 +1,55 @@
<script lang="ts" setup>
const { getUser } = useUser();
const { data, status: userStatus, execute } = await useAsyncData("user", () => getUser());
onMounted(() => {
execute();
});
</script>
<template>
<div
v-if="userStatus !== 'pending'"
className="flex flex-col items-center justify-center min-h-screen p-8 pb-20 gap-16 sm:p-20"
>
<main className="flex flex-col gap-8 row-start-2 justify-center items-center sm:items-start">
<div class="flex flex-row items-center justify-evenly min-w-full">
<img class="size-24" src="/nuxt.svg" alt="Nuxt logo" />
<div class="ml-3.5 mr-2 font-mono opacity-70">&amp;</div>
<img
class="dark:invert"
src="/bknd.svg"
alt="bknd logo"
width="183"
height="59"
/>
</div>
<div v-if="data?.user">
Logged in as {{ data.user.email }}.
<NuxtLink external className="font-medium underline" href='/api/auth/logout'>
Logout
</NuxtLink>
</div>
<div v-else className="flex flex-col gap-1">
<p>
Not logged in.
<NuxtLink external className="font-medium underline" href="/admin/auth/login">
Login
</NuxtLink>
</p>
<p className="text-xs opacity-50">
Sign in with:
<b>
<code>test@bknd.io</code>
</b>
/
<b>
<code>12345678</code>
</b>
</p>
</div>
</main>
<Footer />
</div>
</template>
+59
View File
@@ -0,0 +1,59 @@
import { em, entity, text, boolean, } from "bknd";
import { secureRandomString } from "bknd/utils";
import type { NuxtBkndConfig } from "bknd/adapter/nuxt";
import { registerLocalMediaAdapter } from "bknd/adapter/node";
const local = registerLocalMediaAdapter();
const schema = em({
todos: entity("todos", {
title: text(),
done: boolean(),
}),
});
// register your schema to get automatic type completion
type Database = (typeof schema)["DB"];
declare module "bknd" {
interface DB extends Database { }
}
export default {
connection: { url: "file:data.db" },
options: {
// the seed option is only executed if the database was empty
seed: async (ctx) => {
// create some entries
await ctx.em.mutator("todos").insertMany([
{ title: "Learn bknd", done: true },
{ title: "Build something cool", done: false },
]);
// and create a user
await ctx.app.module.auth.createUser({
email: "test@bknd.io",
password: "12345678",
});
},
},
config: {
data: schema.toJSON(),
auth: {
enabled: true,
jwt: {
secret: secureRandomString(32),
},
},
media: {
enabled: true,
adapter: local({
path: "./public/uploads",
}),
},
},
adminOptions: {
adminBasepath: "/admin",
assetsPath: "/admin/",
logoReturnPath: "../..",
},
} satisfies NuxtBkndConfig;
+12
View File
@@ -0,0 +1,12 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: "2025-07-15",
devtools: { enabled: false },
modules: ["@nuxtjs/tailwindcss"],
app: {
head: {
title: "Nuxt 🤝 Bknd.io",
},
},
css: ["assets/css/main.css"],
});
+21
View File
@@ -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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+14
View File
@@ -0,0 +1,14 @@
<svg
width="578"
height="188"
viewBox="0 0 578 188"
fill="black"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M41.5 34C37.0817 34 33.5 37.5817 33.5 42V146C33.5 150.418 37.0817 154 41.5 154H158.5C162.918 154 166.5 150.418 166.5 146V42C166.5 37.5817 162.918 34 158.5 34H41.5ZM123.434 113.942C124.126 111.752 124.5 109.42 124.5 107C124.5 94.2975 114.203 84 101.5 84C99.1907 84 96.9608 84.3403 94.8579 84.9736L87.2208 65.1172C90.9181 63.4922 93.5 59.7976 93.5 55.5C93.5 49.701 88.799 45 83 45C77.201 45 72.5 49.701 72.5 55.5C72.5 61.299 77.201 66 83 66C83.4453 66 83.8841 65.9723 84.3148 65.9185L92.0483 86.0256C87.1368 88.2423 83.1434 92.1335 80.7957 96.9714L65.4253 91.1648C65.4746 90.7835 65.5 90.3947 65.5 90C65.5 85.0294 61.4706 81 56.5 81C51.5294 81 47.5 85.0294 47.5 90C47.5 94.9706 51.5294 99 56.5 99C60.0181 99 63.0648 96.9814 64.5449 94.0392L79.6655 99.7514C78.9094 102.03 78.5 104.467 78.5 107C78.5 110.387 79.2321 113.603 80.5466 116.498L69.0273 123.731C67.1012 121.449 64.2199 120 61 120C55.201 120 50.5 124.701 50.5 130.5C50.5 136.299 55.201 141 61 141C66.799 141 71.5 136.299 71.5 130.5C71.5 128.997 71.1844 127.569 70.6158 126.276L81.9667 119.149C86.0275 125.664 93.2574 130 101.5 130C110.722 130 118.677 124.572 122.343 116.737L132.747 120.899C132.585 121.573 132.5 122.276 132.5 123C132.5 127.971 136.529 132 141.5 132C146.471 132 150.5 127.971 150.5 123C150.5 118.029 146.471 114 141.5 114C138.32 114 135.525 115.649 133.925 118.139L123.434 113.942Z"
/>
<path d="M243.9 151.5C240.4 151.5 237 151 233.7 150C230.4 149 227.4 147.65 224.7 145.95C222 144.15 219.75 142.15 217.95 139.95C216.15 137.65 215 135.3 214.5 132.9L219.3 131.1L218.25 149.7H198.15V39H219.45V89.25L215.4 87.6C216 85.2 217.15 82.9 218.85 80.7C220.55 78.4 222.7 76.4 225.3 74.7C227.9 72.9 230.75 71.5 233.85 70.5C236.95 69.5 240.15 69 243.45 69C250.35 69 256.5 70.8 261.9 74.4C267.3 77.9 271.55 82.75 274.65 88.95C277.85 95.15 279.45 102.25 279.45 110.25C279.45 118.25 277.9 125.35 274.8 131.55C271.7 137.75 267.45 142.65 262.05 146.25C256.75 149.75 250.7 151.5 243.9 151.5ZM238.8 133.35C242.8 133.35 246.25 132.4 249.15 130.5C252.15 128.5 254.5 125.8 256.2 122.4C257.9 118.9 258.75 114.85 258.75 110.25C258.75 105.75 257.9 101.75 256.2 98.25C254.6 94.75 252.3 92.05 249.3 90.15C246.3 88.25 242.8 87.3 238.8 87.3C234.8 87.3 231.3 88.25 228.3 90.15C225.3 92.05 222.95 94.75 221.25 98.25C219.55 101.75 218.7 105.75 218.7 110.25C218.7 114.85 219.55 118.9 221.25 122.4C222.95 125.8 225.3 128.5 228.3 130.5C231.3 132.4 234.8 133.35 238.8 133.35ZM308.312 126.15L302.012 108.6L339.512 70.65H367.562L308.312 126.15ZM288.062 150V39H309.362V150H288.062ZM341.762 150L313.262 114.15L328.262 102.15L367.412 150H341.762ZM371.675 150V70.65H392.075L392.675 86.85L388.475 88.65C389.575 85.05 391.525 81.8 394.325 78.9C397.225 75.9 400.675 73.5 404.675 71.7C408.675 69.9 412.875 69 417.275 69C423.275 69 428.275 70.2 432.275 72.6C436.375 75 439.425 78.65 441.425 83.55C443.525 88.35 444.575 94.3 444.575 101.4V150H423.275V103.05C423.275 99.45 422.775 96.45 421.775 94.05C420.775 91.65 419.225 89.9 417.125 88.8C415.125 87.6 412.625 87.1 409.625 87.3C407.225 87.3 404.975 87.7 402.875 88.5C400.875 89.2 399.125 90.25 397.625 91.65C396.225 93.05 395.075 94.65 394.175 96.45C393.375 98.25 392.975 100.2 392.975 102.3V150H382.475C380.175 150 378.125 150 376.325 150C374.525 150 372.975 150 371.675 150ZM488.536 151.5C481.636 151.5 475.436 149.75 469.936 146.25C464.436 142.65 460.086 137.8 456.886 131.7C453.786 125.5 452.236 118.35 452.236 110.25C452.236 102.35 453.786 95.3 456.886 89.1C460.086 82.9 464.386 78 469.786 74.4C475.286 70.8 481.536 69 488.536 69C492.236 69 495.786 69.6 499.186 70.8C502.686 71.9 505.786 73.45 508.486 75.45C511.286 77.45 513.536 79.7 515.236 82.2C516.936 84.6 517.886 87.15 518.086 89.85L512.686 90.75V39H533.986V150H513.886L512.986 131.7L517.186 132.15C516.986 134.65 516.086 137.05 514.486 139.35C512.886 141.65 510.736 143.75 508.036 145.65C505.436 147.45 502.436 148.9 499.036 150C495.736 151 492.236 151.5 488.536 151.5ZM493.336 133.8C497.336 133.8 500.836 132.8 503.836 130.8C506.836 128.8 509.186 126.05 510.886 122.55C512.586 119.05 513.436 114.95 513.436 110.25C513.436 105.65 512.586 101.6 510.886 98.1C509.186 94.5 506.836 91.75 503.836 89.85C500.836 87.85 497.336 86.85 493.336 86.85C489.336 86.85 485.836 87.85 482.836 89.85C479.936 91.75 477.636 94.5 475.936 98.1C474.336 101.6 473.536 105.65 473.536 110.25C473.536 114.95 474.336 119.05 475.936 122.55C477.636 126.05 479.936 128.8 482.836 130.8C485.836 132.8 489.336 133.8 493.336 133.8Z" />
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+1
View File
@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg viewBox="0 0 900 900" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M504.908 750H839.476C850.103 750.001 860.542 747.229 869.745 741.963C878.948 736.696 886.589 729.121 891.9 719.999C897.211 710.876 900.005 700.529 900 689.997C899.995 679.465 897.193 669.12 891.873 660.002L667.187 274.289C661.876 265.169 654.237 257.595 645.036 252.329C635.835 247.064 625.398 244.291 614.773 244.291C604.149 244.291 593.711 247.064 584.511 252.329C575.31 257.595 567.67 265.169 562.36 274.289L504.908 372.979L392.581 179.993C387.266 170.874 379.623 163.301 370.42 158.036C361.216 152.772 350.777 150 340.151 150C329.525 150 319.086 152.772 309.883 158.036C300.679 163.301 293.036 170.874 287.721 179.993L8.12649 660.002C2.80743 669.12 0.00462935 679.465 5.72978e-06 689.997C-0.00461789 700.529 2.78909 710.876 8.10015 719.999C13.4112 729.121 21.0523 736.696 30.255 741.963C39.4576 747.229 49.8973 750.001 60.524 750H270.538C353.748 750 415.112 713.775 457.336 643.101L559.849 467.145L614.757 372.979L779.547 655.834H559.849L504.908 750ZM267.114 655.737L120.551 655.704L340.249 278.586L449.87 467.145L376.474 593.175C348.433 639.03 316.577 655.737 267.114 655.737Z" fill="#00DC82"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+2
View File
@@ -0,0 +1,2 @@
User-Agent: *
Disallow:
+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

+15
View File
@@ -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;
}
}
});
+31
View File
@@ -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 };
}
}
});
+21
View File
@@ -0,0 +1,21 @@
import { type NuxtBkndConfig, getApp as getNuxtApp } from "bknd/adapter/nuxt";
import bkndConfig from "../../bknd.config";
export async function getApp<Env = NodeJS.ProcessEnv>(
config: NuxtBkndConfig<Env>,
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();
}
+18
View File
@@ -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"
}
]
}
-1
View File
@@ -23,7 +23,6 @@
"isolatedModules": true, "isolatedModules": true,
"esModuleInterop": true, "esModuleInterop": true,
"skipLibCheck": true, "skipLibCheck": true,
"baseUrl": ".",
"paths": { "paths": {
"bknd": ["../app/src/index.ts"], "bknd": ["../app/src/index.ts"],
"bknd/*": ["../app/src/*"] "bknd/*": ["../app/src/*"]
-1
View File
@@ -13,7 +13,6 @@
"moduleResolution": "bundler", "moduleResolution": "bundler",
"jsx": "react-jsx", "jsx": "react-jsx",
"rootDirs": [".", "./.react-router/types"], "rootDirs": [".", "./.react-router/types"],
"baseUrl": ".",
"paths": { "paths": {
"~/*": ["./app/*"] "~/*": ["./app/*"]
}, },

Some files were not shown because too many files have changed in this diff Show More