mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-03 16:46:00 +00:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 906f80b11e | |||
| 7a3691a1a7 | |||
| 12c955155d | |||
| 15af9f5261 | |||
| 39efb911e9 | |||
| 4729203d47 | |||
| 0db052acca | |||
| 6e08f45857 | |||
| 7bff84d601 | |||
| e66e05b2b0 | |||
| 5b318ce485 | |||
| 22de16fe17 | |||
| 5d26673bc6 | |||
| dfa4bc0e33 | |||
| e51b89a18a | |||
| 63988e0c5f | |||
| 297fd85a4f | |||
| a82fbe7400 | |||
| 74e1e9a03f | |||
| 05a81b5bdc | |||
| 9ff49103fb | |||
| a5b59c004e | |||
| a0edcf483b | |||
| 9a18e354cd | |||
| 58c7aba1a4 | |||
| f8aa242d2b | |||
| 3da9570abe | |||
| 6ee898e606 | |||
| abbd372ddf | |||
| 3e77982996 | |||
| 3fbea8ace7 | |||
| a41f943b43 | |||
| 15a9c549e7 | |||
| 50cadbaa8e | |||
| 7b128c9701 | |||
| 061181d59d | |||
| af6cb0c8f0 | |||
| 5a693c0370 | |||
| 262588decc | |||
| 17ab35e245 | |||
| db795ec050 | |||
| 773df544dd | |||
| 0ac7d1fd6e |
@@ -15,7 +15,7 @@ jobs:
|
|||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
uses: oven-sh/setup-bun@v1
|
uses: oven-sh/setup-bun@v1
|
||||||
with:
|
with:
|
||||||
bun-version: "1.2.5"
|
bun-version: "1.2.14"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
working-directory: ./app
|
working-directory: ./app
|
||||||
|
|||||||
@@ -30,3 +30,5 @@ packages/media/.env
|
|||||||
.vscode
|
.vscode
|
||||||
.git_old
|
.git_old
|
||||||
docker/tmp
|
docker/tmp
|
||||||
|
.debug
|
||||||
|
.history
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
[](https://npmjs.org/package/bknd)
|
[](https://npmjs.org/package/bknd)
|
||||||
[](https://www.npmjs.com/package/bknd)
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -18,14 +17,14 @@ bknd simplifies app development by providing a fully functional backend for data
|
|||||||
> and therefore full backward compatibility is not guaranteed before reaching v1.0.0.
|
> and therefore full backward compatibility is not guaranteed before reaching v1.0.0.
|
||||||
|
|
||||||
## Size
|
## Size
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
The size on npm is misleading, as the `bknd` package includes the backend, the ui components as well as the whole backend bundled into the cli including static assets.
|
The size on npm is misleading, as the `bknd` package includes the backend, the ui components as well as the whole backend bundled into the cli including static assets.
|
||||||
|
|
||||||
Depending on what you use, the size can be higher as additional dependencies are getting pulled in. The minimal size of a full `bknd` app as an API is around 212 kB gzipped (e.g. deployed as Cloudflare Worker).
|
Depending on what you use, the size can be higher as additional dependencies are getting pulled in. The minimal size of a full `bknd` app as an API is around 300 kB gzipped (e.g. deployed as Cloudflare Worker).
|
||||||
|
|
||||||
## Motivation
|
## Motivation
|
||||||
Creating digital products always requires developing both the backend (the logic) and the frontend (the appearance). Building a backend from scratch demands deep knowledge in areas such as authentication and database management. Using a backend framework can speed up initial development, but it still requires ongoing effort to work within its constraints (e.g., *"how to do X with Y?"*), which can quickly slow you down. Choosing a backend system is a tough decision, as you might not be aware of its limitations until you encounter them.
|
Creating digital products always requires developing both the backend (the logic) and the frontend (the appearance). Building a backend from scratch demands deep knowledge in areas such as authentication and database management. Using a backend framework can speed up initial development, but it still requires ongoing effort to work within its constraints (e.g., *"how to do X with Y?"*), which can quickly slow you down. Choosing a backend system is a tough decision, as you might not be aware of its limitations until you encounter them.
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# ===== DB Settings =====
|
||||||
|
VITE_DB_URL=:memory:
|
||||||
|
# you can set a location for a database here, it'll overwrite the previous setting
|
||||||
|
# ideally use the ".db" folder (create it first), it's git ignored
|
||||||
|
VITE_DB_URL=file:.db/dev.db
|
||||||
|
|
||||||
|
# alternatively, you can use url/token combination
|
||||||
|
#VITE_DB_URL=
|
||||||
|
#VITE_DB_TOKEN=
|
||||||
|
|
||||||
|
|
||||||
|
# ===== DEV Server =====
|
||||||
|
# restart the dev server on every change (enable with "1")
|
||||||
|
VITE_APP_FRESH=
|
||||||
|
# displays react-scan widget (enable with "1")
|
||||||
|
VITE_DEBUG_RERENDERS=
|
||||||
|
# console logs registered routes on start (enable with "1")
|
||||||
|
VITE_SHOW_ROUTES=
|
||||||
|
|
||||||
|
|
||||||
|
# ===== Test Credentials =====
|
||||||
|
RESEND_API_KEY=
|
||||||
|
R2_TOKEN=
|
||||||
|
|
||||||
|
R2_ACCESS_KEY=
|
||||||
|
R2_SECRET_ACCESS_KEY=
|
||||||
|
R2_URL=
|
||||||
|
|
||||||
|
AWS_ACCESS_KEY=
|
||||||
|
AWS_SECRET_KEY=
|
||||||
|
AWS_S3_URL=
|
||||||
|
|
||||||
|
OAUTH_CLIENT_ID=
|
||||||
|
OAUTH_CLIENT_SECRET=
|
||||||
|
|
||||||
|
PUBLIC_POSTHOG_KEY=
|
||||||
|
PUBLIC_POSTHOG_HOST=
|
||||||
|
|
||||||
|
# ===== Internals =====
|
||||||
|
BKND_CLI_CREATE_REF=main
|
||||||
|
BKND_CLI_LOG_LEVEL=debug
|
||||||
|
BKND_MODULES_DEBUG=1
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { type ObjectQuery, convert, validate } from "../../../src/core/object/query/object-query";
|
import { type ObjectQuery, convert, validate } from "core/object/query/object-query";
|
||||||
|
|
||||||
describe("object-query", () => {
|
describe("object-query", () => {
|
||||||
const q: ObjectQuery = { name: "Michael" };
|
const q: ObjectQuery = { name: "Michael" };
|
||||||
|
|||||||
@@ -110,4 +110,18 @@ describe("some tests", async () => {
|
|||||||
new EntityManager([entity, entity2], connection);
|
new EntityManager([entity, entity2], connection);
|
||||||
}).toThrow();
|
}).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("primary uuid", async () => {
|
||||||
|
const entity = new Entity("users", [
|
||||||
|
new PrimaryField("id", { format: "uuid" }),
|
||||||
|
new TextField("username"),
|
||||||
|
]);
|
||||||
|
const em = new EntityManager([entity], getDummyConnection().dummyConnection);
|
||||||
|
await em.schema().sync({ force: true });
|
||||||
|
|
||||||
|
const mutator = em.mutator(entity);
|
||||||
|
const data = await mutator.insertOne({ username: "test" });
|
||||||
|
expect(data.data.id).toBeDefined();
|
||||||
|
expect(data.data.id).toBeString();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+4
-71
@@ -1,15 +1,7 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, test, expect } from "bun:test";
|
||||||
import { Value, _jsonp } from "../../src/core/utils";
|
import { getDummyConnection } from "../helper";
|
||||||
import { type RepoQuery, WhereBuilder, type WhereQuery, querySchema } from "../../src/data";
|
import { type WhereQuery, WhereBuilder } from "data";
|
||||||
import type { RepoQueryIn } from "../../src/data/server/data-query-impl";
|
|
||||||
import { getDummyConnection } from "./helper";
|
|
||||||
|
|
||||||
const decode = (input: RepoQueryIn, expected: RepoQuery) => {
|
|
||||||
const result = Value.Decode(querySchema, input);
|
|
||||||
expect(result).toEqual(expected);
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("data-query-impl", () => {
|
|
||||||
function qb() {
|
function qb() {
|
||||||
const c = getDummyConnection();
|
const c = getDummyConnection();
|
||||||
const kysely = c.dummyConnection.kysely;
|
const kysely = c.dummyConnection.kysely;
|
||||||
@@ -20,6 +12,7 @@ describe("data-query-impl", () => {
|
|||||||
return { sql, parameters };
|
return { sql, parameters };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
describe("WhereBuilder", () => {
|
||||||
test("single validation", () => {
|
test("single validation", () => {
|
||||||
const tests: [WhereQuery, string, any[]][] = [
|
const tests: [WhereQuery, string, any[]][] = [
|
||||||
[{ name: "Michael", age: 40 }, '("name" = ? and "age" = ?)', ["Michael", 40]],
|
[{ name: "Michael", age: 40 }, '("name" = ? and "age" = ?)', ["Michael", 40]],
|
||||||
@@ -94,64 +87,4 @@ describe("data-query-impl", () => {
|
|||||||
expect(keys).toEqual(expectedKeys);
|
expect(keys).toEqual(expectedKeys);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("with", () => {
|
|
||||||
decode({ with: ["posts"] }, { with: { posts: {} } });
|
|
||||||
decode({ with: { posts: {} } }, { with: { posts: {} } });
|
|
||||||
decode({ with: { posts: { limit: 1 } } }, { with: { posts: { limit: 1 } } });
|
|
||||||
decode(
|
|
||||||
{
|
|
||||||
with: {
|
|
||||||
posts: {
|
|
||||||
with: {
|
|
||||||
images: {
|
|
||||||
select: ["id"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
with: {
|
|
||||||
posts: {
|
|
||||||
with: {
|
|
||||||
images: {
|
|
||||||
select: ["id"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// over http
|
|
||||||
{
|
|
||||||
const output = { with: { images: {} } };
|
|
||||||
decode({ with: "images" }, output);
|
|
||||||
decode({ with: '["images"]' }, output);
|
|
||||||
decode({ with: ["images"] }, output);
|
|
||||||
decode({ with: { images: {} } }, output);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const output = { with: { images: {}, comments: {} } };
|
|
||||||
decode({ with: "images,comments" }, output);
|
|
||||||
decode({ with: ["images", "comments"] }, output);
|
|
||||||
decode({ with: '["images", "comments"]' }, output);
|
|
||||||
decode({ with: { images: {}, comments: {} } }, output);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("data-query-impl: Typebox", () => {
|
|
||||||
test("sort", async () => {
|
|
||||||
const _dflt = { sort: { by: "id", dir: "asc" } };
|
|
||||||
|
|
||||||
decode({ sort: "" }, _dflt);
|
|
||||||
decode({ sort: "name" }, { sort: { by: "name", dir: "asc" } });
|
|
||||||
decode({ sort: "-name" }, { sort: { by: "name", dir: "desc" } });
|
|
||||||
decode({ sort: "-posts.name" }, { sort: { by: "posts.name", dir: "desc" } });
|
|
||||||
decode({ sort: "-1name" }, _dflt);
|
|
||||||
decode({ sort: { by: "name", dir: "desc" } }, { sort: { by: "name", dir: "desc" } });
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
@@ -39,4 +39,28 @@ describe("[data] PrimaryField", async () => {
|
|||||||
expect(field.transformPersist(1)).rejects.toThrow();
|
expect(field.transformPersist(1)).rejects.toThrow();
|
||||||
expect(field.transformRetrieve(1)).toBe(1);
|
expect(field.transformRetrieve(1)).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("format", () => {
|
||||||
|
const uuid = new PrimaryField("uuid", { format: "uuid" });
|
||||||
|
expect(uuid.format).toBe("uuid");
|
||||||
|
expect(uuid.fieldType).toBe("text");
|
||||||
|
expect(uuid.getNewValue()).toBeString();
|
||||||
|
expect(uuid.toType()).toEqual({
|
||||||
|
required: true,
|
||||||
|
comment: undefined,
|
||||||
|
type: "Generated<string>",
|
||||||
|
import: [{ package: "kysely", name: "Generated" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const integer = new PrimaryField("integer", { format: "integer" });
|
||||||
|
expect(integer.format).toBe("integer");
|
||||||
|
expect(integer.fieldType).toBe("integer");
|
||||||
|
expect(integer.getNewValue()).toBeUndefined();
|
||||||
|
expect(integer.toType()).toEqual({
|
||||||
|
required: true,
|
||||||
|
comment: undefined,
|
||||||
|
type: "Generated<number>",
|
||||||
|
import: [{ package: "kysely", name: "Generated" }],
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -43,8 +43,9 @@ beforeAll(disableConsoleLog);
|
|||||||
afterAll(enableConsoleLog);
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
describe("MediaController", () => {
|
describe("MediaController", () => {
|
||||||
test.only("accepts direct", async () => {
|
test("accepts direct", async () => {
|
||||||
const app = await makeApp();
|
const app = await makeApp();
|
||||||
|
console.log("app", app);
|
||||||
|
|
||||||
const file = Bun.file(path);
|
const file = Bun.file(path);
|
||||||
const name = makeName("png");
|
const name = makeName("png");
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ describe("AppAuth", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await app.build();
|
await app.build();
|
||||||
|
app.registerAdminController();
|
||||||
const spy = spyOn(app.module.auth.authenticator, "requestCookieRefresh");
|
const spy = spyOn(app.module.auth.authenticator, "requestCookieRefresh");
|
||||||
|
|
||||||
// register custom route
|
// register custom route
|
||||||
@@ -162,6 +163,10 @@ describe("AppAuth", () => {
|
|||||||
await app.server.request("/api/system/ping");
|
await app.server.request("/api/system/ping");
|
||||||
await app.server.request("/test");
|
await app.server.request("/test");
|
||||||
|
|
||||||
|
expect(spy.mock.calls.length).toBe(0);
|
||||||
|
|
||||||
|
// admin route
|
||||||
|
await app.server.request("/");
|
||||||
expect(spy.mock.calls.length).toBe(1);
|
expect(spy.mock.calls.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import pkg from "./package.json" with { type: "json" };
|
||||||
|
import c from "picocolors";
|
||||||
|
import { formatNumber } from "core/utils";
|
||||||
|
|
||||||
|
const result = await Bun.build({
|
||||||
|
entrypoints: ["./src/cli/index.ts"],
|
||||||
|
target: "node",
|
||||||
|
outdir: "./dist/cli",
|
||||||
|
env: "PUBLIC_*",
|
||||||
|
minify: true,
|
||||||
|
define: {
|
||||||
|
__isDev: "0",
|
||||||
|
__version: JSON.stringify(pkg.version),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const output of result.outputs) {
|
||||||
|
const size_ = await output.text();
|
||||||
|
console.info(
|
||||||
|
c.cyan(formatNumber.fileSize(size_.length)),
|
||||||
|
c.dim(output.path.replace(import.meta.dir + "/", "")),
|
||||||
|
);
|
||||||
|
}
|
||||||
+17
-8
@@ -1,5 +1,6 @@
|
|||||||
import { $ } from "bun";
|
import { $ } from "bun";
|
||||||
import * as tsup from "tsup";
|
import * as tsup from "tsup";
|
||||||
|
import pkg from "./package.json" with { type: "json" };
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
const watch = args.includes("--watch");
|
const watch = args.includes("--watch");
|
||||||
@@ -8,8 +9,13 @@ const types = args.includes("--types");
|
|||||||
const sourcemap = args.includes("--sourcemap");
|
const sourcemap = args.includes("--sourcemap");
|
||||||
const clean = args.includes("--clean");
|
const clean = args.includes("--clean");
|
||||||
|
|
||||||
|
const define = {
|
||||||
|
__isDev: "0",
|
||||||
|
__version: JSON.stringify(pkg.version),
|
||||||
|
};
|
||||||
|
|
||||||
if (clean) {
|
if (clean) {
|
||||||
console.log("Cleaning dist (w/o static)");
|
console.info("Cleaning dist (w/o static)");
|
||||||
await $`find dist -mindepth 1 ! -path "dist/static/*" ! -path "dist/static" -exec rm -rf {} +`;
|
await $`find dist -mindepth 1 ! -path "dist/static/*" ! -path "dist/static" -exec rm -rf {} +`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,11 +27,11 @@ function buildTypes() {
|
|||||||
Bun.spawn(["bun", "build:types"], {
|
Bun.spawn(["bun", "build:types"], {
|
||||||
stdout: "inherit",
|
stdout: "inherit",
|
||||||
onExit: () => {
|
onExit: () => {
|
||||||
console.log("Types built");
|
console.info("Types built");
|
||||||
Bun.spawn(["bun", "tsc-alias"], {
|
Bun.spawn(["bun", "tsc-alias"], {
|
||||||
stdout: "inherit",
|
stdout: "inherit",
|
||||||
onExit: () => {
|
onExit: () => {
|
||||||
console.log("Types aliased");
|
console.info("Types aliased");
|
||||||
types_running = false;
|
types_running = false;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -47,10 +53,10 @@ if (types && !watch) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function banner(title: string) {
|
function banner(title: string) {
|
||||||
console.log("");
|
console.info("");
|
||||||
console.log("=".repeat(40));
|
console.info("=".repeat(40));
|
||||||
console.log(title.toUpperCase());
|
console.info(title.toUpperCase());
|
||||||
console.log("-".repeat(40));
|
console.info("-".repeat(40));
|
||||||
}
|
}
|
||||||
|
|
||||||
// collection of always-external packages
|
// collection of always-external packages
|
||||||
@@ -65,6 +71,7 @@ async function buildApi() {
|
|||||||
minify,
|
minify,
|
||||||
sourcemap,
|
sourcemap,
|
||||||
watch,
|
watch,
|
||||||
|
define,
|
||||||
entry: [
|
entry: [
|
||||||
"src/index.ts",
|
"src/index.ts",
|
||||||
"src/core/index.ts",
|
"src/core/index.ts",
|
||||||
@@ -101,6 +108,7 @@ async function buildUi() {
|
|||||||
minify,
|
minify,
|
||||||
sourcemap,
|
sourcemap,
|
||||||
watch,
|
watch,
|
||||||
|
define,
|
||||||
external: [
|
external: [
|
||||||
...external,
|
...external,
|
||||||
"react",
|
"react",
|
||||||
@@ -160,6 +168,7 @@ async function buildUiElements() {
|
|||||||
minify,
|
minify,
|
||||||
sourcemap,
|
sourcemap,
|
||||||
watch,
|
watch,
|
||||||
|
define,
|
||||||
entry: ["src/ui/elements/index.ts"],
|
entry: ["src/ui/elements/index.ts"],
|
||||||
outDir: "dist/ui/elements",
|
outDir: "dist/ui/elements",
|
||||||
external: [
|
external: [
|
||||||
@@ -211,7 +220,7 @@ function baseConfig(adapter: string, overrides: Partial<tsup.Options> = {}): tsu
|
|||||||
},
|
},
|
||||||
...overrides,
|
...overrides,
|
||||||
define: {
|
define: {
|
||||||
__isDev: "0",
|
...define,
|
||||||
...overrides.define,
|
...overrides.define,
|
||||||
},
|
},
|
||||||
external: [
|
external: [
|
||||||
|
|||||||
+15
-11
@@ -3,7 +3,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"bin": "./dist/cli/index.js",
|
"bin": "./dist/cli/index.js",
|
||||||
"version": "0.12.0",
|
"version": "0.14.0-rc.2",
|
||||||
"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": {
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
"build": "NODE_ENV=production bun run build.ts --minify --types",
|
"build": "NODE_ENV=production bun run build.ts --minify --types",
|
||||||
"build:all": "rm -rf dist && bun run build:static && NODE_ENV=production bun run build.ts --minify --types --clean && bun run build:cli",
|
"build:all": "rm -rf dist && bun run build:static && NODE_ENV=production bun run build.ts --minify --types --clean && bun run build:cli",
|
||||||
"build:ci": "mkdir -p dist/static/.vite && echo '{}' > dist/static/.vite/manifest.json && NODE_ENV=production bun run build.ts",
|
"build:ci": "mkdir -p dist/static/.vite && echo '{}' > dist/static/.vite/manifest.json && NODE_ENV=production bun run build.ts",
|
||||||
"build:cli": "bun build src/cli/index.ts --target node --outdir dist/cli --env PUBLIC_* --minify",
|
"build:cli": "bun run build.cli.ts",
|
||||||
"build:static": "vite build",
|
"build:static": "vite build",
|
||||||
"watch": "bun run build.ts --types --watch",
|
"watch": "bun run build.ts --types --watch",
|
||||||
"types": "bun tsc -p tsconfig.build.json --noEmit",
|
"types": "bun tsc -p tsconfig.build.json --noEmit",
|
||||||
@@ -49,9 +49,11 @@
|
|||||||
"@codemirror/lang-html": "^6.4.9",
|
"@codemirror/lang-html": "^6.4.9",
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
"@hello-pangea/dnd": "^18.0.1",
|
"@hello-pangea/dnd": "^18.0.1",
|
||||||
|
"@hono/swagger-ui": "^0.5.1",
|
||||||
"@libsql/client": "^0.15.2",
|
"@libsql/client": "^0.15.2",
|
||||||
"@mantine/core": "^7.17.1",
|
"@mantine/core": "^7.17.1",
|
||||||
"@mantine/hooks": "^7.17.1",
|
"@mantine/hooks": "^7.17.1",
|
||||||
|
"@sinclair/typebox": "0.34.30",
|
||||||
"@tanstack/react-form": "^1.0.5",
|
"@tanstack/react-form": "^1.0.5",
|
||||||
"@uiw/react-codemirror": "^4.23.10",
|
"@uiw/react-codemirror": "^4.23.10",
|
||||||
"@xyflow/react": "^12.4.4",
|
"@xyflow/react": "^12.4.4",
|
||||||
@@ -59,24 +61,24 @@
|
|||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"fast-xml-parser": "^5.0.8",
|
"fast-xml-parser": "^5.0.8",
|
||||||
"hono": "^4.7.4",
|
|
||||||
"json-schema-form-react": "^0.0.2",
|
"json-schema-form-react": "^0.0.2",
|
||||||
"json-schema-library": "10.0.0-rc7",
|
"json-schema-library": "10.0.0-rc7",
|
||||||
"json-schema-to-ts": "^3.1.1",
|
"json-schema-to-ts": "^3.1.1",
|
||||||
"kysely": "^0.27.6",
|
"kysely": "^0.27.6",
|
||||||
|
"hono": "^4.7.11",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
"oauth4webapi": "^2.11.1",
|
"oauth4webapi": "^2.11.1",
|
||||||
"object-path-immutable": "^4.1.2",
|
"object-path-immutable": "^4.1.2",
|
||||||
"radix-ui": "^1.1.3",
|
"radix-ui": "^1.1.3",
|
||||||
"swr": "^2.3.3",
|
"swr": "^2.3.3",
|
||||||
"lodash-es": "^4.17.21",
|
"uuid": "^11.1.0"
|
||||||
"@sinclair/typebox": "0.34.30"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.758.0",
|
"@aws-sdk/client-s3": "^3.758.0",
|
||||||
"@bluwy/giget-core": "^0.1.2",
|
"@bluwy/giget-core": "^0.1.2",
|
||||||
"@dagrejs/dagre": "^1.1.4",
|
"@dagrejs/dagre": "^1.1.4",
|
||||||
"@hono/typebox-validator": "^0.3.2",
|
"@hono/typebox-validator": "^0.3.3",
|
||||||
"@hono/vite-dev-server": "^0.19.0",
|
"@hono/vite-dev-server": "^0.19.1",
|
||||||
"@hookform/resolvers": "^4.1.3",
|
"@hookform/resolvers": "^4.1.3",
|
||||||
"@libsql/kysely-libsql": "^0.4.1",
|
"@libsql/kysely-libsql": "^0.4.1",
|
||||||
"@mantine/modals": "^7.17.1",
|
"@mantine/modals": "^7.17.1",
|
||||||
@@ -98,14 +100,15 @@
|
|||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"jotai": "^2.12.2",
|
"jotai": "^2.12.2",
|
||||||
"jsdom": "^26.0.0",
|
"jsdom": "^26.0.0",
|
||||||
|
"jsonv-ts": "^0.1.0",
|
||||||
"kysely-d1": "^0.3.0",
|
"kysely-d1": "^0.3.0",
|
||||||
"open": "^10.1.0",
|
"open": "^10.1.0",
|
||||||
"openapi-types": "^12.1.3",
|
"openapi-types": "^12.1.3",
|
||||||
|
"picocolors": "^1.1.1",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
"postcss-preset-mantine": "^1.17.0",
|
"postcss-preset-mantine": "^1.17.0",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
"posthog-js-lite": "^3.4.2",
|
"posthog-js-lite": "^3.4.2",
|
||||||
"picocolors": "^1.1.1",
|
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
@@ -118,13 +121,14 @@
|
|||||||
"tsc-alias": "^1.8.11",
|
"tsc-alias": "^1.8.11",
|
||||||
"tsup": "^8.4.0",
|
"tsup": "^8.4.0",
|
||||||
"tsx": "^4.19.3",
|
"tsx": "^4.19.3",
|
||||||
"vite": "^6.2.1",
|
"vite": "^6.3.5",
|
||||||
"vite-tsconfig-paths": "^5.1.4",
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
"vitest": "^3.0.9",
|
"vitest": "^3.0.9",
|
||||||
"wouter": "^3.6.0"
|
"wouter": "^3.6.0",
|
||||||
|
"@cloudflare/workers-types": "^4.20250606.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@hono/node-server": "^1.13.8"
|
"@hono/node-server": "^1.14.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": ">=19",
|
"react": ">=19",
|
||||||
|
|||||||
+72
-22
@@ -1,10 +1,11 @@
|
|||||||
import type { SafeUser } from "auth";
|
import type { SafeUser } from "auth";
|
||||||
import { AuthApi } from "auth/api/AuthApi";
|
import { AuthApi, type AuthApiOptions } from "auth/api/AuthApi";
|
||||||
import { DataApi } from "data/api/DataApi";
|
import { DataApi, type DataApiOptions } from "data/api/DataApi";
|
||||||
import { decode } from "hono/jwt";
|
import { decode } from "hono/jwt";
|
||||||
import { MediaApi } from "media/api/MediaApi";
|
import { MediaApi, type MediaApiOptions } from "media/api/MediaApi";
|
||||||
import { SystemApi } from "modules/SystemApi";
|
import { SystemApi } from "modules/SystemApi";
|
||||||
import { omitKeys } from "core/utils";
|
import { omitKeys } from "core/utils";
|
||||||
|
import type { BaseModuleApiOptions } from "modules";
|
||||||
|
|
||||||
export type TApiUser = SafeUser;
|
export type TApiUser = SafeUser;
|
||||||
|
|
||||||
@@ -21,14 +22,24 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SubApiOptions<T extends BaseModuleApiOptions> = Omit<T, keyof BaseModuleApiOptions>;
|
||||||
|
|
||||||
export type ApiOptions = {
|
export type ApiOptions = {
|
||||||
host?: string;
|
host?: string;
|
||||||
headers?: Headers;
|
headers?: Headers;
|
||||||
key?: string;
|
key?: string;
|
||||||
localStorage?: boolean;
|
storage?: {
|
||||||
|
getItem: (key: string) => string | undefined | null | Promise<string | undefined | null>;
|
||||||
|
setItem: (key: string, value: string) => void | Promise<void>;
|
||||||
|
removeItem: (key: string) => void | Promise<void>;
|
||||||
|
};
|
||||||
|
onAuthStateChange?: (state: AuthState) => void;
|
||||||
fetcher?: ApiFetcher;
|
fetcher?: ApiFetcher;
|
||||||
verbose?: boolean;
|
verbose?: boolean;
|
||||||
verified?: boolean;
|
verified?: boolean;
|
||||||
|
data?: SubApiOptions<DataApiOptions>;
|
||||||
|
auth?: SubApiOptions<AuthApiOptions>;
|
||||||
|
media?: SubApiOptions<MediaApiOptions>;
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
token?: string;
|
token?: string;
|
||||||
@@ -61,18 +72,18 @@ export class Api {
|
|||||||
this.verified = options.verified === true;
|
this.verified = options.verified === true;
|
||||||
|
|
||||||
// prefer request if given
|
// prefer request if given
|
||||||
if ("request" in options) {
|
if ("request" in options && options.request) {
|
||||||
this.options.host = options.host ?? new URL(options.request.url).origin;
|
this.options.host = options.host ?? new URL(options.request.url).origin;
|
||||||
this.options.headers = options.headers ?? options.request.headers;
|
this.options.headers = options.headers ?? options.request.headers;
|
||||||
this.extractToken();
|
this.extractToken();
|
||||||
|
|
||||||
// then check for a token
|
// then check for a token
|
||||||
} else if ("token" in options) {
|
} else if ("token" in options && options.token) {
|
||||||
this.token_transport = "header";
|
this.token_transport = "header";
|
||||||
this.updateToken(options.token);
|
this.updateToken(options.token, { trigger: false });
|
||||||
|
|
||||||
// then check for an user object
|
// then check for an user object
|
||||||
} else if ("user" in options) {
|
} else if ("user" in options && options.user) {
|
||||||
this.token_transport = "none";
|
this.token_transport = "none";
|
||||||
this.user = options.user;
|
this.user = options.user;
|
||||||
this.verified = options.verified !== false;
|
this.verified = options.verified !== false;
|
||||||
@@ -115,16 +126,30 @@ export class Api {
|
|||||||
this.updateToken(headerToken);
|
this.updateToken(headerToken);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (this.options.localStorage) {
|
} else if (this.storage) {
|
||||||
const token = localStorage.getItem(this.tokenKey);
|
this.storage.getItem(this.tokenKey).then((token) => {
|
||||||
if (token) {
|
|
||||||
this.token_transport = "header";
|
this.token_transport = "header";
|
||||||
this.updateToken(token);
|
this.updateToken(token ? String(token) : undefined);
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateToken(token?: string, rebuild?: boolean) {
|
private get storage() {
|
||||||
|
if (!this.options.storage) return null;
|
||||||
|
return {
|
||||||
|
getItem: async (key: string) => {
|
||||||
|
return await this.options.storage!.getItem(key);
|
||||||
|
},
|
||||||
|
setItem: async (key: string, value: string) => {
|
||||||
|
return await this.options.storage!.setItem(key, value);
|
||||||
|
},
|
||||||
|
removeItem: async (key: string) => {
|
||||||
|
return await this.options.storage!.removeItem(key);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
updateToken(token?: string, opts?: { rebuild?: boolean; trigger?: boolean }) {
|
||||||
this.token = token;
|
this.token = token;
|
||||||
this.verified = false;
|
this.verified = false;
|
||||||
|
|
||||||
@@ -134,17 +159,25 @@ export class Api {
|
|||||||
this.user = undefined;
|
this.user = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.options.localStorage) {
|
if (this.storage) {
|
||||||
const key = this.tokenKey;
|
const key = this.tokenKey;
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
localStorage.setItem(key, token);
|
this.storage.setItem(key, token).then(() => {
|
||||||
|
this.options.onAuthStateChange?.(this.getAuthState());
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem(key);
|
this.storage.removeItem(key).then(() => {
|
||||||
|
this.options.onAuthStateChange?.(this.getAuthState());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (opts?.trigger !== false) {
|
||||||
|
this.options.onAuthStateChange?.(this.getAuthState());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rebuild) this.buildApis();
|
if (opts?.rebuild) this.buildApis();
|
||||||
}
|
}
|
||||||
|
|
||||||
private markAuthVerified(verfied: boolean) {
|
private markAuthVerified(verfied: boolean) {
|
||||||
@@ -214,15 +247,32 @@ export class Api {
|
|||||||
const fetcher = this.options.fetcher;
|
const fetcher = this.options.fetcher;
|
||||||
|
|
||||||
this.system = new SystemApi(baseParams, fetcher);
|
this.system = new SystemApi(baseParams, fetcher);
|
||||||
this.data = new DataApi(baseParams, fetcher);
|
this.data = new DataApi(
|
||||||
this.auth = new AuthApi(
|
|
||||||
{
|
{
|
||||||
...baseParams,
|
...baseParams,
|
||||||
onTokenUpdate: (token) => this.updateToken(token, true),
|
...this.options.data,
|
||||||
|
},
|
||||||
|
fetcher,
|
||||||
|
);
|
||||||
|
this.auth = new AuthApi(
|
||||||
|
{
|
||||||
|
...baseParams,
|
||||||
|
credentials: this.options.storage ? "omit" : "include",
|
||||||
|
...this.options.auth,
|
||||||
|
onTokenUpdate: (token) => {
|
||||||
|
this.updateToken(token, { rebuild: true });
|
||||||
|
this.options.auth?.onTokenUpdate?.(token);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fetcher,
|
||||||
|
);
|
||||||
|
this.media = new MediaApi(
|
||||||
|
{
|
||||||
|
...baseParams,
|
||||||
|
...this.options.media,
|
||||||
},
|
},
|
||||||
fetcher,
|
fetcher,
|
||||||
);
|
);
|
||||||
this.media = new MediaApi(baseParams, fetcher);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -151,7 +151,7 @@ export class App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get fetch(): Hono["fetch"] {
|
get fetch(): Hono["fetch"] {
|
||||||
return this.server.fetch;
|
return this.server.fetch as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
get module() {
|
get module() {
|
||||||
@@ -253,6 +253,11 @@ export class App {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// call server init if set
|
||||||
|
if (this.options?.manager?.onServerInit) {
|
||||||
|
this.options.manager.onServerInit(server);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
||||||
import { makeApp } from "./modes/fresh";
|
import { makeApp } from "./modes/fresh";
|
||||||
import { makeConfig } from "./config";
|
import { makeConfig, type CfMakeConfigArgs } from "./config";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||||
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
||||||
import { bunTestRunner } from "adapter/bun/test";
|
import { bunTestRunner } from "adapter/bun/test";
|
||||||
@@ -23,7 +23,7 @@ describe("cf adapter", () => {
|
|||||||
{
|
{
|
||||||
connection: { url: DB_URL },
|
connection: { url: DB_URL },
|
||||||
},
|
},
|
||||||
{},
|
$ctx({ DB_URL }),
|
||||||
),
|
),
|
||||||
).toEqual({ connection: { url: DB_URL } });
|
).toEqual({ connection: { url: DB_URL } });
|
||||||
|
|
||||||
@@ -34,15 +34,15 @@ describe("cf adapter", () => {
|
|||||||
connection: { url: env.DB_URL },
|
connection: { url: env.DB_URL },
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
$ctx({ DB_URL }),
|
||||||
DB_URL,
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
).toEqual({ connection: { url: DB_URL } });
|
).toEqual({ connection: { url: DB_URL } });
|
||||||
});
|
});
|
||||||
|
|
||||||
adapterTestSuite<CloudflareBkndConfig, object>(bunTestRunner, {
|
adapterTestSuite<CloudflareBkndConfig, CfMakeConfigArgs<any>>(bunTestRunner, {
|
||||||
makeApp,
|
makeApp: async (c, a, o) => {
|
||||||
|
return await makeApp(c, { env: a } as any, o);
|
||||||
|
},
|
||||||
makeHandler: (c, a, o) => {
|
makeHandler: (c, a, o) => {
|
||||||
return async (request: any) => {
|
return async (request: any) => {
|
||||||
const app = await makeApp(
|
const app = await makeApp(
|
||||||
@@ -50,7 +50,7 @@ describe("cf adapter", () => {
|
|||||||
c ?? {
|
c ?? {
|
||||||
connection: { url: DB_URL },
|
connection: { url: DB_URL },
|
||||||
},
|
},
|
||||||
a,
|
a!,
|
||||||
o,
|
o,
|
||||||
);
|
);
|
||||||
return app.fetch(request);
|
return app.fetch(request);
|
||||||
|
|||||||
@@ -9,7 +9,13 @@ import { getDurable } from "./modes/durable";
|
|||||||
import type { App } from "bknd";
|
import type { App } from "bknd";
|
||||||
import { $console } from "core";
|
import { $console } from "core";
|
||||||
|
|
||||||
export type CloudflareEnv = object;
|
declare global {
|
||||||
|
namespace Cloudflare {
|
||||||
|
interface Env {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CloudflareEnv = Cloudflare.Env;
|
||||||
export type CloudflareBkndConfig<Env = CloudflareEnv> = RuntimeBkndConfig<Env> & {
|
export type CloudflareBkndConfig<Env = CloudflareEnv> = RuntimeBkndConfig<Env> & {
|
||||||
mode?: "warm" | "fresh" | "cache" | "durable";
|
mode?: "warm" | "fresh" | "cache" | "durable";
|
||||||
bindings?: (args: Env) => {
|
bindings?: (args: Env) => {
|
||||||
@@ -17,6 +23,11 @@ export type CloudflareBkndConfig<Env = CloudflareEnv> = RuntimeBkndConfig<Env> &
|
|||||||
dobj?: DurableObjectNamespace;
|
dobj?: DurableObjectNamespace;
|
||||||
db?: D1Database;
|
db?: D1Database;
|
||||||
};
|
};
|
||||||
|
d1?: {
|
||||||
|
session?: boolean;
|
||||||
|
transport?: "header" | "cookie";
|
||||||
|
first?: D1SessionConstraint;
|
||||||
|
};
|
||||||
static?: "kv" | "assets";
|
static?: "kv" | "assets";
|
||||||
key?: string;
|
key?: string;
|
||||||
keepAliveSeconds?: number;
|
keepAliveSeconds?: number;
|
||||||
|
|||||||
@@ -1,37 +1,117 @@
|
|||||||
|
/// <reference types="@cloudflare/workers-types" />
|
||||||
|
|
||||||
import { registerMedia } from "./storage/StorageR2Adapter";
|
import { registerMedia } from "./storage/StorageR2Adapter";
|
||||||
import { getBinding } from "./bindings";
|
import { getBinding } from "./bindings";
|
||||||
import { D1Connection } from "./D1Connection";
|
import { D1Connection } from "./connection/D1Connection";
|
||||||
import type { CloudflareBkndConfig, CloudflareEnv } from ".";
|
import type { CloudflareBkndConfig, CloudflareEnv } from ".";
|
||||||
import { App } from "bknd";
|
import { App } from "bknd";
|
||||||
import { makeConfig as makeAdapterConfig } from "bknd/adapter";
|
import { makeConfig as makeAdapterConfig } from "bknd/adapter";
|
||||||
import type { ExecutionContext } from "hono";
|
import type { Context, ExecutionContext } from "hono";
|
||||||
import { $console } from "core";
|
import { $console } from "core";
|
||||||
|
import { setCookie } from "hono/cookie";
|
||||||
|
|
||||||
export const constants = {
|
export const constants = {
|
||||||
exec_async_event_id: "cf_register_waituntil",
|
exec_async_event_id: "cf_register_waituntil",
|
||||||
cache_endpoint: "/__bknd/cache",
|
cache_endpoint: "/__bknd/cache",
|
||||||
do_endpoint: "/__bknd/do",
|
do_endpoint: "/__bknd/do",
|
||||||
|
d1_session: {
|
||||||
|
cookie: "cf_d1_session",
|
||||||
|
header: "x-cf-d1-session",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CfMakeConfigArgs<Env extends CloudflareEnv = CloudflareEnv> = {
|
||||||
|
env: Env;
|
||||||
|
ctx?: ExecutionContext;
|
||||||
|
request?: Request;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getCookieValue(cookies: string | null, name: string) {
|
||||||
|
if (!cookies) return null;
|
||||||
|
|
||||||
|
for (const cookie of cookies.split("; ")) {
|
||||||
|
const [key, value] = cookie.split("=");
|
||||||
|
if (key === name && value) {
|
||||||
|
return decodeURIComponent(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function d1SessionHelper(config: CloudflareBkndConfig<any>) {
|
||||||
|
const headerKey = constants.d1_session.header;
|
||||||
|
const cookieKey = constants.d1_session.cookie;
|
||||||
|
const transport = config.d1?.transport;
|
||||||
|
|
||||||
|
return {
|
||||||
|
get: (request?: Request): D1SessionBookmark | undefined => {
|
||||||
|
if (!request || !config.d1?.session) return undefined;
|
||||||
|
|
||||||
|
if (!transport || transport === "cookie") {
|
||||||
|
const cookies = request.headers.get("Cookie");
|
||||||
|
if (cookies) {
|
||||||
|
const cookie = getCookieValue(cookies, cookieKey);
|
||||||
|
if (cookie) {
|
||||||
|
return cookie;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!transport || transport === "header") {
|
||||||
|
if (request.headers.has(headerKey)) {
|
||||||
|
return request.headers.get(headerKey) as any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
set: (c: Context, d1?: D1DatabaseSession) => {
|
||||||
|
if (!d1 || !config.d1?.session) return;
|
||||||
|
|
||||||
|
const session = d1.getBookmark();
|
||||||
|
if (session) {
|
||||||
|
if (!transport || transport === "header") {
|
||||||
|
c.header(headerKey, session);
|
||||||
|
}
|
||||||
|
if (!transport || transport === "cookie") {
|
||||||
|
setCookie(c, cookieKey, session, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: true,
|
||||||
|
sameSite: "Lax",
|
||||||
|
maxAge: 60 * 5, // 5 minutes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let media_registered: boolean = false;
|
let media_registered: boolean = false;
|
||||||
export function makeConfig<Env extends CloudflareEnv = CloudflareEnv>(
|
export function makeConfig<Env extends CloudflareEnv = CloudflareEnv>(
|
||||||
config: CloudflareBkndConfig<Env>,
|
config: CloudflareBkndConfig<Env>,
|
||||||
args: Env = {} as Env,
|
args?: CfMakeConfigArgs<Env>,
|
||||||
) {
|
) {
|
||||||
if (!media_registered) {
|
if (!media_registered) {
|
||||||
registerMedia(args as any);
|
registerMedia(args as any);
|
||||||
media_registered = true;
|
media_registered = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const appConfig = makeAdapterConfig(config, args);
|
const appConfig = makeAdapterConfig(config, args?.env);
|
||||||
const bindings = config.bindings?.(args);
|
|
||||||
|
if (args?.env) {
|
||||||
|
const bindings = config.bindings?.(args?.env);
|
||||||
|
|
||||||
|
const sessionHelper = d1SessionHelper(config);
|
||||||
|
const sessionId = sessionHelper.get(args.request);
|
||||||
|
let session: D1DatabaseSession | undefined;
|
||||||
|
|
||||||
if (!appConfig.connection) {
|
if (!appConfig.connection) {
|
||||||
let db: D1Database | undefined;
|
let db: D1Database | undefined;
|
||||||
if (bindings?.db) {
|
if (bindings?.db) {
|
||||||
$console.log("Using database from bindings");
|
$console.log("Using database from bindings");
|
||||||
db = bindings.db;
|
db = bindings.db;
|
||||||
} else if (Object.keys(args).length > 0) {
|
} else if (Object.keys(args).length > 0) {
|
||||||
const binding = getBinding(args, "D1Database");
|
const binding = getBinding(args.env, "D1Database");
|
||||||
if (binding) {
|
if (binding) {
|
||||||
$console.log(`Using database from env "${binding.key}"`);
|
$console.log(`Using database from env "${binding.key}"`);
|
||||||
db = binding.value;
|
db = binding.value;
|
||||||
@@ -39,12 +119,33 @@ export function makeConfig<Env extends CloudflareEnv = CloudflareEnv>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (db) {
|
if (db) {
|
||||||
|
if (config.d1?.session) {
|
||||||
|
session = db.withSession(sessionId ?? config.d1?.first);
|
||||||
|
appConfig.connection = new D1Connection({ binding: session });
|
||||||
|
} else {
|
||||||
appConfig.connection = new D1Connection({ binding: db });
|
appConfig.connection = new D1Connection({ binding: db });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error("No database connection given");
|
throw new Error("No database connection given");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.d1?.session) {
|
||||||
|
appConfig.options = {
|
||||||
|
...appConfig.options,
|
||||||
|
manager: {
|
||||||
|
...appConfig.options?.manager,
|
||||||
|
onServerInit: (server) => {
|
||||||
|
server.use(async (c, next) => {
|
||||||
|
sessionHelper.set(c, session);
|
||||||
|
await next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return appConfig;
|
return appConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+9
-6
@@ -5,8 +5,8 @@ import type { QB } from "data/connection/Connection";
|
|||||||
import { type DatabaseIntrospector, Kysely, ParseJSONResultsPlugin } from "kysely";
|
import { type DatabaseIntrospector, Kysely, ParseJSONResultsPlugin } from "kysely";
|
||||||
import { D1Dialect } from "kysely-d1";
|
import { D1Dialect } from "kysely-d1";
|
||||||
|
|
||||||
export type D1ConnectionConfig = {
|
export type D1ConnectionConfig<DB extends D1Database | D1DatabaseSession = D1Database> = {
|
||||||
binding: D1Database;
|
binding: DB;
|
||||||
};
|
};
|
||||||
|
|
||||||
class CustomD1Dialect extends D1Dialect {
|
class CustomD1Dialect extends D1Dialect {
|
||||||
@@ -17,22 +17,25 @@ class CustomD1Dialect extends D1Dialect {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class D1Connection extends SqliteConnection {
|
export class D1Connection<
|
||||||
|
DB extends D1Database | D1DatabaseSession = D1Database,
|
||||||
|
> extends SqliteConnection {
|
||||||
protected override readonly supported = {
|
protected override readonly supported = {
|
||||||
batching: true,
|
batching: true,
|
||||||
|
counts: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(private config: D1ConnectionConfig) {
|
constructor(private config: D1ConnectionConfig<DB>) {
|
||||||
const plugins = [new ParseJSONResultsPlugin()];
|
const plugins = [new ParseJSONResultsPlugin()];
|
||||||
|
|
||||||
const kysely = new Kysely({
|
const kysely = new Kysely({
|
||||||
dialect: new CustomD1Dialect({ database: config.binding }),
|
dialect: new CustomD1Dialect({ database: config.binding as D1Database }),
|
||||||
plugins,
|
plugins,
|
||||||
});
|
});
|
||||||
super(kysely, {}, plugins);
|
super(kysely, {}, plugins);
|
||||||
}
|
}
|
||||||
|
|
||||||
get client(): D1Database {
|
get client(): DB {
|
||||||
return this.config.binding;
|
return this.config.binding;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { D1Connection, type D1ConnectionConfig } from "./D1Connection";
|
import { D1Connection, type D1ConnectionConfig } from "./connection/D1Connection";
|
||||||
|
|
||||||
export * from "./cloudflare-workers.adapter";
|
export * from "./cloudflare-workers.adapter";
|
||||||
export { makeApp, getFresh } from "./modes/fresh";
|
export { makeApp, getFresh } from "./modes/fresh";
|
||||||
@@ -12,6 +12,7 @@ export {
|
|||||||
type GetBindingType,
|
type GetBindingType,
|
||||||
type BindingMap,
|
type BindingMap,
|
||||||
} from "./bindings";
|
} from "./bindings";
|
||||||
|
export { constants } from "./config";
|
||||||
|
|
||||||
export function d1(config: D1ConnectionConfig) {
|
export function d1(config: D1ConnectionConfig) {
|
||||||
return new D1Connection(config);
|
return new D1Connection(config);
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import { makeConfig, registerAsyncsExecutionContext, constants } from "../config
|
|||||||
|
|
||||||
export async function getCached<Env extends CloudflareEnv = CloudflareEnv>(
|
export async function getCached<Env extends CloudflareEnv = CloudflareEnv>(
|
||||||
config: CloudflareBkndConfig<Env>,
|
config: CloudflareBkndConfig<Env>,
|
||||||
{ env, ctx, ...args }: Context<Env>,
|
args: Context<Env>,
|
||||||
) {
|
) {
|
||||||
|
const { env, ctx } = args;
|
||||||
const { kv } = config.bindings?.(env)!;
|
const { kv } = config.bindings?.(env)!;
|
||||||
if (!kv) throw new Error("kv namespace is not defined in cloudflare.bindings");
|
if (!kv) throw new Error("kv namespace is not defined in cloudflare.bindings");
|
||||||
const key = config.key ?? "app";
|
const key = config.key ?? "app";
|
||||||
@@ -20,7 +21,7 @@ export async function getCached<Env extends CloudflareEnv = CloudflareEnv>(
|
|||||||
|
|
||||||
const app = await createRuntimeApp(
|
const app = await createRuntimeApp(
|
||||||
{
|
{
|
||||||
...makeConfig(config, env),
|
...makeConfig(config, args),
|
||||||
initialConfig,
|
initialConfig,
|
||||||
onBuilt: async (app) => {
|
onBuilt: async (app) => {
|
||||||
registerAsyncsExecutionContext(app, ctx);
|
registerAsyncsExecutionContext(app, ctx);
|
||||||
@@ -41,7 +42,7 @@ export async function getCached<Env extends CloudflareEnv = CloudflareEnv>(
|
|||||||
await config.beforeBuild?.(app);
|
await config.beforeBuild?.(app);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ env, ctx, ...args },
|
args,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!cachedConfig) {
|
if (!cachedConfig) {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
|
import { createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
|
||||||
import type { CloudflareBkndConfig, Context, CloudflareEnv } from "../index";
|
import type { CloudflareBkndConfig, Context, CloudflareEnv } from "../index";
|
||||||
import { makeConfig, registerAsyncsExecutionContext } from "../config";
|
import { makeConfig, registerAsyncsExecutionContext, type CfMakeConfigArgs } from "../config";
|
||||||
|
|
||||||
export async function makeApp<Env extends CloudflareEnv = CloudflareEnv>(
|
export async function makeApp<Env extends CloudflareEnv = CloudflareEnv>(
|
||||||
config: CloudflareBkndConfig<Env>,
|
config: CloudflareBkndConfig<Env>,
|
||||||
args: Env = {} as Env,
|
args?: CfMakeConfigArgs<Env>,
|
||||||
opts?: RuntimeOptions,
|
opts?: RuntimeOptions,
|
||||||
) {
|
) {
|
||||||
return await createRuntimeApp<Env>(makeConfig(config, args), args, opts);
|
return await createRuntimeApp<Env>(makeConfig(config, args), args?.env, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFresh<Env extends CloudflareEnv = CloudflareEnv>(
|
export async function getFresh<Env extends CloudflareEnv = CloudflareEnv>(
|
||||||
@@ -23,7 +23,7 @@ export async function getFresh<Env extends CloudflareEnv = CloudflareEnv>(
|
|||||||
await config.onBuilt?.(app);
|
await config.onBuilt?.(app);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ctx.env,
|
ctx,
|
||||||
opts,
|
opts,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { isNode } from "bknd/utils";
|
|||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
type NextjsEnv = NextApiRequest["env"];
|
type NextjsEnv = NextApiRequest["env"];
|
||||||
|
|
||||||
export type NextjsBkndConfig<Env = NextjsEnv> = FrameworkBkndConfig<Env> & {
|
export type NextjsBkndConfig<Env = NextjsEnv> = FrameworkBkndConfig<Env> & {
|
||||||
cleanRequest?: { searchParams?: string[] };
|
cleanRequest?: { searchParams?: string[] };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,19 +4,21 @@ import type { AuthResponse, SafeUser, Strategy } from "auth/authenticate/Authent
|
|||||||
import { type BaseModuleApiOptions, ModuleApi } from "modules/ModuleApi";
|
import { type BaseModuleApiOptions, ModuleApi } from "modules/ModuleApi";
|
||||||
|
|
||||||
export type AuthApiOptions = BaseModuleApiOptions & {
|
export type AuthApiOptions = BaseModuleApiOptions & {
|
||||||
onTokenUpdate?: (token: string) => void | Promise<void>;
|
onTokenUpdate?: (token?: string) => void | Promise<void>;
|
||||||
|
credentials?: "include" | "same-origin" | "omit";
|
||||||
};
|
};
|
||||||
|
|
||||||
export class AuthApi extends ModuleApi<AuthApiOptions> {
|
export class AuthApi extends ModuleApi<AuthApiOptions> {
|
||||||
protected override getDefaultOptions(): Partial<AuthApiOptions> {
|
protected override getDefaultOptions(): Partial<AuthApiOptions> {
|
||||||
return {
|
return {
|
||||||
basepath: "/api/auth",
|
basepath: "/api/auth",
|
||||||
|
credentials: "include",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async login(strategy: string, input: any) {
|
async login(strategy: string, input: any) {
|
||||||
const res = await this.post<AuthResponse>([strategy, "login"], input, {
|
const res = await this.post<AuthResponse>([strategy, "login"], input, {
|
||||||
credentials: "include",
|
credentials: this.options.credentials,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok && res.body.token) {
|
if (res.ok && res.body.token) {
|
||||||
@@ -27,7 +29,7 @@ export class AuthApi extends ModuleApi<AuthApiOptions> {
|
|||||||
|
|
||||||
async register(strategy: string, input: any) {
|
async register(strategy: string, input: any) {
|
||||||
const res = await this.post<AuthResponse>([strategy, "register"], input, {
|
const res = await this.post<AuthResponse>([strategy, "register"], input, {
|
||||||
credentials: "include",
|
credentials: this.options.credentials,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok && res.body.token) {
|
if (res.ok && res.body.token) {
|
||||||
@@ -68,5 +70,7 @@ export class AuthApi extends ModuleApi<AuthApiOptions> {
|
|||||||
return this.get<Pick<AppAuthSchema, "strategies" | "basepath">>(["strategies"]);
|
return this.get<Pick<AppAuthSchema, "strategies" | "basepath">>(["strategies"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async logout() {}
|
async logout() {
|
||||||
|
await this.options.onTokenUpdate?.(undefined);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { type AppAuth, AuthPermissions, type SafeUser, type Strategy } from "auth";
|
import { type AppAuth, AuthPermissions, type SafeUser, type Strategy } from "auth";
|
||||||
import { tbValidator as tb } from "core";
|
|
||||||
import { TypeInvalidError, parse, transformObject } from "core/utils";
|
import { TypeInvalidError, parse, transformObject } from "core/utils";
|
||||||
import { DataPermissions } from "data";
|
import { DataPermissions } from "data";
|
||||||
import type { Hono } from "hono";
|
import type { Hono } from "hono";
|
||||||
import { Controller, type ServerEnv } from "modules/Controller";
|
import { Controller, type ServerEnv } from "modules/Controller";
|
||||||
import * as tbbox from "@sinclair/typebox";
|
import { describeRoute, jsc, s } from "core/object/schema";
|
||||||
const { Type } = tbbox;
|
|
||||||
|
|
||||||
export type AuthActionResponse = {
|
export type AuthActionResponse = {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@@ -14,10 +12,6 @@ export type AuthActionResponse = {
|
|||||||
errors?: any;
|
errors?: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
const booleanLike = Type.Transform(Type.String())
|
|
||||||
.Decode((v) => v === "1")
|
|
||||||
.Encode((v) => (v ? "1" : "0"));
|
|
||||||
|
|
||||||
export class AuthController extends Controller {
|
export class AuthController extends Controller {
|
||||||
constructor(private auth: AppAuth) {
|
constructor(private auth: AppAuth) {
|
||||||
super();
|
super();
|
||||||
@@ -56,6 +50,10 @@ export class AuthController extends Controller {
|
|||||||
hono.post(
|
hono.post(
|
||||||
"/create",
|
"/create",
|
||||||
permission([AuthPermissions.createUser, DataPermissions.entityCreate]),
|
permission([AuthPermissions.createUser, DataPermissions.entityCreate]),
|
||||||
|
describeRoute({
|
||||||
|
summary: "Create a new user",
|
||||||
|
tags: ["auth"],
|
||||||
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
try {
|
try {
|
||||||
const body = await this.auth.authenticator.getBody(c);
|
const body = await this.auth.authenticator.getBody(c);
|
||||||
@@ -93,9 +91,16 @@ export class AuthController extends Controller {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
hono.get("create/schema.json", async (c) => {
|
hono.get(
|
||||||
|
"create/schema.json",
|
||||||
|
describeRoute({
|
||||||
|
summary: "Get the schema for creating a user",
|
||||||
|
tags: ["auth"],
|
||||||
|
}),
|
||||||
|
async (c) => {
|
||||||
return c.json(create.schema);
|
return c.json(create.schema);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
mainHono.route(`/${name}/actions`, hono);
|
mainHono.route(`/${name}/actions`, hono);
|
||||||
@@ -104,26 +109,34 @@ export class AuthController extends Controller {
|
|||||||
override getController() {
|
override getController() {
|
||||||
const { auth } = this.middlewares;
|
const { auth } = this.middlewares;
|
||||||
const hono = this.create();
|
const hono = this.create();
|
||||||
const strategies = this.auth.authenticator.getStrategies();
|
|
||||||
|
|
||||||
for (const [name, strategy] of Object.entries(strategies)) {
|
hono.get(
|
||||||
if (!this.auth.isStrategyEnabled(strategy)) continue;
|
"/me",
|
||||||
|
describeRoute({
|
||||||
hono.route(`/${name}`, strategy.getController(this.auth.authenticator));
|
summary: "Get the current user",
|
||||||
this.registerStrategyActions(strategy, hono);
|
tags: ["auth"],
|
||||||
}
|
}),
|
||||||
|
auth(),
|
||||||
hono.get("/me", auth(), async (c) => {
|
async (c) => {
|
||||||
const claims = c.get("auth")?.user;
|
const claims = c.get("auth")?.user;
|
||||||
if (claims) {
|
if (claims) {
|
||||||
const { data: user } = await this.userRepo.findId(claims.id);
|
const { data: user } = await this.userRepo.findId(claims.id);
|
||||||
|
await this.auth.authenticator?.requestCookieRefresh(c);
|
||||||
return c.json({ user });
|
return c.json({ user });
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.json({ user: null }, 403);
|
return c.json({ user: null }, 403);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
hono.get("/logout", auth(), async (c) => {
|
hono.get(
|
||||||
|
"/logout",
|
||||||
|
describeRoute({
|
||||||
|
summary: "Logout the current user",
|
||||||
|
tags: ["auth"],
|
||||||
|
}),
|
||||||
|
auth(),
|
||||||
|
async (c) => {
|
||||||
await this.auth.authenticator.logout(c);
|
await this.auth.authenticator.logout(c);
|
||||||
if (this.auth.authenticator.isJsonRequest(c)) {
|
if (this.auth.authenticator.isJsonRequest(c)) {
|
||||||
return c.json({ ok: true });
|
return c.json({ ok: true });
|
||||||
@@ -135,11 +148,16 @@ export class AuthController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return c.redirect("/");
|
return c.redirect("/");
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
hono.get(
|
hono.get(
|
||||||
"/strategies",
|
"/strategies",
|
||||||
tb("query", Type.Object({ include_disabled: Type.Optional(booleanLike) })),
|
describeRoute({
|
||||||
|
summary: "Get the available authentication strategies",
|
||||||
|
tags: ["auth"],
|
||||||
|
}),
|
||||||
|
jsc("query", s.object({ include_disabled: s.boolean().optional() })),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const { include_disabled } = c.req.valid("query");
|
const { include_disabled } = c.req.valid("query");
|
||||||
const { strategies, basepath } = this.auth.toJSON(false);
|
const { strategies, basepath } = this.auth.toJSON(false);
|
||||||
@@ -157,6 +175,15 @@ export class AuthController extends Controller {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const strategies = this.auth.authenticator.getStrategies();
|
||||||
|
|
||||||
|
for (const [name, strategy] of Object.entries(strategies)) {
|
||||||
|
if (!this.auth.isStrategyEnabled(strategy)) continue;
|
||||||
|
|
||||||
|
hono.route(`/${name}`, strategy.getController(this.auth.authenticator));
|
||||||
|
this.registerStrategyActions(strategy, hono);
|
||||||
|
}
|
||||||
|
|
||||||
return hono.all("*", (c) => c.notFound());
|
return hono.all("*", (c) => c.notFound());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -347,6 +347,7 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
|
|||||||
}
|
}
|
||||||
|
|
||||||
async logout(c: Context<ServerEnv>) {
|
async logout(c: Context<ServerEnv>) {
|
||||||
|
$console.info("Logging out");
|
||||||
c.set("auth", undefined);
|
c.set("auth", undefined);
|
||||||
|
|
||||||
const cookie = await this.getAuthCookie(c);
|
const cookie = await this.getAuthCookie(c);
|
||||||
|
|||||||
@@ -60,11 +60,7 @@ export const auth = (options?: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await next();
|
await next();
|
||||||
|
// @todo: potentially add cookie refresh if content-type html and about to expire
|
||||||
if (!skipped) {
|
|
||||||
// renew cookie if applicable
|
|
||||||
authenticator?.requestCookieRefresh(c);
|
|
||||||
}
|
|
||||||
|
|
||||||
// release
|
// release
|
||||||
authCtx.skip = false;
|
authCtx.skip = false;
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
*/
|
*/
|
||||||
import type { Generated } from "kysely";
|
import type { Generated } from "kysely";
|
||||||
|
|
||||||
export type PrimaryFieldType<IdType extends number = number> = IdType | Generated<IdType>;
|
export type PrimaryFieldType<IdType = number | string> = IdType | Generated<IdType>;
|
||||||
|
|
||||||
export interface AppEntity<IdType extends number = number> {
|
export interface AppEntity<IdType = number | string> {
|
||||||
id: PrimaryFieldType<IdType>;
|
id: PrimaryFieldType<IdType>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ function hasColors() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const __consoles = {
|
const __consoles = {
|
||||||
|
critical: {
|
||||||
|
prefix: "CRT",
|
||||||
|
color: colors.red,
|
||||||
|
args_color: colors.red,
|
||||||
|
original: console.error,
|
||||||
|
},
|
||||||
error: {
|
error: {
|
||||||
prefix: "ERR",
|
prefix: "ERR",
|
||||||
color: colors.red,
|
color: colors.red,
|
||||||
|
|||||||
@@ -13,6 +13,15 @@ export function isDebug(): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getVersion(): string {
|
||||||
|
try {
|
||||||
|
// @ts-expect-error - this is a global variable in dev
|
||||||
|
return __version;
|
||||||
|
} catch (e) {
|
||||||
|
return "0.0.0";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const envs = {
|
const envs = {
|
||||||
// used in $console to determine the log level
|
// used in $console to determine the log level
|
||||||
cli_log_level: {
|
cli_log_level: {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export {
|
|||||||
} from "./object/query/query";
|
} from "./object/query/query";
|
||||||
export { Registry, type Constructor } from "./registry/Registry";
|
export { Registry, type Constructor } from "./registry/Registry";
|
||||||
export { getFlashMessage } from "./server/flash";
|
export { getFlashMessage } from "./server/flash";
|
||||||
|
export { s, jsc, describeRoute } from "./object/schema";
|
||||||
|
|
||||||
export * from "./console";
|
export * from "./console";
|
||||||
export * from "./events";
|
export * from "./events";
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ type ExpressionMap<Exps extends Expressions> = {
|
|||||||
? E
|
? E
|
||||||
: never;
|
: never;
|
||||||
};
|
};
|
||||||
|
type ExpressionKeys<Exps extends Expressions> = Exps[number]["key"];
|
||||||
|
|
||||||
type ExpressionCondition<Exps extends Expressions> = {
|
type ExpressionCondition<Exps extends Expressions> = {
|
||||||
[K in keyof ExpressionMap<Exps>]: { [P in K]: ExpressionMap<Exps>[K] };
|
[K in keyof ExpressionMap<Exps>]: { [P in K]: ExpressionMap<Exps>[K] };
|
||||||
}[keyof ExpressionMap<Exps>];
|
}[keyof ExpressionMap<Exps>];
|
||||||
@@ -195,5 +197,7 @@ export function makeValidator<Exps extends Expressions>(expressions: Exps) {
|
|||||||
const fns = _build(query, expressions, options);
|
const fns = _build(query, expressions, options);
|
||||||
return _validate(fns);
|
return _validate(fns);
|
||||||
},
|
},
|
||||||
|
expressions,
|
||||||
|
expressionKeys: expressions.map((e) => e.key) as ExpressionKeys<Exps>,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { mergeObject } from "core/utils";
|
||||||
|
|
||||||
|
//export { jsc, type Options, type Hook } from "./validator";
|
||||||
|
import * as s from "jsonv-ts";
|
||||||
|
|
||||||
|
export { validator as jsc, type Options } from "jsonv-ts/hono";
|
||||||
|
export { describeRoute, schemaToSpec, openAPISpecs } from "jsonv-ts/hono";
|
||||||
|
|
||||||
|
export { s };
|
||||||
|
|
||||||
|
export class InvalidSchemaError extends Error {
|
||||||
|
constructor(
|
||||||
|
public schema: s.TAnySchema,
|
||||||
|
public value: unknown,
|
||||||
|
public errors: s.ErrorDetail[] = [],
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
`Invalid schema given for ${JSON.stringify(value, null, 2)}\n\n` +
|
||||||
|
`Error: ${JSON.stringify(errors[0], null, 2)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ParseOptions = {
|
||||||
|
withDefaults?: boolean;
|
||||||
|
coerse?: boolean;
|
||||||
|
clone?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cloneSchema = <S extends s.TSchema>(schema: S): S => {
|
||||||
|
const json = schema.toJSON();
|
||||||
|
return s.fromSchema(json) as S;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function parse<S extends s.TAnySchema>(
|
||||||
|
_schema: S,
|
||||||
|
v: unknown,
|
||||||
|
opts: ParseOptions = {},
|
||||||
|
): s.StaticCoerced<S> {
|
||||||
|
const schema = (opts.clone ? cloneSchema(_schema as any) : _schema) as s.TSchema;
|
||||||
|
const value = opts.coerse !== false ? schema.coerce(v) : v;
|
||||||
|
const result = schema.validate(value, {
|
||||||
|
shortCircuit: true,
|
||||||
|
ignoreUnsupported: true,
|
||||||
|
});
|
||||||
|
if (!result.valid) throw new InvalidSchemaError(schema, v, result.errors);
|
||||||
|
if (opts.withDefaults) {
|
||||||
|
return mergeObject(schema.template({ withOptional: true }), value) as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value as any;
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import type { Context, Env, Input, MiddlewareHandler, ValidationTargets } from "hono";
|
||||||
|
import { validator as honoValidator } from "hono/validator";
|
||||||
|
import type { Static, StaticCoerced, TAnySchema } from "jsonv-ts";
|
||||||
|
|
||||||
|
export type Options = {
|
||||||
|
coerce?: boolean;
|
||||||
|
includeSchema?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ValidationResult = {
|
||||||
|
valid: boolean;
|
||||||
|
errors: {
|
||||||
|
keywordLocation: string;
|
||||||
|
instanceLocation: string;
|
||||||
|
error: string;
|
||||||
|
data?: unknown;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Hook<T, E extends Env, P extends string> = (
|
||||||
|
result: { result: ValidationResult; data: T },
|
||||||
|
c: Context<E, P>,
|
||||||
|
) => Response | Promise<Response> | void;
|
||||||
|
|
||||||
|
export const validator = <
|
||||||
|
// @todo: somehow hono prevents the usage of TSchema
|
||||||
|
Schema extends TAnySchema,
|
||||||
|
Target extends keyof ValidationTargets,
|
||||||
|
E extends Env,
|
||||||
|
P extends string,
|
||||||
|
Opts extends Options = Options,
|
||||||
|
Out = Opts extends { coerce: false } ? Static<Schema> : StaticCoerced<Schema>,
|
||||||
|
I extends Input = {
|
||||||
|
in: { [K in Target]: Static<Schema> };
|
||||||
|
out: { [K in Target]: Out };
|
||||||
|
},
|
||||||
|
>(
|
||||||
|
target: Target,
|
||||||
|
schema: Schema,
|
||||||
|
options?: Opts,
|
||||||
|
hook?: Hook<Out, E, P>,
|
||||||
|
): MiddlewareHandler<E, P, I> => {
|
||||||
|
// @ts-expect-error not typed well
|
||||||
|
return honoValidator(target, async (_value, c) => {
|
||||||
|
const value = options?.coerce !== false ? schema.coerce(_value) : _value;
|
||||||
|
// @ts-ignore
|
||||||
|
const result = schema.validate(value);
|
||||||
|
if (!result.valid) {
|
||||||
|
return c.json({ ...result, schema }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hook) {
|
||||||
|
const hookResult = hook({ result, data: value as Out }, c);
|
||||||
|
if (hookResult) {
|
||||||
|
return hookResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value as Out;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const jsc = validator;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { tbValidator } from "./tbValidator";
|
||||||
@@ -117,7 +117,9 @@ async function detectMimeType(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFileFromContext(c: Context<any>): Promise<File> {
|
type HonoAnyContext = Context<any, any, any>;
|
||||||
|
|
||||||
|
export async function getFileFromContext(c: HonoAnyContext): Promise<File> {
|
||||||
const contentType = c.req.header("Content-Type") ?? "application/octet-stream";
|
const contentType = c.req.header("Content-Type") ?? "application/octet-stream";
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -149,7 +151,7 @@ export async function getFileFromContext(c: Context<any>): Promise<File> {
|
|||||||
throw new Error("No file found in request");
|
throw new Error("No file found in request");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getBodyFromContext(c: Context<any>): Promise<ReadableStream | File> {
|
export async function getBodyFromContext(c: HonoAnyContext): Promise<ReadableStream | File> {
|
||||||
const contentType = c.req.header("Content-Type") ?? "application/octet-stream";
|
const contentType = c.req.header("Content-Type") ?? "application/octet-stream";
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -406,3 +406,16 @@ export function objectToJsLiteral(value: object, indent: number = 0, _level: num
|
|||||||
|
|
||||||
throw new TypeError(`Unsupported data type: ${t}`);
|
throw new TypeError(`Unsupported data type: ${t}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// lodash-es compatible `pick` with perfect type inference
|
||||||
|
export function pick<T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
|
||||||
|
return keys.reduce(
|
||||||
|
(acc, key) => {
|
||||||
|
if (key in obj) {
|
||||||
|
acc[key] = obj[key];
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Pick<T, K>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export function disableConsoleLog(severities: ConsoleSeverity[] = ["log", "warn"
|
|||||||
severities.forEach((severity) => {
|
severities.forEach((severity) => {
|
||||||
console[severity] = () => null;
|
console[severity] = () => null;
|
||||||
});
|
});
|
||||||
$console.setLevel("error");
|
$console.setLevel("critical");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function enableConsoleLog() {
|
export function enableConsoleLog() {
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
|
import { v4, v7 } from "uuid";
|
||||||
|
|
||||||
// generates v4
|
// generates v4
|
||||||
export function uuid(): string {
|
export function uuid(): string {
|
||||||
return crypto.randomUUID();
|
return v4();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uuidv7(): string {
|
||||||
|
return v7();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { $console, isDebug, tbValidator as tb } from "core";
|
import { $console, isDebug } from "core";
|
||||||
import { StringEnum } from "core/utils";
|
|
||||||
import * as tbbox from "@sinclair/typebox";
|
|
||||||
import {
|
import {
|
||||||
DataPermissions,
|
DataPermissions,
|
||||||
type EntityData,
|
type EntityData,
|
||||||
@@ -8,14 +6,15 @@ import {
|
|||||||
type MutatorResponse,
|
type MutatorResponse,
|
||||||
type RepoQuery,
|
type RepoQuery,
|
||||||
type RepositoryResponse,
|
type RepositoryResponse,
|
||||||
querySchema,
|
repoQuery,
|
||||||
} from "data";
|
} from "data";
|
||||||
import type { Handler } from "hono/types";
|
import type { Handler } from "hono/types";
|
||||||
import type { ModuleBuildContext } from "modules";
|
import type { ModuleBuildContext } from "modules";
|
||||||
import { Controller } from "modules/Controller";
|
import { Controller } from "modules/Controller";
|
||||||
|
import { jsc, s, describeRoute, schemaToSpec } from "core/object/schema";
|
||||||
import * as SystemPermissions from "modules/permissions";
|
import * as SystemPermissions from "modules/permissions";
|
||||||
import type { AppDataConfig } from "../data-schema";
|
import type { AppDataConfig } from "../data-schema";
|
||||||
const { Type } = tbbox;
|
import { omitKeys } from "core/utils";
|
||||||
|
|
||||||
export class DataController extends Controller {
|
export class DataController extends Controller {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -71,6 +70,7 @@ export class DataController extends Controller {
|
|||||||
override getController() {
|
override getController() {
|
||||||
const { permission, auth } = this.middlewares;
|
const { permission, auth } = this.middlewares;
|
||||||
const hono = this.create().use(auth(), permission(SystemPermissions.accessApi));
|
const hono = this.create().use(auth(), permission(SystemPermissions.accessApi));
|
||||||
|
const entitiesEnum = this.getEntitiesEnum(this.em);
|
||||||
|
|
||||||
// @todo: sample implementation how to augment handler with additional info
|
// @todo: sample implementation how to augment handler with additional info
|
||||||
function handler<HH extends Handler>(name: string, h: HH): any {
|
function handler<HH extends Handler>(name: string, h: HH): any {
|
||||||
@@ -83,6 +83,10 @@ export class DataController extends Controller {
|
|||||||
// info
|
// info
|
||||||
hono.get(
|
hono.get(
|
||||||
"/",
|
"/",
|
||||||
|
describeRoute({
|
||||||
|
summary: "Retrieve data configuration",
|
||||||
|
tags: ["data"],
|
||||||
|
}),
|
||||||
handler("data info", (c) => {
|
handler("data info", (c) => {
|
||||||
// sample implementation
|
// sample implementation
|
||||||
return c.json(this.em.toJSON());
|
return c.json(this.em.toJSON());
|
||||||
@@ -90,9 +94,22 @@ export class DataController extends Controller {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// sync endpoint
|
// sync endpoint
|
||||||
hono.get("/sync", permission(DataPermissions.databaseSync), async (c) => {
|
hono.get(
|
||||||
const force = c.req.query("force") === "1";
|
"/sync",
|
||||||
const drop = c.req.query("drop") === "1";
|
permission(DataPermissions.databaseSync),
|
||||||
|
describeRoute({
|
||||||
|
summary: "Sync database schema",
|
||||||
|
tags: ["data"],
|
||||||
|
}),
|
||||||
|
jsc(
|
||||||
|
"query",
|
||||||
|
s.partialObject({
|
||||||
|
force: s.boolean(),
|
||||||
|
drop: s.boolean(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
async (c) => {
|
||||||
|
const { force, drop } = c.req.valid("query");
|
||||||
//console.log("force", force);
|
//console.log("force", force);
|
||||||
const tables = await this.em.schema().introspect();
|
const tables = await this.em.schema().introspect();
|
||||||
//console.log("tables", tables);
|
//console.log("tables", tables);
|
||||||
@@ -101,13 +118,21 @@ export class DataController extends Controller {
|
|||||||
drop,
|
drop,
|
||||||
});
|
});
|
||||||
return c.json({ tables: tables.map((t) => t.name), changes });
|
return c.json({ tables: tables.map((t) => t.name), changes });
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schema endpoints
|
* Schema endpoints
|
||||||
*/
|
*/
|
||||||
// read entity schema
|
// read entity schema
|
||||||
hono.get("/schema.json", permission(DataPermissions.entityRead), async (c) => {
|
hono.get(
|
||||||
|
"/schema.json",
|
||||||
|
permission(DataPermissions.entityRead),
|
||||||
|
describeRoute({
|
||||||
|
summary: "Retrieve data schema",
|
||||||
|
tags: ["data"],
|
||||||
|
}),
|
||||||
|
async (c) => {
|
||||||
const $id = `${this.config.basepath}/schema.json`;
|
const $id = `${this.config.basepath}/schema.json`;
|
||||||
const schemas = Object.fromEntries(
|
const schemas = Object.fromEntries(
|
||||||
this.em.entities.map((e) => [
|
this.em.entities.map((e) => [
|
||||||
@@ -122,17 +147,22 @@ export class DataController extends Controller {
|
|||||||
$id,
|
$id,
|
||||||
properties: schemas,
|
properties: schemas,
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// read schema
|
// read schema
|
||||||
hono.get(
|
hono.get(
|
||||||
"/schemas/:entity/:context?",
|
"/schemas/:entity/:context?",
|
||||||
permission(DataPermissions.entityRead),
|
permission(DataPermissions.entityRead),
|
||||||
tb(
|
describeRoute({
|
||||||
|
summary: "Retrieve entity schema",
|
||||||
|
tags: ["data"],
|
||||||
|
}),
|
||||||
|
jsc(
|
||||||
"param",
|
"param",
|
||||||
Type.Object({
|
s.object({
|
||||||
entity: Type.String(),
|
entity: entitiesEnum,
|
||||||
context: Type.Optional(StringEnum(["create", "update"])),
|
context: s.string({ enum: ["create", "update"], default: "create" }).optional(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
@@ -161,7 +191,15 @@ export class DataController extends Controller {
|
|||||||
/**
|
/**
|
||||||
* Info endpoints
|
* Info endpoints
|
||||||
*/
|
*/
|
||||||
hono.get("/info/:entity", async (c) => {
|
hono.get(
|
||||||
|
"/info/:entity",
|
||||||
|
permission(DataPermissions.entityRead),
|
||||||
|
describeRoute({
|
||||||
|
summary: "Retrieve entity info",
|
||||||
|
tags: ["data"],
|
||||||
|
}),
|
||||||
|
jsc("param", s.object({ entity: entitiesEnum })),
|
||||||
|
async (c) => {
|
||||||
const { entity } = c.req.param();
|
const { entity } = c.req.param();
|
||||||
if (!this.entityExists(entity)) {
|
if (!this.entityExists(entity)) {
|
||||||
return this.notFound(c);
|
return this.notFound(c);
|
||||||
@@ -184,7 +222,8 @@ export class DataController extends Controller {
|
|||||||
target: $rels(this.em.relations.targetRelationsOf(_entity)),
|
target: $rels(this.em.relations.targetRelationsOf(_entity)),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return hono.all("*", (c) => c.notFound());
|
return hono.all("*", (c) => c.notFound());
|
||||||
}
|
}
|
||||||
@@ -193,10 +232,9 @@ export class DataController extends Controller {
|
|||||||
const { permission } = this.middlewares;
|
const { permission } = this.middlewares;
|
||||||
const hono = this.create();
|
const hono = this.create();
|
||||||
|
|
||||||
const definedEntities = this.em.entities.map((e) => e.name);
|
const entitiesEnum = this.getEntitiesEnum(this.em);
|
||||||
const tbNumber = Type.Transform(Type.String({ pattern: "^[1-9][0-9]{0,}$" }))
|
// @todo: make dynamic based on entity
|
||||||
.Decode(Number.parseInt)
|
const idType = s.anyOf([s.number(), s.string()], { coerce: (v) => v as any });
|
||||||
.Encode(String);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function endpoints
|
* Function endpoints
|
||||||
@@ -205,14 +243,19 @@ export class DataController extends Controller {
|
|||||||
hono.post(
|
hono.post(
|
||||||
"/:entity/fn/count",
|
"/:entity/fn/count",
|
||||||
permission(DataPermissions.entityRead),
|
permission(DataPermissions.entityRead),
|
||||||
tb("param", Type.Object({ entity: Type.String() })),
|
describeRoute({
|
||||||
|
summary: "Count entities",
|
||||||
|
tags: ["data"],
|
||||||
|
}),
|
||||||
|
jsc("param", s.object({ entity: entitiesEnum })),
|
||||||
|
jsc("json", repoQuery.properties.where),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const { entity } = c.req.valid("param");
|
const { entity } = c.req.valid("param");
|
||||||
if (!this.entityExists(entity)) {
|
if (!this.entityExists(entity)) {
|
||||||
return this.notFound(c);
|
return this.notFound(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
const where = (await c.req.json()) as any;
|
const where = c.req.valid("json") as any;
|
||||||
const result = await this.em.repository(entity).count(where);
|
const result = await this.em.repository(entity).count(where);
|
||||||
return c.json({ entity, count: result.count });
|
return c.json({ entity, count: result.count });
|
||||||
},
|
},
|
||||||
@@ -222,14 +265,19 @@ export class DataController extends Controller {
|
|||||||
hono.post(
|
hono.post(
|
||||||
"/:entity/fn/exists",
|
"/:entity/fn/exists",
|
||||||
permission(DataPermissions.entityRead),
|
permission(DataPermissions.entityRead),
|
||||||
tb("param", Type.Object({ entity: Type.String() })),
|
describeRoute({
|
||||||
|
summary: "Check if entity exists",
|
||||||
|
tags: ["data"],
|
||||||
|
}),
|
||||||
|
jsc("param", s.object({ entity: entitiesEnum })),
|
||||||
|
jsc("json", repoQuery.properties.where),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const { entity } = c.req.valid("param");
|
const { entity } = c.req.valid("param");
|
||||||
if (!this.entityExists(entity)) {
|
if (!this.entityExists(entity)) {
|
||||||
return this.notFound(c);
|
return this.notFound(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
const where = c.req.json() as any;
|
const where = c.req.valid("json") as any;
|
||||||
const result = await this.em.repository(entity).exists(where);
|
const result = await this.em.repository(entity).exists(where);
|
||||||
return c.json({ entity, exists: result.exists });
|
return c.json({ entity, exists: result.exists });
|
||||||
},
|
},
|
||||||
@@ -239,18 +287,36 @@ export class DataController extends Controller {
|
|||||||
* Read endpoints
|
* Read endpoints
|
||||||
*/
|
*/
|
||||||
// read many
|
// read many
|
||||||
|
const saveRepoQuery = s.partialObject({
|
||||||
|
...omitKeys(repoQuery.properties, ["with"]),
|
||||||
|
sort: s.string({ default: "id" }),
|
||||||
|
select: s.array(s.string()),
|
||||||
|
join: s.array(s.string()),
|
||||||
|
});
|
||||||
|
const saveRepoQueryParams = (pick: string[] = Object.keys(repoQuery.properties)) => [
|
||||||
|
...(schemaToSpec(saveRepoQuery, "query").parameters?.filter(
|
||||||
|
// @ts-ignore
|
||||||
|
(p) => pick.includes(p.name),
|
||||||
|
) as any),
|
||||||
|
];
|
||||||
|
|
||||||
hono.get(
|
hono.get(
|
||||||
"/:entity",
|
"/:entity",
|
||||||
|
describeRoute({
|
||||||
|
summary: "Read many",
|
||||||
|
parameters: saveRepoQueryParams(["limit", "offset", "sort", "select", "join"]),
|
||||||
|
tags: ["data"],
|
||||||
|
}),
|
||||||
permission(DataPermissions.entityRead),
|
permission(DataPermissions.entityRead),
|
||||||
tb("param", Type.Object({ entity: Type.String() })),
|
jsc("param", s.object({ entity: entitiesEnum })),
|
||||||
tb("query", querySchema),
|
jsc("query", repoQuery, { skipOpenAPI: true }),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const { entity } = c.req.param();
|
const { entity } = c.req.valid("param");
|
||||||
if (!this.entityExists(entity)) {
|
if (!this.entityExists(entity)) {
|
||||||
return this.notFound(c);
|
return this.notFound(c);
|
||||||
}
|
}
|
||||||
const options = c.req.valid("query") as RepoQuery;
|
const options = c.req.valid("query") as RepoQuery;
|
||||||
const result = await this.em.repository(entity).findMany(options);
|
const result = await this.em.repo(entity).findMany(options);
|
||||||
|
|
||||||
return c.json(this.repoResult(result), { status: result.data ? 200 : 404 });
|
return c.json(this.repoResult(result), { status: result.data ? 200 : 404 });
|
||||||
},
|
},
|
||||||
@@ -259,22 +325,27 @@ export class DataController extends Controller {
|
|||||||
// read one
|
// read one
|
||||||
hono.get(
|
hono.get(
|
||||||
"/:entity/:id",
|
"/:entity/:id",
|
||||||
|
describeRoute({
|
||||||
|
summary: "Read one",
|
||||||
|
parameters: saveRepoQueryParams(["offset", "sort", "select"]),
|
||||||
|
tags: ["data"],
|
||||||
|
}),
|
||||||
permission(DataPermissions.entityRead),
|
permission(DataPermissions.entityRead),
|
||||||
tb(
|
jsc(
|
||||||
"param",
|
"param",
|
||||||
Type.Object({
|
s.object({
|
||||||
entity: Type.String(),
|
entity: entitiesEnum,
|
||||||
id: tbNumber,
|
id: idType,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
tb("query", querySchema),
|
jsc("query", repoQuery, { skipOpenAPI: true }),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const { entity, id } = c.req.param();
|
const { entity, id } = c.req.valid("param");
|
||||||
if (!this.entityExists(entity)) {
|
if (!this.entityExists(entity)) {
|
||||||
return this.notFound(c);
|
return this.notFound(c);
|
||||||
}
|
}
|
||||||
const options = c.req.valid("query") as RepoQuery;
|
const options = c.req.valid("query") as RepoQuery;
|
||||||
const result = await this.em.repository(entity).findId(Number(id), options);
|
const result = await this.em.repository(entity).findId(id, options);
|
||||||
|
|
||||||
return c.json(this.repoResult(result), { status: result.data ? 200 : 404 });
|
return c.json(this.repoResult(result), { status: result.data ? 200 : 404 });
|
||||||
},
|
},
|
||||||
@@ -283,18 +354,23 @@ export class DataController extends Controller {
|
|||||||
// read many by reference
|
// read many by reference
|
||||||
hono.get(
|
hono.get(
|
||||||
"/:entity/:id/:reference",
|
"/:entity/:id/:reference",
|
||||||
|
describeRoute({
|
||||||
|
summary: "Read many by reference",
|
||||||
|
parameters: saveRepoQueryParams(),
|
||||||
|
tags: ["data"],
|
||||||
|
}),
|
||||||
permission(DataPermissions.entityRead),
|
permission(DataPermissions.entityRead),
|
||||||
tb(
|
jsc(
|
||||||
"param",
|
"param",
|
||||||
Type.Object({
|
s.object({
|
||||||
entity: Type.String(),
|
entity: entitiesEnum,
|
||||||
id: tbNumber,
|
id: idType,
|
||||||
reference: Type.String(),
|
reference: s.string(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
tb("query", querySchema),
|
jsc("query", repoQuery, { skipOpenAPI: true }),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const { entity, id, reference } = c.req.param();
|
const { entity, id, reference } = c.req.valid("param");
|
||||||
if (!this.entityExists(entity)) {
|
if (!this.entityExists(entity)) {
|
||||||
return this.notFound(c);
|
return this.notFound(c);
|
||||||
}
|
}
|
||||||
@@ -302,24 +378,40 @@ export class DataController extends Controller {
|
|||||||
const options = c.req.valid("query") as RepoQuery;
|
const options = c.req.valid("query") as RepoQuery;
|
||||||
const result = await this.em
|
const result = await this.em
|
||||||
.repository(entity)
|
.repository(entity)
|
||||||
.findManyByReference(Number(id), reference, options);
|
.findManyByReference(id, reference, options);
|
||||||
|
|
||||||
return c.json(this.repoResult(result), { status: result.data ? 200 : 404 });
|
return c.json(this.repoResult(result), { status: result.data ? 200 : 404 });
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// func query
|
// func query
|
||||||
|
const fnQuery = s.partialObject({
|
||||||
|
...saveRepoQuery.properties,
|
||||||
|
with: s.object({}),
|
||||||
|
});
|
||||||
hono.post(
|
hono.post(
|
||||||
"/:entity/query",
|
"/:entity/query",
|
||||||
|
describeRoute({
|
||||||
|
summary: "Query entities",
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: fnQuery.toJSON(),
|
||||||
|
example: fnQuery.template({ withOptional: true }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tags: ["data"],
|
||||||
|
}),
|
||||||
permission(DataPermissions.entityRead),
|
permission(DataPermissions.entityRead),
|
||||||
tb("param", Type.Object({ entity: Type.String() })),
|
jsc("param", s.object({ entity: entitiesEnum })),
|
||||||
tb("json", querySchema),
|
jsc("json", repoQuery, { skipOpenAPI: true }),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const { entity } = c.req.param();
|
const { entity } = c.req.valid("param");
|
||||||
if (!this.entityExists(entity)) {
|
if (!this.entityExists(entity)) {
|
||||||
return this.notFound(c);
|
return this.notFound(c);
|
||||||
}
|
}
|
||||||
const options = (await c.req.valid("json")) as RepoQuery;
|
const options = (await c.req.json()) as RepoQuery;
|
||||||
const result = await this.em.repository(entity).findMany(options);
|
const result = await this.em.repository(entity).findMany(options);
|
||||||
|
|
||||||
return c.json(this.repoResult(result), { status: result.data ? 200 : 404 });
|
return c.json(this.repoResult(result), { status: result.data ? 200 : 404 });
|
||||||
@@ -332,11 +424,15 @@ export class DataController extends Controller {
|
|||||||
// insert one
|
// insert one
|
||||||
hono.post(
|
hono.post(
|
||||||
"/:entity",
|
"/:entity",
|
||||||
|
describeRoute({
|
||||||
|
summary: "Insert one or many",
|
||||||
|
tags: ["data"],
|
||||||
|
}),
|
||||||
permission(DataPermissions.entityCreate),
|
permission(DataPermissions.entityCreate),
|
||||||
tb("param", Type.Object({ entity: Type.String() })),
|
jsc("param", s.object({ entity: entitiesEnum })),
|
||||||
tb("json", Type.Union([Type.Object({}), Type.Array(Type.Object({}))])),
|
jsc("json", s.anyOf([s.object({}), s.array(s.object({}))])),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const { entity } = c.req.param();
|
const { entity } = c.req.valid("param");
|
||||||
if (!this.entityExists(entity)) {
|
if (!this.entityExists(entity)) {
|
||||||
return this.notFound(c);
|
return this.notFound(c);
|
||||||
}
|
}
|
||||||
@@ -355,13 +451,17 @@ export class DataController extends Controller {
|
|||||||
// update many
|
// update many
|
||||||
hono.patch(
|
hono.patch(
|
||||||
"/:entity",
|
"/:entity",
|
||||||
|
describeRoute({
|
||||||
|
summary: "Update many",
|
||||||
|
tags: ["data"],
|
||||||
|
}),
|
||||||
permission(DataPermissions.entityUpdate),
|
permission(DataPermissions.entityUpdate),
|
||||||
tb("param", Type.Object({ entity: Type.String() })),
|
jsc("param", s.object({ entity: entitiesEnum })),
|
||||||
tb(
|
jsc(
|
||||||
"json",
|
"json",
|
||||||
Type.Object({
|
s.object({
|
||||||
update: Type.Object({}),
|
update: s.object({}),
|
||||||
where: querySchema.properties.where,
|
where: repoQuery.properties.where,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
@@ -382,15 +482,20 @@ export class DataController extends Controller {
|
|||||||
// update one
|
// update one
|
||||||
hono.patch(
|
hono.patch(
|
||||||
"/:entity/:id",
|
"/:entity/:id",
|
||||||
|
describeRoute({
|
||||||
|
summary: "Update one",
|
||||||
|
tags: ["data"],
|
||||||
|
}),
|
||||||
permission(DataPermissions.entityUpdate),
|
permission(DataPermissions.entityUpdate),
|
||||||
tb("param", Type.Object({ entity: Type.String(), id: tbNumber })),
|
jsc("param", s.object({ entity: entitiesEnum, id: idType })),
|
||||||
|
jsc("json", s.object({})),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const { entity, id } = c.req.param();
|
const { entity, id } = c.req.valid("param");
|
||||||
if (!this.entityExists(entity)) {
|
if (!this.entityExists(entity)) {
|
||||||
return this.notFound(c);
|
return this.notFound(c);
|
||||||
}
|
}
|
||||||
const body = (await c.req.json()) as EntityData;
|
const body = (await c.req.json()) as EntityData;
|
||||||
const result = await this.em.mutator(entity).updateOne(Number(id), body);
|
const result = await this.em.mutator(entity).updateOne(id, body);
|
||||||
|
|
||||||
return c.json(this.mutatorResult(result));
|
return c.json(this.mutatorResult(result));
|
||||||
},
|
},
|
||||||
@@ -399,14 +504,18 @@ export class DataController extends Controller {
|
|||||||
// delete one
|
// delete one
|
||||||
hono.delete(
|
hono.delete(
|
||||||
"/:entity/:id",
|
"/:entity/:id",
|
||||||
|
describeRoute({
|
||||||
|
summary: "Delete one",
|
||||||
|
tags: ["data"],
|
||||||
|
}),
|
||||||
permission(DataPermissions.entityDelete),
|
permission(DataPermissions.entityDelete),
|
||||||
tb("param", Type.Object({ entity: Type.String(), id: tbNumber })),
|
jsc("param", s.object({ entity: entitiesEnum, id: idType })),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const { entity, id } = c.req.param();
|
const { entity, id } = c.req.valid("param");
|
||||||
if (!this.entityExists(entity)) {
|
if (!this.entityExists(entity)) {
|
||||||
return this.notFound(c);
|
return this.notFound(c);
|
||||||
}
|
}
|
||||||
const result = await this.em.mutator(entity).deleteOne(Number(id));
|
const result = await this.em.mutator(entity).deleteOne(id);
|
||||||
|
|
||||||
return c.json(this.mutatorResult(result));
|
return c.json(this.mutatorResult(result));
|
||||||
},
|
},
|
||||||
@@ -415,15 +524,19 @@ export class DataController extends Controller {
|
|||||||
// delete many
|
// delete many
|
||||||
hono.delete(
|
hono.delete(
|
||||||
"/:entity",
|
"/:entity",
|
||||||
|
describeRoute({
|
||||||
|
summary: "Delete many",
|
||||||
|
tags: ["data"],
|
||||||
|
}),
|
||||||
permission(DataPermissions.entityDelete),
|
permission(DataPermissions.entityDelete),
|
||||||
tb("param", Type.Object({ entity: Type.String() })),
|
jsc("param", s.object({ entity: entitiesEnum })),
|
||||||
tb("json", querySchema.properties.where),
|
jsc("json", repoQuery.properties.where),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const { entity } = c.req.param();
|
const { entity } = c.req.valid("param");
|
||||||
if (!this.entityExists(entity)) {
|
if (!this.entityExists(entity)) {
|
||||||
return this.notFound(c);
|
return this.notFound(c);
|
||||||
}
|
}
|
||||||
const where = c.req.valid("json") as RepoQuery["where"];
|
const where = (await c.req.json()) as RepoQuery["where"];
|
||||||
const result = await this.em.mutator(entity).deleteWhere(where);
|
const result = await this.em.mutator(entity).deleteWhere(where);
|
||||||
|
|
||||||
return c.json(this.mutatorResult(result));
|
return c.json(this.mutatorResult(result));
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import {
|
|||||||
type AliasableExpression,
|
type AliasableExpression,
|
||||||
type ColumnBuilderCallback,
|
type ColumnBuilderCallback,
|
||||||
type ColumnDataType,
|
type ColumnDataType,
|
||||||
|
type DatabaseIntrospector,
|
||||||
|
type Dialect,
|
||||||
type Expression,
|
type Expression,
|
||||||
type Kysely,
|
type Kysely,
|
||||||
type KyselyPlugin,
|
type KyselyPlugin,
|
||||||
@@ -12,7 +14,8 @@ import {
|
|||||||
type Simplify,
|
type Simplify,
|
||||||
sql,
|
sql,
|
||||||
} from "kysely";
|
} from "kysely";
|
||||||
import type { BaseIntrospector } from "./BaseIntrospector";
|
import type { BaseIntrospector, BaseIntrospectorConfig } from "./BaseIntrospector";
|
||||||
|
import type { Constructor } from "core";
|
||||||
|
|
||||||
export type QB = SelectQueryBuilder<any, any, any>;
|
export type QB = SelectQueryBuilder<any, any, any>;
|
||||||
|
|
||||||
@@ -79,6 +82,7 @@ export abstract class Connection<DB = any> {
|
|||||||
kysely: Kysely<DB>;
|
kysely: Kysely<DB>;
|
||||||
protected readonly supported = {
|
protected readonly supported = {
|
||||||
batching: false,
|
batching: false,
|
||||||
|
counts: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -159,3 +163,19 @@ export abstract class Connection<DB = any> {
|
|||||||
// no-op by default
|
// no-op by default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function customIntrospector<T extends Constructor<Dialect>>(
|
||||||
|
dialect: T,
|
||||||
|
introspector: Constructor<DatabaseIntrospector>,
|
||||||
|
options: BaseIntrospectorConfig = {},
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
create(...args: ConstructorParameters<T>) {
|
||||||
|
return new (class extends dialect {
|
||||||
|
override createIntrospector(db: Kysely<any>): DatabaseIntrospector {
|
||||||
|
return new introspector(db, options);
|
||||||
|
}
|
||||||
|
})(...args);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Connection, type FieldSpec, type SchemaResponse } from "./Connection";
|
|||||||
export class DummyConnection extends Connection {
|
export class DummyConnection extends Connection {
|
||||||
protected override readonly supported = {
|
protected override readonly supported = {
|
||||||
batching: true,
|
batching: true,
|
||||||
|
counts: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export {
|
|||||||
type IndexSpec,
|
type IndexSpec,
|
||||||
type DbFunctions,
|
type DbFunctions,
|
||||||
type SchemaResponse,
|
type SchemaResponse,
|
||||||
|
customIntrospector,
|
||||||
} from "./Connection";
|
} from "./Connection";
|
||||||
|
|
||||||
// sqlite
|
// sqlite
|
||||||
|
|||||||
@@ -28,13 +28,13 @@ export class LibsqlConnection extends SqliteConnection {
|
|||||||
private client: Client;
|
private client: Client;
|
||||||
protected override readonly supported = {
|
protected override readonly supported = {
|
||||||
batching: true,
|
batching: true,
|
||||||
|
counts: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(client: Client);
|
constructor(client: Client);
|
||||||
constructor(credentials: LibSqlCredentials);
|
constructor(credentials: LibSqlCredentials);
|
||||||
constructor(clientOrCredentials: Client | LibSqlCredentials) {
|
constructor(clientOrCredentials: Client | LibSqlCredentials) {
|
||||||
let client: Client;
|
let client: Client;
|
||||||
let batching_enabled = true;
|
|
||||||
if (clientOrCredentials && "url" in clientOrCredentials) {
|
if (clientOrCredentials && "url" in clientOrCredentials) {
|
||||||
let { url, authToken, protocol } = clientOrCredentials;
|
let { url, authToken, protocol } = clientOrCredentials;
|
||||||
if (protocol && LIBSQL_PROTOCOLS.includes(protocol)) {
|
if (protocol && LIBSQL_PROTOCOLS.includes(protocol)) {
|
||||||
@@ -44,13 +44,6 @@ export class LibsqlConnection extends SqliteConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
client = createClient({ url, authToken });
|
client = createClient({ url, authToken });
|
||||||
|
|
||||||
// currently there is an issue in limbo implementation
|
|
||||||
// that prevents batching from working correctly
|
|
||||||
if (/\.aws.*turso\.io$/.test(url)) {
|
|
||||||
$console.warn("Using an Turso AWS endpoint currently disables batching support");
|
|
||||||
batching_enabled = false;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
client = clientOrCredentials;
|
client = clientOrCredentials;
|
||||||
}
|
}
|
||||||
@@ -63,7 +56,6 @@ export class LibsqlConnection extends SqliteConnection {
|
|||||||
|
|
||||||
super(kysely, {}, plugins);
|
super(kysely, {}, plugins);
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.supported.batching = batching_enabled;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getClient(): Client {
|
getClient(): Client {
|
||||||
|
|||||||
@@ -31,8 +31,12 @@ export class SqliteConnection extends Connection {
|
|||||||
type,
|
type,
|
||||||
(col: ColumnDefinitionBuilder) => {
|
(col: ColumnDefinitionBuilder) => {
|
||||||
if (spec.primary) {
|
if (spec.primary) {
|
||||||
|
if (spec.type === "integer") {
|
||||||
return col.primaryKey().notNull().autoIncrement();
|
return col.primaryKey().notNull().autoIncrement();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return col.primaryKey().notNull();
|
||||||
|
}
|
||||||
if (spec.references) {
|
if (spec.references) {
|
||||||
let relCol = col.references(spec.references);
|
let relCol = col.references(spec.references);
|
||||||
if (spec.onDelete) relCol = relCol.onDelete(spec.onDelete);
|
if (spec.onDelete) relCol = relCol.onDelete(spec.onDelete);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type Static, StringRecord, objectTransform } from "core/utils";
|
import { type Static, StringEnum, StringRecord, objectTransform } from "core/utils";
|
||||||
import * as tb from "@sinclair/typebox";
|
import * as tb from "@sinclair/typebox";
|
||||||
import {
|
import {
|
||||||
FieldClassMap,
|
FieldClassMap,
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
entityTypes,
|
entityTypes,
|
||||||
} from "data";
|
} from "data";
|
||||||
import { MediaField, mediaFieldConfigSchema } from "../media/MediaField";
|
import { MediaField, mediaFieldConfigSchema } from "../media/MediaField";
|
||||||
|
import { primaryFieldTypes } from "./fields";
|
||||||
|
|
||||||
export const FIELDS = {
|
export const FIELDS = {
|
||||||
...FieldClassMap,
|
...FieldClassMap,
|
||||||
@@ -72,6 +73,9 @@ export const indicesSchema = tb.Type.Object(
|
|||||||
export const dataConfigSchema = tb.Type.Object(
|
export const dataConfigSchema = tb.Type.Object(
|
||||||
{
|
{
|
||||||
basepath: tb.Type.Optional(tb.Type.String({ default: "/api/data" })),
|
basepath: tb.Type.Optional(tb.Type.String({ default: "/api/data" })),
|
||||||
|
default_primary_format: tb.Type.Optional(
|
||||||
|
StringEnum(primaryFieldTypes, { default: "integer" }),
|
||||||
|
),
|
||||||
entities: tb.Type.Optional(StringRecord(entitiesSchema, { default: {} })),
|
entities: tb.Type.Optional(StringRecord(entitiesSchema, { default: {} })),
|
||||||
relations: tb.Type.Optional(StringRecord(tb.Type.Union(relationsSchema), { default: {} })),
|
relations: tb.Type.Optional(StringRecord(tb.Type.Union(relationsSchema), { default: {} })),
|
||||||
indices: tb.Type.Optional(StringRecord(indicesSchema, { default: {} })),
|
indices: tb.Type.Optional(StringRecord(indicesSchema, { default: {} })),
|
||||||
|
|||||||
@@ -6,7 +6,13 @@ import {
|
|||||||
snakeToPascalWithSpaces,
|
snakeToPascalWithSpaces,
|
||||||
transformObject,
|
transformObject,
|
||||||
} from "core/utils";
|
} from "core/utils";
|
||||||
import { type Field, PrimaryField, type TActionContext, type TRenderContext } from "../fields";
|
import {
|
||||||
|
type Field,
|
||||||
|
PrimaryField,
|
||||||
|
primaryFieldTypes,
|
||||||
|
type TActionContext,
|
||||||
|
type TRenderContext,
|
||||||
|
} from "../fields";
|
||||||
import * as tbbox from "@sinclair/typebox";
|
import * as tbbox from "@sinclair/typebox";
|
||||||
const { Type } = tbbox;
|
const { Type } = tbbox;
|
||||||
|
|
||||||
@@ -18,6 +24,7 @@ export const entityConfigSchema = Type.Object(
|
|||||||
description: Type.Optional(Type.String()),
|
description: Type.Optional(Type.String()),
|
||||||
sort_field: Type.Optional(Type.String({ default: config.data.default_primary_field })),
|
sort_field: Type.Optional(Type.String({ default: config.data.default_primary_field })),
|
||||||
sort_dir: Type.Optional(StringEnum(["asc", "desc"], { default: "asc" })),
|
sort_dir: Type.Optional(StringEnum(["asc", "desc"], { default: "asc" })),
|
||||||
|
primary_format: Type.Optional(StringEnum(primaryFieldTypes)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
@@ -68,7 +75,14 @@ export class Entity<
|
|||||||
if (primary_count > 1) {
|
if (primary_count > 1) {
|
||||||
throw new Error(`Entity "${name}" has more than one primary field`);
|
throw new Error(`Entity "${name}" has more than one primary field`);
|
||||||
}
|
}
|
||||||
this.fields = primary_count === 1 ? [] : [new PrimaryField()];
|
this.fields =
|
||||||
|
primary_count === 1
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
new PrimaryField(undefined, {
|
||||||
|
format: this.config.primary_format,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
if (fields) {
|
if (fields) {
|
||||||
fields.forEach((field) => this.addField(field));
|
fields.forEach((field) => this.addField(field));
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { Entity, EntityData, EntityManager } from "../entities";
|
|||||||
import { InvalidSearchParamsException } from "../errors";
|
import { InvalidSearchParamsException } from "../errors";
|
||||||
import { MutatorEvents } from "../events";
|
import { MutatorEvents } from "../events";
|
||||||
import { RelationMutator } from "../relations";
|
import { RelationMutator } from "../relations";
|
||||||
import type { RepoQuery } from "../server/data-query-impl";
|
import type { RepoQuery } from "../server/query";
|
||||||
|
|
||||||
type MutatorQB =
|
type MutatorQB =
|
||||||
| InsertQueryBuilder<any, any, any>
|
| InsertQueryBuilder<any, any, any>
|
||||||
@@ -143,7 +143,7 @@ export class Mutator<
|
|||||||
|
|
||||||
// if listener returned, take what's returned
|
// if listener returned, take what's returned
|
||||||
const _data = result.returned ? result.params.data : data;
|
const _data = result.returned ? result.params.data : data;
|
||||||
const validatedData = {
|
let validatedData = {
|
||||||
...entity.getDefaultObject(),
|
...entity.getDefaultObject(),
|
||||||
...(await this.getValidatedData(_data, "create")),
|
...(await this.getValidatedData(_data, "create")),
|
||||||
};
|
};
|
||||||
@@ -159,6 +159,16 @@ export class Mutator<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// primary
|
||||||
|
const primary = entity.getPrimaryField();
|
||||||
|
const primary_value = primary.getNewValue();
|
||||||
|
if (primary_value) {
|
||||||
|
validatedData = {
|
||||||
|
[primary.name]: primary_value,
|
||||||
|
...validatedData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const query = this.conn
|
const query = this.conn
|
||||||
.insertInto(entity.name)
|
.insertInto(entity.name)
|
||||||
.values(validatedData)
|
.values(validatedData)
|
||||||
@@ -175,7 +185,7 @@ export class Mutator<
|
|||||||
|
|
||||||
async updateOne(id: PrimaryFieldType, data: Partial<Input>): Promise<MutatorResponse<Output>> {
|
async updateOne(id: PrimaryFieldType, data: Partial<Input>): Promise<MutatorResponse<Output>> {
|
||||||
const entity = this.entity;
|
const entity = this.entity;
|
||||||
if (!Number.isInteger(id)) {
|
if (!id) {
|
||||||
throw new Error("ID must be provided for update");
|
throw new Error("ID must be provided for update");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,7 +222,7 @@ export class Mutator<
|
|||||||
|
|
||||||
async deleteOne(id: PrimaryFieldType): Promise<MutatorResponse<Output>> {
|
async deleteOne(id: PrimaryFieldType): Promise<MutatorResponse<Output>> {
|
||||||
const entity = this.entity;
|
const entity = this.entity;
|
||||||
if (!Number.isInteger(id)) {
|
if (!id) {
|
||||||
throw new Error("ID must be provided for deletion");
|
throw new Error("ID must be provided for deletion");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ import type { DB as DefaultDB, PrimaryFieldType } from "core";
|
|||||||
import { $console } from "core";
|
import { $console } from "core";
|
||||||
import { type EmitsEvents, EventManager } from "core/events";
|
import { type EmitsEvents, EventManager } from "core/events";
|
||||||
import { type SelectQueryBuilder, sql } from "kysely";
|
import { type SelectQueryBuilder, sql } from "kysely";
|
||||||
import { cloneDeep } from "lodash-es";
|
|
||||||
import { InvalidSearchParamsException } from "../../errors";
|
import { InvalidSearchParamsException } from "../../errors";
|
||||||
import { MutatorEvents, RepositoryEvents } from "../../events";
|
import { MutatorEvents, RepositoryEvents } from "../../events";
|
||||||
import { type RepoQuery, defaultQuerySchema } from "../../server/data-query-impl";
|
import { type RepoQuery, getRepoQueryTemplate } from "data/server/query";
|
||||||
import {
|
import {
|
||||||
type Entity,
|
type Entity,
|
||||||
type EntityData,
|
type EntityData,
|
||||||
@@ -28,6 +27,7 @@ export type RepositoryResponse<T = EntityData[]> = RepositoryRawResponse & {
|
|||||||
data: T;
|
data: T;
|
||||||
meta: {
|
meta: {
|
||||||
items: number;
|
items: number;
|
||||||
|
has_more?: boolean;
|
||||||
total?: number;
|
total?: number;
|
||||||
count?: number;
|
count?: number;
|
||||||
time?: number;
|
time?: number;
|
||||||
@@ -62,6 +62,10 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
|||||||
public entity: Entity,
|
public entity: Entity,
|
||||||
protected options: RepositoryOptions = {},
|
protected options: RepositoryOptions = {},
|
||||||
) {
|
) {
|
||||||
|
this.options = {
|
||||||
|
...options,
|
||||||
|
includeCounts: options?.includeCounts ?? this.em.connection.supports("counts"),
|
||||||
|
};
|
||||||
this.emgr = options?.emgr ?? new EventManager(MutatorEvents);
|
this.emgr = options?.emgr ?? new EventManager(MutatorEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,14 +88,14 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getValidOptions(options?: Partial<RepoQuery>): RepoQuery {
|
getValidOptions(options?: RepoQuery): RepoQuery {
|
||||||
const entity = this.entity;
|
const entity = this.entity;
|
||||||
// @todo: if not cloned deep, it will keep references and error if multiple requests come in
|
// @todo: if not cloned deep, it will keep references and error if multiple requests come in
|
||||||
const validated = {
|
const validated = {
|
||||||
...cloneDeep(defaultQuerySchema),
|
...structuredClone(getRepoQueryTemplate()),
|
||||||
sort: entity.getDefaultSort(),
|
sort: entity.getDefaultSort(),
|
||||||
select: entity.getSelect(),
|
select: entity.getSelect(),
|
||||||
};
|
} satisfies Required<RepoQuery>;
|
||||||
|
|
||||||
if (!options) return validated;
|
if (!options) return validated;
|
||||||
|
|
||||||
@@ -99,12 +103,15 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
|||||||
if (!validated.select.includes(options.sort.by)) {
|
if (!validated.select.includes(options.sort.by)) {
|
||||||
throw new InvalidSearchParamsException(`Invalid sort field "${options.sort.by}"`);
|
throw new InvalidSearchParamsException(`Invalid sort field "${options.sort.by}"`);
|
||||||
}
|
}
|
||||||
if (!["asc", "desc"].includes(options.sort.dir)) {
|
if (!["asc", "desc"].includes(options.sort.dir!)) {
|
||||||
throw new InvalidSearchParamsException(`Invalid sort direction "${options.sort.dir}"`);
|
throw new InvalidSearchParamsException(`Invalid sort direction "${options.sort.dir}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.checkIndex(entity.name, options.sort.by, "sort");
|
this.checkIndex(entity.name, options.sort.by, "sort");
|
||||||
validated.sort = options.sort;
|
validated.sort = {
|
||||||
|
dir: "asc",
|
||||||
|
...options.sort,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.select && options.select.length > 0) {
|
if (options.select && options.select.length > 0) {
|
||||||
@@ -177,6 +184,11 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
|||||||
if (options.limit) validated.limit = options.limit;
|
if (options.limit) validated.limit = options.limit;
|
||||||
if (options.offset) validated.offset = options.offset;
|
if (options.offset) validated.offset = options.offset;
|
||||||
|
|
||||||
|
// if counts disabled, add +1 to limit
|
||||||
|
if (this.options?.includeCounts === false) {
|
||||||
|
validated.limit = (validated.limit ?? 10) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
return validated;
|
return validated;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,6 +220,7 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
|||||||
|
|
||||||
protected async performQuery(qb: RepositoryQB): Promise<RepositoryResponse> {
|
protected async performQuery(qb: RepositoryQB): Promise<RepositoryResponse> {
|
||||||
const entity = this.entity;
|
const entity = this.entity;
|
||||||
|
|
||||||
const compiled = qb.compile();
|
const compiled = qb.compile();
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
@@ -442,6 +455,16 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
|||||||
await this.triggerFindBefore(this.entity, options);
|
await this.triggerFindBefore(this.entity, options);
|
||||||
|
|
||||||
const res = await this.performQuery(qb);
|
const res = await this.performQuery(qb);
|
||||||
|
if (this.options?.includeCounts === false) {
|
||||||
|
const hasMore = res.data.length === (options.limit ?? 10);
|
||||||
|
res.meta.has_more = hasMore;
|
||||||
|
|
||||||
|
if (hasMore) {
|
||||||
|
res.data = res.data.slice(0, -1);
|
||||||
|
res.result = res.result.slice(0, -1);
|
||||||
|
res.meta.items = res.data.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await this.triggerFindAfter(this.entity, options, res.data);
|
await this.triggerFindAfter(this.entity, options, res.data);
|
||||||
return res as any;
|
return res as any;
|
||||||
@@ -505,7 +528,7 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async exists(where: Required<RepoQuery["where"]>): Promise<RepositoryExistsResponse> {
|
async exists(where: Required<RepoQuery>["where"]): Promise<RepositoryExistsResponse> {
|
||||||
const entity = this.entity;
|
const entity = this.entity;
|
||||||
const options = this.getValidOptions({ where });
|
const options = this.getValidOptions({ where });
|
||||||
|
|
||||||
@@ -513,7 +536,7 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
|||||||
let qb = this.conn.selectFrom(entity.name).select(selector);
|
let qb = this.conn.selectFrom(entity.name).select(selector);
|
||||||
|
|
||||||
// add mandatory where
|
// add mandatory where
|
||||||
qb = WhereBuilder.addClause(qb, options.where).limit(1);
|
qb = WhereBuilder.addClause(qb, options.where!).limit(1);
|
||||||
|
|
||||||
const { result, ...compiled } = await this.executeQb(qb);
|
const { result, ...compiled } = await this.executeQb(qb);
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ const expressions = [
|
|||||||
export type WhereQuery = FilterQuery<typeof expressions>;
|
export type WhereQuery = FilterQuery<typeof expressions>;
|
||||||
|
|
||||||
const validator = makeValidator(expressions);
|
const validator = makeValidator(expressions);
|
||||||
|
export const expressionKeys = validator.expressionKeys;
|
||||||
|
|
||||||
export class WhereBuilder {
|
export class WhereBuilder {
|
||||||
static addClause<QB extends WhereQb>(qb: QB, query: WhereQuery) {
|
static addClause<QB extends WhereQb>(qb: QB, query: WhereQuery) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { $console, type PrimaryFieldType } from "core";
|
import { $console, type PrimaryFieldType } from "core";
|
||||||
import { Event, InvalidEventReturn } from "core/events";
|
import { Event, InvalidEventReturn } from "core/events";
|
||||||
import type { Entity, EntityData } from "../entities";
|
import type { Entity, EntityData } from "../entities";
|
||||||
import type { RepoQuery } from "../server/data-query-impl";
|
import type { RepoQuery } from "data/server/query";
|
||||||
|
|
||||||
export class MutatorInsertBefore extends Event<{ entity: Entity; data: EntityData }, EntityData> {
|
export class MutatorInsertBefore extends Event<{ entity: Entity; data: EntityData }, EntityData> {
|
||||||
static override slug = "mutator-insert-before";
|
static override slug = "mutator-insert-before";
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import { config } from "core";
|
import { config } from "core";
|
||||||
import type { Static } from "core/utils";
|
import { StringEnum, uuidv7, type Static } from "core/utils";
|
||||||
import { Field, baseFieldConfigSchema } from "./Field";
|
import { Field, baseFieldConfigSchema } from "./Field";
|
||||||
import * as tbbox from "@sinclair/typebox";
|
import * as tbbox from "@sinclair/typebox";
|
||||||
import type { TFieldTSType } from "data/entities/EntityTypescript";
|
import type { TFieldTSType } from "data/entities/EntityTypescript";
|
||||||
const { Type } = tbbox;
|
const { Type } = tbbox;
|
||||||
|
|
||||||
|
export const primaryFieldTypes = ["integer", "uuid"] as const;
|
||||||
|
export type TPrimaryFieldFormat = (typeof primaryFieldTypes)[number];
|
||||||
|
|
||||||
export const primaryFieldConfigSchema = Type.Composite([
|
export const primaryFieldConfigSchema = Type.Composite([
|
||||||
Type.Omit(baseFieldConfigSchema, ["required"]),
|
Type.Omit(baseFieldConfigSchema, ["required"]),
|
||||||
Type.Object({
|
Type.Object({
|
||||||
|
format: Type.Optional(StringEnum(primaryFieldTypes, { default: "integer" })),
|
||||||
required: Type.Optional(Type.Literal(false)),
|
required: Type.Optional(Type.Literal(false)),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
@@ -21,8 +25,8 @@ export class PrimaryField<Required extends true | false = false> extends Field<
|
|||||||
> {
|
> {
|
||||||
override readonly type = "primary";
|
override readonly type = "primary";
|
||||||
|
|
||||||
constructor(name: string = config.data.default_primary_field) {
|
constructor(name: string = config.data.default_primary_field, cfg?: PrimaryFieldConfig) {
|
||||||
super(name, { fillable: false, required: false });
|
super(name, { fillable: false, required: false, ...cfg });
|
||||||
}
|
}
|
||||||
|
|
||||||
override isRequired(): boolean {
|
override isRequired(): boolean {
|
||||||
@@ -30,32 +34,53 @@ export class PrimaryField<Required extends true | false = false> extends Field<
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected getSchema() {
|
protected getSchema() {
|
||||||
return baseFieldConfigSchema;
|
return primaryFieldConfigSchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
get format() {
|
||||||
|
return this.config.format ?? "integer";
|
||||||
|
}
|
||||||
|
|
||||||
|
get fieldType() {
|
||||||
|
return this.format === "integer" ? "integer" : "text";
|
||||||
}
|
}
|
||||||
|
|
||||||
override schema() {
|
override schema() {
|
||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
type: "integer",
|
type: this.fieldType,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
primary: true,
|
primary: true,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getNewValue(): any {
|
||||||
|
if (this.format === "uuid") {
|
||||||
|
return uuidv7();
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
override async transformPersist(value: any): Promise<number> {
|
override async transformPersist(value: any): Promise<number> {
|
||||||
throw new Error("PrimaryField: This function should not be called");
|
throw new Error("PrimaryField: This function should not be called");
|
||||||
}
|
}
|
||||||
|
|
||||||
override toJsonSchema() {
|
override toJsonSchema() {
|
||||||
|
if (this.format === "uuid") {
|
||||||
|
return this.toSchemaWrapIfRequired(Type.String({ writeOnly: undefined }));
|
||||||
|
}
|
||||||
|
|
||||||
return this.toSchemaWrapIfRequired(Type.Number({ writeOnly: undefined }));
|
return this.toSchemaWrapIfRequired(Type.Number({ writeOnly: undefined }));
|
||||||
}
|
}
|
||||||
|
|
||||||
override toType(): TFieldTSType {
|
override toType(): TFieldTSType {
|
||||||
|
const type = this.format === "integer" ? "number" : "string";
|
||||||
return {
|
return {
|
||||||
...super.toType(),
|
...super.toType(),
|
||||||
required: true,
|
required: true,
|
||||||
import: [{ package: "kysely", name: "Generated" }],
|
import: [{ package: "kysely", name: "Generated" }],
|
||||||
type: "Generated<number>",
|
type: `Generated<${type}>`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,11 @@ export * from "./connection";
|
|||||||
export {
|
export {
|
||||||
type RepoQuery,
|
type RepoQuery,
|
||||||
type RepoQueryIn,
|
type RepoQueryIn,
|
||||||
defaultQuerySchema,
|
getRepoQueryTemplate,
|
||||||
querySchema,
|
repoQuery,
|
||||||
whereSchema,
|
} from "./server/query";
|
||||||
} from "./server/data-query-impl";
|
|
||||||
|
export type { WhereQuery } from "./entities/query/WhereBuilder";
|
||||||
|
|
||||||
export { KyselyPluginRunner } from "./plugins/KyselyPluginRunner";
|
export { KyselyPluginRunner } from "./plugins/KyselyPluginRunner";
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ import {
|
|||||||
type MutationInstructionResponse,
|
type MutationInstructionResponse,
|
||||||
RelationHelper,
|
RelationHelper,
|
||||||
} from "../relations";
|
} from "../relations";
|
||||||
import type { RepoQuery } from "../server/data-query-impl";
|
import type { RepoQuery } from "../server/query";
|
||||||
import type { RelationType } from "./relation-types";
|
import type { RelationType } from "./relation-types";
|
||||||
import * as tbbox from "@sinclair/typebox";
|
import * as tbbox from "@sinclair/typebox";
|
||||||
|
import type { PrimaryFieldType } from "core";
|
||||||
const { Type } = tbbox;
|
const { Type } = tbbox;
|
||||||
|
|
||||||
const directions = ["source", "target"] as const;
|
const directions = ["source", "target"] as const;
|
||||||
@@ -72,7 +73,7 @@ export abstract class EntityRelation<
|
|||||||
reference: string,
|
reference: string,
|
||||||
): KyselyQueryBuilder;
|
): KyselyQueryBuilder;
|
||||||
|
|
||||||
getReferenceQuery(entity: Entity, id: number, reference: string): Partial<RepoQuery> {
|
getReferenceQuery(entity: Entity, id: PrimaryFieldType, reference: string): Partial<RepoQuery> {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { Static } from "core/utils";
|
|||||||
import type { ExpressionBuilder } from "kysely";
|
import type { ExpressionBuilder } from "kysely";
|
||||||
import { Entity, type EntityManager } from "../entities";
|
import { Entity, type EntityManager } from "../entities";
|
||||||
import { type Field, PrimaryField } from "../fields";
|
import { type Field, PrimaryField } from "../fields";
|
||||||
import type { RepoQuery } from "../server/data-query-impl";
|
import type { RepoQuery } from "../server/query";
|
||||||
import { EntityRelation, type KyselyQueryBuilder } from "./EntityRelation";
|
import { EntityRelation, type KyselyQueryBuilder } from "./EntityRelation";
|
||||||
import { EntityRelationAnchor } from "./EntityRelationAnchor";
|
import { EntityRelationAnchor } from "./EntityRelationAnchor";
|
||||||
import { RelationField } from "./RelationField";
|
import { RelationField } from "./RelationField";
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { snakeToPascalWithSpaces } from "core/utils";
|
|||||||
import type { Static } from "core/utils";
|
import type { Static } from "core/utils";
|
||||||
import type { ExpressionBuilder } from "kysely";
|
import type { ExpressionBuilder } from "kysely";
|
||||||
import type { Entity, EntityManager } from "../entities";
|
import type { Entity, EntityManager } from "../entities";
|
||||||
import type { RepoQuery } from "../server/data-query-impl";
|
import type { RepoQuery } from "../server/query";
|
||||||
import { EntityRelation, type KyselyQueryBuilder } from "./EntityRelation";
|
import { EntityRelation, type KyselyQueryBuilder } from "./EntityRelation";
|
||||||
import { EntityRelationAnchor } from "./EntityRelationAnchor";
|
import { EntityRelationAnchor } from "./EntityRelationAnchor";
|
||||||
import { RelationField, type RelationFieldBaseConfig } from "./RelationField";
|
import { RelationField, type RelationFieldBaseConfig } from "./RelationField";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { Static } from "core/utils";
|
|||||||
import type { ExpressionBuilder } from "kysely";
|
import type { ExpressionBuilder } from "kysely";
|
||||||
import type { Entity, EntityManager } from "../entities";
|
import type { Entity, EntityManager } from "../entities";
|
||||||
import { NumberField, TextField } from "../fields";
|
import { NumberField, TextField } from "../fields";
|
||||||
import type { RepoQuery } from "../server/data-query-impl";
|
import type { RepoQuery } from "../server/query";
|
||||||
import { EntityRelation, type KyselyJsonFrom, type KyselyQueryBuilder } from "./EntityRelation";
|
import { EntityRelation, type KyselyJsonFrom, type KyselyQueryBuilder } from "./EntityRelation";
|
||||||
import { EntityRelationAnchor } from "./EntityRelationAnchor";
|
import { EntityRelationAnchor } from "./EntityRelationAnchor";
|
||||||
import { type RelationType, RelationTypes } from "./relation-types";
|
import { type RelationType, RelationTypes } from "./relation-types";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { type Static, StringEnum } from "core/utils";
|
import { type Static, StringEnum } from "core/utils";
|
||||||
import type { EntityManager } from "../entities";
|
import type { EntityManager } from "../entities";
|
||||||
import { Field, baseFieldConfigSchema } from "../fields";
|
import { Field, baseFieldConfigSchema, primaryFieldTypes } from "../fields";
|
||||||
import type { EntityRelation } from "./EntityRelation";
|
import type { EntityRelation } from "./EntityRelation";
|
||||||
import type { EntityRelationAnchor } from "./EntityRelationAnchor";
|
import type { EntityRelationAnchor } from "./EntityRelationAnchor";
|
||||||
import * as tbbox from "@sinclair/typebox";
|
import * as tbbox from "@sinclair/typebox";
|
||||||
@@ -15,6 +15,7 @@ export const relationFieldConfigSchema = Type.Composite([
|
|||||||
reference: Type.String(),
|
reference: Type.String(),
|
||||||
target: Type.String(), // @todo: potentially has to be an instance!
|
target: Type.String(), // @todo: potentially has to be an instance!
|
||||||
target_field: Type.Optional(Type.String({ default: "id" })),
|
target_field: Type.Optional(Type.String({ default: "id" })),
|
||||||
|
target_field_type: Type.Optional(StringEnum(["integer", "text"], { default: "integer" })),
|
||||||
on_delete: Type.Optional(StringEnum(CASCADES, { default: "set null" })),
|
on_delete: Type.Optional(StringEnum(CASCADES, { default: "set null" })),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
@@ -45,6 +46,7 @@ export class RelationField extends Field<RelationFieldConfig> {
|
|||||||
reference: target.reference,
|
reference: target.reference,
|
||||||
target: target.entity.name,
|
target: target.entity.name,
|
||||||
target_field: target.entity.getPrimaryField().name,
|
target_field: target.entity.getPrimaryField().name,
|
||||||
|
target_field_type: target.entity.getPrimaryField().fieldType,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +65,7 @@ export class RelationField extends Field<RelationFieldConfig> {
|
|||||||
override schema() {
|
override schema() {
|
||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
...super.schema()!,
|
...super.schema()!,
|
||||||
type: "integer",
|
type: this.config.target_field_type ?? "integer",
|
||||||
references: `${this.config.target}.${this.config.target_field}`,
|
references: `${this.config.target}.${this.config.target_field}`,
|
||||||
onDelete: this.config.on_delete ?? "set null",
|
onDelete: this.config.on_delete ?? "set null",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { CompiledQuery, TableMetadata } from "kysely";
|
|||||||
import type { IndexMetadata, SchemaResponse } from "../connection/Connection";
|
import type { IndexMetadata, SchemaResponse } from "../connection/Connection";
|
||||||
import type { Entity, EntityManager } from "../entities";
|
import type { Entity, EntityManager } from "../entities";
|
||||||
import { PrimaryField } from "../fields";
|
import { PrimaryField } from "../fields";
|
||||||
|
import { $console } from "core";
|
||||||
|
|
||||||
type IntrospectedTable = TableMetadata & {
|
type IntrospectedTable = TableMetadata & {
|
||||||
indices: IndexMetadata[];
|
indices: IndexMetadata[];
|
||||||
@@ -332,6 +333,7 @@ export class SchemaManager {
|
|||||||
|
|
||||||
if (config.force) {
|
if (config.force) {
|
||||||
try {
|
try {
|
||||||
|
$console.log("[SchemaManager]", sql);
|
||||||
await qb.execute();
|
await qb.execute();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`Failed to execute query: ${sql}: ${(e as any).message}`);
|
throw new Error(`Failed to execute query: ${sql}: ${(e as any).message}`);
|
||||||
|
|||||||
@@ -1,148 +0,0 @@
|
|||||||
import type { TThis } from "@sinclair/typebox";
|
|
||||||
import { type SchemaOptions, type StaticDecode, StringEnum, Value, isObject } from "core/utils";
|
|
||||||
import { WhereBuilder, type WhereQuery } from "../entities";
|
|
||||||
import * as tbbox from "@sinclair/typebox";
|
|
||||||
const { Type } = tbbox;
|
|
||||||
|
|
||||||
const NumberOrString = (options: SchemaOptions = {}) =>
|
|
||||||
Type.Transform(Type.Union([Type.Number(), Type.String()], options))
|
|
||||||
.Decode((value) => Number.parseInt(String(value)))
|
|
||||||
.Encode(String);
|
|
||||||
|
|
||||||
const limit = NumberOrString({ default: 10 });
|
|
||||||
const offset = NumberOrString({ default: 0 });
|
|
||||||
|
|
||||||
const sort_default = { by: "id", dir: "asc" };
|
|
||||||
const sort = Type.Transform(
|
|
||||||
Type.Union(
|
|
||||||
[Type.String(), Type.Object({ by: Type.String(), dir: StringEnum(["asc", "desc"]) })],
|
|
||||||
{
|
|
||||||
default: sort_default,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.Decode((value): { by: string; dir: "asc" | "desc" } => {
|
|
||||||
if (typeof value === "string") {
|
|
||||||
if (/^-?[a-zA-Z_][a-zA-Z0-9_.]*$/.test(value)) {
|
|
||||||
const dir = value[0] === "-" ? "desc" : "asc";
|
|
||||||
return { by: dir === "desc" ? value.slice(1) : value, dir } as any;
|
|
||||||
} else if (/^{.*}$/.test(value)) {
|
|
||||||
return JSON.parse(value) as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
return sort_default as any;
|
|
||||||
}
|
|
||||||
return value as any;
|
|
||||||
})
|
|
||||||
.Encode((value) => value);
|
|
||||||
|
|
||||||
const stringArray = Type.Transform(
|
|
||||||
Type.Union([Type.String(), Type.Array(Type.String())], { default: [] }),
|
|
||||||
)
|
|
||||||
.Decode((value) => {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return value;
|
|
||||||
} else if (value.includes(",")) {
|
|
||||||
return value.split(",");
|
|
||||||
}
|
|
||||||
return [value];
|
|
||||||
})
|
|
||||||
.Encode((value) => (Array.isArray(value) ? value : [value]));
|
|
||||||
|
|
||||||
export const whereSchema = Type.Transform(
|
|
||||||
Type.Union([Type.String(), Type.Object({})], { default: {} }),
|
|
||||||
)
|
|
||||||
.Decode((value) => {
|
|
||||||
const q = typeof value === "string" ? JSON.parse(value) : value;
|
|
||||||
return WhereBuilder.convert(q);
|
|
||||||
})
|
|
||||||
.Encode(JSON.stringify);
|
|
||||||
|
|
||||||
export type RepoWithSchema = Record<
|
|
||||||
string,
|
|
||||||
Omit<RepoQueryIn, "with"> & {
|
|
||||||
with?: unknown;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const withSchema = <TSelf extends TThis>(Self: TSelf) =>
|
|
||||||
Type.Transform(
|
|
||||||
Type.Union([Type.String(), Type.Array(Type.String()), Type.Record(Type.String(), Self)]),
|
|
||||||
)
|
|
||||||
.Decode((value) => {
|
|
||||||
// images
|
|
||||||
// images,comments
|
|
||||||
// ["images","comments"]
|
|
||||||
// { "images": {} }
|
|
||||||
|
|
||||||
if (!Array.isArray(value) && isObject(value)) {
|
|
||||||
return value as RepoWithSchema;
|
|
||||||
}
|
|
||||||
|
|
||||||
let _value: any = null;
|
|
||||||
if (typeof value === "string") {
|
|
||||||
// if stringified object
|
|
||||||
if (value.match(/^\{/)) {
|
|
||||||
return JSON.parse(value) as RepoWithSchema;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if stringified array
|
|
||||||
if (value.match(/^\[/)) {
|
|
||||||
_value = JSON.parse(value) as string[];
|
|
||||||
|
|
||||||
// if comma-separated string
|
|
||||||
} else if (value.includes(",")) {
|
|
||||||
_value = value.split(",");
|
|
||||||
|
|
||||||
// if single string
|
|
||||||
} else {
|
|
||||||
_value = [value];
|
|
||||||
}
|
|
||||||
} else if (Array.isArray(value)) {
|
|
||||||
_value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_value || !Array.isArray(_value) || !_value.every((v) => typeof v === "string")) {
|
|
||||||
throw new Error("Invalid 'with' schema");
|
|
||||||
}
|
|
||||||
|
|
||||||
return _value.reduce((acc, v) => {
|
|
||||||
acc[v] = {};
|
|
||||||
return acc;
|
|
||||||
}, {} as RepoWithSchema);
|
|
||||||
})
|
|
||||||
.Encode((value) => value);
|
|
||||||
|
|
||||||
export const querySchema = Type.Recursive(
|
|
||||||
(Self) =>
|
|
||||||
Type.Partial(
|
|
||||||
Type.Object(
|
|
||||||
{
|
|
||||||
limit: limit,
|
|
||||||
offset: offset,
|
|
||||||
sort: sort,
|
|
||||||
select: stringArray,
|
|
||||||
with: withSchema(Self),
|
|
||||||
join: stringArray,
|
|
||||||
where: whereSchema,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// @todo: determine if unknown is allowed, it's ignore anyway
|
|
||||||
additionalProperties: false,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
{ $id: "query-schema" },
|
|
||||||
);
|
|
||||||
|
|
||||||
export type RepoQueryIn = {
|
|
||||||
limit?: number;
|
|
||||||
offset?: number;
|
|
||||||
sort?: string | { by: string; dir: "asc" | "desc" };
|
|
||||||
select?: string[];
|
|
||||||
with?: string | string[] | Record<string, RepoQueryIn>;
|
|
||||||
join?: string[];
|
|
||||||
where?: WhereQuery;
|
|
||||||
};
|
|
||||||
export type RepoQuery = Required<StaticDecode<typeof querySchema>>;
|
|
||||||
export const defaultQuerySchema = Value.Default(querySchema, {}) as RepoQuery;
|
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
import { test, describe, expect } from "bun:test";
|
||||||
|
import * as q from "./query";
|
||||||
|
import { s as schema, parse as $parse, type ParseOptions } from "core/object/schema";
|
||||||
|
|
||||||
|
const parse = (v: unknown, o: ParseOptions = {}) => $parse(q.repoQuery, v, o);
|
||||||
|
|
||||||
|
// compatibility
|
||||||
|
const decode = (input: any, output: any) => {
|
||||||
|
expect(parse(input)).toEqual(output);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("server/query", () => {
|
||||||
|
test("limit & offset", () => {
|
||||||
|
expect(() => parse({ limit: false })).toThrow();
|
||||||
|
expect(parse({ limit: "11" })).toEqual({ limit: 11 });
|
||||||
|
expect(parse({ limit: 20 })).toEqual({ limit: 20 });
|
||||||
|
expect(parse({ offset: "1" })).toEqual({ offset: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("select", () => {
|
||||||
|
expect(parse({ select: "id" })).toEqual({ select: ["id"] });
|
||||||
|
expect(parse({ select: "id,title" })).toEqual({ select: ["id", "title"] });
|
||||||
|
expect(parse({ select: "id,title,desc" })).toEqual({ select: ["id", "title", "desc"] });
|
||||||
|
expect(parse({ select: ["id", "title"] })).toEqual({ select: ["id", "title"] });
|
||||||
|
|
||||||
|
expect(() => parse({ select: "not allowed" })).toThrow();
|
||||||
|
expect(() => parse({ select: "id," })).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("join", () => {
|
||||||
|
expect(parse({ join: "id" })).toEqual({ join: ["id"] });
|
||||||
|
expect(parse({ join: "id,title" })).toEqual({ join: ["id", "title"] });
|
||||||
|
expect(parse({ join: ["id", "title"] })).toEqual({ join: ["id", "title"] });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("sort", () => {
|
||||||
|
expect(parse({ sort: "id" }).sort).toEqual({
|
||||||
|
by: "id",
|
||||||
|
dir: "asc",
|
||||||
|
});
|
||||||
|
expect(parse({ sort: "-id" }).sort).toEqual({
|
||||||
|
by: "id",
|
||||||
|
dir: "desc",
|
||||||
|
});
|
||||||
|
expect(parse({ sort: { by: "title" } }).sort).toEqual({
|
||||||
|
by: "title",
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
parse(
|
||||||
|
{ sort: { by: "id" } },
|
||||||
|
{
|
||||||
|
withDefaults: true,
|
||||||
|
},
|
||||||
|
).sort,
|
||||||
|
).toEqual({
|
||||||
|
by: "id",
|
||||||
|
dir: "asc",
|
||||||
|
});
|
||||||
|
expect(parse({ sort: { by: "count", dir: "desc" } }).sort).toEqual({
|
||||||
|
by: "count",
|
||||||
|
dir: "desc",
|
||||||
|
});
|
||||||
|
// invalid gives default
|
||||||
|
expect(parse({ sort: "not allowed" }).sort).toEqual({
|
||||||
|
by: "id",
|
||||||
|
dir: "asc",
|
||||||
|
});
|
||||||
|
|
||||||
|
// json
|
||||||
|
expect(parse({ sort: JSON.stringify({ by: "count", dir: "desc" }) }).sort).toEqual({
|
||||||
|
by: "count",
|
||||||
|
dir: "desc",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("sort2", () => {
|
||||||
|
const _dflt = { sort: { by: "id", dir: "asc" } } as const;
|
||||||
|
|
||||||
|
decode({ sort: "" }, _dflt);
|
||||||
|
decode({ sort: "name" }, { sort: { by: "name", dir: "asc" } });
|
||||||
|
decode({ sort: "-name" }, { sort: { by: "name", dir: "desc" } });
|
||||||
|
decode({ sort: "-posts.name" }, { sort: { by: "posts.name", dir: "desc" } });
|
||||||
|
decode({ sort: "-1name" }, _dflt);
|
||||||
|
decode({ sort: { by: "name", dir: "desc" } }, { sort: { by: "name", dir: "desc" } });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("where", () => {
|
||||||
|
expect(parse({ where: { id: 1 } }).where).toEqual({
|
||||||
|
id: { $eq: 1 },
|
||||||
|
});
|
||||||
|
expect(parse({ where: JSON.stringify({ id: 1 }) }).where).toEqual({
|
||||||
|
id: { $eq: 1 },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(parse({ where: { count: { $gt: 1 } } }).where).toEqual({
|
||||||
|
count: { $gt: 1 },
|
||||||
|
});
|
||||||
|
expect(parse({ where: JSON.stringify({ count: { $gt: 1 } }) }).where).toEqual({
|
||||||
|
count: { $gt: 1 },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("template", () => {
|
||||||
|
expect(
|
||||||
|
q.repoQuery.template({
|
||||||
|
withOptional: true,
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
limit: 10,
|
||||||
|
offset: 0,
|
||||||
|
sort: { by: "id", dir: "asc" },
|
||||||
|
where: {},
|
||||||
|
select: [],
|
||||||
|
join: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("with", () => {
|
||||||
|
let example = {
|
||||||
|
limit: 10,
|
||||||
|
with: {
|
||||||
|
posts: { limit: "10", with: ["comments"] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(parse(example)).toEqual({
|
||||||
|
limit: 10,
|
||||||
|
with: {
|
||||||
|
posts: {
|
||||||
|
limit: 10,
|
||||||
|
with: {
|
||||||
|
comments: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
decode({ with: ["posts"] }, { with: { posts: {} } });
|
||||||
|
decode({ with: { posts: {} } }, { with: { posts: {} } });
|
||||||
|
decode({ with: { posts: { limit: 1 } } }, { with: { posts: { limit: 1 } } });
|
||||||
|
decode(
|
||||||
|
{
|
||||||
|
with: {
|
||||||
|
posts: {
|
||||||
|
with: {
|
||||||
|
images: {
|
||||||
|
limit: "10",
|
||||||
|
select: "id",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
with: {
|
||||||
|
posts: {
|
||||||
|
with: {
|
||||||
|
images: {
|
||||||
|
limit: 10,
|
||||||
|
select: ["id"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// over http
|
||||||
|
{
|
||||||
|
const output = { with: { images: {} } };
|
||||||
|
decode({ with: "images" }, output);
|
||||||
|
decode({ with: '["images"]' }, output);
|
||||||
|
decode({ with: ["images"] }, output);
|
||||||
|
decode({ with: { images: {} } }, output);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const output = { with: { images: {}, comments: {} } };
|
||||||
|
decode({ with: "images,comments" }, output);
|
||||||
|
decode({ with: ["images", "comments"] }, output);
|
||||||
|
decode({ with: '["images", "comments"]' }, output);
|
||||||
|
decode({ with: { images: {}, comments: {} } }, output);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import { s } from "core/object/schema";
|
||||||
|
import { WhereBuilder, type WhereQuery } from "data";
|
||||||
|
import { $console } from "core";
|
||||||
|
import { isObject } from "core/utils";
|
||||||
|
import type { CoercionOptions, TAnyOf } from "jsonv-ts";
|
||||||
|
|
||||||
|
// -------
|
||||||
|
// helpers
|
||||||
|
const stringIdentifier = s.string({
|
||||||
|
// allow "id", "id,title" – but not "id," or "not allowed"
|
||||||
|
pattern: "^(?:[a-zA-Z_$][\\w$]*)(?:,[a-zA-Z_$][\\w$]*)*$",
|
||||||
|
});
|
||||||
|
const stringArray = s.anyOf(
|
||||||
|
[
|
||||||
|
stringIdentifier,
|
||||||
|
s.array(stringIdentifier, {
|
||||||
|
uniqueItems: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
{
|
||||||
|
default: [],
|
||||||
|
coerce: (v): string[] => {
|
||||||
|
if (Array.isArray(v)) {
|
||||||
|
return v;
|
||||||
|
} else if (typeof v === "string") {
|
||||||
|
if (v.includes(",")) {
|
||||||
|
return v.split(",");
|
||||||
|
}
|
||||||
|
return [v];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// -------
|
||||||
|
// sorting
|
||||||
|
const sortDefault = { by: "id", dir: "asc" };
|
||||||
|
const sortSchema = s.object({
|
||||||
|
by: s.string(),
|
||||||
|
dir: s.string({ enum: ["asc", "desc"] }).optional(),
|
||||||
|
});
|
||||||
|
type SortSchema = s.Static<typeof sortSchema>;
|
||||||
|
const sort = s.anyOf([s.string(), sortSchema], {
|
||||||
|
default: sortDefault,
|
||||||
|
coerce: (v): SortSchema => {
|
||||||
|
if (typeof v === "string") {
|
||||||
|
if (/^-?[a-zA-Z_][a-zA-Z0-9_.]*$/.test(v)) {
|
||||||
|
const dir = v[0] === "-" ? "desc" : "asc";
|
||||||
|
return { by: dir === "desc" ? v.slice(1) : v, dir } as any;
|
||||||
|
} else if (/^{.*}$/.test(v)) {
|
||||||
|
return JSON.parse(v) as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
$console.warn(`Invalid sort given: '${JSON.stringify(v)}'`);
|
||||||
|
return sortDefault as any;
|
||||||
|
}
|
||||||
|
return v as any;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ------
|
||||||
|
// filter
|
||||||
|
const where = s.anyOf([s.string(), s.object({})], {
|
||||||
|
default: {},
|
||||||
|
examples: [
|
||||||
|
{
|
||||||
|
attribute: {
|
||||||
|
$eq: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
coerce: (value: unknown) => {
|
||||||
|
const q = typeof value === "string" ? JSON.parse(value) : value;
|
||||||
|
return WhereBuilder.convert(q);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
//type WhereSchemaIn = s.Static<typeof where>;
|
||||||
|
//type WhereSchema = s.StaticCoerced<typeof where>;
|
||||||
|
|
||||||
|
// ------
|
||||||
|
// with
|
||||||
|
// @todo: waiting for recursion support
|
||||||
|
export type RepoWithSchema = Record<
|
||||||
|
string,
|
||||||
|
Omit<RepoQueryIn, "with"> & {
|
||||||
|
with?: unknown;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
const withSchema = <In, Out = In>(self: s.TSchema): s.TSchemaInOut<In, Out> =>
|
||||||
|
s.anyOf([stringIdentifier, s.array(stringIdentifier), self], {
|
||||||
|
coerce: function (this: TAnyOf<any>, _value: unknown, opts: CoercionOptions = {}) {
|
||||||
|
let value: any = _value;
|
||||||
|
|
||||||
|
if (typeof value === "string") {
|
||||||
|
// if stringified object
|
||||||
|
if (value.match(/^\{/) || value.match(/^\[/)) {
|
||||||
|
value = JSON.parse(value);
|
||||||
|
} else if (value.includes(",")) {
|
||||||
|
value = value.split(",");
|
||||||
|
} else {
|
||||||
|
value = [value];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert arrays to objects
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value = value.reduce((acc, v) => {
|
||||||
|
acc[v] = {};
|
||||||
|
return acc;
|
||||||
|
}, {} as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle object case
|
||||||
|
if (isObject(value)) {
|
||||||
|
for (const k in value) {
|
||||||
|
value[k] = self.coerce(value[k], opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value as unknown as any;
|
||||||
|
},
|
||||||
|
}) as any;
|
||||||
|
|
||||||
|
// ==========
|
||||||
|
// REPO QUERY
|
||||||
|
export const repoQuery = s.recursive((self) =>
|
||||||
|
s.partialObject({
|
||||||
|
limit: s.number({ default: 10 }),
|
||||||
|
offset: s.number({ default: 0 }),
|
||||||
|
sort,
|
||||||
|
where,
|
||||||
|
select: stringArray,
|
||||||
|
join: stringArray,
|
||||||
|
with: withSchema<RepoWithSchema>(self),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
export const getRepoQueryTemplate = () =>
|
||||||
|
repoQuery.template({
|
||||||
|
withOptional: true,
|
||||||
|
}) as Required<RepoQuery>;
|
||||||
|
|
||||||
|
export type RepoQueryIn = {
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
sort?: string | { by: string; dir: "asc" | "desc" };
|
||||||
|
select?: string[];
|
||||||
|
with?: string | string[] | Record<string, RepoQueryIn>;
|
||||||
|
join?: string[];
|
||||||
|
where?: WhereQuery;
|
||||||
|
};
|
||||||
|
export type RepoQuery = s.StaticCoerced<typeof repoQuery>;
|
||||||
@@ -6,12 +6,7 @@ import { DataPermissions } from "data";
|
|||||||
import { Controller } from "modules/Controller";
|
import { Controller } from "modules/Controller";
|
||||||
import type { AppMedia } from "../AppMedia";
|
import type { AppMedia } from "../AppMedia";
|
||||||
import { MediaField } from "../MediaField";
|
import { MediaField } from "../MediaField";
|
||||||
import * as tbbox from "@sinclair/typebox";
|
import { jsc, s, describeRoute } from "core/object/schema";
|
||||||
const { Type } = tbbox;
|
|
||||||
|
|
||||||
const booleanLike = Type.Transform(Type.String())
|
|
||||||
.Decode((v) => v === "1")
|
|
||||||
.Encode((v) => (v ? "1" : "0"));
|
|
||||||
|
|
||||||
export class MediaController extends Controller {
|
export class MediaController extends Controller {
|
||||||
constructor(private readonly media: AppMedia) {
|
constructor(private readonly media: AppMedia) {
|
||||||
@@ -31,22 +26,40 @@ export class MediaController extends Controller {
|
|||||||
// @todo: implement range requests
|
// @todo: implement range requests
|
||||||
const { auth, permission } = this.middlewares;
|
const { auth, permission } = this.middlewares;
|
||||||
const hono = this.create().use(auth());
|
const hono = this.create().use(auth());
|
||||||
|
const entitiesEnum = this.getEntitiesEnum(this.media.em);
|
||||||
|
|
||||||
// get files list (temporary)
|
// get files list (temporary)
|
||||||
hono.get("/files", permission(MediaPermissions.listFiles), async (c) => {
|
hono.get(
|
||||||
|
"/files",
|
||||||
|
describeRoute({
|
||||||
|
summary: "Get the list of files",
|
||||||
|
tags: ["media"],
|
||||||
|
}),
|
||||||
|
permission(MediaPermissions.listFiles),
|
||||||
|
async (c) => {
|
||||||
const files = await this.getStorageAdapter().listObjects();
|
const files = await this.getStorageAdapter().listObjects();
|
||||||
return c.json(files);
|
return c.json(files);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// get file by name
|
// get file by name
|
||||||
// @todo: implement more aggressive cache? (configurable)
|
// @todo: implement more aggressive cache? (configurable)
|
||||||
hono.get("/file/:filename", permission(MediaPermissions.readFile), async (c) => {
|
hono.get(
|
||||||
|
"/file/:filename",
|
||||||
|
describeRoute({
|
||||||
|
summary: "Get a file by name",
|
||||||
|
tags: ["media"],
|
||||||
|
}),
|
||||||
|
permission(MediaPermissions.readFile),
|
||||||
|
async (c) => {
|
||||||
const { filename } = c.req.param();
|
const { filename } = c.req.param();
|
||||||
if (!filename) {
|
if (!filename) {
|
||||||
throw new Error("No file name provided");
|
throw new Error("No file name provided");
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.getStorage().emgr.emit(new StorageEvents.FileAccessEvent({ name: filename }));
|
await this.getStorage().emgr.emit(
|
||||||
|
new StorageEvents.FileAccessEvent({ name: filename }),
|
||||||
|
);
|
||||||
const res = await this.getStorageAdapter().getObject(filename, c.req.raw.headers);
|
const res = await this.getStorageAdapter().getObject(filename, c.req.raw.headers);
|
||||||
|
|
||||||
const headers = new Headers(res.headers);
|
const headers = new Headers(res.headers);
|
||||||
@@ -57,10 +70,18 @@ export class MediaController extends Controller {
|
|||||||
statusText: res.statusText,
|
statusText: res.statusText,
|
||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// delete a file by name
|
// delete a file by name
|
||||||
hono.delete("/file/:filename", permission(MediaPermissions.deleteFile), async (c) => {
|
hono.delete(
|
||||||
|
"/file/:filename",
|
||||||
|
describeRoute({
|
||||||
|
summary: "Delete a file by name",
|
||||||
|
tags: ["media"],
|
||||||
|
}),
|
||||||
|
permission(MediaPermissions.deleteFile),
|
||||||
|
async (c) => {
|
||||||
const { filename } = c.req.param();
|
const { filename } = c.req.param();
|
||||||
if (!filename) {
|
if (!filename) {
|
||||||
throw new Error("No file name provided");
|
throw new Error("No file name provided");
|
||||||
@@ -68,24 +89,64 @@ export class MediaController extends Controller {
|
|||||||
await this.getStorage().deleteFile(filename);
|
await this.getStorage().deleteFile(filename);
|
||||||
|
|
||||||
return c.json({ message: "File deleted" });
|
return c.json({ message: "File deleted" });
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const maxSize = this.getStorage().getConfig().body_max_size ?? Number.POSITIVE_INFINITY;
|
const maxSize = this.getStorage().getConfig().body_max_size ?? Number.POSITIVE_INFINITY;
|
||||||
|
|
||||||
if (isDebug()) {
|
if (isDebug()) {
|
||||||
hono.post("/inspect", async (c) => {
|
hono.post(
|
||||||
|
"/inspect",
|
||||||
|
describeRoute({
|
||||||
|
summary: "Inspect a file",
|
||||||
|
tags: ["media"],
|
||||||
|
}),
|
||||||
|
async (c) => {
|
||||||
const file = await getFileFromContext(c);
|
const file = await getFileFromContext(c);
|
||||||
return c.json({
|
return c.json({
|
||||||
type: file?.type,
|
type: file?.type,
|
||||||
name: file?.name,
|
name: file?.name,
|
||||||
size: file?.size,
|
size: file?.size,
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const requestBody = {
|
||||||
|
content: {
|
||||||
|
"multipart/form-data": {
|
||||||
|
schema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
file: {
|
||||||
|
type: "string",
|
||||||
|
format: "binary",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["file"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"application/octet-stream": {
|
||||||
|
schema: {
|
||||||
|
type: "string",
|
||||||
|
format: "binary",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
// upload file
|
// upload file
|
||||||
// @todo: add required type for "upload endpoints"
|
// @todo: add required type for "upload endpoints"
|
||||||
hono.post("/upload/:filename?", permission(MediaPermissions.uploadFile), async (c) => {
|
hono.post(
|
||||||
|
"/upload/:filename?",
|
||||||
|
describeRoute({
|
||||||
|
summary: "Upload a file",
|
||||||
|
tags: ["media"],
|
||||||
|
requestBody,
|
||||||
|
}),
|
||||||
|
jsc("param", s.object({ filename: s.string().optional() })),
|
||||||
|
permission(MediaPermissions.uploadFile),
|
||||||
|
async (c) => {
|
||||||
const reqname = c.req.param("filename");
|
const reqname = c.req.param("filename");
|
||||||
|
|
||||||
const body = await getFileFromContext(c);
|
const body = await getFileFromContext(c);
|
||||||
@@ -103,18 +164,27 @@ export class MediaController extends Controller {
|
|||||||
const res = await this.getStorage().uploadFile(body, filename);
|
const res = await this.getStorage().uploadFile(body, filename);
|
||||||
|
|
||||||
return c.json(res, HttpStatus.CREATED);
|
return c.json(res, HttpStatus.CREATED);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// add upload file to entity
|
// add upload file to entity
|
||||||
// @todo: add required type for "upload endpoints"
|
// @todo: add required type for "upload endpoints"
|
||||||
hono.post(
|
hono.post(
|
||||||
"/entity/:entity/:id/:field",
|
"/entity/:entity/:id/:field",
|
||||||
tb(
|
describeRoute({
|
||||||
"query",
|
summary: "Add a file to an entity",
|
||||||
Type.Object({
|
tags: ["media"],
|
||||||
overwrite: Type.Optional(booleanLike),
|
requestBody,
|
||||||
|
}),
|
||||||
|
jsc(
|
||||||
|
"param",
|
||||||
|
s.object({
|
||||||
|
entity: entitiesEnum,
|
||||||
|
id: s.number(),
|
||||||
|
field: s.string(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
jsc("query", s.object({ overwrite: s.boolean().optional() })),
|
||||||
permission([DataPermissions.entityCreate, MediaPermissions.uploadFile]),
|
permission([DataPermissions.entityCreate, MediaPermissions.uploadFile]),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const entity_name = c.req.param("entity");
|
const entity_name = c.req.param("entity");
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import type { App } from "App";
|
import type { App } from "App";
|
||||||
import { type Context, Hono } from "hono";
|
import { type Context, type Env, Hono } from "hono";
|
||||||
import * as middlewares from "modules/middlewares";
|
import * as middlewares from "modules/middlewares";
|
||||||
import type { SafeUser } from "auth";
|
import type { SafeUser } from "auth";
|
||||||
|
import type { EntityManager } from "data";
|
||||||
|
import { s } from "core/object/schema";
|
||||||
|
|
||||||
export type ServerEnv = {
|
export type ServerEnv = Env & {
|
||||||
Variables: {
|
Variables: {
|
||||||
app: App;
|
app: App;
|
||||||
// to prevent resolving auth multiple times
|
// to prevent resolving auth multiple times
|
||||||
@@ -46,4 +48,9 @@ export class Controller {
|
|||||||
|
|
||||||
return c.notFound();
|
return c.notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected getEntitiesEnum(em: EntityManager<any>) {
|
||||||
|
const entities = em.entities.map((e) => e.name);
|
||||||
|
return entities.length > 0 ? s.string({ enum: entities }) : s.string();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ export class ModuleManager {
|
|||||||
const context = this.ctx(true);
|
const context = this.ctx(true);
|
||||||
|
|
||||||
for (const key in MODULES) {
|
for (const key in MODULES) {
|
||||||
const moduleConfig = key in initial ? initial[key] : {};
|
const moduleConfig = initial && key in initial ? initial[key] : {};
|
||||||
const module = new MODULES[key](moduleConfig, context) as Module;
|
const module = new MODULES[key](moduleConfig, context) as Module;
|
||||||
module.setListener(async (c) => {
|
module.setListener(async (c) => {
|
||||||
await this.onModuleConfigUpdated(key, c);
|
await this.onModuleConfigUpdated(key, c);
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export class AdminController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private withBasePath(route: string = "") {
|
private withBasePath(route: string = "") {
|
||||||
return (this.basepath + route).replace(/(?<!:)\/+/g, "/");
|
return (this.options.basepath + route).replace(/(?<!:)\/+/g, "/");
|
||||||
}
|
}
|
||||||
|
|
||||||
private withAdminBasePath(route: string = "") {
|
private withAdminBasePath(route: string = "") {
|
||||||
@@ -80,13 +80,37 @@ export class AdminController extends Controller {
|
|||||||
loggedOut: configs.auth.cookie.pathLoggedOut ?? this.withAdminBasePath("/"),
|
loggedOut: configs.auth.cookie.pathLoggedOut ?? this.withAdminBasePath("/"),
|
||||||
login: this.withAdminBasePath("/auth/login"),
|
login: this.withAdminBasePath("/auth/login"),
|
||||||
register: this.withAdminBasePath("/auth/register"),
|
register: this.withAdminBasePath("/auth/register"),
|
||||||
logout: this.withAdminBasePath("/auth/logout"),
|
logout: "/api/auth/logout",
|
||||||
};
|
};
|
||||||
|
|
||||||
hono.use("*", async (c, next) => {
|
const paths = ["/", "/data/*", "/auth/*", "/media/*", "/flows/*", "/settings/*"];
|
||||||
|
if (isDebug()) {
|
||||||
|
paths.push("/test/*");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const path of paths) {
|
||||||
|
hono.get(
|
||||||
|
path,
|
||||||
|
permission(SystemPermissions.accessAdmin, {
|
||||||
|
onDenied: async (c) => {
|
||||||
|
if (!path.startsWith("/auth")) {
|
||||||
|
addFlashMessage(c, "You are not authorized to access the Admin UI", "error");
|
||||||
|
|
||||||
|
$console.log("redirecting", authRoutes.login);
|
||||||
|
return c.redirect(authRoutes.login);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
permission(SystemPermissions.schemaRead, {
|
||||||
|
onDenied: async (c) => {
|
||||||
|
addFlashMessage(c, "You not allowed to read the schema", "warning");
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
async (c) => {
|
||||||
const obj = {
|
const obj = {
|
||||||
user: c.get("auth")?.user,
|
user: c.get("auth")?.user,
|
||||||
logout_route: this.withAdminBasePath(authRoutes.logout),
|
logout_route: authRoutes.logout,
|
||||||
admin_basepath: this.options.adminBasepath,
|
admin_basepath: this.options.adminBasepath,
|
||||||
};
|
};
|
||||||
const html = await this.getHtml(obj);
|
const html = await this.getHtml(obj);
|
||||||
@@ -95,10 +119,12 @@ export class AdminController extends Controller {
|
|||||||
// re-casting to void as a return is not required
|
// re-casting to void as a return is not required
|
||||||
return c.notFound() as unknown as void;
|
return c.notFound() as unknown as void;
|
||||||
}
|
}
|
||||||
c.set("html", html);
|
|
||||||
|
|
||||||
await next();
|
await auth.authenticator?.requestCookieRefresh(c);
|
||||||
});
|
return c.html(html);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (auth_enabled) {
|
if (auth_enabled) {
|
||||||
const redirectRouteParams = [
|
const redirectRouteParams = [
|
||||||
@@ -126,27 +152,6 @@ export class AdminController extends Controller {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// @todo: only load known paths
|
|
||||||
hono.get(
|
|
||||||
"/*",
|
|
||||||
permission(SystemPermissions.accessAdmin, {
|
|
||||||
onDenied: async (c) => {
|
|
||||||
addFlashMessage(c, "You are not authorized to access the Admin UI", "error");
|
|
||||||
|
|
||||||
$console.log("redirecting");
|
|
||||||
return c.redirect(authRoutes.login);
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
permission(SystemPermissions.schemaRead, {
|
|
||||||
onDenied: async (c) => {
|
|
||||||
addFlashMessage(c, "You not allowed to read the schema", "warning");
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
async (c) => {
|
|
||||||
return c.html(c.get("html")!);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return hono;
|
return hono;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,9 +199,13 @@ export class AdminController extends Controller {
|
|||||||
}).then((res) => res.default);
|
}).then((res) => res.default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
// @todo: load all marked as entry (incl. css)
|
// @todo: load all marked as entry (incl. css)
|
||||||
assets.js = manifest["src/ui/main.tsx"].file;
|
assets.js = manifest["src/ui/main.tsx"].file;
|
||||||
assets.css = manifest["src/ui/main.tsx"].css[0] as any;
|
assets.css = manifest["src/ui/main.tsx"].css[0] as any;
|
||||||
|
} catch (e) {
|
||||||
|
$console.warn("Couldn't find assets in manifest", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const favicon = isProd ? this.options.assetsPath + "favicon.ico" : "/favicon.ico";
|
const favicon = isProd ? this.options.assetsPath + "favicon.ico" : "/favicon.ico";
|
||||||
|
|||||||
@@ -13,9 +13,8 @@ import {
|
|||||||
import { getRuntimeKey } from "core/utils";
|
import { getRuntimeKey } from "core/utils";
|
||||||
import type { Context, Hono } from "hono";
|
import type { Context, Hono } from "hono";
|
||||||
import { Controller } from "modules/Controller";
|
import { Controller } from "modules/Controller";
|
||||||
import * as tbbox from "@sinclair/typebox";
|
import { openAPISpecs } from "jsonv-ts/hono";
|
||||||
const { Type } = tbbox;
|
import { swaggerUI } from "@hono/swagger-ui";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MODULE_NAMES,
|
MODULE_NAMES,
|
||||||
type ModuleConfigs,
|
type ModuleConfigs,
|
||||||
@@ -24,12 +23,8 @@ import {
|
|||||||
getDefaultConfig,
|
getDefaultConfig,
|
||||||
} from "modules/ModuleManager";
|
} from "modules/ModuleManager";
|
||||||
import * as SystemPermissions from "modules/permissions";
|
import * as SystemPermissions from "modules/permissions";
|
||||||
import { generateOpenAPI } from "modules/server/openapi";
|
import { jsc, s, describeRoute } from "core/object/schema";
|
||||||
|
import { getVersion } from "core/env";
|
||||||
const booleanLike = Type.Transform(Type.String())
|
|
||||||
.Decode((v) => v === "1")
|
|
||||||
.Encode((v) => (v ? "1" : "0"));
|
|
||||||
|
|
||||||
export type ConfigUpdate<Key extends ModuleKey = ModuleKey> = {
|
export type ConfigUpdate<Key extends ModuleKey = ModuleKey> = {
|
||||||
success: true;
|
success: true;
|
||||||
module: Key;
|
module: Key;
|
||||||
@@ -61,20 +56,27 @@ export class SystemController extends Controller {
|
|||||||
|
|
||||||
hono.use(permission(SystemPermissions.configRead));
|
hono.use(permission(SystemPermissions.configRead));
|
||||||
|
|
||||||
hono.get("/raw", permission([SystemPermissions.configReadSecrets]), async (c) => {
|
hono.get(
|
||||||
|
"/raw",
|
||||||
|
describeRoute({
|
||||||
|
summary: "Get the raw config",
|
||||||
|
tags: ["system"],
|
||||||
|
}),
|
||||||
|
permission([SystemPermissions.configReadSecrets]),
|
||||||
|
async (c) => {
|
||||||
// @ts-expect-error "fetch" is private
|
// @ts-expect-error "fetch" is private
|
||||||
return c.json(await this.app.modules.fetch());
|
return c.json(await this.app.modules.fetch());
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
hono.get(
|
hono.get(
|
||||||
"/:module?",
|
"/:module?",
|
||||||
tb("param", Type.Object({ module: Type.Optional(StringEnum(MODULE_NAMES)) })),
|
describeRoute({
|
||||||
tb(
|
summary: "Get the config for a module",
|
||||||
"query",
|
tags: ["system"],
|
||||||
Type.Object({
|
|
||||||
secrets: Type.Optional(booleanLike),
|
|
||||||
}),
|
}),
|
||||||
),
|
jsc("param", s.object({ module: s.string({ enum: MODULE_NAMES }).optional() })),
|
||||||
|
jsc("query", s.object({ secrets: s.boolean().optional() })),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
// @todo: allow secrets if authenticated user is admin
|
// @todo: allow secrets if authenticated user is admin
|
||||||
const { secrets } = c.req.valid("query");
|
const { secrets } = c.req.valid("query");
|
||||||
@@ -119,12 +121,7 @@ export class SystemController extends Controller {
|
|||||||
hono.post(
|
hono.post(
|
||||||
"/set/:module",
|
"/set/:module",
|
||||||
permission(SystemPermissions.configWrite),
|
permission(SystemPermissions.configWrite),
|
||||||
tb(
|
jsc("query", s.object({ force: s.boolean().optional() }), { skipOpenAPI: true }),
|
||||||
"query",
|
|
||||||
Type.Object({
|
|
||||||
force: Type.Optional(booleanLike),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const module = c.req.param("module") as any;
|
const module = c.req.param("module") as any;
|
||||||
const { force } = c.req.valid("query");
|
const { force } = c.req.valid("query");
|
||||||
@@ -230,13 +227,17 @@ export class SystemController extends Controller {
|
|||||||
|
|
||||||
hono.get(
|
hono.get(
|
||||||
"/schema/:module?",
|
"/schema/:module?",
|
||||||
|
describeRoute({
|
||||||
|
summary: "Get the schema for a module",
|
||||||
|
tags: ["system"],
|
||||||
|
}),
|
||||||
permission(SystemPermissions.schemaRead),
|
permission(SystemPermissions.schemaRead),
|
||||||
tb(
|
jsc(
|
||||||
"query",
|
"query",
|
||||||
Type.Object({
|
s.partialObject({
|
||||||
config: Type.Optional(booleanLike),
|
config: s.boolean(),
|
||||||
secrets: Type.Optional(booleanLike),
|
secrets: s.boolean(),
|
||||||
fresh: Type.Optional(booleanLike),
|
fresh: s.boolean(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
@@ -274,13 +275,11 @@ export class SystemController extends Controller {
|
|||||||
|
|
||||||
hono.post(
|
hono.post(
|
||||||
"/build",
|
"/build",
|
||||||
tb(
|
describeRoute({
|
||||||
"query",
|
summary: "Build the app",
|
||||||
Type.Object({
|
tags: ["system"],
|
||||||
sync: Type.Optional(booleanLike),
|
|
||||||
fetch: Type.Optional(booleanLike),
|
|
||||||
}),
|
}),
|
||||||
),
|
jsc("query", s.object({ sync: s.boolean().optional(), fetch: s.boolean().optional() })),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const options = c.req.valid("query") as Record<string, boolean>;
|
const options = c.req.valid("query") as Record<string, boolean>;
|
||||||
this.ctx.guard.throwUnlessGranted(SystemPermissions.build, c);
|
this.ctx.guard.throwUnlessGranted(SystemPermissions.build, c);
|
||||||
@@ -293,9 +292,22 @@ export class SystemController extends Controller {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
hono.get("/ping", (c) => c.json({ pong: true }));
|
hono.get(
|
||||||
|
"/ping",
|
||||||
|
describeRoute({
|
||||||
|
summary: "Ping the server",
|
||||||
|
tags: ["system"],
|
||||||
|
}),
|
||||||
|
(c) => c.json({ pong: true }),
|
||||||
|
);
|
||||||
|
|
||||||
hono.get("/info", (c) =>
|
hono.get(
|
||||||
|
"/info",
|
||||||
|
describeRoute({
|
||||||
|
summary: "Get the server info",
|
||||||
|
tags: ["system"],
|
||||||
|
}),
|
||||||
|
(c) =>
|
||||||
c.json({
|
c.json({
|
||||||
version: c.get("app")?.version(),
|
version: c.get("app")?.version(),
|
||||||
runtime: getRuntimeKey(),
|
runtime: getRuntimeKey(),
|
||||||
@@ -308,11 +320,17 @@ export class SystemController extends Controller {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
hono.get("/openapi.json", async (c) => {
|
hono.get(
|
||||||
const config = getDefaultConfig();
|
"/openapi.json",
|
||||||
return c.json(generateOpenAPI(config));
|
openAPISpecs(this.ctx.server, {
|
||||||
});
|
info: {
|
||||||
|
title: "bknd API",
|
||||||
|
version: getVersion(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
hono.get("/swagger", swaggerUI({ url: "/api/system/openapi.json" }));
|
||||||
|
|
||||||
return hono.all("*", (c) => c.notFound());
|
return hono;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +1,64 @@
|
|||||||
import { Api, type ApiOptions, type TApiUser } from "Api";
|
import { Api, type ApiOptions, type AuthState } from "Api";
|
||||||
import { isDebug } from "core";
|
import { isDebug } from "core";
|
||||||
import { createContext, type ReactNode, useContext } from "react";
|
import { createContext, type ReactNode, useContext, useMemo, useState } from "react";
|
||||||
import type { AdminBkndWindowContext } from "modules/server/AdminController";
|
import type { AdminBkndWindowContext } from "modules/server/AdminController";
|
||||||
|
|
||||||
const ClientContext = createContext<{ baseUrl: string; api: Api }>({
|
export type BkndClientContext = {
|
||||||
baseUrl: undefined,
|
baseUrl: string;
|
||||||
} as any);
|
api: Api;
|
||||||
|
authState?: Partial<AuthState>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ClientContext = createContext<BkndClientContext>(undefined!);
|
||||||
|
|
||||||
export type ClientProviderProps = {
|
export type ClientProviderProps = {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
} & (
|
baseUrl?: string;
|
||||||
| { baseUrl?: string; user?: TApiUser | null | undefined }
|
} & ApiOptions;
|
||||||
| {
|
|
||||||
api: Api;
|
export const ClientProvider = ({
|
||||||
|
children,
|
||||||
|
host,
|
||||||
|
baseUrl: _baseUrl = host,
|
||||||
|
...props
|
||||||
|
}: ClientProviderProps) => {
|
||||||
|
const winCtx = useBkndWindowContext();
|
||||||
|
const _ctx = useClientContext();
|
||||||
|
let actualBaseUrl = _baseUrl ?? _ctx?.baseUrl ?? "";
|
||||||
|
let user: any = undefined;
|
||||||
|
|
||||||
|
if (winCtx) {
|
||||||
|
user = winCtx.user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!actualBaseUrl) {
|
||||||
|
try {
|
||||||
|
actualBaseUrl = window.location.origin;
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiProps = { user, ...props, host: actualBaseUrl };
|
||||||
|
const api = useMemo(
|
||||||
|
() =>
|
||||||
|
new Api({
|
||||||
|
...apiProps,
|
||||||
|
verbose: isDebug(),
|
||||||
|
onAuthStateChange: (state) => {
|
||||||
|
props.onAuthStateChange?.(state);
|
||||||
|
if (!authState?.token || state.token !== authState?.token) {
|
||||||
|
setAuthState(state);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[JSON.stringify(apiProps)],
|
||||||
);
|
);
|
||||||
|
|
||||||
export const ClientProvider = ({ children, ...props }: ClientProviderProps) => {
|
const [authState, setAuthState] = useState<Partial<AuthState> | undefined>(
|
||||||
let api: Api;
|
apiProps.user ? api.getAuthState() : undefined,
|
||||||
|
);
|
||||||
if (props && "api" in props) {
|
|
||||||
api = props.api;
|
|
||||||
} else {
|
|
||||||
const winCtx = useBkndWindowContext();
|
|
||||||
const _ctx_baseUrl = useBaseUrl();
|
|
||||||
const { baseUrl, user } = props;
|
|
||||||
let actualBaseUrl = baseUrl ?? _ctx_baseUrl ?? "";
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!baseUrl) {
|
|
||||||
if (_ctx_baseUrl) {
|
|
||||||
actualBaseUrl = _ctx_baseUrl;
|
|
||||||
console.warn("wrapped many times, take from context", actualBaseUrl);
|
|
||||||
} else if (typeof window !== "undefined") {
|
|
||||||
actualBaseUrl = window.location.origin;
|
|
||||||
//console.log("setting from window", actualBaseUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error in ClientProvider", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
//console.log("api init", { host: actualBaseUrl, user: user ?? winCtx.user });
|
|
||||||
api = new Api({ host: actualBaseUrl, user: user ?? winCtx.user, verbose: isDebug() });
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ClientContext.Provider value={{ baseUrl: api.baseUrl, api }}>
|
<ClientContext.Provider value={{ baseUrl: api.baseUrl, api, authState }}>
|
||||||
{children}
|
{children}
|
||||||
</ClientContext.Provider>
|
</ClientContext.Provider>
|
||||||
);
|
);
|
||||||
@@ -61,12 +73,16 @@ export const useApi = (host?: ApiOptions["host"]): Api => {
|
|||||||
return context.api;
|
return context.api;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useClientContext = () => {
|
||||||
|
return useContext(ClientContext);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated use useApi().baseUrl instead
|
* @deprecated use useApi().baseUrl instead
|
||||||
*/
|
*/
|
||||||
export const useBaseUrl = () => {
|
export const useBaseUrl = () => {
|
||||||
const context = useContext(ClientContext);
|
const context = useClientContext();
|
||||||
return context.baseUrl;
|
return context?.baseUrl;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useBkndWindowContext(): AdminBkndWindowContext {
|
export function useBkndWindowContext(): AdminBkndWindowContext {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { AuthState } from "Api";
|
import type { AuthState } from "Api";
|
||||||
import type { AuthResponse } from "auth";
|
import type { AuthResponse } from "auth";
|
||||||
import { useState } from "react";
|
|
||||||
import { useApi, useInvalidate } from "ui/client";
|
import { useApi, useInvalidate } from "ui/client";
|
||||||
|
import { useClientContext } from "ui/client/ClientProvider";
|
||||||
|
|
||||||
type LoginData = {
|
type LoginData = {
|
||||||
email: string;
|
email: string;
|
||||||
@@ -10,7 +10,7 @@ type LoginData = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type UseAuth = {
|
type UseAuth = {
|
||||||
data: AuthState | undefined;
|
data: Partial<AuthState> | undefined;
|
||||||
user: AuthState["user"] | undefined;
|
user: AuthState["user"] | undefined;
|
||||||
token: AuthState["token"] | undefined;
|
token: AuthState["token"] | undefined;
|
||||||
verified: boolean;
|
verified: boolean;
|
||||||
@@ -24,46 +24,36 @@ type UseAuth = {
|
|||||||
export const useAuth = (options?: { baseUrl?: string }): UseAuth => {
|
export const useAuth = (options?: { baseUrl?: string }): UseAuth => {
|
||||||
const api = useApi(options?.baseUrl);
|
const api = useApi(options?.baseUrl);
|
||||||
const invalidate = useInvalidate();
|
const invalidate = useInvalidate();
|
||||||
const authState = api.getAuthState();
|
const { authState } = useClientContext();
|
||||||
const [authData, setAuthData] = useState<UseAuth["data"]>(authState);
|
|
||||||
const verified = authState?.verified ?? false;
|
const verified = authState?.verified ?? false;
|
||||||
|
|
||||||
function updateAuthState() {
|
|
||||||
setAuthData(api.getAuthState());
|
|
||||||
}
|
|
||||||
|
|
||||||
async function login(input: LoginData) {
|
async function login(input: LoginData) {
|
||||||
const res = await api.auth.loginWithPassword(input);
|
const res = await api.auth.login("password", input);
|
||||||
updateAuthState();
|
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function register(input: LoginData) {
|
async function register(input: LoginData) {
|
||||||
const res = await api.auth.registerWithPassword(input);
|
const res = await api.auth.register("password", input);
|
||||||
updateAuthState();
|
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setToken(token: string) {
|
function setToken(token: string) {
|
||||||
api.updateToken(token);
|
api.updateToken(token);
|
||||||
updateAuthState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logout() {
|
async function logout() {
|
||||||
await api.updateToken(undefined);
|
api.updateToken(undefined);
|
||||||
setAuthData(undefined);
|
|
||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function verify() {
|
async function verify() {
|
||||||
await api.verifyAuth();
|
await api.verifyAuth();
|
||||||
updateAuthState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: authData,
|
data: authState,
|
||||||
user: authData?.user,
|
user: authState?.user,
|
||||||
token: authData?.token,
|
token: authState?.token,
|
||||||
verified,
|
verified,
|
||||||
login,
|
login,
|
||||||
register,
|
register,
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { default as CodeMirror, type ReactCodeMirrorProps } from "@uiw/react-codemirror";
|
import {
|
||||||
|
default as CodeMirror,
|
||||||
|
type ReactCodeMirrorProps,
|
||||||
|
EditorView,
|
||||||
|
} from "@uiw/react-codemirror";
|
||||||
import { json } from "@codemirror/lang-json";
|
import { json } from "@codemirror/lang-json";
|
||||||
import { html } from "@codemirror/lang-html";
|
import { html } from "@codemirror/lang-html";
|
||||||
import { useTheme } from "ui/client/use-theme";
|
import { useTheme } from "ui/client/use-theme";
|
||||||
@@ -43,7 +47,7 @@ export default function CodeEditor({
|
|||||||
theme={theme === "dark" ? "dark" : "light"}
|
theme={theme === "dark" ? "dark" : "light"}
|
||||||
editable={editable}
|
editable={editable}
|
||||||
basicSetup={_basicSetup}
|
basicSetup={_basicSetup}
|
||||||
extensions={extensions}
|
extensions={[...extensions, EditorView.lineWrapping]}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export type DropdownItem =
|
|||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
destructive?: boolean;
|
destructive?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
title?: string;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -142,6 +143,7 @@ export function Dropdown({
|
|||||||
item.destructive && "text-red-500 hover:bg-red-600 hover:text-white",
|
item.destructive && "text-red-500 hover:bg-red-600 hover:text-white",
|
||||||
)}
|
)}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
title={item.title}
|
||||||
>
|
>
|
||||||
{space_for_icon && (
|
{space_for_icon && (
|
||||||
<div className="size-[16px] text-left mr-1.5 opacity-80">
|
<div className="size-[16px] text-left mr-1.5 opacity-80">
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export type DataTableProps<Data> = {
|
|||||||
checkable?: boolean;
|
checkable?: boolean;
|
||||||
onClickRow?: (row: Data) => void;
|
onClickRow?: (row: Data) => void;
|
||||||
onClickPage?: (page: number) => void;
|
onClickPage?: (page: number) => void;
|
||||||
|
hasMore?: boolean;
|
||||||
total?: number;
|
total?: number;
|
||||||
page?: number;
|
page?: number;
|
||||||
perPage?: number;
|
perPage?: number;
|
||||||
@@ -59,6 +60,7 @@ export function DataTable<Data extends Record<string, any> = Record<string, any>
|
|||||||
onClickRow,
|
onClickRow,
|
||||||
onClickPage,
|
onClickPage,
|
||||||
onClickSort,
|
onClickSort,
|
||||||
|
hasMore,
|
||||||
total,
|
total,
|
||||||
sort,
|
sort,
|
||||||
page = 1,
|
page = 1,
|
||||||
@@ -75,7 +77,7 @@ export function DataTable<Data extends Record<string, any> = Record<string, any>
|
|||||||
page = page || 1;
|
page = page || 1;
|
||||||
|
|
||||||
const select = columns && columns.length > 0 ? columns : Object.keys(data?.[0] || {});
|
const select = columns && columns.length > 0 ? columns : Object.keys(data?.[0] || {});
|
||||||
const pages = Math.max(Math.ceil(total / perPage), 1);
|
const pages = hasMore === undefined ? Math.max(Math.ceil(total / perPage), 1) : undefined;
|
||||||
const CellRender = renderValue || CellValue;
|
const CellRender = renderValue || CellValue;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -196,12 +198,14 @@ export function DataTable<Data extends Record<string, any> = Record<string, any>
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-center justify-between">
|
<div className="flex flex-row items-center justify-between">
|
||||||
<div className="hidden md:flex text-primary/40">
|
<div className="hidden md:flex text-primary/40">
|
||||||
|
{hasMore === undefined ? (
|
||||||
<TableDisplay
|
<TableDisplay
|
||||||
perPage={perPage}
|
perPage={perPage}
|
||||||
page={page}
|
page={page}
|
||||||
items={data?.length || 0}
|
items={data?.length || 0}
|
||||||
total={total}
|
total={total}
|
||||||
/>
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row gap-2 md:gap-10 items-center">
|
<div className="flex flex-row gap-2 md:gap-10 items-center">
|
||||||
{perPageOptions && (
|
{perPageOptions && (
|
||||||
@@ -220,11 +224,16 @@ export function DataTable<Data extends Record<string, any> = Record<string, any>
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="text-primary/40">
|
<div className="text-primary/40">
|
||||||
Page {page} of {pages}
|
Page {page} {pages ? `of ${pages}` : ""}
|
||||||
</div>
|
</div>
|
||||||
{onClickPage && (
|
{onClickPage && (
|
||||||
<div className="flex flex-row gap-1.5">
|
<div className="flex flex-row gap-1.5">
|
||||||
<TableNav current={page} total={pages} onClick={onClickPage} />
|
<TableNav
|
||||||
|
current={page}
|
||||||
|
total={pages}
|
||||||
|
hasMore={hasMore}
|
||||||
|
onClick={onClickPage}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -274,28 +283,46 @@ const TableDisplay = ({ perPage, page, items, total }) => {
|
|||||||
return <>Showing 1 row</>;
|
return <>Showing 1 row</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const text = `Showing ${perPage * (page - 1) + 1}-${perPage * (page - 1) + items}`;
|
||||||
<>
|
if (total) {
|
||||||
Showing {perPage * (page - 1) + 1}-{perPage * (page - 1) + items} of {total} rows
|
return `${text} of ${total} rows`;
|
||||||
</>
|
}
|
||||||
);
|
|
||||||
|
return text;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TableNavProps = {
|
type TableNavProps = {
|
||||||
current: number;
|
current: number;
|
||||||
total: number;
|
total?: number;
|
||||||
|
hasMore?: boolean;
|
||||||
onClick?: (page: number) => void;
|
onClick?: (page: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TableNav: React.FC<TableNavProps> = ({ current, total, onClick }: TableNavProps) => {
|
const TableNav: React.FC<TableNavProps> = ({
|
||||||
|
current,
|
||||||
|
total: _total,
|
||||||
|
hasMore: _hasMore,
|
||||||
|
onClick,
|
||||||
|
}: TableNavProps) => {
|
||||||
|
const total = _total !== undefined ? _total : _hasMore !== undefined ? current + 1 : current;
|
||||||
|
const hasMore = _hasMore !== undefined ? _hasMore : current < (total ?? 0);
|
||||||
|
|
||||||
const navMap = [
|
const navMap = [
|
||||||
{ value: 1, Icon: TbChevronsLeft, disabled: current === 1 },
|
{ value: 1, Icon: TbChevronsLeft, disabled: current === 1 },
|
||||||
{ value: current - 1, Icon: TbChevronLeft, disabled: current === 1 },
|
{ value: current - 1, Icon: TbChevronLeft, disabled: current === 1 },
|
||||||
{ value: current + 1, Icon: TbChevronRight, disabled: current === total },
|
{ value: current + 1, Icon: TbChevronRight, disabled: !hasMore },
|
||||||
{ value: total, Icon: TbChevronsRight, disabled: current === total },
|
{
|
||||||
|
value: _hasMore === undefined ? total : undefined,
|
||||||
|
Icon: TbChevronsRight,
|
||||||
|
disabled: !hasMore,
|
||||||
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
return navMap.map((nav, key) => (
|
return navMap.map((nav, key) => {
|
||||||
|
const page = nav.value;
|
||||||
|
if (page === undefined) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
role="button"
|
role="button"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -303,12 +330,12 @@ const TableNav: React.FC<TableNavProps> = ({ current, total, onClick }: TableNav
|
|||||||
disabled={nav.disabled}
|
disabled={nav.disabled}
|
||||||
className="px-2 py-2 border-muted border rounded-md enabled:link text-lg enabled:hover:bg-primary/5 text-primary/90 disabled:opacity-50 disabled:cursor-not-allowed"
|
className="px-2 py-2 border-muted border rounded-md enabled:link text-lg enabled:hover:bg-primary/5 text-primary/90 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const page = nav.value;
|
|
||||||
const safePage = page < 1 ? 1 : page > total ? total : page;
|
const safePage = page < 1 ? 1 : page > total ? total : page;
|
||||||
onClick?.(safePage);
|
onClick?.(safePage);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<nav.Icon />
|
<nav.Icon />
|
||||||
</button>
|
</button>
|
||||||
));
|
);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import type { Api } from "bknd/client";
|
import type { Api } from "bknd/client";
|
||||||
|
import type { PrimaryFieldType } from "core";
|
||||||
import type { RepoQueryIn } from "data";
|
import type { RepoQueryIn } from "data";
|
||||||
import type { MediaFieldSchema } from "media/AppMedia";
|
import type { MediaFieldSchema } from "media/AppMedia";
|
||||||
import type { TAppMediaConfig } from "media/media-schema";
|
import type { TAppMediaConfig } from "media/media-schema";
|
||||||
import { useId, useEffect, useRef, useState } from "react";
|
import { useId, useEffect, useRef, useState } from "react";
|
||||||
import { useApi, useApiInfiniteQuery, useApiQuery, useInvalidate } from "ui/client";
|
import { useApi, useApiInfiniteQuery, useApiQuery, useInvalidate } from "bknd/client";
|
||||||
import { useEvent } from "ui/hooks/use-event";
|
import { useEvent } from "ui/hooks/use-event";
|
||||||
import { Dropzone, type DropzoneProps } from "./Dropzone";
|
import { Dropzone, type DropzoneProps } from "./Dropzone";
|
||||||
import { mediaItemsToFileStates } from "./helper";
|
import { mediaItemsToFileStates } from "./helper";
|
||||||
@@ -14,7 +15,7 @@ export type DropzoneContainerProps = {
|
|||||||
infinite?: boolean;
|
infinite?: boolean;
|
||||||
entity?: {
|
entity?: {
|
||||||
name: string;
|
name: string;
|
||||||
id: number;
|
id: PrimaryFieldType;
|
||||||
field: string;
|
field: string;
|
||||||
};
|
};
|
||||||
media?: Pick<TAppMediaConfig, "entity_name" | "storage">;
|
media?: Pick<TAppMediaConfig, "entity_name" | "storage">;
|
||||||
|
|||||||
@@ -1,46 +1,47 @@
|
|||||||
import {
|
import { decodeSearch, encodeSearch, mergeObject, parseDecode } from "core/utils";
|
||||||
type Static,
|
|
||||||
type StaticDecode,
|
|
||||||
type TSchema,
|
|
||||||
decodeSearch,
|
|
||||||
encodeSearch,
|
|
||||||
parseDecode,
|
|
||||||
} from "core/utils";
|
|
||||||
import { isEqual, transform } from "lodash-es";
|
import { isEqual, transform } from "lodash-es";
|
||||||
import { useLocation, useSearch as useWouterSearch } from "wouter";
|
import { useLocation, useSearch as useWouterSearch } from "wouter";
|
||||||
|
import { type s, parse, cloneSchema } from "core/object/schema";
|
||||||
|
|
||||||
// @todo: migrate to Typebox
|
// @todo: migrate to Typebox
|
||||||
export function useSearch<Schema extends TSchema = TSchema>(
|
export function useSearch<Schema extends s.TAnySchema = s.TAnySchema>(
|
||||||
schema: Schema,
|
_schema: Schema,
|
||||||
defaultValue?: Partial<StaticDecode<Schema>>,
|
defaultValue?: Partial<s.StaticCoerced<Schema>>,
|
||||||
) {
|
) {
|
||||||
|
const schema = cloneSchema(_schema as any) as s.TSchema;
|
||||||
const searchString = useWouterSearch();
|
const searchString = useWouterSearch();
|
||||||
const [location, navigate] = useLocation();
|
const [location, navigate] = useLocation();
|
||||||
let value: StaticDecode<Schema> = defaultValue ? parseDecode(schema, defaultValue as any) : {};
|
const initial = searchString.length > 0 ? decodeSearch(searchString) : (defaultValue ?? {});
|
||||||
|
const value = parse(schema, initial, {
|
||||||
|
withDefaults: true,
|
||||||
|
clone: true,
|
||||||
|
}) as s.StaticCoerced<Schema>;
|
||||||
|
|
||||||
if (searchString.length > 0) {
|
// @ts-ignore
|
||||||
value = parseDecode(schema, decodeSearch(searchString));
|
const _defaults = mergeObject(schema.template({ withOptional: true }), defaultValue ?? {});
|
||||||
//console.log("search:decode", value);
|
|
||||||
|
function set<Update extends Partial<s.StaticCoerced<Schema>>>(update: Update): void {
|
||||||
|
// @ts-ignore
|
||||||
|
if (schema.validate(update).valid) {
|
||||||
|
const search = getWithoutDefaults(mergeObject(value, update), _defaults);
|
||||||
|
const encoded = encodeSearch(search, { encode: false });
|
||||||
|
navigate(location + (encoded.length > 0 ? "?" + encoded : ""));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @todo: add option to set multiple keys at once
|
return {
|
||||||
function set<Key extends keyof Static<Schema>>(key: Key, value: Static<Schema>[Key]): void {
|
value: value as Required<s.StaticCoerced<Schema>>,
|
||||||
//console.log("set", key, value);
|
set,
|
||||||
const update = parseDecode(schema, { ...decodeSearch(searchString), [key]: value });
|
};
|
||||||
const search = transform(
|
}
|
||||||
update as any,
|
|
||||||
|
function getWithoutDefaults(value: object, defaultValue: object) {
|
||||||
|
return transform(
|
||||||
|
value as any,
|
||||||
(result, value, key) => {
|
(result, value, key) => {
|
||||||
if (defaultValue && isEqual(value, defaultValue[key])) return;
|
if (defaultValue && isEqual(value, defaultValue[key])) return;
|
||||||
result[key] = value;
|
result[key] = value;
|
||||||
},
|
},
|
||||||
{} as Static<Schema>,
|
{} as object,
|
||||||
);
|
);
|
||||||
const encoded = encodeSearch(search, { encode: false });
|
|
||||||
navigate(location + (encoded.length > 0 ? "?" + encoded : ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
value: value as Required<StaticDecode<Schema>>,
|
|
||||||
set,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { SegmentedControl, Tooltip } from "@mantine/core";
|
import { SegmentedControl, Tooltip } from "@mantine/core";
|
||||||
import { IconKeyOff, IconSettings, IconUser } from "@tabler/icons-react";
|
import { IconApi, IconBook, IconKeyOff, IconSettings, IconUser } from "@tabler/icons-react";
|
||||||
import {
|
import {
|
||||||
TbDatabase,
|
TbDatabase,
|
||||||
TbFingerprint,
|
TbFingerprint,
|
||||||
@@ -24,6 +24,7 @@ import { useLocation } from "wouter";
|
|||||||
import { NavLink } from "./AppShell";
|
import { NavLink } from "./AppShell";
|
||||||
import { autoFormatString } from "core/utils";
|
import { autoFormatString } from "core/utils";
|
||||||
import { appShellStore } from "ui/store";
|
import { appShellStore } from "ui/store";
|
||||||
|
import { getVersion } from "core/env";
|
||||||
|
|
||||||
export function HeaderNavigation() {
|
export function HeaderNavigation() {
|
||||||
const [location, navigate] = useLocation();
|
const [location, navigate] = useLocation();
|
||||||
@@ -159,6 +160,16 @@ function UserMenu() {
|
|||||||
|
|
||||||
const items: DropdownItem[] = [
|
const items: DropdownItem[] = [
|
||||||
{ label: "Settings", onClick: () => navigate("/settings"), icon: IconSettings },
|
{ label: "Settings", onClick: () => navigate("/settings"), icon: IconSettings },
|
||||||
|
{
|
||||||
|
label: "OpenAPI",
|
||||||
|
onClick: () => window.open("/api/system/swagger", "_blank"),
|
||||||
|
icon: IconApi,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Docs",
|
||||||
|
onClick: () => window.open("https://docs.bknd.io", "_blank"),
|
||||||
|
icon: IconBook,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (config.auth.enabled) {
|
if (config.auth.enabled) {
|
||||||
@@ -166,7 +177,8 @@ function UserMenu() {
|
|||||||
items.push({ label: "Login", onClick: handleLogin, icon: IconUser });
|
items.push({ label: "Login", onClick: handleLogin, icon: IconUser });
|
||||||
} else {
|
} else {
|
||||||
items.push({
|
items.push({
|
||||||
label: `Logout ${auth.user.email}`,
|
label: "Logout",
|
||||||
|
title: `Logout ${auth.user.email}`,
|
||||||
onClick: handleLogout,
|
onClick: handleLogout,
|
||||||
icon: IconKeyOff,
|
icon: IconKeyOff,
|
||||||
});
|
});
|
||||||
@@ -176,6 +188,11 @@ function UserMenu() {
|
|||||||
if (!options.theme) {
|
if (!options.theme) {
|
||||||
items.push(() => <UserMenuThemeToggler />);
|
items.push(() => <UserMenuThemeToggler />);
|
||||||
}
|
}
|
||||||
|
items.push(() => (
|
||||||
|
<div className="font-mono leading-none text-xs text-primary/50 text-center pb-1 pt-2 mt-1 border-t border-primary/5">
|
||||||
|
{getVersion()}
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { EntityRelationalFormField } from "./fields/EntityRelationalFormField";
|
|||||||
import ErrorBoundary from "ui/components/display/ErrorBoundary";
|
import ErrorBoundary from "ui/components/display/ErrorBoundary";
|
||||||
import { Alert } from "ui/components/display/Alert";
|
import { Alert } from "ui/components/display/Alert";
|
||||||
import { bkndModals } from "ui/modals";
|
import { bkndModals } from "ui/modals";
|
||||||
|
import type { PrimaryFieldType } from "core";
|
||||||
|
|
||||||
// simplify react form types 🤦
|
// simplify react form types 🤦
|
||||||
export type FormApi = ReactFormExtendedApi<any, any, any, any, any, any, any, any, any, any>;
|
export type FormApi = ReactFormExtendedApi<any, any, any, any, any, any, any, any, any, any>;
|
||||||
@@ -30,7 +31,7 @@ export type TFieldApi = FieldApi<any, any, any, any, any, any, any, any, any, an
|
|||||||
|
|
||||||
type EntityFormProps = {
|
type EntityFormProps = {
|
||||||
entity: Entity;
|
entity: Entity;
|
||||||
entityId?: number;
|
entityId?: PrimaryFieldType;
|
||||||
data?: EntityData;
|
data?: EntityData;
|
||||||
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
|
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||||
fieldsDisabled: boolean;
|
fieldsDisabled: boolean;
|
||||||
@@ -225,7 +226,7 @@ function EntityMediaFormField({
|
|||||||
formApi: FormApi;
|
formApi: FormApi;
|
||||||
field: MediaField;
|
field: MediaField;
|
||||||
entity: Entity;
|
entity: Entity;
|
||||||
entityId?: number;
|
entityId?: PrimaryFieldType;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}) {
|
}) {
|
||||||
if (!entityId) return;
|
if (!entityId) return;
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ import {
|
|||||||
type EntityFieldsFormRef,
|
type EntityFieldsFormRef,
|
||||||
} from "ui/routes/data/forms/entity.fields.form";
|
} from "ui/routes/data/forms/entity.fields.form";
|
||||||
import { ModalBody, ModalFooter, type TCreateModalSchema, useStepContext } from "./CreateModal";
|
import { ModalBody, ModalFooter, type TCreateModalSchema, useStepContext } from "./CreateModal";
|
||||||
|
import { useBkndData } from "ui/client/schema/data/use-bknd-data";
|
||||||
|
|
||||||
const schema = entitiesSchema;
|
const schema = entitiesSchema;
|
||||||
type Schema = Static<typeof schema>;
|
type Schema = Static<typeof schema>;
|
||||||
|
|
||||||
export function StepEntityFields() {
|
export function StepEntityFields() {
|
||||||
const { nextStep, stepBack, state, setState } = useStepContext<TCreateModalSchema>();
|
const { nextStep, stepBack, state, setState } = useStepContext<TCreateModalSchema>();
|
||||||
|
const { config } = useBkndData();
|
||||||
const entity = state.entities?.create?.[0]!;
|
const entity = state.entities?.create?.[0]!;
|
||||||
const defaultFields = { id: { type: "primary", name: "id" } } as const;
|
const defaultFields = { id: { type: "primary", name: "id" } } as const;
|
||||||
const ref = useRef<EntityFieldsFormRef>(null);
|
const ref = useRef<EntityFieldsFormRef>(null);
|
||||||
@@ -82,6 +84,8 @@ export function StepEntityFields() {
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
fields={initial.fields as any}
|
fields={initial.fields as any}
|
||||||
onChange={updateListener}
|
onChange={updateListener}
|
||||||
|
defaultPrimaryFormat={config?.default_primary_format}
|
||||||
|
isNew={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,12 +10,13 @@ import {
|
|||||||
entitySchema,
|
entitySchema,
|
||||||
useStepContext,
|
useStepContext,
|
||||||
} from "./CreateModal";
|
} from "./CreateModal";
|
||||||
|
import { MantineSelect } from "ui/components/form/hook-form-mantine/MantineSelect";
|
||||||
|
|
||||||
export function StepEntity() {
|
export function StepEntity() {
|
||||||
const focusTrapRef = useFocusTrap();
|
const focusTrapRef = useFocusTrap();
|
||||||
|
|
||||||
const { nextStep, stepBack, state, setState } = useStepContext<TCreateModalSchema>();
|
const { nextStep, stepBack, state, setState } = useStepContext<TCreateModalSchema>();
|
||||||
const { register, handleSubmit, formState, watch } = useForm({
|
const { register, handleSubmit, formState, watch, control } = useForm({
|
||||||
mode: "onTouched",
|
mode: "onTouched",
|
||||||
resolver: typeboxResolver(entitySchema),
|
resolver: typeboxResolver(entitySchema),
|
||||||
defaultValues: state.entities?.create?.[0] ?? {},
|
defaultValues: state.entities?.create?.[0] ?? {},
|
||||||
@@ -56,7 +57,6 @@ export function StepEntity() {
|
|||||||
label="What's the name of the entity?"
|
label="What's the name of the entity?"
|
||||||
description="Use plural form, and all lowercase. It will be used as the database table."
|
description="Use plural form, and all lowercase. It will be used as the database table."
|
||||||
/>
|
/>
|
||||||
{/*<input type="submit" value="submit" />*/}
|
|
||||||
<TextInput
|
<TextInput
|
||||||
{...register("config.name")}
|
{...register("config.name")}
|
||||||
error={formState.errors.config?.name?.message}
|
error={formState.errors.config?.name?.message}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { PrimaryFieldType } from "core";
|
||||||
import { ucFirst } from "core/utils";
|
import { ucFirst } from "core/utils";
|
||||||
import type { Entity, EntityData, EntityRelation } from "data";
|
import type { Entity, EntityData, EntityRelation } from "data";
|
||||||
import { Fragment, useState } from "react";
|
import { Fragment, useState } from "react";
|
||||||
@@ -24,7 +25,7 @@ export function DataEntityUpdate({ params }) {
|
|||||||
return <Message.NotFound description={`Entity "${params.entity}" doesn't exist.`} />;
|
return <Message.NotFound description={`Entity "${params.entity}" doesn't exist.`} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const entityId = Number.parseInt(params.id as string);
|
const entityId = params.id as PrimaryFieldType;
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [navigate] = useNavigate();
|
const [navigate] = useNavigate();
|
||||||
useBrowserTitle(["Data", entity.label, `#${entityId}`]);
|
useBrowserTitle(["Data", entity.label, `#${entityId}`]);
|
||||||
@@ -202,7 +203,7 @@ function EntityDetailRelations({
|
|||||||
entity,
|
entity,
|
||||||
relations,
|
relations,
|
||||||
}: {
|
}: {
|
||||||
id: number;
|
id: PrimaryFieldType;
|
||||||
entity: Entity;
|
entity: Entity;
|
||||||
relations: EntityRelation[];
|
relations: EntityRelation[];
|
||||||
}) {
|
}) {
|
||||||
@@ -250,7 +251,7 @@ function EntityDetailInner({
|
|||||||
entity,
|
entity,
|
||||||
relation,
|
relation,
|
||||||
}: {
|
}: {
|
||||||
id: number;
|
id: PrimaryFieldType;
|
||||||
entity: Entity;
|
entity: Entity;
|
||||||
relation: EntityRelation;
|
relation: EntityRelation;
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ import { Breadcrumbs2 } from "ui/layouts/AppShell/Breadcrumbs2";
|
|||||||
import { routes } from "ui/lib/routes";
|
import { routes } from "ui/lib/routes";
|
||||||
import { EntityForm } from "ui/modules/data/components/EntityForm";
|
import { EntityForm } from "ui/modules/data/components/EntityForm";
|
||||||
import { useEntityForm } from "ui/modules/data/hooks/useEntityForm";
|
import { useEntityForm } from "ui/modules/data/hooks/useEntityForm";
|
||||||
import * as tbbox from "@sinclair/typebox";
|
import { s } from "core/object/schema";
|
||||||
const { Type } = tbbox;
|
|
||||||
|
|
||||||
export function DataEntityCreate({ params }) {
|
export function DataEntityCreate({ params }) {
|
||||||
const { $data } = useBkndData();
|
const { $data } = useBkndData();
|
||||||
@@ -29,7 +28,7 @@ export function DataEntityCreate({ params }) {
|
|||||||
const $q = useEntityMutate(entity.name);
|
const $q = useEntityMutate(entity.name);
|
||||||
|
|
||||||
// @todo: use entity schema for prefilling
|
// @todo: use entity schema for prefilling
|
||||||
const search = useSearch(Type.Object({}), {});
|
const search = useSearch(s.object({}), {});
|
||||||
|
|
||||||
function goBack() {
|
function goBack() {
|
||||||
window.history.go(-1);
|
window.history.go(-1);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type Entity, querySchema } from "data";
|
import { type Entity, repoQuery } from "data";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { TbDots } from "react-icons/tb";
|
import { TbDots } from "react-icons/tb";
|
||||||
import { useApiQuery } from "ui/client";
|
import { useApiQuery } from "ui/client";
|
||||||
@@ -14,22 +14,16 @@ import * as AppShell from "ui/layouts/AppShell/AppShell";
|
|||||||
import { routes, useNavigate } from "ui/lib/routes";
|
import { routes, useNavigate } from "ui/lib/routes";
|
||||||
import { useCreateUserModal } from "ui/modules/auth/hooks/use-create-user-modal";
|
import { useCreateUserModal } from "ui/modules/auth/hooks/use-create-user-modal";
|
||||||
import { EntityTable2 } from "ui/modules/data/components/EntityTable2";
|
import { EntityTable2 } from "ui/modules/data/components/EntityTable2";
|
||||||
import * as tbbox from "@sinclair/typebox";
|
import { s } from "core/object/schema";
|
||||||
const { Type } = tbbox;
|
import { pick } from "core/utils/objects";
|
||||||
|
|
||||||
// @todo: migrate to Typebox
|
const searchSchema = s.partialObject({
|
||||||
const searchSchema = Type.Composite(
|
...pick(repoQuery.properties, ["select", "where", "sort"]),
|
||||||
[
|
page: s.number({ default: 1 }).optional(),
|
||||||
Type.Pick(querySchema, ["select", "where", "sort"]),
|
perPage: s.number({ default: 10 }).optional(),
|
||||||
Type.Object({
|
});
|
||||||
page: Type.Optional(Type.Number({ default: 1 })),
|
|
||||||
perPage: Type.Optional(Type.Number({ default: 10 })),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
{ additionalProperties: false },
|
|
||||||
);
|
|
||||||
|
|
||||||
const PER_PAGE_OPTIONS = [5, 10, 25];
|
const PER_PAGE_OPTIONS = [5, 10, 25, 50, 100];
|
||||||
|
|
||||||
export function DataEntityList({ params }) {
|
export function DataEntityList({ params }) {
|
||||||
const { $data } = useBkndData();
|
const { $data } = useBkndData();
|
||||||
@@ -67,21 +61,18 @@ export function DataEntityList({ params }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleClickPage(page: number) {
|
function handleClickPage(page: number) {
|
||||||
search.set("page", page);
|
search.set({ page });
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSortClick(name: string) {
|
function handleSortClick(name: string) {
|
||||||
const sort = search.value.sort!;
|
const sort = search.value.sort!;
|
||||||
const newSort = { by: name, dir: sort.by === name && sort.dir === "asc" ? "desc" : "asc" };
|
const newSort = { by: name, dir: sort.by === name && sort.dir === "asc" ? "desc" : "asc" };
|
||||||
|
|
||||||
// // @ts-expect-error - somehow all search keys are optional
|
search.set({ sort: newSort as any });
|
||||||
console.log("new sort", newSort);
|
|
||||||
search.set("sort", newSort as any);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClickPerPage(perPage: number) {
|
function handleClickPerPage(perPage: number) {
|
||||||
// @todo: also reset page to 1
|
search.set({ perPage, page: 1 });
|
||||||
search.set("perPage", perPage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isUpdating = $q.isLoading || $q.isValidating;
|
const isUpdating = $q.isLoading || $q.isValidating;
|
||||||
@@ -140,6 +131,7 @@ export function DataEntityList({ params }) {
|
|||||||
perPage={search.value.perPage}
|
perPage={search.value.perPage}
|
||||||
perPageOptions={PER_PAGE_OPTIONS}
|
perPageOptions={PER_PAGE_OPTIONS}
|
||||||
total={meta?.count}
|
total={meta?.count}
|
||||||
|
hasMore={meta?.has_more}
|
||||||
onClickPage={handleClickPage}
|
onClickPage={handleClickPage}
|
||||||
onClickPerPage={handleClickPerPage}
|
onClickPerPage={handleClickPerPage}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ export function DataSchemaEntity({ params }) {
|
|||||||
const Fields = ({ entity }: { entity: Entity }) => {
|
const Fields = ({ entity }: { entity: Entity }) => {
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [updates, setUpdates] = useState(0);
|
const [updates, setUpdates] = useState(0);
|
||||||
const { actions, $data } = useBkndData();
|
const { actions, $data, config } = useBkndData();
|
||||||
const [res, setRes] = useState<any>();
|
const [res, setRes] = useState<any>();
|
||||||
const ref = useRef<EntityFieldsFormRef>(null);
|
const ref = useRef<EntityFieldsFormRef>(null);
|
||||||
async function handleUpdate() {
|
async function handleUpdate() {
|
||||||
@@ -201,6 +201,8 @@ const Fields = ({ entity }: { entity: Entity }) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}))}
|
}))}
|
||||||
|
defaultPrimaryFormat={config?.default_primary_format}
|
||||||
|
isNew={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isDebug() && (
|
{isDebug() && (
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ import { type TFieldSpec, fieldSpecs } from "ui/modules/data/components/fields-s
|
|||||||
import { dataFieldsUiSchema } from "../../settings/routes/data.settings";
|
import { dataFieldsUiSchema } from "../../settings/routes/data.settings";
|
||||||
import * as tbbox from "@sinclair/typebox";
|
import * as tbbox from "@sinclair/typebox";
|
||||||
import { useRoutePathState } from "ui/hooks/use-route-path-state";
|
import { useRoutePathState } from "ui/hooks/use-route-path-state";
|
||||||
|
import { MantineSelect } from "ui/components/form/hook-form-mantine/MantineSelect";
|
||||||
|
import type { TPrimaryFieldFormat } from "data/fields/PrimaryField";
|
||||||
const { Type } = tbbox;
|
const { Type } = tbbox;
|
||||||
|
|
||||||
const fieldsSchemaObject = originalFieldsSchemaObject;
|
const fieldsSchemaObject = originalFieldsSchemaObject;
|
||||||
@@ -65,6 +67,8 @@ export type EntityFieldsFormProps = {
|
|||||||
sortable?: boolean;
|
sortable?: boolean;
|
||||||
additionalFieldTypes?: (TFieldSpec & { onClick: () => void })[];
|
additionalFieldTypes?: (TFieldSpec & { onClick: () => void })[];
|
||||||
routePattern?: string;
|
routePattern?: string;
|
||||||
|
defaultPrimaryFormat?: TPrimaryFieldFormat;
|
||||||
|
isNew?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EntityFieldsFormRef = {
|
export type EntityFieldsFormRef = {
|
||||||
@@ -77,7 +81,7 @@ export type EntityFieldsFormRef = {
|
|||||||
|
|
||||||
export const EntityFieldsForm = forwardRef<EntityFieldsFormRef, EntityFieldsFormProps>(
|
export const EntityFieldsForm = forwardRef<EntityFieldsFormRef, EntityFieldsFormProps>(
|
||||||
function EntityFieldsForm(
|
function EntityFieldsForm(
|
||||||
{ fields: _fields, sortable, additionalFieldTypes, routePattern, ...props },
|
{ fields: _fields, sortable, additionalFieldTypes, routePattern, isNew, ...props },
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
const entityFields = Object.entries(_fields).map(([name, field]) => ({
|
const entityFields = Object.entries(_fields).map(([name, field]) => ({
|
||||||
@@ -172,6 +176,10 @@ export const EntityFieldsForm = forwardRef<EntityFieldsFormRef, EntityFieldsForm
|
|||||||
remove={remove}
|
remove={remove}
|
||||||
dnd={dnd}
|
dnd={dnd}
|
||||||
routePattern={routePattern}
|
routePattern={routePattern}
|
||||||
|
primary={{
|
||||||
|
defaultFormat: props.defaultPrimaryFormat,
|
||||||
|
editable: isNew,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -186,6 +194,10 @@ export const EntityFieldsForm = forwardRef<EntityFieldsFormRef, EntityFieldsForm
|
|||||||
errors={errors}
|
errors={errors}
|
||||||
remove={remove}
|
remove={remove}
|
||||||
routePattern={routePattern}
|
routePattern={routePattern}
|
||||||
|
primary={{
|
||||||
|
defaultFormat: props.defaultPrimaryFormat,
|
||||||
|
editable: isNew,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -281,6 +293,7 @@ function EntityField({
|
|||||||
errors,
|
errors,
|
||||||
dnd,
|
dnd,
|
||||||
routePattern,
|
routePattern,
|
||||||
|
primary,
|
||||||
}: {
|
}: {
|
||||||
field: FieldArrayWithId<TFieldsFormSchema, "fields", "id">;
|
field: FieldArrayWithId<TFieldsFormSchema, "fields", "id">;
|
||||||
index: number;
|
index: number;
|
||||||
@@ -292,6 +305,10 @@ function EntityField({
|
|||||||
errors: any;
|
errors: any;
|
||||||
dnd?: SortableItemProps;
|
dnd?: SortableItemProps;
|
||||||
routePattern?: string;
|
routePattern?: string;
|
||||||
|
primary?: {
|
||||||
|
defaultFormat?: TPrimaryFieldFormat;
|
||||||
|
editable?: boolean;
|
||||||
|
};
|
||||||
}) {
|
}) {
|
||||||
const prefix = `fields.${index}.field` as const;
|
const prefix = `fields.${index}.field` as const;
|
||||||
const type = field.field.type;
|
const type = field.field.type;
|
||||||
@@ -363,15 +380,29 @@ function EntityField({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex-col gap-1 hidden md:flex">
|
<div className="flex-col gap-1 hidden md:flex">
|
||||||
<span className="text-xs text-primary/50 leading-none">Required</span>
|
|
||||||
{is_primary ? (
|
{is_primary ? (
|
||||||
<Switch size="sm" defaultChecked disabled />
|
<>
|
||||||
|
<MantineSelect
|
||||||
|
data={["integer", "uuid"]}
|
||||||
|
defaultValue={primary?.defaultFormat}
|
||||||
|
disabled={!primary?.editable}
|
||||||
|
placeholder="Select format"
|
||||||
|
name={`${prefix}.config.format`}
|
||||||
|
allowDeselect={false}
|
||||||
|
control={control}
|
||||||
|
size="xs"
|
||||||
|
className="w-22"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="text-xs text-primary/50 leading-none">Required</span>
|
||||||
<MantineSwitch
|
<MantineSwitch
|
||||||
size="sm"
|
size="sm"
|
||||||
name={`${prefix}.config.required`}
|
name={`${prefix}.config.required`}
|
||||||
control={control}
|
control={control}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ input[type="date"]::-webkit-calendar-picker-indicator {
|
|||||||
.cm-editor {
|
.cm-editor {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-fade-in {
|
.animate-fade-in {
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
{
|
{
|
||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist/types",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
|
||||||
|
},
|
||||||
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
|
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
|
||||||
"exclude": ["./node_modules", "./__test__", "./e2e"]
|
"exclude": ["./node_modules", "./__test__", "./e2e"]
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-4
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"types": ["bun-types", "@cloudflare/workers-types"],
|
"types": ["bun-types"],
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
@@ -26,12 +26,18 @@
|
|||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"rootDir": "./src",
|
"rootDir": ".",
|
||||||
"outDir": "./dist/types",
|
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
|
"outDir": "./dist/types",
|
||||||
"paths": {
|
"paths": {
|
||||||
"*": ["./src/*"],
|
"*": ["./src/*"],
|
||||||
"bknd": ["./src/*"]
|
"bknd": ["./src/index.ts"],
|
||||||
|
"bknd/core": ["./src/core/index.ts"],
|
||||||
|
"bknd/adapter": ["./src/adapter/index.ts"],
|
||||||
|
"bknd/client": ["./src/ui/client/index.ts"],
|
||||||
|
"bknd/data": ["./src/data/index.ts"],
|
||||||
|
"bknd/media": ["./src/media/index.ts"],
|
||||||
|
"bknd/auth": ["./src/auth/index.ts"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
@@ -39,6 +45,7 @@
|
|||||||
"./src/**/*.tsx",
|
"./src/**/*.tsx",
|
||||||
"vite.dev.ts",
|
"vite.dev.ts",
|
||||||
"build.ts",
|
"build.ts",
|
||||||
|
"build.cli.ts",
|
||||||
"__test__",
|
"__test__",
|
||||||
"e2e/**/*.ts"
|
"e2e/**/*.ts"
|
||||||
],
|
],
|
||||||
|
|||||||
+6
-1
@@ -4,11 +4,13 @@ import { defineConfig } from "vite";
|
|||||||
import tsconfigPaths from "vite-tsconfig-paths";
|
import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
import { devServerConfig } from "./src/adapter/vite/dev-server-config";
|
import { devServerConfig } from "./src/adapter/vite/dev-server-config";
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import pkg from "./package.json" with { type: "json" };
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
define: {
|
define: {
|
||||||
__isDev: process.env.NODE_ENV === "production" ? "0" : "1",
|
__isDev: process.env.NODE_ENV === "production" ? "0" : "1",
|
||||||
|
__version: JSON.stringify(pkg.version),
|
||||||
},
|
},
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
publicDir: "./src/ui/assets",
|
publicDir: "./src/ui/assets",
|
||||||
@@ -21,7 +23,10 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
react(),
|
react(),
|
||||||
tsconfigPaths(),
|
tsconfigPaths({
|
||||||
|
// otherwise it'll throw an error because of examples/astro
|
||||||
|
ignoreConfigErrors: true,
|
||||||
|
}),
|
||||||
devServer({
|
devServer({
|
||||||
...devServerConfig,
|
...devServerConfig,
|
||||||
entry: "./vite.dev.ts",
|
entry: "./vite.dev.ts",
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.9.4",
|
"@biomejs/biome": "1.9.4",
|
||||||
"@clack/prompts": "^0.10.0",
|
"@clack/prompts": "^0.10.0",
|
||||||
"@cloudflare/workers-types": "^4.20240620.0",
|
|
||||||
"@tsconfig/strictest": "^2.0.5",
|
"@tsconfig/strictest": "^2.0.5",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"bun-types": "^1.1.18",
|
"bun-types": "^1.1.18",
|
||||||
@@ -27,18 +26,18 @@
|
|||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"name": "bknd",
|
"name": "bknd",
|
||||||
"version": "0.11.0",
|
"version": "0.14.0-rc.0",
|
||||||
"bin": "./dist/cli/index.js",
|
"bin": "./dist/cli/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cfworker/json-schema": "^4.1.1",
|
"@cfworker/json-schema": "^4.1.1",
|
||||||
"@codemirror/lang-html": "^6.4.9",
|
"@codemirror/lang-html": "^6.4.9",
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
"@codemirror/lang-liquid": "^6.2.2",
|
|
||||||
"@hello-pangea/dnd": "^18.0.1",
|
"@hello-pangea/dnd": "^18.0.1",
|
||||||
|
"@hono/swagger-ui": "^0.5.1",
|
||||||
"@libsql/client": "^0.15.2",
|
"@libsql/client": "^0.15.2",
|
||||||
"@mantine/core": "^7.17.1",
|
"@mantine/core": "^7.17.1",
|
||||||
"@mantine/hooks": "^7.17.1",
|
"@mantine/hooks": "^7.17.1",
|
||||||
"@sinclair/typebox": "^0.34.30",
|
"@sinclair/typebox": "0.34.30",
|
||||||
"@tanstack/react-form": "^1.0.5",
|
"@tanstack/react-form": "^1.0.5",
|
||||||
"@uiw/react-codemirror": "^4.23.10",
|
"@uiw/react-codemirror": "^4.23.10",
|
||||||
"@xyflow/react": "^12.4.4",
|
"@xyflow/react": "^12.4.4",
|
||||||
@@ -46,26 +45,25 @@
|
|||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"fast-xml-parser": "^5.0.8",
|
"fast-xml-parser": "^5.0.8",
|
||||||
"hono": "^4.7.4",
|
"hono": "^4.7.11",
|
||||||
"json-schema-form-react": "^0.0.2",
|
"json-schema-form-react": "^0.0.2",
|
||||||
"json-schema-library": "^10.0.0-rc7",
|
"json-schema-library": "10.0.0-rc7",
|
||||||
"json-schema-to-ts": "^3.1.1",
|
"json-schema-to-ts": "^3.1.1",
|
||||||
"kysely": "^0.27.6",
|
"kysely": "^0.27.6",
|
||||||
"liquidjs": "^10.21.0",
|
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"oauth4webapi": "^2.11.1",
|
"oauth4webapi": "^2.11.1",
|
||||||
"object-path-immutable": "^4.1.2",
|
"object-path-immutable": "^4.1.2",
|
||||||
"picocolors": "^1.1.1",
|
|
||||||
"radix-ui": "^1.1.3",
|
"radix-ui": "^1.1.3",
|
||||||
"swr": "^2.3.3",
|
"swr": "^2.3.3",
|
||||||
"wrangler": "^4.4.1",
|
"uuid": "^11.1.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.758.0",
|
"@aws-sdk/client-s3": "^3.758.0",
|
||||||
"@bluwy/giget-core": "^0.1.2",
|
"@bluwy/giget-core": "^0.1.2",
|
||||||
|
"@cloudflare/workers-types": "^4.20250606.0",
|
||||||
"@dagrejs/dagre": "^1.1.4",
|
"@dagrejs/dagre": "^1.1.4",
|
||||||
"@hono/typebox-validator": "^0.3.2",
|
"@hono/typebox-validator": "^0.3.3",
|
||||||
"@hono/vite-dev-server": "^0.19.0",
|
"@hono/vite-dev-server": "^0.19.1",
|
||||||
"@hookform/resolvers": "^4.1.3",
|
"@hookform/resolvers": "^4.1.3",
|
||||||
"@libsql/kysely-libsql": "^0.4.1",
|
"@libsql/kysely-libsql": "^0.4.1",
|
||||||
"@mantine/modals": "^7.17.1",
|
"@mantine/modals": "^7.17.1",
|
||||||
@@ -87,9 +85,11 @@
|
|||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"jotai": "^2.12.2",
|
"jotai": "^2.12.2",
|
||||||
"jsdom": "^26.0.0",
|
"jsdom": "^26.0.0",
|
||||||
|
"jsonv-ts": "^0.1.0",
|
||||||
"kysely-d1": "^0.3.0",
|
"kysely-d1": "^0.3.0",
|
||||||
"open": "^10.1.0",
|
"open": "^10.1.0",
|
||||||
"openapi-types": "^12.1.3",
|
"openapi-types": "^12.1.3",
|
||||||
|
"picocolors": "^1.1.1",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
"postcss-preset-mantine": "^1.17.0",
|
"postcss-preset-mantine": "^1.17.0",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
@@ -106,13 +106,13 @@
|
|||||||
"tsc-alias": "^1.8.11",
|
"tsc-alias": "^1.8.11",
|
||||||
"tsup": "^8.4.0",
|
"tsup": "^8.4.0",
|
||||||
"tsx": "^4.19.3",
|
"tsx": "^4.19.3",
|
||||||
"vite": "^6.2.1",
|
"vite": "^6.3.5",
|
||||||
"vite-tsconfig-paths": "^5.1.4",
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
"vitest": "^3.0.9",
|
"vitest": "^3.0.9",
|
||||||
"wouter": "^3.6.0",
|
"wouter": "^3.6.0",
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@hono/node-server": "^1.13.8",
|
"@hono/node-server": "^1.14.3",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": ">=19",
|
"react": ">=19",
|
||||||
@@ -133,7 +133,6 @@
|
|||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"bknd": "workspace:*",
|
|
||||||
"tsdx": "^0.14.1",
|
"tsdx": "^0.14.1",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
},
|
},
|
||||||
@@ -150,19 +149,24 @@
|
|||||||
},
|
},
|
||||||
"packages/postgres": {
|
"packages/postgres": {
|
||||||
"name": "@bknd/postgres",
|
"name": "@bknd/postgres",
|
||||||
"version": "0.0.1",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
|
||||||
"kysely": "^0.27.6",
|
|
||||||
"pg": "^8.14.0",
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.2.5",
|
"@types/bun": "^1.2.5",
|
||||||
"@types/node": "^22.13.10",
|
"@types/node": "^22.13.10",
|
||||||
"@types/pg": "^8.11.11",
|
"@types/pg": "^8.11.11",
|
||||||
|
"@xata.io/client": "^0.0.0-next.v93343b9646f57a1e5c51c35eccf0767c2bb80baa",
|
||||||
|
"@xata.io/kysely": "^0.2.1",
|
||||||
"bknd": "workspace:*",
|
"bknd": "workspace:*",
|
||||||
|
"kysely-neon": "^1.3.0",
|
||||||
"tsup": "^8.4.0",
|
"tsup": "^8.4.0",
|
||||||
"typescript": "^5.8.2",
|
"typescript": "^5.8.2",
|
||||||
},
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"kysely": "^0.27.6",
|
||||||
|
"kysely-postgres-js": "^2.0.0",
|
||||||
|
"pg": "^8.14.0",
|
||||||
|
"postgres": "^3.4.7",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"packages/sqlocal": {
|
"packages/sqlocal": {
|
||||||
"name": "@bknd/sqlocal",
|
"name": "@bknd/sqlocal",
|
||||||
@@ -514,8 +518,6 @@
|
|||||||
|
|
||||||
"@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.3.4", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q=="],
|
"@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.3.4", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q=="],
|
||||||
|
|
||||||
"@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.3.0", "", { "peerDependencies": { "unenv": "2.0.0-rc.15", "workerd": "^1.20250311.0" }, "optionalPeers": ["workerd"] }, "sha512-AaKYnbFpHaVDZGh3Hjy3oLYd12+LZw9aupAOudYJ+tjekahxcIqlSAr0zK9kPOdtgn10tzaqH7QJFUWcLE+k7g=="],
|
|
||||||
|
|
||||||
"@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20250224.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-sBbaAF2vgQ9+T50ik1ihekdepStBp0w4fvNghBfXIw1iWqfNWnypcjDMmi/7JhXJt2uBxBrSlXCvE5H7Gz+kbw=="],
|
"@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20250224.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-sBbaAF2vgQ9+T50ik1ihekdepStBp0w4fvNghBfXIw1iWqfNWnypcjDMmi/7JhXJt2uBxBrSlXCvE5H7Gz+kbw=="],
|
||||||
|
|
||||||
"@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20250224.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-naetGefgjAaDbEacpwaVruJXNwxmRRL7v3ppStgEiqAlPmTpQ/Edjn2SQ284QwOw3MvaVPHrWcaTBupUpkqCyg=="],
|
"@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20250224.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-naetGefgjAaDbEacpwaVruJXNwxmRRL7v3ppStgEiqAlPmTpQ/Edjn2SQ284QwOw3MvaVPHrWcaTBupUpkqCyg=="],
|
||||||
@@ -526,7 +528,7 @@
|
|||||||
|
|
||||||
"@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250224.0", "", { "os": "win32", "cpu": "x64" }, "sha512-x2iF1CsmYmmPEorWb1GRpAAouX5rRjmhuHMC259ojIlozR4G0LarlB9XfmeLEvtw537Ea0kJ6SOhjvUcWzxSvA=="],
|
"@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250224.0", "", { "os": "win32", "cpu": "x64" }, "sha512-x2iF1CsmYmmPEorWb1GRpAAouX5rRjmhuHMC259ojIlozR4G0LarlB9XfmeLEvtw537Ea0kJ6SOhjvUcWzxSvA=="],
|
||||||
|
|
||||||
"@cloudflare/workers-types": ["@cloudflare/workers-types@4.20250310.0", "", {}, "sha512-SNE2ohlL9/VxFbcHQc28n3Nj70FiS1Ea0wrUhCXUIbR2lsr4ceRVndNxhuzhcF9EZd2UXm2wwow34RIS1mm+Mg=="],
|
"@cloudflare/workers-types": ["@cloudflare/workers-types@4.20250606.0", "", {}, "sha512-9T/Y/Mxe57UVzqgfjJKheiMplnStj/3CmCHlgoZNLU8JW2waRbXvpY3EEeliiYAJfeHZTjeAaKO2pCabxAoyCw=="],
|
||||||
|
|
||||||
"@cnakazawa/watch": ["@cnakazawa/watch@1.0.4", "", { "dependencies": { "exec-sh": "^0.3.2", "minimist": "^1.2.0" }, "bin": { "watch": "cli.js" } }, "sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ=="],
|
"@cnakazawa/watch": ["@cnakazawa/watch@1.0.4", "", { "dependencies": { "exec-sh": "^0.3.2", "minimist": "^1.2.0" }, "bin": { "watch": "cli.js" } }, "sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ=="],
|
||||||
|
|
||||||
@@ -542,8 +544,6 @@
|
|||||||
|
|
||||||
"@codemirror/lang-json": ["@codemirror/lang-json@6.0.1", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/json": "^1.0.0" } }, "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ=="],
|
"@codemirror/lang-json": ["@codemirror/lang-json@6.0.1", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/json": "^1.0.0" } }, "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ=="],
|
||||||
|
|
||||||
"@codemirror/lang-liquid": ["@codemirror/lang-liquid@6.2.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.1" } }, "sha512-7Dm841fk37+JQW6j2rI1/uGkJyESrjzyhiIkaLjbbR0U6aFFQvMrJn35WxQreRMADMhzkyVkZM4467OR7GR8nQ=="],
|
|
||||||
|
|
||||||
"@codemirror/language": ["@codemirror/language@6.10.8", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", "@lezer/common": "^1.1.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" } }, "sha512-wcP8XPPhDH2vTqf181U8MbZnW+tDyPYy0UzVOa+oHORjyT+mhhom9vBd7dApJwoDz9Nb/a8kHjJIsuA/t8vNFw=="],
|
"@codemirror/language": ["@codemirror/language@6.10.8", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", "@lezer/common": "^1.1.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" } }, "sha512-wcP8XPPhDH2vTqf181U8MbZnW+tDyPYy0UzVOa+oHORjyT+mhhom9vBd7dApJwoDz9Nb/a8kHjJIsuA/t8vNFw=="],
|
||||||
|
|
||||||
"@codemirror/lint": ["@codemirror/lint@6.8.4", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.35.0", "crelt": "^1.0.5" } }, "sha512-u4q7PnZlJUojeRe8FJa/njJcMctISGgPQ4PnWsd9268R4ZTtU+tfFYmwkBvgcrK2+QQ8tYFVALVb5fVJykKc5A=="],
|
"@codemirror/lint": ["@codemirror/lint@6.8.4", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.35.0", "crelt": "^1.0.5" } }, "sha512-u4q7PnZlJUojeRe8FJa/njJcMctISGgPQ4PnWsd9268R4ZTtU+tfFYmwkBvgcrK2+QQ8tYFVALVb5fVJykKc5A=="],
|
||||||
@@ -644,11 +644,13 @@
|
|||||||
|
|
||||||
"@hello-pangea/dnd": ["@hello-pangea/dnd@18.0.1", "", { "dependencies": { "@babel/runtime": "^7.26.7", "css-box-model": "^1.2.1", "raf-schd": "^4.0.3", "react-redux": "^9.2.0", "redux": "^5.0.1" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ=="],
|
"@hello-pangea/dnd": ["@hello-pangea/dnd@18.0.1", "", { "dependencies": { "@babel/runtime": "^7.26.7", "css-box-model": "^1.2.1", "raf-schd": "^4.0.3", "react-redux": "^9.2.0", "redux": "^5.0.1" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ=="],
|
||||||
|
|
||||||
"@hono/node-server": ["@hono/node-server@1.13.8", "", { "peerDependencies": { "hono": "^4" } }, "sha512-fsn8ucecsAXUoVxrUil0m13kOEq4mkX4/4QozCqmY+HpGfKl74OYSn8JcMA8GnG0ClfdRI4/ZSeG7zhFaVg+wg=="],
|
"@hono/node-server": ["@hono/node-server@1.14.3", "", { "peerDependencies": { "hono": "^4" } }, "sha512-KuDMwwghtFYSmIpr4WrKs1VpelTrptvJ+6x6mbUcZnFcc213cumTF5BdqfHyW93B19TNI4Vaev14vOI2a0Ie3w=="],
|
||||||
|
|
||||||
"@hono/typebox-validator": ["@hono/typebox-validator@0.3.2", "", { "peerDependencies": { "@sinclair/typebox": ">=0.31.15 <1", "hono": ">=3.9.0" } }, "sha512-MIxYk80vtuFnkvbNreMubZ/vLoNCCQivLH8n3vNDY5dFNsZ12BFaZV3FmsLJHGibNMMpmkO6y4w5gNWY4KzSdg=="],
|
"@hono/swagger-ui": ["@hono/swagger-ui@0.5.1", "", { "peerDependencies": { "hono": "*" } }, "sha512-XpUCfszLJ9b1rtFdzqOSHfdg9pfBiC2J5piEjuSanYpDDTIwpMz0ciiv5N3WWUaQpz9fEgH8lttQqL41vIFuDA=="],
|
||||||
|
|
||||||
"@hono/vite-dev-server": ["@hono/vite-dev-server@0.19.0", "", { "dependencies": { "@hono/node-server": "^1.12.0", "minimatch": "^9.0.3" }, "peerDependencies": { "hono": "*", "miniflare": "*", "wrangler": "*" }, "optionalPeers": ["miniflare", "wrangler"] }, "sha512-myMc4Nm0nFQSPaeE6I/a1ODyDR5KpQ4EHodX4Tu/7qlB31GfUemhUH/WsO91HJjDEpRRpsT4Zbg+PleMlpTljw=="],
|
"@hono/typebox-validator": ["@hono/typebox-validator@0.3.3", "", { "peerDependencies": { "@sinclair/typebox": ">=0.31.15 <1", "hono": ">=3.9.0" } }, "sha512-BH6TOkVKlLIYYX4qfadpkNZDP/knxtCXp4210T9apKioA7q8mq1m3ELEvMMLhrtZBhzPOAlTr83A6RLZ3awDtg=="],
|
||||||
|
|
||||||
|
"@hono/vite-dev-server": ["@hono/vite-dev-server@0.19.1", "", { "dependencies": { "@hono/node-server": "^1.14.2", "minimatch": "^9.0.3" }, "peerDependencies": { "hono": "*", "miniflare": "*", "wrangler": "*" }, "optionalPeers": ["miniflare", "wrangler"] }, "sha512-hh+0u3IxHErEyj4YwHk/U+2f+qAHEQZ9EIQtadG9jeHfxEXH6r/ZecjnpyEkQbDK7JtgEEoVAq/JGOkd3Dvqww=="],
|
||||||
|
|
||||||
"@hookform/resolvers": ["@hookform/resolvers@4.1.3", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.0.0" } }, "sha512-Jsv6UOWYTrEFJ/01ZrnwVXs7KDvP8XIo115i++5PWvNkNvkrsTfGiLS6w+eJ57CYtUtDQalUWovCZDHFJ8u1VQ=="],
|
"@hookform/resolvers": ["@hookform/resolvers@4.1.3", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.0.0" } }, "sha512-Jsv6UOWYTrEFJ/01ZrnwVXs7KDvP8XIo115i++5PWvNkNvkrsTfGiLS6w+eJ57CYtUtDQalUWovCZDHFJ8u1VQ=="],
|
||||||
|
|
||||||
@@ -794,6 +796,8 @@
|
|||||||
|
|
||||||
"@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="],
|
"@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="],
|
||||||
|
|
||||||
|
"@neondatabase/serverless": ["@neondatabase/serverless@0.4.26", "", { "dependencies": { "@types/pg": "8.6.6" } }, "sha512-6DYEKos2GYn8NTgcJf33BLAx//LcgqzHVavQWe6ZkaDqmEq0I0Xtub6pzwFdq9iayNdCj7e2b0QKr5a8QKB8kQ=="],
|
||||||
|
|
||||||
"@next/env": ["@next/env@15.2.1", "", {}, "sha512-JmY0qvnPuS2NCWOz2bbby3Pe0VzdAQ7XpEB6uLIHmtXNfAsAO0KLQLkuAoc42Bxbo3/jMC3dcn9cdf+piCcG2Q=="],
|
"@next/env": ["@next/env@15.2.1", "", {}, "sha512-JmY0qvnPuS2NCWOz2bbby3Pe0VzdAQ7XpEB6uLIHmtXNfAsAO0KLQLkuAoc42Bxbo3/jMC3dcn9cdf+piCcG2Q=="],
|
||||||
|
|
||||||
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aWXT+5KEREoy3K5AKtiKwioeblmOvFFjd+F3dVleLvvLiQ/mD//jOOuUcx5hzcO9ISSw4lrqtUPntTpK32uXXQ=="],
|
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aWXT+5KEREoy3K5AKtiKwioeblmOvFFjd+F3dVleLvvLiQ/mD//jOOuUcx5hzcO9ISSw4lrqtUPntTpK32uXXQ=="],
|
||||||
@@ -1230,7 +1234,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.2.9", "", { "dependencies": { "bun-types": "1.2.9" } }, "sha512-epShhLGQYc4Bv/aceHbmBhOz1XgUnuTZgcxjxk+WXwNyDXavv5QHD1QEFV0FwbTSQtNq6g4ZcV6y0vZakTjswg=="],
|
"@types/bun": ["@types/bun@1.2.15", "", { "dependencies": { "bun-types": "1.2.15" } }, "sha512-U1ljPdBEphF0nw1MIk0hI7kPg7dFdPyM7EenHsp6W5loNHl7zqy6JQf/RKCgnUn2KDzUpkBwHPnEJEjII594bA=="],
|
||||||
|
|
||||||
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
|
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
|
||||||
|
|
||||||
@@ -1398,6 +1402,10 @@
|
|||||||
|
|
||||||
"@web3-storage/multipart-parser": ["@web3-storage/multipart-parser@1.0.0", "", {}, "sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw=="],
|
"@web3-storage/multipart-parser": ["@web3-storage/multipart-parser@1.0.0", "", {}, "sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw=="],
|
||||||
|
|
||||||
|
"@xata.io/client": ["@xata.io/client@0.0.0-next.v93343b9646f57a1e5c51c35eccf0767c2bb80baa", "", { "peerDependencies": { "typescript": ">=4.5" } }, "sha512-4Js4SAKwmmOPmZVIS1l2K8XVGGkUOi8L1jXuagDfeUX56n95wfA4xYMSmsVS0RLMmRWI4UM4bp5UcFJxwbFYGw=="],
|
||||||
|
|
||||||
|
"@xata.io/kysely": ["@xata.io/kysely@0.2.1", "", { "dependencies": { "@xata.io/client": "0.30.1" }, "peerDependencies": { "kysely": "*" } }, "sha512-0+WBcFkBSNEu11wVTyJyeNMOPUuolDKJMjXQr1nheHTNZLfsL0qKshTZOKIC/bGInjepGA7DQ/HFeKDHe5CDpA=="],
|
||||||
|
|
||||||
"@xyflow/react": ["@xyflow/react@12.4.4", "", { "dependencies": { "@xyflow/system": "0.0.52", "classcat": "^5.0.3", "zustand": "^4.4.0" }, "peerDependencies": { "react": ">=17", "react-dom": ">=17" } }, "sha512-9RZ9dgKZNJOlbrXXST5HPb5TcXPOIDGondjwcjDro44OQRPl1E0ZRPTeWPGaQtVjbg4WpR4BUYwOeshNI2TuVg=="],
|
"@xyflow/react": ["@xyflow/react@12.4.4", "", { "dependencies": { "@xyflow/system": "0.0.52", "classcat": "^5.0.3", "zustand": "^4.4.0" }, "peerDependencies": { "react": ">=17", "react-dom": ">=17" } }, "sha512-9RZ9dgKZNJOlbrXXST5HPb5TcXPOIDGondjwcjDro44OQRPl1E0ZRPTeWPGaQtVjbg4WpR4BUYwOeshNI2TuVg=="],
|
||||||
|
|
||||||
"@xyflow/system": ["@xyflow/system@0.0.52", "", { "dependencies": { "@types/d3-drag": "^3.0.7", "@types/d3-selection": "^3.0.10", "@types/d3-transition": "^3.0.8", "@types/d3-zoom": "^3.0.8", "d3-drag": "^3.0.0", "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0" } }, "sha512-pJBMaoh/GEebIABWEIxAai0yf57dm+kH7J/Br+LnLFPuJL87Fhcmm4KFWd/bCUy/kCWUg+2/yFAGY0AUHRPOnQ=="],
|
"@xyflow/system": ["@xyflow/system@0.0.52", "", { "dependencies": { "@types/d3-drag": "^3.0.7", "@types/d3-selection": "^3.0.10", "@types/d3-transition": "^3.0.8", "@types/d3-zoom": "^3.0.8", "d3-drag": "^3.0.0", "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0" } }, "sha512-pJBMaoh/GEebIABWEIxAai0yf57dm+kH7J/Br+LnLFPuJL87Fhcmm4KFWd/bCUy/kCWUg+2/yFAGY0AUHRPOnQ=="],
|
||||||
@@ -2038,8 +2046,6 @@
|
|||||||
|
|
||||||
"express-rate-limit": ["express-rate-limit@5.5.1", "", {}, "sha512-MTjE2eIbHv5DyfuFz4zLYWxpqVhEhkTiwFGuB74Q9CSou2WHO52nlE5y3Zlg6SIsiYUIPj6ifFxnkPz6O3sIUg=="],
|
"express-rate-limit": ["express-rate-limit@5.5.1", "", {}, "sha512-MTjE2eIbHv5DyfuFz4zLYWxpqVhEhkTiwFGuB74Q9CSou2WHO52nlE5y3Zlg6SIsiYUIPj6ifFxnkPz6O3sIUg=="],
|
||||||
|
|
||||||
"exsolve": ["exsolve@1.0.4", "", {}, "sha512-xsZH6PXaER4XoV+NiT7JHp1bJodJVT+cxeSH1G0f0tlT0lJqYuHUP3bUx2HtfTDvOagMINYp8rsqusxud3RXhw=="],
|
|
||||||
|
|
||||||
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||||
|
|
||||||
"extend-shallow": ["extend-shallow@3.0.2", "", { "dependencies": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" } }, "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q=="],
|
"extend-shallow": ["extend-shallow@3.0.2", "", { "dependencies": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" } }, "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q=="],
|
||||||
@@ -2080,7 +2086,7 @@
|
|||||||
|
|
||||||
"fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
|
"fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
|
||||||
|
|
||||||
"fdir": ["fdir@6.4.3", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw=="],
|
"fdir": ["fdir@6.4.5", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw=="],
|
||||||
|
|
||||||
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
|
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
|
||||||
|
|
||||||
@@ -2230,7 +2236,7 @@
|
|||||||
|
|
||||||
"headers-polyfill": ["headers-polyfill@4.0.3", "", {}, "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ=="],
|
"headers-polyfill": ["headers-polyfill@4.0.3", "", {}, "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ=="],
|
||||||
|
|
||||||
"hono": ["hono@4.7.4", "", {}, "sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg=="],
|
"hono": ["hono@4.7.11", "", {}, "sha512-rv0JMwC0KALbbmwJDEnxvQCeJh+xbS3KEWW5PC9cMJ08Ur9xgatI0HmtgYZfOdOSOeYsp5LO2cOhdI8cLEbDEQ=="],
|
||||||
|
|
||||||
"hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="],
|
"hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="],
|
||||||
|
|
||||||
@@ -2526,6 +2532,8 @@
|
|||||||
|
|
||||||
"jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="],
|
"jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="],
|
||||||
|
|
||||||
|
"jsonv-ts": ["jsonv-ts@0.1.0", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-wJ+79o49MNie2Xk9w1hPN8ozjqemVWXOfWUTdioLui/SeGDC7C+QKXTDxsmUaIay86lorkjb3CCGo6JDKbyTZQ=="],
|
||||||
|
|
||||||
"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.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=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
@@ -2546,6 +2554,10 @@
|
|||||||
|
|
||||||
"kysely-d1": ["kysely-d1@0.3.0", "", { "peerDependencies": { "kysely": "*" } }, "sha512-9wTbE6ooLiYtBa4wPg9e4fjfcmvRtgE/2j9pAjYrIq+iz+EsH/Hj9YbtxpEXA6JoRgfulVQ1EtGj6aycGGRpYw=="],
|
"kysely-d1": ["kysely-d1@0.3.0", "", { "peerDependencies": { "kysely": "*" } }, "sha512-9wTbE6ooLiYtBa4wPg9e4fjfcmvRtgE/2j9pAjYrIq+iz+EsH/Hj9YbtxpEXA6JoRgfulVQ1EtGj6aycGGRpYw=="],
|
||||||
|
|
||||||
|
"kysely-neon": ["kysely-neon@1.3.0", "", { "peerDependencies": { "@neondatabase/serverless": "^0.4.3", "kysely": "0.x.x", "ws": "^8.13.0" }, "optionalPeers": ["ws"] }, "sha512-CIIlbmqpIXVJDdBEYtEOwbmALag0jmqYrGfBeM4cHKb9AgBGs+X1SvXUZ8TqkDacQEqEZN2XtsDoUkcMIISjHw=="],
|
||||||
|
|
||||||
|
"kysely-postgres-js": ["kysely-postgres-js@2.0.0", "", { "peerDependencies": { "kysely": ">= 0.24.0 < 1", "postgres": ">= 3.4.0 < 4" } }, "sha512-R1tWx6/x3tSatWvsmbHJxpBZYhNNxcnMw52QzZaHKg7ZOWtHib4iZyEaw4gb2hNKVctWQ3jfMxZT/ZaEMK6kBQ=="],
|
||||||
|
|
||||||
"language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="],
|
"language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="],
|
||||||
|
|
||||||
"language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="],
|
"language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="],
|
||||||
@@ -2586,8 +2598,6 @@
|
|||||||
|
|
||||||
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
||||||
|
|
||||||
"liquidjs": ["liquidjs@10.21.0", "", { "dependencies": { "commander": "^10.0.0" }, "bin": { "liquidjs": "bin/liquid.js", "liquid": "bin/liquid.js" } }, "sha512-DouqxNU2jfoZzb1LinVjOc/f6ssitGIxiDJT+kEKyYqPSSSd+WmGOAhtWbVm1/n75svu4aQ+FyQ3ctd3wh1bbw=="],
|
|
||||||
|
|
||||||
"load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="],
|
"load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="],
|
||||||
|
|
||||||
"locate-app": ["locate-app@2.5.0", "", { "dependencies": { "@promptbook/utils": "0.69.5", "type-fest": "4.26.0", "userhome": "1.0.1" } }, "sha512-xIqbzPMBYArJRmPGUZD9CzV9wOqmVtQnaAn3wrj3s6WYW0bQvPI7x+sPYUGmDTYMHefVK//zc6HEYZ1qnxIK+Q=="],
|
"locate-app": ["locate-app@2.5.0", "", { "dependencies": { "@promptbook/utils": "0.69.5", "type-fest": "4.26.0", "userhome": "1.0.1" } }, "sha512-xIqbzPMBYArJRmPGUZD9CzV9wOqmVtQnaAn3wrj3s6WYW0bQvPI7x+sPYUGmDTYMHefVK//zc6HEYZ1qnxIK+Q=="],
|
||||||
@@ -2910,7 +2920,7 @@
|
|||||||
|
|
||||||
"pg-protocol": ["pg-protocol@1.8.0", "", {}, "sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g=="],
|
"pg-protocol": ["pg-protocol@1.8.0", "", {}, "sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g=="],
|
||||||
|
|
||||||
"pg-types": ["pg-types@4.0.2", "", { "dependencies": { "pg-int8": "1.0.1", "pg-numeric": "1.0.2", "postgres-array": "~3.0.1", "postgres-bytea": "~3.0.0", "postgres-date": "~2.1.0", "postgres-interval": "^3.0.0", "postgres-range": "^1.1.1" } }, "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng=="],
|
"pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
|
||||||
|
|
||||||
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
|
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
|
||||||
|
|
||||||
@@ -2966,13 +2976,15 @@
|
|||||||
|
|
||||||
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||||
|
|
||||||
"postgres-array": ["postgres-array@3.0.4", "", {}, "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ=="],
|
"postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="],
|
||||||
|
|
||||||
"postgres-bytea": ["postgres-bytea@3.0.0", "", { "dependencies": { "obuf": "~1.1.2" } }, "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw=="],
|
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
|
||||||
|
|
||||||
"postgres-date": ["postgres-date@2.1.0", "", {}, "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA=="],
|
"postgres-bytea": ["postgres-bytea@1.0.0", "", {}, "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="],
|
||||||
|
|
||||||
"postgres-interval": ["postgres-interval@3.0.0", "", {}, "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw=="],
|
"postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
|
||||||
|
|
||||||
|
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
|
||||||
|
|
||||||
"postgres-range": ["postgres-range@1.1.4", "", {}, "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w=="],
|
"postgres-range": ["postgres-range@1.1.4", "", {}, "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w=="],
|
||||||
|
|
||||||
@@ -3608,7 +3620,7 @@
|
|||||||
|
|
||||||
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
|
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
|
||||||
|
|
||||||
"uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
|
"uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
|
||||||
|
|
||||||
"v8-compile-cache": ["v8-compile-cache@2.4.0", "", {}, "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw=="],
|
"v8-compile-cache": ["v8-compile-cache@2.4.0", "", {}, "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw=="],
|
||||||
|
|
||||||
@@ -3640,7 +3652,7 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
"vite": ["vite@6.2.1", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q=="],
|
"vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="],
|
||||||
|
|
||||||
"vite-node": ["vite-node@3.0.8", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.0", "es-module-lexer": "^1.6.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-6PhR4H9VGlcwXZ+KWCdMqbtG649xCPZqfI9j2PsK1FcXgEzro5bGHcVKFCTqPLaNKZES8Evqv4LwvZARsq5qlg=="],
|
"vite-node": ["vite-node@3.0.8", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.0", "es-module-lexer": "^1.6.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-6PhR4H9VGlcwXZ+KWCdMqbtG649xCPZqfI9j2PsK1FcXgEzro5bGHcVKFCTqPLaNKZES8Evqv4LwvZARsq5qlg=="],
|
||||||
|
|
||||||
@@ -3852,13 +3864,11 @@
|
|||||||
|
|
||||||
"@babel/runtime/regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="],
|
"@babel/runtime/regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="],
|
||||||
|
|
||||||
"@bknd/postgres/@types/bun": ["@types/bun@1.2.5", "", { "dependencies": { "bun-types": "1.2.5" } }, "sha512-w2OZTzrZTVtbnJew1pdFmgV99H0/L+Pvw+z1P67HaR18MHOzYnTYOi6qzErhK8HyT+DB782ADVPPE92Xu2/Opg=="],
|
|
||||||
|
|
||||||
"@bundled-es-modules/cookie/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
"@bundled-es-modules/cookie/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||||
|
|
||||||
"@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=="],
|
||||||
|
|
||||||
"@cloudflare/unenv-preset/unenv": ["unenv@2.0.0-rc.15", "", { "dependencies": { "defu": "^6.1.4", "exsolve": "^1.0.4", "ohash": "^2.0.11", "pathe": "^2.0.3", "ufo": "^1.5.4" } }, "sha512-J/rEIZU8w6FOfLNz/hNKsnY+fFHWnu9MH4yRbSZF3xbbGHovcetXPs7sD+9p8L6CeNC//I9bhRYAOsBt2u7/OA=="],
|
"@cypress/request/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
|
||||||
|
|
||||||
"@emnapi/runtime/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"@emnapi/runtime/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
@@ -3916,6 +3926,8 @@
|
|||||||
|
|
||||||
"@libsql/kysely-libsql/@libsql/client": ["@libsql/client@0.8.1", "", { "dependencies": { "@libsql/core": "^0.8.1", "@libsql/hrana-client": "^0.6.2", "js-base64": "^3.7.5", "libsql": "^0.3.10", "promise-limit": "^2.7.0" } }, "sha512-xGg0F4iTDFpeBZ0r4pA6icGsYa5rG6RAG+i/iLDnpCAnSuTqEWMDdPlVseiq4Z/91lWI9jvvKKiKpovqJ1kZWA=="],
|
"@libsql/kysely-libsql/@libsql/client": ["@libsql/client@0.8.1", "", { "dependencies": { "@libsql/core": "^0.8.1", "@libsql/hrana-client": "^0.6.2", "js-base64": "^3.7.5", "libsql": "^0.3.10", "promise-limit": "^2.7.0" } }, "sha512-xGg0F4iTDFpeBZ0r4pA6icGsYa5rG6RAG+i/iLDnpCAnSuTqEWMDdPlVseiq4Z/91lWI9jvvKKiKpovqJ1kZWA=="],
|
||||||
|
|
||||||
|
"@neondatabase/serverless/@types/pg": ["@types/pg@8.6.6", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-O2xNmXebtwVekJDD+02udOncjVcMZQuTEQEMpKJ0ZRf5E7/9JJX3izhKUcUifBkyKpljyUM6BTgy2trmviKlpw=="],
|
||||||
|
|
||||||
"@plasmicapp/query/swr": ["swr@1.3.0", "", { "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0" } }, "sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw=="],
|
"@plasmicapp/query/swr": ["swr@1.3.0", "", { "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0" } }, "sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw=="],
|
||||||
|
|
||||||
"@remix-run/node/cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
"@remix-run/node/cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||||
@@ -4066,12 +4078,14 @@
|
|||||||
|
|
||||||
"@testing-library/jest-dom/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="],
|
"@testing-library/jest-dom/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="],
|
||||||
|
|
||||||
"@types/bun/bun-types": ["bun-types@1.2.9", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-dk/kOEfQbajENN/D6FyiSgOKEuUi9PWfqKQJEgwKrCMWbjS/S6tEXp178mWvWAcUSYm9ArDlWHZKO3T/4cLXiw=="],
|
"@types/bun/bun-types": ["bun-types@1.2.15", "", { "dependencies": { "@types/node": "*" } }, "sha512-NarRIaS+iOaQU1JPfyKhZm4AsUOrwUOqRNHY0XxI8GI8jYxiLXLcdjYMG9UKS+fwWasc1uw1htV9AX24dD+p4w=="],
|
||||||
|
|
||||||
"@types/jest/jest-diff": ["jest-diff@25.5.0", "", { "dependencies": { "chalk": "^3.0.0", "diff-sequences": "^25.2.6", "jest-get-type": "^25.2.6", "pretty-format": "^25.5.0" } }, "sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A=="],
|
"@types/jest/jest-diff": ["jest-diff@25.5.0", "", { "dependencies": { "chalk": "^3.0.0", "diff-sequences": "^25.2.6", "jest-get-type": "^25.2.6", "pretty-format": "^25.5.0" } }, "sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A=="],
|
||||||
|
|
||||||
"@types/jest/pretty-format": ["pretty-format@25.5.0", "", { "dependencies": { "@jest/types": "^25.5.0", "ansi-regex": "^5.0.0", "ansi-styles": "^4.0.0", "react-is": "^16.12.0" } }, "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ=="],
|
"@types/jest/pretty-format": ["pretty-format@25.5.0", "", { "dependencies": { "@jest/types": "^25.5.0", "ansi-regex": "^5.0.0", "ansi-styles": "^4.0.0", "react-is": "^16.12.0" } }, "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ=="],
|
||||||
|
|
||||||
|
"@types/pg/pg-types": ["pg-types@4.0.2", "", { "dependencies": { "pg-int8": "1.0.1", "pg-numeric": "1.0.2", "postgres-array": "~3.0.1", "postgres-bytea": "~3.0.0", "postgres-date": "~2.1.0", "postgres-interval": "^3.0.0", "postgres-range": "^1.1.1" } }, "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng=="],
|
||||||
|
|
||||||
"@typescript-eslint/experimental-utils/eslint-utils": ["eslint-utils@2.1.0", "", { "dependencies": { "eslint-visitor-keys": "^1.1.0" } }, "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg=="],
|
"@typescript-eslint/experimental-utils/eslint-utils": ["eslint-utils@2.1.0", "", { "dependencies": { "eslint-visitor-keys": "^1.1.0" } }, "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
"@typescript-eslint/typescript-estree/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||||
@@ -4132,6 +4146,8 @@
|
|||||||
|
|
||||||
"@wdio/utils/decamelize": ["decamelize@6.0.0", "", {}, "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA=="],
|
"@wdio/utils/decamelize": ["decamelize@6.0.0", "", {}, "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA=="],
|
||||||
|
|
||||||
|
"@xata.io/kysely/@xata.io/client": ["@xata.io/client@0.30.1", "", { "peerDependencies": { "typescript": ">=4.5" } }, "sha512-dAzDPHmIfenVIpF39m1elmW5ngjWu2mO8ZqJBN7dmYdXr98uhPANfLdVZnc3mUNG+NH37LqY1dSO862hIo2oRw=="],
|
||||||
|
|
||||||
"accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
"accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
||||||
|
|
||||||
"acorn-globals/acorn": ["acorn@6.4.2", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ=="],
|
"acorn-globals/acorn": ["acorn@6.4.2", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ=="],
|
||||||
@@ -4164,10 +4180,10 @@
|
|||||||
|
|
||||||
"bknd/vitest": ["vitest@3.0.9", "", { "dependencies": { "@vitest/expect": "3.0.9", "@vitest/mocker": "3.0.9", "@vitest/pretty-format": "^3.0.9", "@vitest/runner": "3.0.9", "@vitest/snapshot": "3.0.9", "@vitest/spy": "3.0.9", "@vitest/utils": "3.0.9", "chai": "^5.2.0", "debug": "^4.4.0", "expect-type": "^1.1.0", "magic-string": "^0.30.17", "pathe": "^2.0.3", "std-env": "^3.8.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", "vite-node": "3.0.9", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.0.9", "@vitest/ui": "3.0.9", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-BbcFDqNyBlfSpATmTtXOAOj71RNKDDvjBM/uPfnxxVGrG+FSH2RQIwgeEngTaTkuU/h0ScFvf+tRcKfYXzBybQ=="],
|
"bknd/vitest": ["vitest@3.0.9", "", { "dependencies": { "@vitest/expect": "3.0.9", "@vitest/mocker": "3.0.9", "@vitest/pretty-format": "^3.0.9", "@vitest/runner": "3.0.9", "@vitest/snapshot": "3.0.9", "@vitest/spy": "3.0.9", "@vitest/utils": "3.0.9", "chai": "^5.2.0", "debug": "^4.4.0", "expect-type": "^1.1.0", "magic-string": "^0.30.17", "pathe": "^2.0.3", "std-env": "^3.8.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", "vite-node": "3.0.9", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.0.9", "@vitest/ui": "3.0.9", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-BbcFDqNyBlfSpATmTtXOAOj71RNKDDvjBM/uPfnxxVGrG+FSH2RQIwgeEngTaTkuU/h0ScFvf+tRcKfYXzBybQ=="],
|
||||||
|
|
||||||
"bknd/wrangler": ["wrangler@4.4.1", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.0", "@cloudflare/unenv-preset": "2.3.0", "blake3-wasm": "2.1.5", "esbuild": "0.24.2", "miniflare": "4.20250321.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.15", "workerd": "1.20250321.0" }, "optionalDependencies": { "fsevents": "~2.3.2", "sharp": "^0.33.5" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20250321.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-EFwr7hiVeAmPOuOGQ7HFfeaLKLxEXQMJ86kyn6RFB8pGjMEUtvZMsVa9cPubKkKgNi3WcDEFeFLalclGyq+tGA=="],
|
|
||||||
|
|
||||||
"bknd-cli/@libsql/client": ["@libsql/client@0.14.0", "", { "dependencies": { "@libsql/core": "^0.14.0", "@libsql/hrana-client": "^0.7.0", "js-base64": "^3.7.5", "libsql": "^0.4.4", "promise-limit": "^2.7.0" } }, "sha512-/9HEKfn6fwXB5aTEEoMeFh4CtG0ZzbncBb1e++OCdVpgKZ/xyMsIVYXm0w7Pv4RUel803vE6LwniB3PqD72R0Q=="],
|
"bknd-cli/@libsql/client": ["@libsql/client@0.14.0", "", { "dependencies": { "@libsql/core": "^0.14.0", "@libsql/hrana-client": "^0.7.0", "js-base64": "^3.7.5", "libsql": "^0.4.4", "promise-limit": "^2.7.0" } }, "sha512-/9HEKfn6fwXB5aTEEoMeFh4CtG0ZzbncBb1e++OCdVpgKZ/xyMsIVYXm0w7Pv4RUel803vE6LwniB3PqD72R0Q=="],
|
||||||
|
|
||||||
|
"bknd-cli/hono": ["hono@4.7.4", "", {}, "sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg=="],
|
||||||
|
|
||||||
"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/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=="],
|
||||||
@@ -4428,8 +4444,6 @@
|
|||||||
|
|
||||||
"libsql/detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="],
|
"libsql/detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="],
|
||||||
|
|
||||||
"liquidjs/commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="],
|
|
||||||
|
|
||||||
"locate-app/type-fest": ["type-fest@4.26.0", "", {}, "sha512-OduNjVJsFbifKb57UqZ2EMP1i4u64Xwow3NYXUtBbD4vIwJdQd4+xl8YDou1dlm4DVrtwT/7Ky8z8WyCULVfxw=="],
|
"locate-app/type-fest": ["type-fest@4.26.0", "", {}, "sha512-OduNjVJsFbifKb57UqZ2EMP1i4u64Xwow3NYXUtBbD4vIwJdQd4+xl8YDou1dlm4DVrtwT/7Ky8z8WyCULVfxw=="],
|
||||||
|
|
||||||
"log-update/ansi-escapes": ["ansi-escapes@3.2.0", "", {}, "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ=="],
|
"log-update/ansi-escapes": ["ansi-escapes@3.2.0", "", {}, "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ=="],
|
||||||
@@ -4478,8 +4492,6 @@
|
|||||||
|
|
||||||
"peek-stream/duplexify": ["duplexify@3.7.1", "", { "dependencies": { "end-of-stream": "^1.0.0", "inherits": "^2.0.1", "readable-stream": "^2.0.0", "stream-shift": "^1.0.0" } }, "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g=="],
|
"peek-stream/duplexify": ["duplexify@3.7.1", "", { "dependencies": { "end-of-stream": "^1.0.0", "inherits": "^2.0.1", "readable-stream": "^2.0.0", "stream-shift": "^1.0.0" } }, "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g=="],
|
||||||
|
|
||||||
"pg/pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
|
|
||||||
|
|
||||||
"playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
"playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
||||||
|
|
||||||
"pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
|
"pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
|
||||||
@@ -4620,6 +4632,8 @@
|
|||||||
|
|
||||||
"through2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
"through2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||||
|
|
||||||
|
"tinyglobby/fdir": ["fdir@6.4.3", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw=="],
|
||||||
|
|
||||||
"to-object-path/kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="],
|
"to-object-path/kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="],
|
||||||
|
|
||||||
"ts-jest/mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
|
"ts-jest/mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
|
||||||
@@ -4668,6 +4682,12 @@
|
|||||||
|
|
||||||
"vite/esbuild": ["esbuild@0.25.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.1", "@esbuild/android-arm": "0.25.1", "@esbuild/android-arm64": "0.25.1", "@esbuild/android-x64": "0.25.1", "@esbuild/darwin-arm64": "0.25.1", "@esbuild/darwin-x64": "0.25.1", "@esbuild/freebsd-arm64": "0.25.1", "@esbuild/freebsd-x64": "0.25.1", "@esbuild/linux-arm": "0.25.1", "@esbuild/linux-arm64": "0.25.1", "@esbuild/linux-ia32": "0.25.1", "@esbuild/linux-loong64": "0.25.1", "@esbuild/linux-mips64el": "0.25.1", "@esbuild/linux-ppc64": "0.25.1", "@esbuild/linux-riscv64": "0.25.1", "@esbuild/linux-s390x": "0.25.1", "@esbuild/linux-x64": "0.25.1", "@esbuild/netbsd-arm64": "0.25.1", "@esbuild/netbsd-x64": "0.25.1", "@esbuild/openbsd-arm64": "0.25.1", "@esbuild/openbsd-x64": "0.25.1", "@esbuild/sunos-x64": "0.25.1", "@esbuild/win32-arm64": "0.25.1", "@esbuild/win32-ia32": "0.25.1", "@esbuild/win32-x64": "0.25.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ=="],
|
"vite/esbuild": ["esbuild@0.25.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.1", "@esbuild/android-arm": "0.25.1", "@esbuild/android-arm64": "0.25.1", "@esbuild/android-x64": "0.25.1", "@esbuild/darwin-arm64": "0.25.1", "@esbuild/darwin-x64": "0.25.1", "@esbuild/freebsd-arm64": "0.25.1", "@esbuild/freebsd-x64": "0.25.1", "@esbuild/linux-arm": "0.25.1", "@esbuild/linux-arm64": "0.25.1", "@esbuild/linux-ia32": "0.25.1", "@esbuild/linux-loong64": "0.25.1", "@esbuild/linux-mips64el": "0.25.1", "@esbuild/linux-ppc64": "0.25.1", "@esbuild/linux-riscv64": "0.25.1", "@esbuild/linux-s390x": "0.25.1", "@esbuild/linux-x64": "0.25.1", "@esbuild/netbsd-arm64": "0.25.1", "@esbuild/netbsd-x64": "0.25.1", "@esbuild/openbsd-arm64": "0.25.1", "@esbuild/openbsd-x64": "0.25.1", "@esbuild/sunos-x64": "0.25.1", "@esbuild/win32-arm64": "0.25.1", "@esbuild/win32-ia32": "0.25.1", "@esbuild/win32-x64": "0.25.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ=="],
|
||||||
|
|
||||||
|
"vite/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
|
||||||
|
|
||||||
|
"vite-node/vite": ["vite@6.2.1", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q=="],
|
||||||
|
|
||||||
|
"vitest/vite": ["vite@6.2.1", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q=="],
|
||||||
|
|
||||||
"webdriver/@types/node": ["@types/node@20.17.24", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-d7fGCyB96w9BnWQrOsJtpyiSaBcAYYr75bnK6ZRjDbql2cGLj/3GsL5OYmLPNq76l7Gf2q4Rv9J2o6h5CrD9sA=="],
|
"webdriver/@types/node": ["@types/node@20.17.24", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-d7fGCyB96w9BnWQrOsJtpyiSaBcAYYr75bnK6ZRjDbql2cGLj/3GsL5OYmLPNq76l7Gf2q4Rv9J2o6h5CrD9sA=="],
|
||||||
|
|
||||||
"webdriver/undici": ["undici@6.21.1", "", {}, "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ=="],
|
"webdriver/undici": ["undici@6.21.1", "", {}, "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ=="],
|
||||||
@@ -4708,8 +4728,6 @@
|
|||||||
|
|
||||||
"@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/unenv-preset/unenv/ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
|
|
||||||
|
|
||||||
"@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
"@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||||
|
|
||||||
"@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
|
"@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
|
||||||
@@ -4754,6 +4772,14 @@
|
|||||||
|
|
||||||
"@types/jest/pretty-format/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
"@types/jest/pretty-format/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||||
|
|
||||||
|
"@types/pg/pg-types/postgres-array": ["postgres-array@3.0.4", "", {}, "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ=="],
|
||||||
|
|
||||||
|
"@types/pg/pg-types/postgres-bytea": ["postgres-bytea@3.0.0", "", { "dependencies": { "obuf": "~1.1.2" } }, "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw=="],
|
||||||
|
|
||||||
|
"@types/pg/pg-types/postgres-date": ["postgres-date@2.1.0", "", {}, "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA=="],
|
||||||
|
|
||||||
|
"@types/pg/pg-types/postgres-interval": ["postgres-interval@3.0.0", "", {}, "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
"@typescript-eslint/typescript-estree/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||||
|
|
||||||
"@verdaccio/local-storage-legacy/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="],
|
"@verdaccio/local-storage-legacy/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="],
|
||||||
@@ -4778,6 +4804,8 @@
|
|||||||
|
|
||||||
"@verdaccio/middleware/express/path-to-regexp": ["path-to-regexp@0.1.10", "", {}, "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="],
|
"@verdaccio/middleware/express/path-to-regexp": ["path-to-regexp@0.1.10", "", {}, "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="],
|
||||||
|
|
||||||
|
"@vitest/coverage-v8/vitest/vite": ["vite@6.2.1", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q=="],
|
||||||
|
|
||||||
"@wdio/logger/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
"@wdio/logger/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||||
|
|
||||||
"@wdio/repl/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="],
|
"@wdio/repl/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="],
|
||||||
@@ -4790,15 +4818,7 @@
|
|||||||
|
|
||||||
"bknd-cli/@libsql/client/libsql": ["libsql@0.4.7", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.4.7", "@libsql/darwin-x64": "0.4.7", "@libsql/linux-arm64-gnu": "0.4.7", "@libsql/linux-arm64-musl": "0.4.7", "@libsql/linux-x64-gnu": "0.4.7", "@libsql/linux-x64-musl": "0.4.7", "@libsql/win32-x64-msvc": "0.4.7" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ] }, "sha512-T9eIRCs6b0J1SHKYIvD8+KCJMcWZ900iZyxdnSCdqxN12Z1ijzT+jY5nrk72Jw4B0HGzms2NgpryArlJqvc3Lw=="],
|
"bknd-cli/@libsql/client/libsql": ["libsql@0.4.7", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.4.7", "@libsql/darwin-x64": "0.4.7", "@libsql/linux-arm64-gnu": "0.4.7", "@libsql/linux-arm64-musl": "0.4.7", "@libsql/linux-x64-gnu": "0.4.7", "@libsql/linux-x64-musl": "0.4.7", "@libsql/win32-x64-msvc": "0.4.7" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ] }, "sha512-T9eIRCs6b0J1SHKYIvD8+KCJMcWZ900iZyxdnSCdqxN12Z1ijzT+jY5nrk72Jw4B0HGzms2NgpryArlJqvc3Lw=="],
|
||||||
|
|
||||||
"bknd/wrangler/@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.0", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA=="],
|
"bknd/vitest/vite": ["vite@6.2.1", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q=="],
|
||||||
|
|
||||||
"bknd/wrangler/esbuild": ["esbuild@0.24.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.24.2", "@esbuild/android-arm": "0.24.2", "@esbuild/android-arm64": "0.24.2", "@esbuild/android-x64": "0.24.2", "@esbuild/darwin-arm64": "0.24.2", "@esbuild/darwin-x64": "0.24.2", "@esbuild/freebsd-arm64": "0.24.2", "@esbuild/freebsd-x64": "0.24.2", "@esbuild/linux-arm": "0.24.2", "@esbuild/linux-arm64": "0.24.2", "@esbuild/linux-ia32": "0.24.2", "@esbuild/linux-loong64": "0.24.2", "@esbuild/linux-mips64el": "0.24.2", "@esbuild/linux-ppc64": "0.24.2", "@esbuild/linux-riscv64": "0.24.2", "@esbuild/linux-s390x": "0.24.2", "@esbuild/linux-x64": "0.24.2", "@esbuild/netbsd-arm64": "0.24.2", "@esbuild/netbsd-x64": "0.24.2", "@esbuild/openbsd-arm64": "0.24.2", "@esbuild/openbsd-x64": "0.24.2", "@esbuild/sunos-x64": "0.24.2", "@esbuild/win32-arm64": "0.24.2", "@esbuild/win32-ia32": "0.24.2", "@esbuild/win32-x64": "0.24.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA=="],
|
|
||||||
|
|
||||||
"bknd/wrangler/miniflare": ["miniflare@4.20250321.0", "", { "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.20250321.0", "ws": "8.18.0", "youch": "3.2.3", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-os+NJA7Eqi00BJHdVhzIa+3PMotnCtZg3hiUIRYcsZF5W7He8SK2EkV8csAb+npZq3jZ4SNpDebO01swM5dcWw=="],
|
|
||||||
|
|
||||||
"bknd/wrangler/unenv": ["unenv@2.0.0-rc.15", "", { "dependencies": { "defu": "^6.1.4", "exsolve": "^1.0.4", "ohash": "^2.0.11", "pathe": "^2.0.3", "ufo": "^1.5.4" } }, "sha512-J/rEIZU8w6FOfLNz/hNKsnY+fFHWnu9MH4yRbSZF3xbbGHovcetXPs7sD+9p8L6CeNC//I9bhRYAOsBt2u7/OA=="],
|
|
||||||
|
|
||||||
"bknd/wrangler/workerd": ["workerd@1.20250321.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20250321.0", "@cloudflare/workerd-darwin-arm64": "1.20250321.0", "@cloudflare/workerd-linux-64": "1.20250321.0", "@cloudflare/workerd-linux-arm64": "1.20250321.0", "@cloudflare/workerd-windows-64": "1.20250321.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-vyuz9pdJ+7o1lC79vQ2UVRLXPARa2Lq94PbTfqEcYQeSxeR9X+YqhNq2yysv8Zs5vpokmexLCtMniPp9u+2LVQ=="],
|
|
||||||
|
|
||||||
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||||
|
|
||||||
@@ -4952,14 +4972,6 @@
|
|||||||
|
|
||||||
"peek-stream/duplexify/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
"peek-stream/duplexify/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||||
|
|
||||||
"pg/pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
|
|
||||||
|
|
||||||
"pg/pg-types/postgres-bytea": ["postgres-bytea@1.0.0", "", {}, "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="],
|
|
||||||
|
|
||||||
"pg/pg-types/postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
|
|
||||||
|
|
||||||
"pg/pg-types/postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
|
|
||||||
|
|
||||||
"progress-estimator/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
|
"progress-estimator/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
|
||||||
|
|
||||||
"progress-estimator/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
|
"progress-estimator/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
|
||||||
@@ -5142,6 +5154,8 @@
|
|||||||
|
|
||||||
"verdaccio-audit/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
|
"verdaccio-audit/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
|
||||||
|
|
||||||
|
"vite-node/vite/esbuild": ["esbuild@0.25.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.1", "@esbuild/android-arm": "0.25.1", "@esbuild/android-arm64": "0.25.1", "@esbuild/android-x64": "0.25.1", "@esbuild/darwin-arm64": "0.25.1", "@esbuild/darwin-x64": "0.25.1", "@esbuild/freebsd-arm64": "0.25.1", "@esbuild/freebsd-x64": "0.25.1", "@esbuild/linux-arm": "0.25.1", "@esbuild/linux-arm64": "0.25.1", "@esbuild/linux-ia32": "0.25.1", "@esbuild/linux-loong64": "0.25.1", "@esbuild/linux-mips64el": "0.25.1", "@esbuild/linux-ppc64": "0.25.1", "@esbuild/linux-riscv64": "0.25.1", "@esbuild/linux-s390x": "0.25.1", "@esbuild/linux-x64": "0.25.1", "@esbuild/netbsd-arm64": "0.25.1", "@esbuild/netbsd-x64": "0.25.1", "@esbuild/openbsd-arm64": "0.25.1", "@esbuild/openbsd-x64": "0.25.1", "@esbuild/sunos-x64": "0.25.1", "@esbuild/win32-arm64": "0.25.1", "@esbuild/win32-ia32": "0.25.1", "@esbuild/win32-x64": "0.25.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ=="],
|
||||||
|
|
||||||
"vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ=="],
|
"vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ=="],
|
||||||
|
|
||||||
"vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.1", "", { "os": "android", "cpu": "arm" }, "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q=="],
|
"vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.1", "", { "os": "android", "cpu": "arm" }, "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q=="],
|
||||||
@@ -5190,6 +5204,8 @@
|
|||||||
|
|
||||||
"vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.1", "", { "os": "win32", "cpu": "x64" }, "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg=="],
|
"vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.1", "", { "os": "win32", "cpu": "x64" }, "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg=="],
|
||||||
|
|
||||||
|
"vitest/vite/esbuild": ["esbuild@0.25.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.1", "@esbuild/android-arm": "0.25.1", "@esbuild/android-arm64": "0.25.1", "@esbuild/android-x64": "0.25.1", "@esbuild/darwin-arm64": "0.25.1", "@esbuild/darwin-x64": "0.25.1", "@esbuild/freebsd-arm64": "0.25.1", "@esbuild/freebsd-x64": "0.25.1", "@esbuild/linux-arm": "0.25.1", "@esbuild/linux-arm64": "0.25.1", "@esbuild/linux-ia32": "0.25.1", "@esbuild/linux-loong64": "0.25.1", "@esbuild/linux-mips64el": "0.25.1", "@esbuild/linux-ppc64": "0.25.1", "@esbuild/linux-riscv64": "0.25.1", "@esbuild/linux-s390x": "0.25.1", "@esbuild/linux-x64": "0.25.1", "@esbuild/netbsd-arm64": "0.25.1", "@esbuild/netbsd-x64": "0.25.1", "@esbuild/openbsd-arm64": "0.25.1", "@esbuild/openbsd-x64": "0.25.1", "@esbuild/sunos-x64": "0.25.1", "@esbuild/win32-arm64": "0.25.1", "@esbuild/win32-ia32": "0.25.1", "@esbuild/win32-x64": "0.25.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ=="],
|
||||||
|
|
||||||
"webdriver/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="],
|
"webdriver/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="],
|
||||||
|
|
||||||
"webdriverio/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="],
|
"webdriverio/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="],
|
||||||
@@ -5288,6 +5304,8 @@
|
|||||||
|
|
||||||
"@verdaccio/middleware/express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
"@verdaccio/middleware/express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||||
|
|
||||||
|
"@vitest/coverage-v8/vitest/vite/esbuild": ["esbuild@0.25.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.1", "@esbuild/android-arm": "0.25.1", "@esbuild/android-arm64": "0.25.1", "@esbuild/android-x64": "0.25.1", "@esbuild/darwin-arm64": "0.25.1", "@esbuild/darwin-x64": "0.25.1", "@esbuild/freebsd-arm64": "0.25.1", "@esbuild/freebsd-x64": "0.25.1", "@esbuild/linux-arm": "0.25.1", "@esbuild/linux-arm64": "0.25.1", "@esbuild/linux-ia32": "0.25.1", "@esbuild/linux-loong64": "0.25.1", "@esbuild/linux-mips64el": "0.25.1", "@esbuild/linux-ppc64": "0.25.1", "@esbuild/linux-riscv64": "0.25.1", "@esbuild/linux-s390x": "0.25.1", "@esbuild/linux-x64": "0.25.1", "@esbuild/netbsd-arm64": "0.25.1", "@esbuild/netbsd-x64": "0.25.1", "@esbuild/openbsd-arm64": "0.25.1", "@esbuild/openbsd-x64": "0.25.1", "@esbuild/sunos-x64": "0.25.1", "@esbuild/win32-arm64": "0.25.1", "@esbuild/win32-ia32": "0.25.1", "@esbuild/win32-x64": "0.25.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ=="],
|
||||||
|
|
||||||
"bknd-cli/@libsql/client/libsql/@libsql/darwin-arm64": ["@libsql/darwin-arm64@0.4.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-yOL742IfWUlUevnI5PdnIT4fryY3LYTdLm56bnY0wXBw7dhFcnjuA7jrH3oSVz2mjZTHujxoITgAE7V6Z+eAbg=="],
|
"bknd-cli/@libsql/client/libsql/@libsql/darwin-arm64": ["@libsql/darwin-arm64@0.4.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-yOL742IfWUlUevnI5PdnIT4fryY3LYTdLm56bnY0wXBw7dhFcnjuA7jrH3oSVz2mjZTHujxoITgAE7V6Z+eAbg=="],
|
||||||
|
|
||||||
"bknd-cli/@libsql/client/libsql/@libsql/darwin-x64": ["@libsql/darwin-x64@0.4.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-ezc7V75+eoyyH07BO9tIyJdqXXcRfZMbKcLCeF8+qWK5nP8wWuMcfOVywecsXGRbT99zc5eNra4NEx6z5PkSsA=="],
|
"bknd-cli/@libsql/client/libsql/@libsql/darwin-x64": ["@libsql/darwin-x64@0.4.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-ezc7V75+eoyyH07BO9tIyJdqXXcRfZMbKcLCeF8+qWK5nP8wWuMcfOVywecsXGRbT99zc5eNra4NEx6z5PkSsA=="],
|
||||||
@@ -5304,67 +5322,7 @@
|
|||||||
|
|
||||||
"bknd-cli/@libsql/client/libsql/detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="],
|
"bknd-cli/@libsql/client/libsql/detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="],
|
||||||
|
|
||||||
"bknd/wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.24.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA=="],
|
"bknd/vitest/vite/esbuild": ["esbuild@0.25.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.1", "@esbuild/android-arm": "0.25.1", "@esbuild/android-arm64": "0.25.1", "@esbuild/android-x64": "0.25.1", "@esbuild/darwin-arm64": "0.25.1", "@esbuild/darwin-x64": "0.25.1", "@esbuild/freebsd-arm64": "0.25.1", "@esbuild/freebsd-x64": "0.25.1", "@esbuild/linux-arm": "0.25.1", "@esbuild/linux-arm64": "0.25.1", "@esbuild/linux-ia32": "0.25.1", "@esbuild/linux-loong64": "0.25.1", "@esbuild/linux-mips64el": "0.25.1", "@esbuild/linux-ppc64": "0.25.1", "@esbuild/linux-riscv64": "0.25.1", "@esbuild/linux-s390x": "0.25.1", "@esbuild/linux-x64": "0.25.1", "@esbuild/netbsd-arm64": "0.25.1", "@esbuild/netbsd-x64": "0.25.1", "@esbuild/openbsd-arm64": "0.25.1", "@esbuild/openbsd-x64": "0.25.1", "@esbuild/sunos-x64": "0.25.1", "@esbuild/win32-arm64": "0.25.1", "@esbuild/win32-ia32": "0.25.1", "@esbuild/win32-x64": "0.25.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ=="],
|
||||||
|
|
||||||
"bknd/wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.24.2", "", { "os": "android", "cpu": "arm" }, "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q=="],
|
|
||||||
|
|
||||||
"bknd/wrangler/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.24.2", "", { "os": "android", "cpu": "arm64" }, "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg=="],
|
|
||||||
|
|
||||||
"bknd/wrangler/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.24.2", "", { "os": "android", "cpu": "x64" }, "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw=="],
|
|
||||||
|
|
||||||
"bknd/wrangler/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.24.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA=="],
|
|
||||||
|
|
||||||
"bknd/wrangler/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.24.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA=="],
|
|
||||||
|
|
||||||
"bknd/wrangler/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.24.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg=="],
|
|
||||||
|
|
||||||
"bknd/wrangler/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.24.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q=="],
|
|
||||||
|
|
||||||
"bknd/wrangler/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.24.2", "", { "os": "linux", "cpu": "arm" }, "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA=="],
|
|
||||||
|
|
||||||
"bknd/wrangler/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.24.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg=="],
|
|
||||||
|
|
||||||
"bknd/wrangler/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.24.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw=="],
|
|
||||||
|
|
||||||
"bknd/wrangler/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ=="],
|
|
||||||
|
|
||||||
"bknd/wrangler/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw=="],
|
|
||||||
|
|
||||||
"bknd/wrangler/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.24.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw=="],
|
|
||||||
|
|
||||||
"bknd/wrangler/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q=="],
|
|
||||||
|
|
||||||
"bknd/wrangler/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.24.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw=="],
|
|
||||||
|
|
||||||
"bknd/wrangler/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.24.2", "", { "os": "linux", "cpu": "x64" }, "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q=="],
|
|
||||||
|
|
||||||
"bknd/wrangler/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.24.2", "", { "os": "none", "cpu": "arm64" }, "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw=="],
|
|
||||||
|
|
||||||
"bknd/wrangler/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.24.2", "", { "os": "none", "cpu": "x64" }, "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw=="],
|
|
||||||
|
|
||||||
"bknd/wrangler/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.24.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A=="],
|
|
||||||
|
|
||||||
"bknd/wrangler/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.24.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA=="],
|
|
||||||
|
|
||||||
"bknd/wrangler/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.24.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig=="],
|
|
||||||
|
|
||||||
"bknd/wrangler/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.24.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ=="],
|
|
||||||
|
|
||||||
"bknd/wrangler/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.24.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA=="],
|
|
||||||
|
|
||||||
"bknd/wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.24.2", "", { "os": "win32", "cpu": "x64" }, "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg=="],
|
|
||||||
|
|
||||||
"bknd/wrangler/unenv/ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
|
|
||||||
|
|
||||||
"bknd/wrangler/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20250321.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-y273GfLaNCxkL8hTfo0c8FZKkOPdq+CPZAKJXPWB+YpS1JCOULu6lNTptpD7ZtF14dTYPkn5Weug31TTlviJmw=="],
|
|
||||||
|
|
||||||
"bknd/wrangler/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20250321.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-qvf7/gkkQq7fAsoMlntJSimN/WfwQqxi2oL0aWZMGodTvs/yRHO2I4oE0eOihVdK1BXyBHJXNxEvNDBjF0+Yuw=="],
|
|
||||||
|
|
||||||
"bknd/wrangler/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20250321.0", "", { "os": "linux", "cpu": "x64" }, "sha512-AEp3xjWFrNPO/h0StCOgOb0bWCcNThnkESpy91Wf4mfUF2p7tOCdp37Nk/1QIRqSxnfv4Hgxyi7gcWud9cJuMw=="],
|
|
||||||
|
|
||||||
"bknd/wrangler/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20250321.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-wRWyMIoPIS1UBXCisW0FYTgGsfZD4AVS0hXA5nuLc0c21CvzZpmmTjqEWMcwPFenwy/MNL61NautVOC4qJqQ3Q=="],
|
|
||||||
|
|
||||||
"bknd/wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250321.0", "", { "os": "win32", "cpu": "x64" }, "sha512-8vYP3QYO0zo2faUDfWl88jjfUvz7Si9GS3mUYaTh/TR9LcAUtsO7muLxPamqEyoxNFtbQgy08R4rTid94KRi3w=="],
|
|
||||||
|
|
||||||
"eslint/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
|
"eslint/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
|
||||||
|
|
||||||
@@ -5438,8 +5396,200 @@
|
|||||||
|
|
||||||
"verdaccio-audit/express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
"verdaccio-audit/express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||||
|
|
||||||
|
"vite-node/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ=="],
|
||||||
|
|
||||||
|
"vite-node/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.1", "", { "os": "android", "cpu": "arm" }, "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q=="],
|
||||||
|
|
||||||
|
"vite-node/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.1", "", { "os": "android", "cpu": "arm64" }, "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA=="],
|
||||||
|
|
||||||
|
"vite-node/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.1", "", { "os": "android", "cpu": "x64" }, "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw=="],
|
||||||
|
|
||||||
|
"vite-node/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ=="],
|
||||||
|
|
||||||
|
"vite-node/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA=="],
|
||||||
|
|
||||||
|
"vite-node/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A=="],
|
||||||
|
|
||||||
|
"vite-node/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww=="],
|
||||||
|
|
||||||
|
"vite-node/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.1", "", { "os": "linux", "cpu": "arm" }, "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ=="],
|
||||||
|
|
||||||
|
"vite-node/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ=="],
|
||||||
|
|
||||||
|
"vite-node/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ=="],
|
||||||
|
|
||||||
|
"vite-node/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg=="],
|
||||||
|
|
||||||
|
"vite-node/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg=="],
|
||||||
|
|
||||||
|
"vite-node/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg=="],
|
||||||
|
|
||||||
|
"vite-node/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ=="],
|
||||||
|
|
||||||
|
"vite-node/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ=="],
|
||||||
|
|
||||||
|
"vite-node/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA=="],
|
||||||
|
|
||||||
|
"vite-node/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.1", "", { "os": "none", "cpu": "x64" }, "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA=="],
|
||||||
|
|
||||||
|
"vite-node/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg=="],
|
||||||
|
|
||||||
|
"vite-node/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw=="],
|
||||||
|
|
||||||
|
"vite-node/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg=="],
|
||||||
|
|
||||||
|
"vite-node/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ=="],
|
||||||
|
|
||||||
|
"vite-node/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A=="],
|
||||||
|
|
||||||
|
"vite-node/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.1", "", { "os": "win32", "cpu": "x64" }, "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg=="],
|
||||||
|
|
||||||
|
"vitest/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ=="],
|
||||||
|
|
||||||
|
"vitest/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.1", "", { "os": "android", "cpu": "arm" }, "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q=="],
|
||||||
|
|
||||||
|
"vitest/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.1", "", { "os": "android", "cpu": "arm64" }, "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA=="],
|
||||||
|
|
||||||
|
"vitest/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.1", "", { "os": "android", "cpu": "x64" }, "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw=="],
|
||||||
|
|
||||||
|
"vitest/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ=="],
|
||||||
|
|
||||||
|
"vitest/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA=="],
|
||||||
|
|
||||||
|
"vitest/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A=="],
|
||||||
|
|
||||||
|
"vitest/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww=="],
|
||||||
|
|
||||||
|
"vitest/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.1", "", { "os": "linux", "cpu": "arm" }, "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ=="],
|
||||||
|
|
||||||
|
"vitest/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ=="],
|
||||||
|
|
||||||
|
"vitest/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ=="],
|
||||||
|
|
||||||
|
"vitest/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg=="],
|
||||||
|
|
||||||
|
"vitest/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg=="],
|
||||||
|
|
||||||
|
"vitest/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg=="],
|
||||||
|
|
||||||
|
"vitest/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ=="],
|
||||||
|
|
||||||
|
"vitest/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ=="],
|
||||||
|
|
||||||
|
"vitest/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA=="],
|
||||||
|
|
||||||
|
"vitest/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.1", "", { "os": "none", "cpu": "x64" }, "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA=="],
|
||||||
|
|
||||||
|
"vitest/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg=="],
|
||||||
|
|
||||||
|
"vitest/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw=="],
|
||||||
|
|
||||||
|
"vitest/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg=="],
|
||||||
|
|
||||||
|
"vitest/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ=="],
|
||||||
|
|
||||||
|
"vitest/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A=="],
|
||||||
|
|
||||||
|
"vitest/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.1", "", { "os": "win32", "cpu": "x64" }, "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg=="],
|
||||||
|
|
||||||
"@jest/core/rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
|
"@jest/core/rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
|
||||||
|
|
||||||
|
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ=="],
|
||||||
|
|
||||||
|
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.1", "", { "os": "android", "cpu": "arm" }, "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q=="],
|
||||||
|
|
||||||
|
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.1", "", { "os": "android", "cpu": "arm64" }, "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA=="],
|
||||||
|
|
||||||
|
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.1", "", { "os": "android", "cpu": "x64" }, "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw=="],
|
||||||
|
|
||||||
|
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ=="],
|
||||||
|
|
||||||
|
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA=="],
|
||||||
|
|
||||||
|
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A=="],
|
||||||
|
|
||||||
|
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww=="],
|
||||||
|
|
||||||
|
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.1", "", { "os": "linux", "cpu": "arm" }, "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ=="],
|
||||||
|
|
||||||
|
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ=="],
|
||||||
|
|
||||||
|
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ=="],
|
||||||
|
|
||||||
|
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg=="],
|
||||||
|
|
||||||
|
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg=="],
|
||||||
|
|
||||||
|
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg=="],
|
||||||
|
|
||||||
|
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ=="],
|
||||||
|
|
||||||
|
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ=="],
|
||||||
|
|
||||||
|
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA=="],
|
||||||
|
|
||||||
|
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.1", "", { "os": "none", "cpu": "x64" }, "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA=="],
|
||||||
|
|
||||||
|
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg=="],
|
||||||
|
|
||||||
|
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw=="],
|
||||||
|
|
||||||
|
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg=="],
|
||||||
|
|
||||||
|
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ=="],
|
||||||
|
|
||||||
|
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A=="],
|
||||||
|
|
||||||
|
"@vitest/coverage-v8/vitest/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.1", "", { "os": "win32", "cpu": "x64" }, "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg=="],
|
||||||
|
|
||||||
|
"bknd/vitest/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ=="],
|
||||||
|
|
||||||
|
"bknd/vitest/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.1", "", { "os": "android", "cpu": "arm" }, "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q=="],
|
||||||
|
|
||||||
|
"bknd/vitest/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.1", "", { "os": "android", "cpu": "arm64" }, "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA=="],
|
||||||
|
|
||||||
|
"bknd/vitest/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.1", "", { "os": "android", "cpu": "x64" }, "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw=="],
|
||||||
|
|
||||||
|
"bknd/vitest/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ=="],
|
||||||
|
|
||||||
|
"bknd/vitest/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA=="],
|
||||||
|
|
||||||
|
"bknd/vitest/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A=="],
|
||||||
|
|
||||||
|
"bknd/vitest/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww=="],
|
||||||
|
|
||||||
|
"bknd/vitest/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.1", "", { "os": "linux", "cpu": "arm" }, "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ=="],
|
||||||
|
|
||||||
|
"bknd/vitest/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ=="],
|
||||||
|
|
||||||
|
"bknd/vitest/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ=="],
|
||||||
|
|
||||||
|
"bknd/vitest/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg=="],
|
||||||
|
|
||||||
|
"bknd/vitest/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg=="],
|
||||||
|
|
||||||
|
"bknd/vitest/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg=="],
|
||||||
|
|
||||||
|
"bknd/vitest/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ=="],
|
||||||
|
|
||||||
|
"bknd/vitest/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ=="],
|
||||||
|
|
||||||
|
"bknd/vitest/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA=="],
|
||||||
|
|
||||||
|
"bknd/vitest/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.1", "", { "os": "none", "cpu": "x64" }, "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA=="],
|
||||||
|
|
||||||
|
"bknd/vitest/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg=="],
|
||||||
|
|
||||||
|
"bknd/vitest/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw=="],
|
||||||
|
|
||||||
|
"bknd/vitest/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg=="],
|
||||||
|
|
||||||
|
"bknd/vitest/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ=="],
|
||||||
|
|
||||||
|
"bknd/vitest/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A=="],
|
||||||
|
|
||||||
|
"bknd/vitest/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.1", "", { "os": "win32", "cpu": "x64" }, "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg=="],
|
||||||
|
|
||||||
"eslint/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
|
"eslint/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
|
||||||
|
|
||||||
"flat-cache/rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
|
"flat-cache/rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ WORKDIR /app
|
|||||||
|
|
||||||
# define bknd version to be used as:
|
# define bknd version to be used as:
|
||||||
# `docker build --build-arg VERSION=<version> -t bknd .`
|
# `docker build --build-arg VERSION=<version> -t bknd .`
|
||||||
ARG VERSION=0.10.2
|
ARG VERSION=0.13.0
|
||||||
|
|
||||||
# Install & copy required cli
|
# Install & copy required cli
|
||||||
RUN npm install --omit=dev bknd@${VERSION}
|
RUN npm install --omit=dev bknd@${VERSION}
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
---
|
||||||
|
title: bknd.config.ts
|
||||||
|
---
|
||||||
|
|
||||||
|
The central configuration file to extend bknd should be placed in the root of your project, so that the [CLI](/usage/cli#using-configuration-file-bknd-config) can automatically pick it up. It allows to:
|
||||||
|
* define your database connection centrally
|
||||||
|
* pass in initial configuration or data seeds when booting the first time
|
||||||
|
* add plugins to the app
|
||||||
|
* hook into system events
|
||||||
|
* define custom routes and endpoints
|
||||||
|
|
||||||
|
|
||||||
|
A simple example of a `bknd.config.ts` file:
|
||||||
|
|
||||||
|
```typescript bknd.config.ts
|
||||||
|
import type { BkndConfig } from "bknd/adapter";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
connection: {
|
||||||
|
url: "file:data.db",
|
||||||
|
}
|
||||||
|
} satisfies BkndConfig;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The `BkndConfig` extends the [`CreateAppConfig`](/usage/introduction#configuration-createappconfig) type with the following properties:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export type BkndConfig = CreateAppConfig & {
|
||||||
|
// return the app configuration as object or from a function
|
||||||
|
app?: CreateAppConfig | ((args: Args) => CreateAppConfig);
|
||||||
|
// called before the app is built
|
||||||
|
beforeBuild?: (app: App) => Promise<void>;
|
||||||
|
// called after the app has been built
|
||||||
|
onBuilt?: (app: App) => Promise<void>;
|
||||||
|
// passed as the first argument to the `App.build` method
|
||||||
|
buildConfig?: Parameters<App["build"]>[0];
|
||||||
|
// force the app to be recreated
|
||||||
|
force?: boolean;
|
||||||
|
// the id of the app, defaults to `app`
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
The supported configuration file extensions are `js`, `ts`, `mjs`, `cjs` and `json`. Throughout the documentation, we'll use `ts` for the file extension.
|
||||||
|
|
||||||
|
### `app` (CreateAppConfig)
|
||||||
|
The `app` property is a function that returns a `CreateAppConfig` object. It allows to pass in the environment variables to the configuration object.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { BkndConfig } from "bknd/adapter";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
app: ({ env }) => ({
|
||||||
|
connection: {
|
||||||
|
url: env.DB_URL,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} satisfies BkndConfig;
|
||||||
|
```
|
||||||
|
|
||||||
|
See [Database](/usage/database) for more information on how to configure the database connection.
|
||||||
|
|
||||||
|
### `beforeBuild`
|
||||||
|
The `beforeBuild` property is an async function that is called before the app is built. It allows to modify the app instance that may influence the build process.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { BkndConfig } from "bknd/adapter";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
beforeBuild: async (app: App) => {
|
||||||
|
// do something that has to happen before the app is built
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `onBuilt`
|
||||||
|
The `onBuilt` property is an async function that is called after the app has been built. It allows to hook into the app after it has been built. This is useful for defining event listeners or register custom routes, as both the event manager and the server are recreated during the build process.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { type App, AppEvents } from "bknd";
|
||||||
|
import type { BkndConfig } from "bknd/adapter";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
onBuilt: async (app: App) => {
|
||||||
|
console.log("App built", app.version());
|
||||||
|
|
||||||
|
// registering an event listener
|
||||||
|
app.emgr.onEvent(AppEvents.AppRequest, (event) => {
|
||||||
|
console.log("Request received", event.request.url);
|
||||||
|
})
|
||||||
|
|
||||||
|
// registering a custom route
|
||||||
|
app.server.get("/hello", (c) => c.text("Hello World"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `force` & `id`
|
||||||
|
The `force` property is a boolean that forces the app to be recreated. This is mainly useful for serverless environments where the execution environment is re-used, and you may or may not want to recreate the app on every request.
|
||||||
|
|
||||||
|
The `id` property is the reference in a cache map. You may create multiple instances of apps in the same process by using different ids (e.g. multi tenant applications).
|
||||||
|
|
||||||
|
## Framework & Runtime configuration
|
||||||
|
|
||||||
|
Depending on which framework or runtime you're using to run bknd, the configuration object will extend the `BkndConfig` type with additional properties.
|
||||||
|
|
||||||
|
### `RuntimeBkndConfig`
|
||||||
|
|
||||||
|
[Runtime adapters](/integration/runtime) need additional configuration to serve static assets for the admin UI.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export type RuntimeBkndConfig<Args = any> = BkndConfig<Args> & {
|
||||||
|
// the path to the dist folder to serve static assets for the admin UI
|
||||||
|
distPath?: string;
|
||||||
|
// custom middleware to serve static assets for the admin UI
|
||||||
|
serveStatic?: MiddlewareHandler | [string, MiddlewareHandler];
|
||||||
|
// options for the admin UI
|
||||||
|
adminOptions?: AdminControllerOptions | false;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### `FrameworkBkndConfig`
|
||||||
|
|
||||||
|
[Framework adapters](/integration/framework) may need additional configuration based on the framework's requirements. For example, the `NextjsBkndConfig` type extends the `BkndConfig` type with the following additional properties:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type NextjsEnv = NextApiRequest["env"];
|
||||||
|
export type NextjsBkndConfig<Env = NextjsEnv> = FrameworkBkndConfig<Env> & {
|
||||||
|
cleanRequest?: { searchParams?: string[] };
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Next.js adds the mounted path to the request object, so that the `cleanRequest` property can be used to remove the mounted path from the request URL. See other frameworks for more information on how to configure them.
|
||||||
|
|
||||||
|
|
||||||
|
## Using the configuration file
|
||||||
|
|
||||||
|
The configuration file is automatically picked up if you're using the [CLI](/usage/cli). This allows interacting with your application using the `bknd` command. For example, you can run the following command in the root of your project to start an instance:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx bknd run
|
||||||
|
```
|
||||||
|
|
||||||
|
When serving your application, you need to make sure to import the contents of your configuration file. If you're using Next.js for example, it's recommended to follow these steps:
|
||||||
|
|
||||||
|
1. create a `bknd.config.ts` file in the root of your project which defines the connection to the database, adds event listeners and custom routes.
|
||||||
|
2. create a `bknd.ts` file inside your app folder which exports helper functions to instantiate the bknd instance and retrieve the API.
|
||||||
|
3. create a catch-all route file at `src/api/[[...bknd]]/route.ts` which serves the bknd API.
|
||||||
|
|
||||||
|
This way, your application and the CLI are using the same configuration.
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
---
|
||||||
|
title: Events & Hooks
|
||||||
|
---
|
||||||
|
|
||||||
|
bknd comes with a powerful built-in event system that allows you to hook into the app lifecycle and extend its functionality. You can hook into these events in two ways:
|
||||||
|
|
||||||
|
- `async`: Your listener is not blocking the main execution flow. E.g. on Cloudflare Workers, by default, the `ExecutionContext`'s `waitUntil` method is used so that the listeners runs after the response is sent.
|
||||||
|
- `sync`: Your listener is blocking the main execution flow. This allows to abort the request in your custom conditions. Some events also allow to return a modified event payload.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Don't mistake `async` with JavaScript's `async` keyword. The `async` keyword is used to indicate that the listener is not blocking the main execution flow.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
### Listening to events
|
||||||
|
|
||||||
|
You can listen to events by using the `EventManager` exposed at `app.emgr`. To register a listener, you can use either of these methods:
|
||||||
|
|
||||||
|
- `onEvent`: Register a listener for a specific event with a typed event.
|
||||||
|
- `on`: Register a listener for an event by its slug.
|
||||||
|
- `onAny`: Register a listener for all events.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createApp, AppEvents } from "bknd";
|
||||||
|
const app = createApp();
|
||||||
|
|
||||||
|
app.emgr.onEvent(AppEvents.AppRequest, async (event) => {
|
||||||
|
// ^? AppRequest
|
||||||
|
console.log("Request received", event.request.url);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.emgr.on("app-request", async (event) => {
|
||||||
|
console.log("Request received", event.request.url);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.emgr.onAny(async (event) => {
|
||||||
|
console.log("Event received", event.slug);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
You may want to register your listeners inside [`bknd.config.ts`](/extending/config) to make sure they are registered before the app is built:
|
||||||
|
|
||||||
|
```typescript bknd.config.ts
|
||||||
|
import { AppEvents } from "bknd";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
onBuilt: (app) => {
|
||||||
|
app.emgr.onEvent(AppEvents.AppRequest, async (event) => {
|
||||||
|
console.log("Request received", event.request.url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
### Setting a mode
|
||||||
|
By default, listeners are registered as `async` listeners, meaning that the listener is not blocking the main execution flow. You can change this by passing the mode as the third argument:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
app.emgr.onEvent(AppEvents.AppRequest, async (event) => {
|
||||||
|
console.log("Request received", event.request.url);
|
||||||
|
}, { mode: "sync" });
|
||||||
|
```
|
||||||
|
|
||||||
|
This works for all three methods.
|
||||||
|
|
||||||
|
## App Events
|
||||||
|
These events are emitted by the `App` class and are available on the `AppEvents` object.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { AppEvents } from "bknd";
|
||||||
|
```
|
||||||
|
|
||||||
|
Available events:
|
||||||
|
|
||||||
|
| Event | Params | Description |
|
||||||
|
|:------|:--------|:------------|
|
||||||
|
| `AppConfigUpdatedEvent` | `{ app: App }` | Emitted when the app configuration is updated |
|
||||||
|
| `AppBuiltEvent` | `{ app: App }` | Emitted when the app is built |
|
||||||
|
| `AppFirstBoot` | `{ app: App }` | Emitted when the app is first booted |
|
||||||
|
| `AppRequest` | `{ app: App, request: Request }` | Emitted when a request is received |
|
||||||
|
| `AppBeforeResponse` | `{ app: App, request: Request, response: Response }` | Emitted before a response is sent |
|
||||||
|
|
||||||
|
## Database Events
|
||||||
|
These events are emitted by the `Database` class and are available on the `DatabaseEvents` object. These are divided by events triggered by the `Mutator` and `Repository` classes.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { DatabaseEvents } from "bknd/data";
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mutator Events
|
||||||
|
These events are emitted during database mutations (insert, update, delete operations).
|
||||||
|
|
||||||
|
| Event | Params | Description |
|
||||||
|
|:------|:--------|:------------|
|
||||||
|
| `MutatorInsertBefore` | `{ entity: Entity, data: EntityData }` | Emitted before inserting a new record. Can modify the data. |
|
||||||
|
| `MutatorInsertAfter` | `{ entity: Entity, data: EntityData, changed: EntityData }` | Emitted after inserting a new record. |
|
||||||
|
| `MutatorUpdateBefore` | `{ entity: Entity, entityId: PrimaryFieldType, data: EntityData }` | Emitted before updating a record. Can modify the data. |
|
||||||
|
| `MutatorUpdateAfter` | `{ entity: Entity, entityId: PrimaryFieldType, data: EntityData, changed: EntityData }` | Emitted after updating a record. |
|
||||||
|
| `MutatorDeleteBefore` | `{ entity: Entity, entityId: PrimaryFieldType }` | Emitted before deleting a record. |
|
||||||
|
| `MutatorDeleteAfter` | `{ entity: Entity, entityId: PrimaryFieldType, data: EntityData }` | Emitted after deleting a record. |
|
||||||
|
|
||||||
|
### Repository Events
|
||||||
|
These events are emitted during database queries (find operations).
|
||||||
|
|
||||||
|
| Event | Params | Description |
|
||||||
|
|:------|:--------|:------------|
|
||||||
|
| `RepositoryFindOneBefore` | `{ entity: Entity, options: RepoQuery }` | Emitted before finding a single record. |
|
||||||
|
| `RepositoryFindOneAfter` | `{ entity: Entity, options: RepoQuery, data: EntityData }` | Emitted after finding a single record. |
|
||||||
|
| `RepositoryFindManyBefore` | `{ entity: Entity, options: RepoQuery }` | Emitted before finding multiple records. |
|
||||||
|
| `RepositoryFindManyAfter` | `{ entity: Entity, options: RepoQuery, data: EntityData }` | Emitted after finding multiple records. |
|
||||||
|
|
||||||
|
## Media Events
|
||||||
|
These events are emitted by the `Storage` class and are available on the `MediaEvents` object.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { MediaEvents } from "bknd/media";
|
||||||
|
```
|
||||||
|
|
||||||
|
| Event | Params | Description |
|
||||||
|
|:------|:--------|:------------|
|
||||||
|
| `FileUploadedEvent` | `{ file: FileBody } & FileUploadPayload` | Emitted when a file is successfully uploaded. Can modify the event data. |
|
||||||
|
| `FileDeletedEvent` | `{ name: string }` | Emitted when a file is deleted. |
|
||||||
|
| `FileAccessEvent` | `{ name: string }` | Emitted when a file is accessed. |
|
||||||
|
|
||||||
|
## Auth Events
|
||||||
|
Coming soon.
|
||||||
@@ -28,14 +28,14 @@ To get started with Astro and bknd you can either install the package manually,
|
|||||||
npx astro add react
|
npx astro add react
|
||||||
```
|
```
|
||||||
|
|
||||||
You also need to make sure to set the output to `hybrid` in your Astro config:
|
You also need to make sure to set the output to `server` in your Astro config:
|
||||||
```js {6}
|
```js {6}
|
||||||
// astro.config.mjs
|
// astro.config.mjs
|
||||||
import { defineConfig } from "astro/config";
|
import { defineConfig } from "astro/config";
|
||||||
import react from "@astrojs/react";
|
import react from "@astrojs/react";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
output: "hybrid",
|
output: "server",
|
||||||
integrations: [react()]
|
integrations: [react()]
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
@@ -48,8 +48,58 @@ To get started with Astro and bknd you can either install the package manually,
|
|||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
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 bknd.config.ts
|
||||||
|
import type { AstroBkndConfig } from "bknd/adapter/astro";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
connection: {
|
||||||
|
url: "file:data.db"
|
||||||
|
},
|
||||||
|
} satisfies AstroBkndConfig;
|
||||||
|
```
|
||||||
|
|
||||||
|
See [bknd.config.ts](/extending/config) for more information on how to configure bknd. The `AstroBkndConfig` type extends the `BkndConfig` type with the following additional properties:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type AstroEnv = NodeJS.ProcessEnv;
|
||||||
|
export type AstroBkndConfig<Env = AstroEnv> = FrameworkBkndConfig<Env>;
|
||||||
|
```
|
||||||
|
|
||||||
## Serve the API
|
## Serve the API
|
||||||
Create a new catch-all route at `src/pages/api/[...api].ts`:
|
Create a helper file to instantiate the bknd instance and retrieve the API, importing the configurationfrom the `bknd.config.ts` file:
|
||||||
|
|
||||||
|
```ts src/bknd.ts
|
||||||
|
import type { AstroGlobal } from "astro";
|
||||||
|
import { getApp as getBkndApp } from "bknd/adapter/astro";
|
||||||
|
import config from "../bknd.config";
|
||||||
|
|
||||||
|
export { config };
|
||||||
|
|
||||||
|
export async function getApp() {
|
||||||
|
return await getBkndApp(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getApi(
|
||||||
|
astro: AstroGlobal,
|
||||||
|
opts?: { mode: "static" } | { mode?: "dynamic"; verify?: boolean },
|
||||||
|
) {
|
||||||
|
const app = await getApp();
|
||||||
|
if (opts?.mode !== "static" && opts?.verify) {
|
||||||
|
const api = app.getApi({ headers: astro.request.headers });
|
||||||
|
await api.verifyAuth();
|
||||||
|
return api;
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.getApi();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a new catch-all route at `src/pages/api/[...api].ts`.
|
||||||
|
|
||||||
```ts src/pages/api/[...api].ts
|
```ts src/pages/api/[...api].ts
|
||||||
import { serve } from "bknd/adapter/astro";
|
import { serve } from "bknd/adapter/astro";
|
||||||
|
|
||||||
@@ -63,6 +113,7 @@ export const ALL = serve({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
For more information about the connection object, refer to the [Database](/usage/database) guide. In the
|
For more information about the connection object, refer to the [Database](/usage/database) guide. In the
|
||||||
special case of astro, you may also use your Astro DB credentials since it's also using LibSQL
|
special case of astro, you may also use your Astro DB credentials since it's also using LibSQL
|
||||||
under the hood. Refer to the [Astro DB documentation](https://docs.astro.build/en/guides/astro-db/) for more information.
|
under the hood. Refer to the [Astro DB documentation](https://docs.astro.build/en/guides/astro-db/) for more information.
|
||||||
|
|||||||
@@ -206,3 +206,33 @@ tag = "v2"
|
|||||||
renamed_classes = [{from = "DurableBkndApp", to = "CustomDurableBkndApp"}]
|
renamed_classes = [{from = "DurableBkndApp", to = "CustomDurableBkndApp"}]
|
||||||
deleted_classes = ["DurableBkndApp"]
|
deleted_classes = ["DurableBkndApp"]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## D1 Sessions (experimental)
|
||||||
|
D1 now supports to enable [global read replication](https://developers.cloudflare.com/d1/best-practices/read-replication/). This allows to reduce latency by reading from the closest region. In order for this to work, D1 has to be started from a bookmark. You can enable this behavior on bknd by setting the `d1.session` property:
|
||||||
|
|
||||||
|
```typescript src/index.ts
|
||||||
|
import { serve } from "bknd/adapter/cloudflare";
|
||||||
|
|
||||||
|
export default serve({
|
||||||
|
// currently recommended to use "fresh" mode
|
||||||
|
// otherwise consecutive requests will use the same bookmark
|
||||||
|
mode: "fresh",
|
||||||
|
// ...
|
||||||
|
d1: {
|
||||||
|
// enables D1 sessions
|
||||||
|
session: true,
|
||||||
|
// (optional) restrict the transport, options: "header" | "cookie"
|
||||||
|
// if not specified, it supports both
|
||||||
|
transport: "cookie",
|
||||||
|
// (optional) choose session constraint if not bookmark present
|
||||||
|
// options: "first-primary" | "first-unconstrained"
|
||||||
|
first: "first-primary"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
If bknd is used in a stateful user context (like in a browser), it'll automatically send the session cookie to the server to set the correct bookmark. If you need to manually set the bookmark, you can do so by setting the `x-cf-d1-session` header:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H "x-cf-d1-session: <bookmark>" ...
|
||||||
|
```
|
||||||
@@ -22,21 +22,42 @@ To get started with Next.js and bknd you can either install the package manually
|
|||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
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 bknd.config.ts
|
||||||
|
import type { NextjsBkndConfig } from "bknd/adapter/nextjs";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
connection: {
|
||||||
|
url: "file:data.db"
|
||||||
|
},
|
||||||
|
} satisfies NextjsBkndConfig;
|
||||||
|
```
|
||||||
|
|
||||||
|
See [bknd.config.ts](/extending/config) for more information on how to configure bknd. The `NextjsBkndConfig` type extends the `BkndConfig` type with the following additional properties:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type NextjsEnv = NextApiRequest["env"];
|
||||||
|
export type NextjsBkndConfig<Env = NextjsEnv> = FrameworkBkndConfig<Env> & {
|
||||||
|
cleanRequest?: { searchParams?: string[] };
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
## Serve the API
|
## Serve the API
|
||||||
Create a helper file to instantiate the bknd instance and retrieve the API:
|
Create a helper file to instantiate the bknd instance and retrieve the API, importing the configurationfrom the `bknd.config.ts` file:
|
||||||
|
|
||||||
```ts src/bknd.ts
|
```ts src/bknd.ts
|
||||||
import { type NextjsBkndConfig, getApp as getBkndApp } from "bknd/adapter/nextjs";
|
import { type NextjsBkndConfig, getApp as getBkndApp } from "bknd/adapter/nextjs";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
|
import config from "../bknd.config";
|
||||||
|
|
||||||
export const config = {
|
export { config };
|
||||||
connection: {
|
|
||||||
url: "file:data.db"
|
|
||||||
},
|
|
||||||
} as const satisfies NextjsBkndConfig;
|
|
||||||
|
|
||||||
export async function getApp() {
|
export async function getApp() {
|
||||||
return await getBkndApp(config);
|
return await getBkndApp(config, process.env);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getApi(opts?: { verify?: boolean }) {
|
export async function getApi(opts?: { verify?: boolean }) {
|
||||||
|
|||||||
@@ -22,21 +22,44 @@ To get started with React Router and bknd you can either install the package man
|
|||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
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 bknd.config.ts
|
||||||
|
import type { ReactRouterBkndConfig } from "bknd/adapter/react-router";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
connection: {
|
||||||
|
url: "file:data.db"
|
||||||
|
},
|
||||||
|
} satisfies ReactRouterBkndConfig;
|
||||||
|
```
|
||||||
|
|
||||||
|
See [bknd.config.ts](/extending/config) for more information on how to configure bknd. The `ReactRouterBkndConfig` type extends the `BkndConfig` type with the following additional properties:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type ReactRouterEnv = NodeJS.ProcessEnv;
|
||||||
|
type ReactRouterFunctionArgs = {
|
||||||
|
request: Request;
|
||||||
|
};
|
||||||
|
export type ReactRouterBkndConfig<Env = ReactRouterEnv> = FrameworkBkndConfig<Env>;
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Serve the API
|
## Serve the API
|
||||||
Create a helper file to instantiate the bknd instance and retrieve the API:
|
Create a helper file to instantiate the bknd instance and retrieve the API, importing the configurationfrom the `bknd.config.ts` file:
|
||||||
|
|
||||||
```ts app/bknd.ts
|
```ts app/bknd.ts
|
||||||
import { type ReactRouterBkndConfig, getApp as getBkndApp } from "bknd/adapter/react-router";
|
import { type ReactRouterBkndConfig, getApp as getBkndApp } from "bknd/adapter/react-router";
|
||||||
|
import config from "../bknd.config";
|
||||||
|
|
||||||
const config = {
|
export { config };
|
||||||
connection: {
|
|
||||||
url: "file:data.db"
|
|
||||||
}
|
|
||||||
} as const satisfies ReactRouterBkndConfig;
|
|
||||||
|
|
||||||
export async function getApp(args?: { request: Request }) {
|
// you may adjust this function depending on your runtime environment.
|
||||||
return await getBkndApp(config, args);
|
// e.g. when deploying to cloudflare workers, you'd want the FunctionArgs to be passed in
|
||||||
|
// to resolve environment variables
|
||||||
|
export async function getApp() {
|
||||||
|
return await getBkndApp(config, process.env as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getApi(
|
export async function getApi(
|
||||||
@@ -60,7 +83,7 @@ Create a new api splat route file at `app/routes/api.$.ts`:
|
|||||||
import { getApp } from "~/bknd";
|
import { getApp } from "~/bknd";
|
||||||
|
|
||||||
const handler = async (args: { request: Request }) => {
|
const handler = async (args: { request: Request }) => {
|
||||||
const app = await getApp(args);
|
const app = await getApp();
|
||||||
return app.fetch(args.request);
|
return app.fetch(args.request);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+43
-16
@@ -16,25 +16,42 @@ Additionally, install required dependencies:
|
|||||||
npm install @hono/vite-dev-server
|
npm install @hono/vite-dev-server
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
Now create a `bknd.config.ts` file in the root of your project.
|
||||||
|
|
||||||
|
```typescript bknd.config.ts
|
||||||
|
import type { ViteBkndConfig } from "bknd/adapter/vite";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
connection: {
|
||||||
|
url: "file:data.db"
|
||||||
|
}
|
||||||
|
} satisfies ViteBkndConfig;
|
||||||
|
```
|
||||||
|
|
||||||
|
See [bknd.config.ts](/extending/config) for more information on how to configure bknd.
|
||||||
|
The `ViteBkndConfig` type extends the `BkndConfig` type with the following additional properties:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export type ViteEnv = NodeJS.ProcessEnv;
|
||||||
|
export type ViteBkndConfig<Env = ViteEnv> = RuntimeBkndConfig<Env> & {};
|
||||||
|
```
|
||||||
|
|
||||||
## Serve the API
|
## Serve the API
|
||||||
To serve the **bknd** API, you first have to create a local server file for you vite environment.
|
To serve the **bknd** API, you first have to create a local server file for you vite environment.
|
||||||
Create a `server.ts` file:
|
Create a `server.ts` file:
|
||||||
```ts
|
|
||||||
|
```typescript
|
||||||
import { serve } from "bknd/adapter/vite";
|
import { serve } from "bknd/adapter/vite";
|
||||||
|
import config from "./bknd.config";
|
||||||
|
|
||||||
// the configuration given is optional
|
export default serve(config)
|
||||||
export default serve({
|
|
||||||
mode: "cached", // that's the default
|
|
||||||
connection: {
|
|
||||||
url: ":memory:"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
```
|
||||||
For more information about the connection object, refer to the [Database](/usage/database) guide.
|
|
||||||
|
|
||||||
You can also run your vite server in `mode: "fresh"`, this will re-create the app on every fetch.
|
You can also run your vite server in `mode: "fresh"`, this will re-create the app on every fetch.
|
||||||
This is only useful for when working on the `bknd` repository directly.
|
This is only useful for when working on the `bknd` repository directly.
|
||||||
|
|
||||||
|
For more information about the connection object, refer to the [Database](/usage/database) guide.
|
||||||
|
|
||||||
Next, adjust your `vite.config.ts` to look like the following:
|
Next, adjust your `vite.config.ts` to look like the following:
|
||||||
```ts
|
```ts
|
||||||
import { devServer } from "bknd/adapter/vite";
|
import { devServer } from "bknd/adapter/vite";
|
||||||
@@ -82,17 +99,22 @@ options you can make use of to adjust it according to your setup.
|
|||||||
There might be cases you want to be sure to be in control over the HTML that is being used.
|
There might be cases you want to be sure to be in control over the HTML that is being used.
|
||||||
`bknd` generates it automatically, but you use your own one as follows:
|
`bknd` generates it automatically, but you use your own one as follows:
|
||||||
|
|
||||||
```ts server.ts
|
```typescript server.ts
|
||||||
import { serve, addViteScript } from "bknd/adapter/vite";
|
import { serve, addViteScript } from "bknd/adapter/vite";
|
||||||
import { readFile } from "node:fs/promises"
|
import { readFile } from "node:fs/promises"
|
||||||
|
import config from "./bknd.config";
|
||||||
|
|
||||||
let html = await readFile("./index.html", "utf-8");
|
let html = await readFile("./index.html", "utf-8");
|
||||||
|
|
||||||
// add vite scripts
|
|
||||||
html = addViteScript(html);
|
|
||||||
|
|
||||||
// then add it as an option
|
// then add it as an option
|
||||||
export default serve({ html })
|
export default serve({
|
||||||
|
...config,
|
||||||
|
adminOptions: {
|
||||||
|
html: addViteScript(html),
|
||||||
|
// optionally, you can change the base path for the admin UI
|
||||||
|
adminBasePath: "/admin"
|
||||||
|
}
|
||||||
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
The vite scripts has to be added manually currently, as adding them automatically with
|
The vite scripts has to be added manually currently, as adding them automatically with
|
||||||
@@ -101,13 +123,18 @@ The vite scripts has to be added manually currently, as adding them automaticall
|
|||||||
### Use a custom entry point
|
### Use a custom entry point
|
||||||
By default, the entry point `/src/main.tsx` is used and should fit most cases. If that's not you,
|
By default, the entry point `/src/main.tsx` is used and should fit most cases. If that's not you,
|
||||||
you can supply a different one like so:
|
you can supply a different one like so:
|
||||||
```ts server.ts
|
|
||||||
|
```typescript server.ts
|
||||||
import { serve } from "bknd/adapter/vite";
|
import { serve } from "bknd/adapter/vite";
|
||||||
|
import config from "./bknd.config";
|
||||||
|
|
||||||
// the configuration given is optional
|
// the configuration given is optional
|
||||||
export default serve({
|
export default serve({
|
||||||
|
...config,
|
||||||
|
adminOptions: {
|
||||||
forceDev: {
|
forceDev: {
|
||||||
mainPath: "/src/special.tsx"
|
mainPath: "/src/special.tsx"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
@@ -74,6 +74,10 @@
|
|||||||
"usage/elements"
|
"usage/elements"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"group": "Extending",
|
||||||
|
"pages": ["extending/config", "extending/events"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"group": "Integration",
|
"group": "Integration",
|
||||||
"pages": [
|
"pages": [
|
||||||
|
|||||||
+75
-7
@@ -14,20 +14,21 @@ Here is the output:
|
|||||||
$ npx bknd
|
$ npx bknd
|
||||||
Usage: bknd [options] [command]
|
Usage: bknd [options] [command]
|
||||||
|
|
||||||
⚡ bknd cli v0.10.3-rc.1
|
⚡ bknd cli v0.13.0
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-V, --version output the version number
|
-V, --version output the version number
|
||||||
-h, --help display help for command
|
-h, --help display help for command
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
config [options] get default config
|
user <action> create/update users, or generate a token (auth)
|
||||||
copy-assets [options] copy static assets
|
types [options] generate types
|
||||||
create [options] create a new project
|
|
||||||
debug <subject> debug bknd
|
|
||||||
run [options] run an instance
|
|
||||||
schema [options] get schema
|
schema [options] get schema
|
||||||
user <action> create and update user (auth)
|
run [options] run an instance
|
||||||
|
debug <subject> debug bknd
|
||||||
|
create [options] create a new project
|
||||||
|
copy-assets [options] copy static assets
|
||||||
|
config [options] get default config
|
||||||
help [command] display help for command
|
help [command] display help for command
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -124,3 +125,70 @@ To start an instance with an ephemeral in-memory database, run the following:
|
|||||||
npx bknd run --memory
|
npx bknd run --memory
|
||||||
```
|
```
|
||||||
Keep in mind that the database is not persisted and will be lost when the process is terminated.
|
Keep in mind that the database is not persisted and will be lost when the process is terminated.
|
||||||
|
|
||||||
|
## Generating types (`types`)
|
||||||
|
To see all available `types` options, execute `npx bknd types --help`.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ npx bknd types --help
|
||||||
|
Usage: bknd types [options]
|
||||||
|
|
||||||
|
generate types
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-o, --outfile <outfile> output file (default: "bknd-types.d.ts")
|
||||||
|
--no-write do not write to file
|
||||||
|
-h, --help display help for command
|
||||||
|
```
|
||||||
|
|
||||||
|
To generate types for the database, run the following:
|
||||||
|
```
|
||||||
|
npx bknd types
|
||||||
|
```
|
||||||
|
|
||||||
|
This will generate types for your database schema in `bknd-types.d.ts`. The generated file could look like this:
|
||||||
|
|
||||||
|
```typescript bknd-types.d.ts
|
||||||
|
import type { DB } from "bknd/core";
|
||||||
|
import type { Insertable, Selectable, Updateable, Generated } from "kysely";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
type BkndEntity<T extends keyof DB> = Selectable<DB[T]>;
|
||||||
|
type BkndEntityCreate<T extends keyof DB> = Insertable<DB[T]>;
|
||||||
|
type BkndEntityUpdate<T extends keyof DB> = Updateable<DB[T]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Todos {
|
||||||
|
id: Generated<number>;
|
||||||
|
title?: string;
|
||||||
|
done?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Database {
|
||||||
|
todos: Todos;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "bknd/core" {
|
||||||
|
interface DB extends Database {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Make sure to add the generated file in your `tsconfig.json` file:
|
||||||
|
|
||||||
|
```json tsconfig.json
|
||||||
|
{
|
||||||
|
"include": [
|
||||||
|
"bknd-types.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can then use the types by importing them from `bknd/core`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { DB } from "bknd/core";
|
||||||
|
|
||||||
|
type Todo = DB["todos"];
|
||||||
|
```
|
||||||
|
|
||||||
|
All bknd methods that involve your database schema will be automatically typed.
|
||||||
+81
-14
@@ -3,13 +3,8 @@ title: 'Database'
|
|||||||
description: 'Choosing the right database configuration'
|
description: 'Choosing the right database configuration'
|
||||||
---
|
---
|
||||||
|
|
||||||
In order to use **bknd**, you need to prepare access information to your database and install
|
In order to use **bknd**, you need to prepare access information to your database and install the dependencies. Connections to the database are managed using Kysely. Therefore, all [its dialects](https://kysely.dev/docs/dialects) are theoretically supported.
|
||||||
the dependencies.
|
|
||||||
|
|
||||||
<Note>
|
|
||||||
Connections to the database are managed using Kysely. Therefore, all its dialects are
|
|
||||||
theoretically supported. However, only the `SQLite` dialect is implemented as of now.
|
|
||||||
</Note>
|
|
||||||
|
|
||||||
## Database
|
## Database
|
||||||
### SQLite in-memory
|
### SQLite in-memory
|
||||||
@@ -56,7 +51,9 @@ connection object to your new database:
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Cloudflare D1
|
### Cloudflare D1
|
||||||
Using the [Cloudflare Adapter](/integration/cloudflare), you can choose to use a D1 database binding. To do so, you only need to add a D1 database to your `wrangler.toml` and it'll pick up automatically. To manually specify which D1 database to take, you can specify it manually:
|
Using the [Cloudflare Adapter](/integration/cloudflare), you can choose to use a D1 database binding. To do so, you only need to add a D1 database to your `wrangler.toml` and it'll pick up automatically.
|
||||||
|
|
||||||
|
To manually specify which D1 database to take, you can specify it explicitly:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { serve, d1 } from "bknd/adapter/cloudflare";
|
import { serve, d1 } from "bknd/adapter/cloudflare";
|
||||||
@@ -73,17 +70,19 @@ To use bknd with Postgres, you need to install the `@bknd/postgres` package. You
|
|||||||
npm install @bknd/postgres
|
npm install @bknd/postgres
|
||||||
```
|
```
|
||||||
|
|
||||||
This package uses `pg` under the hood. If you'd like to see `postgres` or any other flavor, please create an [issue on Github](https://github.com/bknd-io/bknd/issues/new).
|
You can connect to your Postgres database using `pg` or `postgres` dialects. Additionally, you may also define your custom connection.
|
||||||
|
|
||||||
To establish a connection to your database, you can use any connection options available on the [`pg`](https://node-postgres.com/apis/client) package. Here is a quick example using the [Node.js Adapter](http://localhost:3000/integration/node):
|
#### Using `pg`
|
||||||
|
|
||||||
|
To establish a connection to your database, you can use any connection options available on the [`pg`](https://node-postgres.com/apis/client) package.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import { serve } from "bknd/adapter/node";
|
import { serve } from "bknd/adapter/node";
|
||||||
import { PostgresConnection } from "@bknd/postgres";
|
import { pg } from "@bknd/postgres";
|
||||||
|
|
||||||
/** @type {import("bknd/adapter/node").NodeBkndConfig} */
|
/** @type {import("bknd/adapter/node").NodeBkndConfig} */
|
||||||
const config = {
|
const config = {
|
||||||
connection: new PostgresConnection({
|
connection: pg({
|
||||||
connectionString:
|
connectionString:
|
||||||
"postgresql://user:password@localhost:5432/database",
|
"postgresql://user:password@localhost:5432/database",
|
||||||
}),
|
}),
|
||||||
@@ -92,6 +91,64 @@ const config = {
|
|||||||
serve(config);
|
serve(config);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Using `postgres`
|
||||||
|
|
||||||
|
To establish a connection to your database, you can use any connection options available on the [`postgres`](https://github.com/porsager/postgres) package.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { serve } from "bknd/adapter/node";
|
||||||
|
import { postgresJs } from "@bknd/postgres";
|
||||||
|
|
||||||
|
serve({
|
||||||
|
connection: postgresJs("postgresql://user:password@localhost:5432/database"),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Using custom connection
|
||||||
|
|
||||||
|
Several Postgres hosting providers offer their own clients to connect to their database, e.g. suitable for serverless environments.
|
||||||
|
|
||||||
|
Example using `@neondatabase/serverless`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { createCustomPostgresConnection } from "@bknd/postgres";
|
||||||
|
import { NeonDialect } from "kysely-neon";
|
||||||
|
|
||||||
|
const connection = createCustomPostgresConnection(NeonDialect)({
|
||||||
|
connectionString: process.env.NEON,
|
||||||
|
});
|
||||||
|
|
||||||
|
serve({
|
||||||
|
connection: connection,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Example using `@xata.io/client`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { createCustomPostgresConnection } from "@bknd/postgres";
|
||||||
|
import { XataDialect } from "@xata.io/kysely";
|
||||||
|
import { buildClient } from "@xata.io/client";
|
||||||
|
|
||||||
|
const client = buildClient();
|
||||||
|
const xata = new client({
|
||||||
|
databaseURL: process.env.XATA_URL,
|
||||||
|
apiKey: process.env.XATA_API_KEY,
|
||||||
|
branch: process.env.XATA_BRANCH,
|
||||||
|
});
|
||||||
|
|
||||||
|
const connection = createCustomPostgresConnection(XataDialect, {
|
||||||
|
supports: {
|
||||||
|
batching: false,
|
||||||
|
},
|
||||||
|
})({ xata });
|
||||||
|
|
||||||
|
serve({
|
||||||
|
connection: connection,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### SQLocal
|
### SQLocal
|
||||||
To use bknd with `sqlocal` for a offline expierence, you need to install the `@bknd/sqlocal` package. You can do so by running the following command:
|
To use bknd with `sqlocal` for a offline expierence, you need to install the `@bknd/sqlocal` package. You can do so by running the following command:
|
||||||
|
|
||||||
@@ -138,7 +195,11 @@ const app = createApp({ connection })
|
|||||||
## Initial Structure
|
## Initial Structure
|
||||||
To provide an initial database structure, you can pass `initialConfig` to the creation of an app. This will only be used if there isn't an existing configuration found in the database given. Here is a quick example:
|
To provide an initial database structure, you can pass `initialConfig` to the creation of an app. This will only be used if there isn't an existing configuration found in the database given. Here is a quick example:
|
||||||
|
|
||||||
```ts
|
<Note>
|
||||||
|
The initial structure is only respected if the database is empty! If you made updates, ensure to delete the database first, or perform updates through the Admin UI.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
```typescript
|
||||||
import { createApp } from "bknd";
|
import { createApp } from "bknd";
|
||||||
import { em, entity, text, number } from "bknd/data";
|
import { em, entity, text, number } from "bknd/data";
|
||||||
|
|
||||||
@@ -193,9 +254,13 @@ Note that we didn't add relational fields directly to the entity, but instead de
|
|||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
### Type completion
|
### Type completion
|
||||||
|
To get type completion, there are two options:
|
||||||
|
1. Use the CLI to [generate the types](/usage/cli#generating-types-types)
|
||||||
|
2. If you have an initial structure created with the prototype functions, you can extend the `DB` interface with your own schema.
|
||||||
|
|
||||||
All entity related functions use the types defined in `DB` from `bknd/core`. To get type completion, you can extend that interface with your own schema:
|
All entity related functions use the types defined in `DB` from `bknd/core`. To get type completion, you can extend that interface with your own schema:
|
||||||
|
|
||||||
```ts
|
```typescript
|
||||||
import { em } from "bknd/data";
|
import { em } from "bknd/data";
|
||||||
import { Api } from "bknd/client";
|
import { Api } from "bknd/client";
|
||||||
|
|
||||||
@@ -217,10 +282,12 @@ The type completion is available for the API as well as all provided [React hook
|
|||||||
To seed your database with initial data, you can pass a `seed` function to the configuration. It
|
To seed your database with initial data, you can pass a `seed` function to the configuration. It
|
||||||
provides the `ModuleBuildContext` as the first argument.
|
provides the `ModuleBuildContext` as the first argument.
|
||||||
|
|
||||||
|
<Note>
|
||||||
Note that the seed function will only be executed on app's first boot. If a configuration
|
Note that the seed function will only be executed on app's first boot. If a configuration
|
||||||
already exists in the database, it will not be executed.
|
already exists in the database, it will not be executed.
|
||||||
|
</Note>
|
||||||
|
|
||||||
```ts
|
```typescript
|
||||||
import { createApp, type ModuleBuildContext } from "bknd";
|
import { createApp, type ModuleBuildContext } from "bknd";
|
||||||
|
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user