From 773df544dda57284f1acbf2e67b4421b0e3940fa Mon Sep 17 00:00:00 2001 From: dswbx Date: Thu, 22 May 2025 08:52:25 +0200 Subject: [PATCH] feat/custom-json-schema (#172) * init * update * finished new repo query, removed old implementation * remove debug folder --- .gitignore | 3 +- app/__test__/core/object/object-query.spec.ts | 2 +- .../WhereBuilder.spec.ts} | 93 ++------- app/__test__/media/MediaController.spec.ts | 3 +- app/package.json | 9 +- app/src/App.ts | 2 +- app/src/core/object/query/query.ts | 4 + app/src/core/object/schema/index.ts | 43 ++++ app/src/core/object/schema/validator.ts | 63 ++++++ app/src/core/server/lib/index.ts | 1 + app/src/core/server/lib/jscValidator.ts | 29 +++ app/src/core/utils/objects.ts | 13 ++ app/src/data/api/DataController.ts | 76 ++++---- app/src/data/entities/Mutator.ts | 2 +- app/src/data/entities/query/JoinBuilder.ts | 1 + app/src/data/entities/query/Repository.ts | 20 +- app/src/data/entities/query/WhereBuilder.ts | 1 + app/src/data/events/index.ts | 2 +- app/src/data/index.ts | 9 +- app/src/data/relations/EntityRelation.ts | 2 +- app/src/data/relations/ManyToManyRelation.ts | 2 +- app/src/data/relations/ManyToOneRelation.ts | 2 +- app/src/data/relations/PolymorphicRelation.ts | 2 +- app/src/data/server/data-query-impl.ts | 148 -------------- app/src/data/server/query.spec.ts | 184 ++++++++++++++++++ app/src/data/server/query.ts | 158 +++++++++++++++ app/src/modules/Controller.ts | 4 +- app/src/ui/hooks/use-search.ts | 29 ++- .../ui/routes/data/data.$entity.create.tsx | 5 +- app/src/ui/routes/data/data.$entity.index.tsx | 24 +-- bun.lock | 102 +--------- 31 files changed, 614 insertions(+), 424 deletions(-) rename app/__test__/data/{data-query-impl.spec.ts => specs/WhereBuilder.spec.ts} (52%) create mode 100644 app/src/core/object/schema/index.ts create mode 100644 app/src/core/object/schema/validator.ts create mode 100644 app/src/core/server/lib/index.ts create mode 100644 app/src/core/server/lib/jscValidator.ts delete mode 100644 app/src/data/server/data-query-impl.ts create mode 100644 app/src/data/server/query.spec.ts create mode 100644 app/src/data/server/query.ts diff --git a/.gitignore b/.gitignore index 3712ddf..1bda749 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,5 @@ packages/media/.env .idea .vscode .git_old -docker/tmp \ No newline at end of file +docker/tmp +.debug \ No newline at end of file diff --git a/app/__test__/core/object/object-query.spec.ts b/app/__test__/core/object/object-query.spec.ts index 70ea70c..dc03fb6 100644 --- a/app/__test__/core/object/object-query.spec.ts +++ b/app/__test__/core/object/object-query.spec.ts @@ -1,5 +1,5 @@ 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", () => { const q: ObjectQuery = { name: "Michael" }; diff --git a/app/__test__/data/data-query-impl.spec.ts b/app/__test__/data/specs/WhereBuilder.spec.ts similarity index 52% rename from app/__test__/data/data-query-impl.spec.ts rename to app/__test__/data/specs/WhereBuilder.spec.ts index d96247e..a477cbe 100644 --- a/app/__test__/data/data-query-impl.spec.ts +++ b/app/__test__/data/specs/WhereBuilder.spec.ts @@ -1,25 +1,18 @@ -import { describe, expect, test } from "bun:test"; -import { Value, _jsonp } from "../../src/core/utils"; -import { type RepoQuery, WhereBuilder, type WhereQuery, querySchema } from "../../src/data"; -import type { RepoQueryIn } from "../../src/data/server/data-query-impl"; -import { getDummyConnection } from "./helper"; +import { describe, test, expect } from "bun:test"; +import { getDummyConnection } from "../helper"; +import { type WhereQuery, WhereBuilder } from "data"; -const decode = (input: RepoQueryIn, expected: RepoQuery) => { - const result = Value.Decode(querySchema, input); - expect(result).toEqual(expected); -}; - -describe("data-query-impl", () => { - function qb() { - const c = getDummyConnection(); - const kysely = c.dummyConnection.kysely; - return kysely.selectFrom("t").selectAll(); - } - function compile(q: WhereQuery) { - const { sql, parameters } = WhereBuilder.addClause(qb(), q).compile(); - return { sql, parameters }; - } +function qb() { + const c = getDummyConnection(); + const kysely = c.dummyConnection.kysely; + return kysely.selectFrom("t").selectAll(); +} +function compile(q: WhereQuery) { + const { sql, parameters } = WhereBuilder.addClause(qb(), q).compile(); + return { sql, parameters }; +} +describe("WhereBuilder", () => { test("single validation", () => { const tests: [WhereQuery, string, any[]][] = [ [{ name: "Michael", age: 40 }, '("name" = ? and "age" = ?)', ["Michael", 40]], @@ -94,64 +87,4 @@ describe("data-query-impl", () => { 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" } }); - }); }); diff --git a/app/__test__/media/MediaController.spec.ts b/app/__test__/media/MediaController.spec.ts index f55591b..d0e56c9 100644 --- a/app/__test__/media/MediaController.spec.ts +++ b/app/__test__/media/MediaController.spec.ts @@ -43,8 +43,9 @@ beforeAll(disableConsoleLog); afterAll(enableConsoleLog); describe("MediaController", () => { - test.only("accepts direct", async () => { + test("accepts direct", async () => { const app = await makeApp(); + console.log("app", app); const file = Bun.file(path); const name = makeName("png"); diff --git a/app/package.json b/app/package.json index 05de587..e5dbc49 100644 --- a/app/package.json +++ b/app/package.json @@ -52,6 +52,7 @@ "@libsql/client": "^0.15.2", "@mantine/core": "^7.17.1", "@mantine/hooks": "^7.17.1", + "@sinclair/typebox": "0.34.30", "@tanstack/react-form": "^1.0.5", "@uiw/react-codemirror": "^4.23.10", "@xyflow/react": "^12.4.4", @@ -63,13 +64,13 @@ "json-schema-form-react": "^0.0.2", "json-schema-library": "10.0.0-rc7", "json-schema-to-ts": "^3.1.1", + "jsonv-ts": "^0.0.11", "kysely": "^0.27.6", + "lodash-es": "^4.17.21", "oauth4webapi": "^2.11.1", "object-path-immutable": "^4.1.2", "radix-ui": "^1.1.3", - "swr": "^2.3.3", - "lodash-es": "^4.17.21", - "@sinclair/typebox": "0.34.30" + "swr": "^2.3.3" }, "devDependencies": { "@aws-sdk/client-s3": "^3.758.0", @@ -101,11 +102,11 @@ "kysely-d1": "^0.3.0", "open": "^10.1.0", "openapi-types": "^12.1.3", + "picocolors": "^1.1.1", "postcss": "^8.5.3", "postcss-preset-mantine": "^1.17.0", "postcss-simple-vars": "^7.0.1", "posthog-js-lite": "^3.4.2", - "picocolors": "^1.1.1", "react": "^19.0.0", "react-dom": "^19.0.0", "react-hook-form": "^7.54.2", diff --git a/app/src/App.ts b/app/src/App.ts index 9683616..639d891 100644 --- a/app/src/App.ts +++ b/app/src/App.ts @@ -151,7 +151,7 @@ export class App { } get fetch(): Hono["fetch"] { - return this.server.fetch; + return this.server.fetch as any; } get module() { diff --git a/app/src/core/object/query/query.ts b/app/src/core/object/query/query.ts index c432daf..27180ee 100644 --- a/app/src/core/object/query/query.ts +++ b/app/src/core/object/query/query.ts @@ -34,6 +34,8 @@ type ExpressionMap = { ? E : never; }; +type ExpressionKeys = Exps[number]["key"]; + type ExpressionCondition = { [K in keyof ExpressionMap]: { [P in K]: ExpressionMap[K] }; }[keyof ExpressionMap]; @@ -195,5 +197,7 @@ export function makeValidator(expressions: Exps) { const fns = _build(query, expressions, options); return _validate(fns); }, + expressions, + expressionKeys: expressions.map((e) => e.key) as ExpressionKeys, }; } diff --git a/app/src/core/object/schema/index.ts b/app/src/core/object/schema/index.ts new file mode 100644 index 0000000..28ac2fe --- /dev/null +++ b/app/src/core/object/schema/index.ts @@ -0,0 +1,43 @@ +import { mergeObject } from "core/utils"; + +export { jsc, type Options, type Hook } from "./validator"; +import * as s from "jsonv-ts"; + +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; +}; + +export function parse( + _schema: S, + v: unknown, + opts: ParseOptions = {}, +): s.StaticCoerced { + const schema = _schema as unknown 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; +} diff --git a/app/src/core/object/schema/validator.ts b/app/src/core/object/schema/validator.ts new file mode 100644 index 0000000..7e8c61c --- /dev/null +++ b/app/src/core/object/schema/validator.ts @@ -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 = ( + result: { result: ValidationResult; data: T }, + c: Context, +) => Response | Promise | 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 : StaticCoerced, + I extends Input = { + in: { [K in Target]: Static }; + out: { [K in Target]: Out }; + }, +>( + target: Target, + schema: Schema, + options?: Opts, + hook?: Hook, +): MiddlewareHandler => { + // @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; diff --git a/app/src/core/server/lib/index.ts b/app/src/core/server/lib/index.ts new file mode 100644 index 0000000..d6eea75 --- /dev/null +++ b/app/src/core/server/lib/index.ts @@ -0,0 +1 @@ +export { tbValidator } from "./tbValidator"; diff --git a/app/src/core/server/lib/jscValidator.ts b/app/src/core/server/lib/jscValidator.ts new file mode 100644 index 0000000..c61f362 --- /dev/null +++ b/app/src/core/server/lib/jscValidator.ts @@ -0,0 +1,29 @@ +import type { Env, Input, MiddlewareHandler, ValidationTargets } from "hono"; +import { validator } from "hono/validator"; +import type { Static, TSchema } from "simple-jsonschema-ts"; + +export const honoValidator = < + Target extends keyof ValidationTargets, + E extends Env, + P extends string, + const Schema extends TSchema = TSchema, + Out = Static, + I extends Input = { + in: { [K in Target]: Static }; + out: { [K in Target]: Static }; + }, +>( + target: Target, + schema: Schema, +): MiddlewareHandler => { + // @ts-expect-error not typed well + return validator(target, async (value, c) => { + const coersed = schema.coerce(value); + const result = schema.validate(coersed); + if (!result.valid) { + return c.json({ ...result, schema }, 400); + } + + return coersed as Out; + }); +}; diff --git a/app/src/core/utils/objects.ts b/app/src/core/utils/objects.ts index a8469c7..a14f1c3 100644 --- a/app/src/core/utils/objects.ts +++ b/app/src/core/utils/objects.ts @@ -406,3 +406,16 @@ export function objectToJsLiteral(value: object, indent: number = 0, _level: num throw new TypeError(`Unsupported data type: ${t}`); } + +// lodash-es compatible `pick` with perfect type inference +export function pick(obj: T, keys: K[]): Pick { + return keys.reduce( + (acc, key) => { + if (key in obj) { + acc[key] = obj[key]; + } + return acc; + }, + {} as Pick, + ); +} diff --git a/app/src/data/api/DataController.ts b/app/src/data/api/DataController.ts index 333b9a3..793b5e4 100644 --- a/app/src/data/api/DataController.ts +++ b/app/src/data/api/DataController.ts @@ -8,11 +8,12 @@ import { type MutatorResponse, type RepoQuery, type RepositoryResponse, - querySchema, + repoQuery, } from "data"; import type { Handler } from "hono/types"; import type { ModuleBuildContext } from "modules"; import { Controller } from "modules/Controller"; +import { jsc, s } from "core/object/schema"; import * as SystemPermissions from "modules/permissions"; import type { AppDataConfig } from "../data-schema"; const { Type } = tbbox; @@ -205,7 +206,7 @@ export class DataController extends Controller { hono.post( "/:entity/fn/count", permission(DataPermissions.entityRead), - tb("param", Type.Object({ entity: Type.String() })), + jsc("param", s.object({ entity: s.string() })), async (c) => { const { entity } = c.req.valid("param"); if (!this.entityExists(entity)) { @@ -222,7 +223,7 @@ export class DataController extends Controller { hono.post( "/:entity/fn/exists", permission(DataPermissions.entityRead), - tb("param", Type.Object({ entity: Type.String() })), + jsc("param", s.object({ entity: s.string() })), async (c) => { const { entity } = c.req.valid("param"); if (!this.entityExists(entity)) { @@ -242,10 +243,10 @@ export class DataController extends Controller { hono.get( "/:entity", permission(DataPermissions.entityRead), - tb("param", Type.Object({ entity: Type.String() })), - tb("query", querySchema), + jsc("param", s.object({ entity: s.string() })), + jsc("query", repoQuery), async (c) => { - const { entity } = c.req.param(); + const { entity } = c.req.valid("param"); if (!this.entityExists(entity)) { return this.notFound(c); } @@ -260,16 +261,16 @@ export class DataController extends Controller { hono.get( "/:entity/:id", permission(DataPermissions.entityRead), - tb( + jsc( "param", - Type.Object({ - entity: Type.String(), - id: tbNumber, + s.object({ + entity: s.string(), + id: s.string(), }), ), - tb("query", querySchema), + jsc("query", repoQuery), async (c) => { - const { entity, id } = c.req.param(); + const { entity, id } = c.req.valid("param"); if (!this.entityExists(entity)) { return this.notFound(c); } @@ -284,17 +285,17 @@ export class DataController extends Controller { hono.get( "/:entity/:id/:reference", permission(DataPermissions.entityRead), - tb( + jsc( "param", - Type.Object({ - entity: Type.String(), - id: tbNumber, - reference: Type.String(), + s.object({ + entity: s.string(), + id: s.string(), + reference: s.string(), }), ), - tb("query", querySchema), + jsc("query", repoQuery), async (c) => { - const { entity, id, reference } = c.req.param(); + const { entity, id, reference } = c.req.valid("param"); if (!this.entityExists(entity)) { return this.notFound(c); } @@ -312,10 +313,10 @@ export class DataController extends Controller { hono.post( "/:entity/query", permission(DataPermissions.entityRead), - tb("param", Type.Object({ entity: Type.String() })), - tb("json", querySchema), + jsc("param", s.object({ entity: s.string() })), + jsc("json", repoQuery), async (c) => { - const { entity } = c.req.param(); + const { entity } = c.req.valid("param"); if (!this.entityExists(entity)) { return this.notFound(c); } @@ -333,10 +334,11 @@ export class DataController extends Controller { hono.post( "/:entity", permission(DataPermissions.entityCreate), - tb("param", Type.Object({ entity: Type.String() })), - tb("json", Type.Union([Type.Object({}), Type.Array(Type.Object({}))])), + jsc("param", s.object({ entity: s.string() })), + jsc("json", s.anyOf([s.object({}), s.array(s.object({}))])), + //tb("json", Type.Union([Type.Object({}), Type.Array(Type.Object({}))])), async (c) => { - const { entity } = c.req.param(); + const { entity } = c.req.valid("param"); if (!this.entityExists(entity)) { return this.notFound(c); } @@ -356,12 +358,12 @@ export class DataController extends Controller { hono.patch( "/:entity", permission(DataPermissions.entityUpdate), - tb("param", Type.Object({ entity: Type.String() })), - tb( + jsc("param", s.object({ entity: s.string() })), + jsc( "json", - Type.Object({ - update: Type.Object({}), - where: querySchema.properties.where, + s.object({ + update: s.object({}), + where: repoQuery.properties.where, }), ), async (c) => { @@ -383,9 +385,9 @@ export class DataController extends Controller { hono.patch( "/:entity/:id", permission(DataPermissions.entityUpdate), - tb("param", Type.Object({ entity: Type.String(), id: tbNumber })), + jsc("param", s.object({ entity: s.string(), id: s.number() })), async (c) => { - const { entity, id } = c.req.param(); + const { entity, id } = c.req.valid("param"); if (!this.entityExists(entity)) { return this.notFound(c); } @@ -400,9 +402,9 @@ export class DataController extends Controller { hono.delete( "/:entity/:id", permission(DataPermissions.entityDelete), - tb("param", Type.Object({ entity: Type.String(), id: tbNumber })), + jsc("param", s.object({ entity: s.string(), id: s.number() })), async (c) => { - const { entity, id } = c.req.param(); + const { entity, id } = c.req.valid("param"); if (!this.entityExists(entity)) { return this.notFound(c); } @@ -416,10 +418,10 @@ export class DataController extends Controller { hono.delete( "/:entity", permission(DataPermissions.entityDelete), - tb("param", Type.Object({ entity: Type.String() })), - tb("json", querySchema.properties.where), + jsc("param", s.object({ entity: s.string() })), + jsc("json", repoQuery.properties.where), async (c) => { - const { entity } = c.req.param(); + const { entity } = c.req.valid("param"); if (!this.entityExists(entity)) { return this.notFound(c); } diff --git a/app/src/data/entities/Mutator.ts b/app/src/data/entities/Mutator.ts index 0e4dae4..104ca33 100644 --- a/app/src/data/entities/Mutator.ts +++ b/app/src/data/entities/Mutator.ts @@ -6,7 +6,7 @@ import type { Entity, EntityData, EntityManager } from "../entities"; import { InvalidSearchParamsException } from "../errors"; import { MutatorEvents } from "../events"; import { RelationMutator } from "../relations"; -import type { RepoQuery } from "../server/data-query-impl"; +import type { RepoQuery } from "../server/query"; type MutatorQB = | InsertQueryBuilder diff --git a/app/src/data/entities/query/JoinBuilder.ts b/app/src/data/entities/query/JoinBuilder.ts index 0aa1ca7..6bd6ec6 100644 --- a/app/src/data/entities/query/JoinBuilder.ts +++ b/app/src/data/entities/query/JoinBuilder.ts @@ -20,6 +20,7 @@ export class JoinBuilder { // @todo: returns multiple on manytomany (edit: so?) static getJoinedEntityNames(em: EntityManager, entity: Entity, joins: string[]): string[] { + console.log("join", joins); return joins.flatMap((join) => { const relation = em.relationOf(entity.name, join); if (!relation) { diff --git a/app/src/data/entities/query/Repository.ts b/app/src/data/entities/query/Repository.ts index 1d2897a..625e701 100644 --- a/app/src/data/entities/query/Repository.ts +++ b/app/src/data/entities/query/Repository.ts @@ -2,10 +2,9 @@ import type { DB as DefaultDB, PrimaryFieldType } from "core"; import { $console } from "core"; import { type EmitsEvents, EventManager } from "core/events"; import { type SelectQueryBuilder, sql } from "kysely"; -import { cloneDeep } from "lodash-es"; import { InvalidSearchParamsException } from "../../errors"; import { MutatorEvents, RepositoryEvents } from "../../events"; -import { type RepoQuery, defaultQuerySchema } from "../../server/data-query-impl"; +import { type RepoQuery, getRepoQueryTemplate } from "data/server/query"; import { type Entity, type EntityData, @@ -84,14 +83,14 @@ export class Repository): RepoQuery { + getValidOptions(options?: RepoQuery): RepoQuery { const entity = this.entity; // @todo: if not cloned deep, it will keep references and error if multiple requests come in const validated = { - ...cloneDeep(defaultQuerySchema), + ...structuredClone(getRepoQueryTemplate()), sort: entity.getDefaultSort(), select: entity.getSelect(), - }; + } satisfies Required; if (!options) return validated; @@ -99,12 +98,15 @@ export class Repository 0) { @@ -505,7 +507,7 @@ export class Repository): Promise { + async exists(where: Required["where"]): Promise { const entity = this.entity; const options = this.getValidOptions({ where }); @@ -513,7 +515,7 @@ export class Repository; const validator = makeValidator(expressions); +export const expressionKeys = validator.expressionKeys; export class WhereBuilder { static addClause(qb: QB, query: WhereQuery) { diff --git a/app/src/data/events/index.ts b/app/src/data/events/index.ts index 8549750..b10f3b6 100644 --- a/app/src/data/events/index.ts +++ b/app/src/data/events/index.ts @@ -1,7 +1,7 @@ import { $console, type PrimaryFieldType } from "core"; import { Event, InvalidEventReturn } from "core/events"; 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> { static override slug = "mutator-insert-before"; diff --git a/app/src/data/index.ts b/app/src/data/index.ts index eb3e893..5069fce 100644 --- a/app/src/data/index.ts +++ b/app/src/data/index.ts @@ -10,10 +10,11 @@ export * from "./connection"; export { type RepoQuery, type RepoQueryIn, - defaultQuerySchema, - querySchema, - whereSchema, -} from "./server/data-query-impl"; + getRepoQueryTemplate, + repoQuery, +} from "./server/query"; + +export type { WhereQuery } from "./entities/query/WhereBuilder"; export { KyselyPluginRunner } from "./plugins/KyselyPluginRunner"; diff --git a/app/src/data/relations/EntityRelation.ts b/app/src/data/relations/EntityRelation.ts index 232476a..bf0d8e6 100644 --- a/app/src/data/relations/EntityRelation.ts +++ b/app/src/data/relations/EntityRelation.ts @@ -6,7 +6,7 @@ import { type MutationInstructionResponse, RelationHelper, } from "../relations"; -import type { RepoQuery } from "../server/data-query-impl"; +import type { RepoQuery } from "../server/query"; import type { RelationType } from "./relation-types"; import * as tbbox from "@sinclair/typebox"; const { Type } = tbbox; diff --git a/app/src/data/relations/ManyToManyRelation.ts b/app/src/data/relations/ManyToManyRelation.ts index 4767432..bd05108 100644 --- a/app/src/data/relations/ManyToManyRelation.ts +++ b/app/src/data/relations/ManyToManyRelation.ts @@ -2,7 +2,7 @@ import type { Static } from "core/utils"; import type { ExpressionBuilder } from "kysely"; import { Entity, type EntityManager } from "../entities"; 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 { EntityRelationAnchor } from "./EntityRelationAnchor"; import { RelationField } from "./RelationField"; diff --git a/app/src/data/relations/ManyToOneRelation.ts b/app/src/data/relations/ManyToOneRelation.ts index 192b965..7fde72a 100644 --- a/app/src/data/relations/ManyToOneRelation.ts +++ b/app/src/data/relations/ManyToOneRelation.ts @@ -3,7 +3,7 @@ import { snakeToPascalWithSpaces } from "core/utils"; import type { Static } from "core/utils"; import type { ExpressionBuilder } from "kysely"; 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 { EntityRelationAnchor } from "./EntityRelationAnchor"; import { RelationField, type RelationFieldBaseConfig } from "./RelationField"; diff --git a/app/src/data/relations/PolymorphicRelation.ts b/app/src/data/relations/PolymorphicRelation.ts index 16d8b4e..d0e7cdd 100644 --- a/app/src/data/relations/PolymorphicRelation.ts +++ b/app/src/data/relations/PolymorphicRelation.ts @@ -2,7 +2,7 @@ import type { Static } from "core/utils"; import type { ExpressionBuilder } from "kysely"; import type { Entity, EntityManager } from "../entities"; 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 { EntityRelationAnchor } from "./EntityRelationAnchor"; import { type RelationType, RelationTypes } from "./relation-types"; diff --git a/app/src/data/server/data-query-impl.ts b/app/src/data/server/data-query-impl.ts deleted file mode 100644 index 72ef50f..0000000 --- a/app/src/data/server/data-query-impl.ts +++ /dev/null @@ -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 & { - with?: unknown; - } ->; - -export const withSchema = (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; - join?: string[]; - where?: WhereQuery; -}; -export type RepoQuery = Required>; -export const defaultQuerySchema = Value.Default(querySchema, {}) as RepoQuery; diff --git a/app/src/data/server/query.spec.ts b/app/src/data/server/query.spec.ts new file mode 100644 index 0000000..4c1552d --- /dev/null +++ b/app/src/data/server/query.spec.ts @@ -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); + } + }); +}); diff --git a/app/src/data/server/query.ts b/app/src/data/server/query.ts new file mode 100644 index 0000000..fc0c4b3 --- /dev/null +++ b/app/src/data/server/query.ts @@ -0,0 +1,158 @@ +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 numberOrString = (c: N = {} as N) => + s.anyOf([s.number(), s.string()], { + ...c, + coerse: function (this: s.TSchema, v): number { + if (typeof v === "string") { + const n = Number.parseInt(v); + if (Number.isNaN(n)) return this.default ?? 10; + return n; + } + return v as number; + }, + }) as unknown as s.TSchemaInOut; +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; +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: {}, + coerce: (value: unknown) => { + const q = typeof value === "string" ? JSON.parse(value) : value; + return WhereBuilder.convert(q); + }, +}); +//type WhereSchemaIn = s.Static; +//type WhereSchema = s.StaticCoerced; + +// ------ +// with +// @todo: waiting for recursion support +export type RepoWithSchema = Record< + string, + Omit & { + with?: unknown; + } +>; + +const withSchema = (self: s.TSchema): s.TSchemaInOut => + s.anyOf([stringIdentifier, s.array(stringIdentifier), self], { + coerce: function (this: TAnyOf, _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: numberOrString({ default: 10 }), + offset: numberOrString({ default: 0 }), + sort, + where, + select: stringArray, + join: stringArray, + with: withSchema(self), + }), +); +export const getRepoQueryTemplate = () => + repoQuery.template({ + withOptional: true, + }) as Required; + +export type RepoQueryIn = { + limit?: number; + offset?: number; + sort?: string | { by: string; dir: "asc" | "desc" }; + select?: string[]; + with?: string | string[] | Record; + join?: string[]; + where?: WhereQuery; +}; +export type RepoQuery = s.StaticCoerced; diff --git a/app/src/modules/Controller.ts b/app/src/modules/Controller.ts index 95023fc..134019a 100644 --- a/app/src/modules/Controller.ts +++ b/app/src/modules/Controller.ts @@ -1,9 +1,9 @@ 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 type { SafeUser } from "auth"; -export type ServerEnv = { +export type ServerEnv = Env & { Variables: { app: App; // to prevent resolving auth multiple times diff --git a/app/src/ui/hooks/use-search.ts b/app/src/ui/hooks/use-search.ts index b550c2c..c2ce8a2 100644 --- a/app/src/ui/hooks/use-search.ts +++ b/app/src/ui/hooks/use-search.ts @@ -1,46 +1,43 @@ -import { - type Static, - type StaticDecode, - type TSchema, - decodeSearch, - encodeSearch, - parseDecode, -} from "core/utils"; +import { decodeSearch, encodeSearch, parseDecode } from "core/utils"; import { isEqual, transform } from "lodash-es"; import { useLocation, useSearch as useWouterSearch } from "wouter"; +import { type s, parse } from "core/object/schema"; // @todo: migrate to Typebox -export function useSearch( +export function useSearch( schema: Schema, - defaultValue?: Partial>, + defaultValue?: Partial>, ) { const searchString = useWouterSearch(); const [location, navigate] = useLocation(); - let value: StaticDecode = defaultValue ? parseDecode(schema, defaultValue as any) : {}; + let value = (defaultValue ? parse(schema, defaultValue as any) : {}) as s.StaticCoerced; if (searchString.length > 0) { - value = parseDecode(schema, decodeSearch(searchString)); + value = parse(schema, decodeSearch(searchString)); //console.log("search:decode", value); } // @todo: add option to set multiple keys at once - function set>(key: Key, value: Static[Key]): void { + function set>( + key: Key, + value: s.StaticCoerced[Key], + ): void { //console.log("set", key, value); - const update = parseDecode(schema, { ...decodeSearch(searchString), [key]: value }); + const update = parse(schema, { ...decodeSearch(searchString), [key]: value }); const search = transform( update as any, (result, value, key) => { if (defaultValue && isEqual(value, defaultValue[key])) return; result[key] = value; }, - {} as Static, + {} as s.StaticCoerced, ); const encoded = encodeSearch(search, { encode: false }); navigate(location + (encoded.length > 0 ? "?" + encoded : "")); } return { - value: value as Required>, + value: value as Required>, set, }; } diff --git a/app/src/ui/routes/data/data.$entity.create.tsx b/app/src/ui/routes/data/data.$entity.create.tsx index 6f7adea..bb83eaf 100644 --- a/app/src/ui/routes/data/data.$entity.create.tsx +++ b/app/src/ui/routes/data/data.$entity.create.tsx @@ -11,8 +11,7 @@ import { Breadcrumbs2 } from "ui/layouts/AppShell/Breadcrumbs2"; import { routes } from "ui/lib/routes"; import { EntityForm } from "ui/modules/data/components/EntityForm"; import { useEntityForm } from "ui/modules/data/hooks/useEntityForm"; -import * as tbbox from "@sinclair/typebox"; -const { Type } = tbbox; +import { s } from "core/object/schema"; export function DataEntityCreate({ params }) { const { $data } = useBkndData(); @@ -29,7 +28,7 @@ export function DataEntityCreate({ params }) { const $q = useEntityMutate(entity.name); // @todo: use entity schema for prefilling - const search = useSearch(Type.Object({}), {}); + const search = useSearch(s.object({}), {}); function goBack() { window.history.go(-1); diff --git a/app/src/ui/routes/data/data.$entity.index.tsx b/app/src/ui/routes/data/data.$entity.index.tsx index 757e10a..e71452d 100644 --- a/app/src/ui/routes/data/data.$entity.index.tsx +++ b/app/src/ui/routes/data/data.$entity.index.tsx @@ -1,4 +1,4 @@ -import { type Entity, querySchema } from "data"; +import { type Entity, repoQuery } from "data"; import { Fragment } from "react"; import { TbDots } from "react-icons/tb"; import { useApiQuery } from "ui/client"; @@ -14,20 +14,14 @@ import * as AppShell from "ui/layouts/AppShell/AppShell"; import { routes, useNavigate } from "ui/lib/routes"; import { useCreateUserModal } from "ui/modules/auth/hooks/use-create-user-modal"; import { EntityTable2 } from "ui/modules/data/components/EntityTable2"; -import * as tbbox from "@sinclair/typebox"; -const { Type } = tbbox; +import { s } from "core/object/schema"; +import { pick } from "core/utils/objects"; -// @todo: migrate to Typebox -const searchSchema = Type.Composite( - [ - Type.Pick(querySchema, ["select", "where", "sort"]), - Type.Object({ - page: Type.Optional(Type.Number({ default: 1 })), - perPage: Type.Optional(Type.Number({ default: 10 })), - }), - ], - { additionalProperties: false }, -); +const searchSchema = s.partialObject({ + ...pick(repoQuery.properties, ["select", "where", "sort"]), + page: s.number({ default: 1 }).optional(), + perPage: s.number({ default: 10 }).optional(), +}); const PER_PAGE_OPTIONS = [5, 10, 25]; @@ -74,8 +68,6 @@ export function DataEntityList({ params }) { const sort = search.value.sort!; const newSort = { by: name, dir: sort.by === name && sort.dir === "asc" ? "desc" : "asc" }; - // // @ts-expect-error - somehow all search keys are optional - console.log("new sort", newSort); search.set("sort", newSort as any); } diff --git a/bun.lock b/bun.lock index a35322e..f6f5d32 100644 --- a/bun.lock +++ b/bun.lock @@ -27,18 +27,17 @@ }, "app": { "name": "bknd", - "version": "0.11.0", + "version": "0.12.0", "bin": "./dist/cli/index.js", "dependencies": { "@cfworker/json-schema": "^4.1.1", "@codemirror/lang-html": "^6.4.9", "@codemirror/lang-json": "^6.0.1", - "@codemirror/lang-liquid": "^6.2.2", "@hello-pangea/dnd": "^18.0.1", "@libsql/client": "^0.15.2", "@mantine/core": "^7.17.1", "@mantine/hooks": "^7.17.1", - "@sinclair/typebox": "^0.34.30", + "@sinclair/typebox": "0.34.30", "@tanstack/react-form": "^1.0.5", "@uiw/react-codemirror": "^4.23.10", "@xyflow/react": "^12.4.4", @@ -48,17 +47,15 @@ "fast-xml-parser": "^5.0.8", "hono": "^4.7.4", "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", + "jsonv-ts": "^0.0.11", "kysely": "^0.27.6", - "liquidjs": "^10.21.0", "lodash-es": "^4.17.21", "oauth4webapi": "^2.11.1", "object-path-immutable": "^4.1.2", - "picocolors": "^1.1.1", "radix-ui": "^1.1.3", "swr": "^2.3.3", - "wrangler": "^4.4.1", }, "devDependencies": { "@aws-sdk/client-s3": "^3.758.0", @@ -90,6 +87,7 @@ "kysely-d1": "^0.3.0", "open": "^10.1.0", "openapi-types": "^12.1.3", + "picocolors": "^1.1.1", "postcss": "^8.5.3", "postcss-preset-mantine": "^1.17.0", "postcss-simple-vars": "^7.0.1", @@ -514,8 +512,6 @@ "@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-arm64": ["@cloudflare/workerd-darwin-arm64@1.20250224.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-naetGefgjAaDbEacpwaVruJXNwxmRRL7v3ppStgEiqAlPmTpQ/Edjn2SQ284QwOw3MvaVPHrWcaTBupUpkqCyg=="], @@ -542,8 +538,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-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/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=="], @@ -2038,8 +2032,6 @@ "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-shallow": ["extend-shallow@3.0.2", "", { "dependencies": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" } }, "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q=="], @@ -2526,6 +2518,8 @@ "jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="], + "jsonv-ts": ["jsonv-ts@0.0.11", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-W5WC6iwQvOuB0gRaAW9jAQKqT56pXjTA7XCjjAXZIM92/VBVNczTmV7iPtClqV1Zpgy4CtzaUsOJj4kWNeB5YQ=="], + "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=="], @@ -2586,8 +2580,6 @@ "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=="], "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=="], @@ -3858,8 +3850,6 @@ "@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=="], - "@emnapi/runtime/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@inquirer/core/cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], @@ -4164,8 +4154,6 @@ "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=="], "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], @@ -4428,8 +4416,6 @@ "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=="], "log-update/ansi-escapes": ["ansi-escapes@3.2.0", "", {}, "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ=="], @@ -4708,8 +4694,6 @@ "@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/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], @@ -4790,16 +4774,6 @@ "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/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=="], "class-utils/define-property/is-descriptor": ["is-descriptor@0.1.7", "", { "dependencies": { "is-accessor-descriptor": "^1.0.1", "is-data-descriptor": "^1.0.1" } }, "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg=="], @@ -5304,68 +5278,6 @@ "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/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/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="],