From 340b65a972f3b6e8a6acb9010fe64f5e3f2aafab Mon Sep 17 00:00:00 2001 From: Dylan Vorster Date: Tue, 7 Oct 2025 12:11:38 -0600 Subject: [PATCH 1/5] initial commit --- .editorconfig | 9 + .envrc | 3 + .gitignore | 13 + .ncurc.json | 3 + .npmrc | 0 .nvmrc | 1 + package.json | 19 + packages/codecs/package.json | 19 + packages/codecs/src/codecs.ts | 136 + packages/codecs/src/index.ts | 2 + packages/codecs/src/parsers.ts | 77 + .../tests/__snapshots__/parsers.test.ts.snap | 190 ++ packages/codecs/tests/parsers.test.ts | 54 + packages/codecs/tsconfig.json | 9 + packages/errors/package.json | 17 + packages/errors/src/errors.ts | 174 ++ packages/errors/src/index.ts | 2 + packages/errors/src/utils.ts | 20 + .../tests/__snapshots__/errors.test.ts.snap | 10 + packages/errors/tests/errors.test.ts | 53 + packages/errors/tsconfig.json | 9 + packages/schema/package.json | 23 + packages/schema/src/better-ajv-errors.d.ts | 26 + packages/schema/src/definitions.ts | 22 + packages/schema/src/index.ts | 6 + packages/schema/src/json-schema/keywords.ts | 27 + packages/schema/src/json-schema/parser.ts | 134 + packages/schema/src/utils.ts | 51 + packages/schema/src/validators/index.ts | 3 + .../schema/src/validators/schema-validator.ts | 86 + .../src/validators/ts-codec-validator.ts | 34 + .../schema/src/validators/zod-validator.ts | 33 + .../tests/__snapshots__/parser.test.ts.snap | 73 + .../schema-validation.test.ts.snap | 66 + .../ts-codec-validation.test.ts.snap | 17 + .../tests/__snapshots__/utils.test.ts.snap | 91 + .../__snapshots__/zod-validation.test.ts.snap | 16 + packages/schema/tests/fixtures/schema.ts | 49 + packages/schema/tests/parser.test.ts | 84 + .../schema/tests/schema-validation.test.ts | 180 ++ .../schema/tests/ts-codec-validation.test.ts | 103 + packages/schema/tests/utils.test.ts | 41 + packages/schema/tests/zod-validation.test.ts | 45 + packages/schema/tsconfig.json | 21 + packages/streaming/package.json | 29 + packages/streaming/src/bson/buffer-array.ts | 91 + packages/streaming/src/bson/constants.ts | 1 + packages/streaming/src/bson/decoder.ts | 81 + packages/streaming/src/bson/encoder.ts | 38 + packages/streaming/src/bson/header.ts | 77 + packages/streaming/src/bson/index.ts | 5 + packages/streaming/src/core/backpressure.ts | 38 + packages/streaming/src/core/cross-stream.ts | 19 + packages/streaming/src/core/index.ts | 4 + packages/streaming/src/core/node-utils.ts | 13 + packages/streaming/src/core/transformers.ts | 106 + packages/streaming/src/core/utils.ts | 87 + packages/streaming/src/index.ts | 4 + packages/streaming/src/middleware/index.ts | 2 + .../src/middleware/streamed-body-parser-v1.ts | 62 + .../src/middleware/streamed-body-parser-v2.ts | 34 + packages/streaming/src/web.ts | 5 + .../__snapshots__/bson-transform.test.ts.snap | 83 + .../tests/__snapshots__/common.test.ts.snap | 66 + .../streaming/tests/bson-transform.test.ts | 74 + packages/streaming/tests/common.test.ts | 76 + packages/streaming/tests/header.test.ts | 78 + packages/streaming/tsconfig.json | 18 + pnpm-lock.yaml | 2223 +++++++++++++++++ pnpm-workspace.yaml | 2 + tsconfig.base.json | 14 + tsconfig.json | 17 + 72 files changed, 5398 insertions(+) create mode 100644 .editorconfig create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 .ncurc.json create mode 100644 .npmrc create mode 100644 .nvmrc create mode 100644 package.json create mode 100644 packages/codecs/package.json create mode 100644 packages/codecs/src/codecs.ts create mode 100644 packages/codecs/src/index.ts create mode 100644 packages/codecs/src/parsers.ts create mode 100644 packages/codecs/tests/__snapshots__/parsers.test.ts.snap create mode 100644 packages/codecs/tests/parsers.test.ts create mode 100644 packages/codecs/tsconfig.json create mode 100644 packages/errors/package.json create mode 100644 packages/errors/src/errors.ts create mode 100644 packages/errors/src/index.ts create mode 100644 packages/errors/src/utils.ts create mode 100644 packages/errors/tests/__snapshots__/errors.test.ts.snap create mode 100644 packages/errors/tests/errors.test.ts create mode 100644 packages/errors/tsconfig.json create mode 100644 packages/schema/package.json create mode 100644 packages/schema/src/better-ajv-errors.d.ts create mode 100644 packages/schema/src/definitions.ts create mode 100644 packages/schema/src/index.ts create mode 100644 packages/schema/src/json-schema/keywords.ts create mode 100644 packages/schema/src/json-schema/parser.ts create mode 100644 packages/schema/src/utils.ts create mode 100644 packages/schema/src/validators/index.ts create mode 100644 packages/schema/src/validators/schema-validator.ts create mode 100644 packages/schema/src/validators/ts-codec-validator.ts create mode 100644 packages/schema/src/validators/zod-validator.ts create mode 100644 packages/schema/tests/__snapshots__/parser.test.ts.snap create mode 100644 packages/schema/tests/__snapshots__/schema-validation.test.ts.snap create mode 100644 packages/schema/tests/__snapshots__/ts-codec-validation.test.ts.snap create mode 100644 packages/schema/tests/__snapshots__/utils.test.ts.snap create mode 100644 packages/schema/tests/__snapshots__/zod-validation.test.ts.snap create mode 100644 packages/schema/tests/fixtures/schema.ts create mode 100644 packages/schema/tests/parser.test.ts create mode 100644 packages/schema/tests/schema-validation.test.ts create mode 100644 packages/schema/tests/ts-codec-validation.test.ts create mode 100644 packages/schema/tests/utils.test.ts create mode 100644 packages/schema/tests/zod-validation.test.ts create mode 100644 packages/schema/tsconfig.json create mode 100644 packages/streaming/package.json create mode 100644 packages/streaming/src/bson/buffer-array.ts create mode 100644 packages/streaming/src/bson/constants.ts create mode 100644 packages/streaming/src/bson/decoder.ts create mode 100644 packages/streaming/src/bson/encoder.ts create mode 100644 packages/streaming/src/bson/header.ts create mode 100644 packages/streaming/src/bson/index.ts create mode 100644 packages/streaming/src/core/backpressure.ts create mode 100644 packages/streaming/src/core/cross-stream.ts create mode 100644 packages/streaming/src/core/index.ts create mode 100644 packages/streaming/src/core/node-utils.ts create mode 100644 packages/streaming/src/core/transformers.ts create mode 100644 packages/streaming/src/core/utils.ts create mode 100644 packages/streaming/src/index.ts create mode 100644 packages/streaming/src/middleware/index.ts create mode 100644 packages/streaming/src/middleware/streamed-body-parser-v1.ts create mode 100644 packages/streaming/src/middleware/streamed-body-parser-v2.ts create mode 100644 packages/streaming/src/web.ts create mode 100644 packages/streaming/tests/__snapshots__/bson-transform.test.ts.snap create mode 100644 packages/streaming/tests/__snapshots__/common.test.ts.snap create mode 100644 packages/streaming/tests/bson-transform.test.ts create mode 100644 packages/streaming/tests/common.test.ts create mode 100644 packages/streaming/tests/header.test.ts create mode 100644 packages/streaming/tsconfig.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 tsconfig.base.json create mode 100644 tsconfig.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6dedf36 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true \ No newline at end of file diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..cca759a --- /dev/null +++ b/.envrc @@ -0,0 +1,3 @@ +layout node +use node + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..81da146 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +.idea/ +.vscode +.DS_Store +.swc + +node_modules +coverage +dist + +yarn-error.log +.pnpm-debug.log + +tsconfig.tsbuildinfo diff --git a/.ncurc.json b/.ncurc.json new file mode 100644 index 0000000..18e4722 --- /dev/null +++ b/.ncurc.json @@ -0,0 +1,3 @@ +{ + "reject": ["/@opentelemetry.*/", "chalk", "node-fetch", "@types/node-fetch"] +} diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..e69de29 diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..e551598 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v20.15 diff --git a/package.json b/package.json new file mode 100644 index 0000000..53b5057 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "micro", + "private": "true", + "packageManager": "pnpm@10.14.0", + "scripts": { + "preinstall": "npx only-allow pnpm", + "format": "prettier --write \"**/*.{ts,tsx,js,jsx}\"", + "release": "pnpm build && pnpm changeset publish", + "build": "./node_modules/.bin/tsc --build", + "test": "pnpm run -r test", + "ncu": "ncu -u && pnpm recursive exec -- ncu -u" + }, + "devDependencies": { + "@changesets/cli": "^2.29.5", + "npm-check-updates": "^18.0.2", + "prettier": "3.6.2", + "typescript": "^5.9.2" + } +} \ No newline at end of file diff --git a/packages/codecs/package.json b/packages/codecs/package.json new file mode 100644 index 0000000..17dde0c --- /dev/null +++ b/packages/codecs/package.json @@ -0,0 +1,19 @@ +{ + "name": "@journeyapps-labs/micro-codecs", + "main": "./dist/index", + "typings": "./dist/index", + "version": "1.0.0", + "repository": "https://github.com/journeyapps-labs/journey-micro", + "files": [ + "dist/**" + ], + "scripts": { + "test": "vitest" + }, + "dependencies": { + "@types/node": "^20.17.6", + "bson": "^6.7.0", + "ts-codec": "^1.3.0", + "vitest": "^3.2.4" + } +} diff --git a/packages/codecs/src/codecs.ts b/packages/codecs/src/codecs.ts new file mode 100644 index 0000000..fc38fa3 --- /dev/null +++ b/packages/codecs/src/codecs.ts @@ -0,0 +1,136 @@ +import * as t from "ts-codec"; +import * as bson from "bson"; + +export const buffer = t.codec( + "Buffer", + (buffer) => { + if (!Buffer.isBuffer(buffer)) { + throw new t.TransformError([`Expected buffer but got ${typeof buffer}`]); + } + return buffer.toString("base64"); + }, + (buffer) => Buffer.from(buffer, "base64"), +); + +export const date = t.codec( + "Date", + (date) => { + if (!(date instanceof Date)) { + throw new t.TransformError([`Expected Date but got ${typeof date}`]); + } + return date.toISOString(); + }, + (date) => { + const parsed = new Date(date); + if (isNaN(parsed.getTime())) { + throw new t.TransformError([`Invalid date`]); + } + return parsed; + }, +); + +const assertObjectId = (value: any) => { + if (!bson.ObjectId.isValid(value)) { + throw new t.TransformError([ + `Expected an ObjectId but got ${typeof value}`, + ]); + } +}; +export const ObjectId = t.codec( + "ObjectId", + (id) => { + assertObjectId(id); + return id.toHexString(); + }, + (id) => { + assertObjectId(id); + return new bson.ObjectId(id); + }, +); + +const assertObjectWithField = (field: string, data: any) => { + if (typeof data !== "object") { + throw new t.TransformError([`Expected an object but got ${typeof data}`]); + } + if (!(field in data)) { + throw new t.TransformError([`Expected ${field} to be a member of object`]); + } +}; +export const ResourceId = t.codec<{ _id: bson.ObjectId }, { id: string }>( + "ResourceId", + (data) => { + assertObjectWithField("_id", data); + return { + id: ObjectId.encode(data._id), + }; + }, + (data) => { + assertObjectWithField("id", data); + return { + _id: ObjectId.decode(data.id), + }; + }, +); + +export const Timestamps = t.object({ + created_at: date, + updated_at: date, +}); + +export const Resource = ResourceId.and(Timestamps); + +export const QueryFilter = t.object({ + exists: t.boolean, +}); + +export const makeQueryFilter = (type: t.AnyCodec) => { + return type.or(t.array(type)).or(QueryFilter).optional(); +}; + +export const FilterProperties = ( + type: T, +): t.Codec< + { + [K in keyof t.Encoded]?: + | t.Ix[K] + | t.Ix[K][] + | t.Ix; + }, + { + [K in keyof t.Encoded]?: + | t.Ox[K] + | t.Ox[K][] + | t.Ox; + }, + "FilterProperties" +> => { + let codecs = new Map(); + + const addCodecs = (codec: t.ObjectCodec) => { + if (codec.props?.shape) { + Object.keys(codec.props.shape).forEach((k) => { + codecs.set(k, codec.props.shape[k]); + }); + } + }; + + if (type._tag === t.CodecType.Object) { + addCodecs(type); + } else if (type._tag === t.CodecType.Intersection) { + type.props.codecs.forEach((c: t.AnyCodec) => { + addCodecs(c); + }); + } + + t.object({ + test: t.string, + }); + + // @ts-ignore + return t.object( + Array.from(codecs.keys()).reduce((prev: any, cur) => { + prev[cur] = makeQueryFilter(codecs.get(cur)!); + return prev; + }, {}), + ); +}; diff --git a/packages/codecs/src/index.ts b/packages/codecs/src/index.ts new file mode 100644 index 0000000..b003005 --- /dev/null +++ b/packages/codecs/src/index.ts @@ -0,0 +1,2 @@ +export * from "./parsers"; +export * from "./codecs"; diff --git a/packages/codecs/src/parsers.ts b/packages/codecs/src/parsers.ts new file mode 100644 index 0000000..e3b9968 --- /dev/null +++ b/packages/codecs/src/parsers.ts @@ -0,0 +1,77 @@ +import * as codecs from "./codecs"; +import * as t from "ts-codec"; + +export const ObjectIdParser = t.createParser( + codecs.ObjectId._tag, + (_, { target }) => { + switch (target) { + case t.TransformTarget.Encoded: { + return { type: "string" }; + } + case t.TransformTarget.Decoded: { + return { bsonType: "ObjectId" }; + } + } + }, +); + +export const ResourceIdParser = t.createParser( + codecs.ResourceId._tag, + (_, { target }) => { + switch (target) { + case t.TransformTarget.Encoded: { + return { + type: "object", + properties: { + id: { type: "string" }, + }, + required: ["id"], + }; + } + case t.TransformTarget.Decoded: { + return { + type: "object", + properties: { + _id: { bsonType: "ObjectId" }, + }, + required: ["_id"], + }; + } + } + }, +); + +export const DateParser = t.createParser( + codecs.date._tag, + (_, { target }) => { + switch (target) { + case t.TransformTarget.Encoded: { + return { type: "string" }; + } + case t.TransformTarget.Decoded: { + return { nodeType: "date" }; + } + } + }, +); + +export const BufferParser = t.createParser( + codecs.buffer._tag, + (_, { target }) => { + switch (target) { + case t.TransformTarget.Encoded: { + return { type: "string" }; + } + case t.TransformTarget.Decoded: { + return { nodeType: "buffer" }; + } + } + }, +); + +export const parsers = [ + ObjectIdParser, + ResourceIdParser, + DateParser, + BufferParser, +]; diff --git a/packages/codecs/tests/__snapshots__/parsers.test.ts.snap b/packages/codecs/tests/__snapshots__/parsers.test.ts.snap new file mode 100644 index 0000000..ae79196 --- /dev/null +++ b/packages/codecs/tests/__snapshots__/parsers.test.ts.snap @@ -0,0 +1,190 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`parsers > should correctly generate Filterable schema from Object 1`] = ` +{ + "decoded": { + "additionalProperties": false, + "definitions": {}, + "properties": { + "test1": { + "anyOf": [ + { + "type": "string", + }, + { + "items": { + "type": "string", + }, + "type": "array", + }, + { + "additionalProperties": false, + "properties": { + "exists": { + "type": "boolean", + }, + }, + "required": [ + "exists", + ], + "type": "object", + }, + ], + }, + "test2": { + "anyOf": [ + { + "type": "number", + }, + { + "items": { + "type": "number", + }, + "type": "array", + }, + { + "additionalProperties": false, + "properties": { + "exists": { + "type": "boolean", + }, + }, + "required": [ + "exists", + ], + "type": "object", + }, + ], + }, + }, + "required": [], + "type": "object", + }, + "encoded": { + "additionalProperties": false, + "definitions": {}, + "properties": { + "test1": { + "anyOf": [ + { + "type": "string", + }, + { + "items": { + "type": "string", + }, + "type": "array", + }, + { + "additionalProperties": false, + "properties": { + "exists": { + "type": "boolean", + }, + }, + "required": [ + "exists", + ], + "type": "object", + }, + ], + }, + "test2": { + "anyOf": [ + { + "type": "number", + }, + { + "items": { + "type": "number", + }, + "type": "array", + }, + { + "additionalProperties": false, + "properties": { + "exists": { + "type": "boolean", + }, + }, + "required": [ + "exists", + ], + "type": "object", + }, + ], + }, + }, + "required": [], + "type": "object", + }, +} +`; + +exports[`parsers > should correctly generate ObjectId schemas 1`] = ` +{ + "decoded": { + "bsonType": "ObjectId", + "definitions": {}, + }, + "encoded": { + "definitions": {}, + "type": "string", + }, +} +`; + +exports[`parsers > should correctly generate ResourceId schemas 1`] = ` +{ + "decoded": { + "definitions": {}, + "properties": { + "_id": { + "bsonType": "ObjectId", + }, + }, + "required": [ + "_id", + ], + "type": "object", + }, + "encoded": { + "definitions": {}, + "properties": { + "id": { + "type": "string", + }, + }, + "required": [ + "id", + ], + "type": "object", + }, +} +`; + +exports[`parsers > should correctly generate buffer schemas 1`] = ` +{ + "decoded": { + "definitions": {}, + "nodeType": "buffer", + }, + "encoded": { + "definitions": {}, + "type": "string", + }, +} +`; + +exports[`parsers > should correctly generate date schemas 1`] = ` +{ + "decoded": { + "definitions": {}, + "nodeType": "date", + }, + "encoded": { + "definitions": {}, + "type": "string", + }, +} +`; diff --git a/packages/codecs/tests/parsers.test.ts b/packages/codecs/tests/parsers.test.ts new file mode 100644 index 0000000..50032c6 --- /dev/null +++ b/packages/codecs/tests/parsers.test.ts @@ -0,0 +1,54 @@ +import { describe, it, expect } from "vitest"; + +import * as codecs from "../src"; +import * as t from "ts-codec"; + +const generate = (codec: t.AnyCodec) => { + const encoded = t.generateJSONSchema(codec, { + parsers: codecs.parsers, + target: t.TransformTarget.Encoded, + }); + const decoded = t.generateJSONSchema(codec, { + parsers: codecs.parsers, + target: t.TransformTarget.Decoded, + }); + + return { encoded, decoded }; +}; + +describe("parsers", () => { + it("should correctly generate date schemas", () => { + expect(generate(codecs.date)).toMatchSnapshot(); + }); + + it("should correctly generate buffer schemas", () => { + expect(generate(codecs.buffer)).toMatchSnapshot(); + }); + + it("should correctly generate ObjectId schemas", () => { + expect(generate(codecs.ObjectId)).toMatchSnapshot(); + }); + + it("should correctly generate ResourceId schemas", () => { + expect(generate(codecs.ResourceId)).toMatchSnapshot(); + }); + + it("should correctly generate Filterable schema from Object", () => { + const filterObject1 = t.object({ + test1: t.string, + }); + + const filterObject2 = t.object({ + test2: t.number, + }); + + const filterObject3 = t.object({ + test1: t.string, + }); + + const filterObject4 = filterObject1.and(filterObject2).and(filterObject3); + + const filterableObject = codecs.FilterProperties(filterObject4); + expect(generate(filterableObject)).toMatchSnapshot(); + }); +}); diff --git a/packages/codecs/tsconfig.json b/packages/codecs/tsconfig.json new file mode 100644 index 0000000..5dcb608 --- /dev/null +++ b/packages/codecs/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base", + "compilerOptions": { + "outDir": "dist", + "declarationDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/packages/errors/package.json b/packages/errors/package.json new file mode 100644 index 0000000..349efd6 --- /dev/null +++ b/packages/errors/package.json @@ -0,0 +1,17 @@ +{ + "name": "@journeyapps-labs/micro-errors", + "main": "./dist/index", + "typings": "./dist/index", + "version": "1.0.0", + "repository": "https://github.com/journeyapps-labs/journey-micro", + "files": [ + "dist/**" + ], + "scripts": { + "test": "vitest" + }, + "devDependencies": { + "@types/node": "^20.17.6", + "vitest": "^3.2.4" + } +} diff --git a/packages/errors/src/errors.ts b/packages/errors/src/errors.ts new file mode 100644 index 0000000..0b5207b --- /dev/null +++ b/packages/errors/src/errors.ts @@ -0,0 +1,174 @@ +/** + * This error class is designed to give consumers of Journey Micro + * a consistent way of "throwing" errors. Specifically, these errors + * will give clients to Journey Micro implementations two things: + * + * 1. An consistent, static error code by which to easily classify errors + * 2. An error message intended for humans + * + * Errors will usually assume that there is some client side error and default to 400 for + * a rest-like response. This can be changed however to more accurately, in restful terms, + * indicate what went wrong. + * + */ + +export enum ErrorSeverity { + INFO = "info", + WARNING = "warning", + ERROR = "error", +} + +export type ErrorData = { + name?: string; + /** + * enum such as `VALIDATION_ERROR` etc.. + */ + code: string; + /** + * Human-readable description of the error. This is typically shown to the user in various UI + */ + description: string; + severity?: ErrorSeverity; + details?: string; + /** + * Status code (400 / 500 etc..) + */ + status?: number; + /** + * Multi-line stack trace + */ + stack?: string; + + origin?: string; + + trace_id?: string; + /** + * If this error should be reported to tools such as Sentry. + * In some cases we may deliberately throw validation errors to show information to end users which are picked + * up by our routes, and then we don't want these reported (in which case set this to false) + * + * @default true + */ + report?: boolean; +}; + +export class JourneyError extends Error { + is_journey_error = true; + + errorData: ErrorData; + + constructor(data: ErrorData) { + super(`[${data.code}] ${data.description}\n ${data.details}`); + + this.errorData = data; + if (data.stack) { + this.stack = data.stack; + } + + this.name = data.name || this.constructor.name; + this.errorData.name = this.name; + } + + get shouldReport() { + return this.errorData.report ?? true; + } + + toString() { + return this.stack; + } + + toJSON(): ErrorData { + if (process.env.NODE_ENV !== "production") { + return this.errorData; + } + return { + name: this.errorData.name, + code: this.errorData.code, + status: this.errorData.status, + description: this.errorData.description, + details: this.errorData.details, + trace_id: this.errorData.trace_id, + severity: this.errorData.severity, + origin: this.errorData.origin, + }; + } + + setTraceId(id: string) { + this.errorData.trace_id = id; + } +} + +export class ValidationError extends JourneyError { + static CODE = "VALIDATION_ERROR"; + constructor(errors: any) { + super({ + code: ValidationError.CODE, + status: 400, + description: "Validation failed", + details: JSON.stringify(errors), + report: false, + }); + } +} + +export class AuthorizationError extends JourneyError { + static CODE = "AUTHORIZATION"; + constructor(errors: any) { + super({ + code: AuthorizationError.CODE, + status: 401, + description: "Authorization failed", + details: errors, + }); + } +} + +export class InternalServerError extends JourneyError { + static CODE = "INTERNAL_SERVER_ERROR"; + constructor(err: Error) { + super({ + code: InternalServerError.CODE, + severity: ErrorSeverity.ERROR, + status: 500, + description: "Something went wrong", + details: err.message, + stack: process.env.NODE_ENV !== "production" ? err.stack : undefined, + }); + } +} + +export class ResourceNotFound extends JourneyError { + static CODE = "RESOURCE_NOT_FOUND"; + + /** + * @deprecated Use the (resource, id) constructor instead. + * @param id + */ + constructor(id: string); + constructor(resource: string, id: string); + + constructor(resource: string, id?: string) { + const combinedId = id ? `${resource}/${id}` : resource; + super({ + code: ResourceNotFound.CODE, + status: 404, + description: "The requested resource does not exist on this server", + details: `The resource ${combinedId} does not exist on this server`, + severity: ErrorSeverity.INFO, + }); + } +} + +export class ResourceConflict extends JourneyError { + static CODE = "RESOURCE_CONFLICT"; + + constructor(details: string) { + super({ + code: ResourceConflict.CODE, + status: 409, + description: "The specified resource already exists on this server", + details: details, + severity: ErrorSeverity.INFO, + }); + } +} diff --git a/packages/errors/src/index.ts b/packages/errors/src/index.ts new file mode 100644 index 0000000..b330f3d --- /dev/null +++ b/packages/errors/src/index.ts @@ -0,0 +1,2 @@ +export * from "./errors"; +export * from "./utils"; diff --git a/packages/errors/src/utils.ts b/packages/errors/src/utils.ts new file mode 100644 index 0000000..55e8ebd --- /dev/null +++ b/packages/errors/src/utils.ts @@ -0,0 +1,20 @@ +import { ErrorData, JourneyError } from "./errors"; + +export const isJourneyError = (err: any): err is JourneyError => { + const matches = err instanceof JourneyError || err.is_journey_error; + return !!matches; +}; + +export const getErrorData = (err: Error | any): ErrorData | undefined => { + if (!isJourneyError(err)) { + return; + } + return err.toJSON(); +}; + +export const matchesErrorCode = (err: Error | any, code: string) => { + if (isJourneyError(err)) { + return err.errorData.code === code; + } + return false; +}; diff --git a/packages/errors/tests/__snapshots__/errors.test.ts.snap b/packages/errors/tests/__snapshots__/errors.test.ts.snap new file mode 100644 index 0000000..93bda28 --- /dev/null +++ b/packages/errors/tests/__snapshots__/errors.test.ts.snap @@ -0,0 +1,10 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`errors > utilities should properly match a journey error 1`] = ` +{ + "code": "CUSTOM_JOURNEY_ERROR", + "description": "This is a custom error", + "details": "this is some more detailed information", + "name": "CustomJourneyError", +} +`; diff --git a/packages/errors/tests/errors.test.ts b/packages/errors/tests/errors.test.ts new file mode 100644 index 0000000..aaae3de --- /dev/null +++ b/packages/errors/tests/errors.test.ts @@ -0,0 +1,53 @@ +import { describe, test, expect } from "vitest"; + +import * as micro_errors from "../src"; + +class CustomJourneyError extends micro_errors.JourneyError { + constructor() { + super({ + code: "CUSTOM_JOURNEY_ERROR", + description: "This is a custom error", + details: "this is some more detailed information", + }); + } +} + +describe("errors", () => { + test("it should respond to instanceof checks", () => { + const error = new CustomJourneyError(); + + expect(error instanceof Error).toBe(true); + expect(error instanceof micro_errors.JourneyError).toBe(true); + expect(error.name).toBe("CustomJourneyError"); + }); + + test("it should serialize properly", () => { + const error = new CustomJourneyError(); + + // The error stack will contain host specific path information. We only care about the header + // anyway and that the stack is shown - indicated by the initial `at` text + const initial = `CustomJourneyError: [CUSTOM_JOURNEY_ERROR] This is a custom error + this is some more detailed information + at`; + + expect(`${error}`.startsWith(initial)).toBe(true); + }); + + test("utilities should properly match a journey error", () => { + const standard_error = new Error("non-journey error"); + const error = new CustomJourneyError(); + + expect(micro_errors.isJourneyError(standard_error)).toBe(false); + expect(micro_errors.isJourneyError(error)).toBe(true); + + expect(micro_errors.matchesErrorCode(error, "CUSTOM_JOURNEY_ERROR")).toBe( + true, + ); + expect( + micro_errors.matchesErrorCode(standard_error, "CUSTOM_JOURNEY_ERROR"), + ).toBe(false); + + expect(micro_errors.getErrorData(error)).toMatchSnapshot(); + expect(micro_errors.getErrorData(standard_error)).toBe(undefined); + }); +}); diff --git a/packages/errors/tsconfig.json b/packages/errors/tsconfig.json new file mode 100644 index 0000000..c546971 --- /dev/null +++ b/packages/errors/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base", + "compilerOptions": { + "outDir": "dist", + "declarationDir": "dist", + "rootDir": "src" + }, + "include": ["./src/**/*.ts"] +} diff --git a/packages/schema/package.json b/packages/schema/package.json new file mode 100644 index 0000000..6e8cf35 --- /dev/null +++ b/packages/schema/package.json @@ -0,0 +1,23 @@ +{ + "name": "@journeyapps-labs/micro-schema", + "main": "./dist/index", + "typings": "./dist/index", + "version": "1.0.0", + "repository": "https://github.com/journeyapps-labs/journey-micro", + "scripts": { + "test": "vitest" + }, + "files": [ + "dist/**" + ], + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.1.0", + "@journeyapps-labs/micro-codecs": "workspace:^", + "@journeyapps-labs/micro-errors": "workspace:^", + "ajv": "^8.11.2", + "better-ajv-errors": "^1.2.0", + "ts-codec": "^1.3.0", + "vitest": "^3.2.4", + "zod": "^3.19.1" + } +} diff --git a/packages/schema/src/better-ajv-errors.d.ts b/packages/schema/src/better-ajv-errors.d.ts new file mode 100644 index 0000000..1f64d8e --- /dev/null +++ b/packages/schema/src/better-ajv-errors.d.ts @@ -0,0 +1,26 @@ +declare module "better-ajv-errors" { + import type { ErrorObject } from "ajv"; + + export interface IOutputError { + start: { line: number; column: number; offset: number }; + // Optional for required + end?: { line: number; column: number; offset: number }; + error: string; + suggestion?: string; + } + + export interface IInputOptions { + format?: "cli" | "js"; + indent?: number | null; + + /** Raw JSON used when highlighting error location */ + json?: string | null; + } + + export default function ( + schema: S, + data: T, + errors: Array, + options?: Options, + ): Options extends { format: "js" } ? Array : string; +} diff --git a/packages/schema/src/definitions.ts b/packages/schema/src/definitions.ts new file mode 100644 index 0000000..03ce3a7 --- /dev/null +++ b/packages/schema/src/definitions.ts @@ -0,0 +1,22 @@ +export type JSONSchema = { + definitions?: Record; + [key: string]: any; +}; + +export type IValidationRight = { + valid: true; +}; + +export type ValidationLeft = { + valid: false; + errors: T; +}; + +export type ValidationResponse = + | ValidationLeft + | IValidationRight; + +export type MicroValidator = { + validate: (data: T) => ValidationResponse; + toJSONSchema?: () => JSONSchema; +}; diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts new file mode 100644 index 0000000..b2d817d --- /dev/null +++ b/packages/schema/src/index.ts @@ -0,0 +1,6 @@ +export * from "./json-schema/keywords"; +export * from "./json-schema/parser"; + +export * from "./definitions"; +export * from "./validators"; +export * from "./utils"; diff --git a/packages/schema/src/json-schema/keywords.ts b/packages/schema/src/json-schema/keywords.ts new file mode 100644 index 0000000..1f8011d --- /dev/null +++ b/packages/schema/src/json-schema/keywords.ts @@ -0,0 +1,27 @@ +import * as ajv from "ajv"; + +export const BufferNodeType: ajv.KeywordDefinition = { + keyword: "nodeType", + metaSchema: { + type: "string", + enum: ["buffer", "date"], + }, + error: { + message: ({ schemaCode }) => { + return ajv.str`should be a ${schemaCode}`; + }, + }, + code(context) { + switch (context.schema) { + case "buffer": { + return context.fail(ajv._`!Buffer.isBuffer(${context.data})`); + } + case "date": { + return context.fail(ajv._`!(${context.data} instanceof Date)`); + } + default: { + context.fail(ajv._`true`); + } + } + }, +}; diff --git a/packages/schema/src/json-schema/parser.ts b/packages/schema/src/json-schema/parser.ts new file mode 100644 index 0000000..5906870 --- /dev/null +++ b/packages/schema/src/json-schema/parser.ts @@ -0,0 +1,134 @@ +import * as schema_validator from "../validators/schema-validator"; +import * as defs from "../definitions"; + +/** + * Recursively walk a given schema resolving a list of refs that are actively used in some way by the + * root schema. This information can then later be used to prune unused definitions. + * + * This only works for top-level references to 'definitions' as this is intended to be used in + * conjunction with tools that generate schemas in deterministic ways. For a more general + * implementation one should make use of `$RefParser`. + * + * We don't use this here as it resolves to a Promise, which we want to avoid for this tool + */ +export const findUsedRefs = ( + schema: any, + definitions = schema.definitions, + cache: string[] = [], +): string[] => { + if (Array.isArray(schema)) { + return schema + .map((subschema) => { + return findUsedRefs(subschema, definitions, cache); + }) + .flat(); + } + + if (typeof schema === "object") { + return Object.keys(schema).reduce((used: string[], key) => { + const value = schema[key]; + if (key === "$ref") { + if (cache.includes(value)) { + return used; + } + cache.push(value); + const subschema = definitions[value.replace("#/definitions/", "")]; + used.push(value, ...findUsedRefs(subschema, definitions, cache)); + return used; + } + if (key === "definitions") { + return used; + } + return used.concat(findUsedRefs(value, definitions, cache)); + }, []); + } + + return []; +}; + +/** + * Prune a given subschema definitions map by comparing keys against a given collection of + * definition keys that are referenced in some way, either directly or indirectly, by the + * root schema + */ +export const pruneDefinitions = ( + definitions: Record, + refs: string[], +) => { + return Object.keys(definitions).reduce((pruned: Record, key) => { + if (refs.includes(`#/definitions/${key}`)) { + pruned[key] = definitions[key]; + } + return pruned; + }, {}); +}; + +export type CompilerFunction = () => defs.JSONSchema; +export type ValidatorFunction = ( + params?: schema_validator.CreateSchemaValidatorParams, +) => schema_validator.SchemaValidator; + +export type Compilers = { + compile: CompilerFunction; + validator: ValidatorFunction; +}; +export type WithCompilers = Compilers & { + [P in keyof T]: T[P] extends Record ? WithCompilers : T[P]; +}; + +/** + * Given a JSON Schema containing a `definitions` entry, return a Proxy representation of the same + * schema which responds to `compile` and `validator` arbitrarily deep. + * + * Calling compile on a sub-schema will 'inject' the root schema `definitions` mapping and remove + * the Proxy wrapping. + * + * Calling `validator` on a sub-schema will `compile` and then create a SchemaValidator from the + * resulting schema + */ +export const parseJSONSchema = ( + schema: T, + definitions = schema.definitions, +): WithCompilers => { + return new Proxy(schema, { + get(target: any, prop) { + const compile: CompilerFunction = () => { + const schema = { + definitions: definitions, + ...target, + }; + + if (!schema.definitions) { + return schema; + } + + const used = findUsedRefs(schema); + return { + ...schema, + definitions: pruneDefinitions(schema.definitions, used), + }; + }; + const validator: ValidatorFunction = (options) => { + return schema_validator.createSchemaValidator(compile(), options); + }; + + if (prop === "compile") { + return compile; + } + if (prop === "validator") { + return validator; + } + + const subschema = target[prop]; + + if (Array.isArray(subschema)) { + return subschema; + } + if (typeof subschema !== "object") { + return subschema; + } + + return parseJSONSchema(subschema, definitions); + }, + }); +}; diff --git a/packages/schema/src/utils.ts b/packages/schema/src/utils.ts new file mode 100644 index 0000000..1882b36 --- /dev/null +++ b/packages/schema/src/utils.ts @@ -0,0 +1,51 @@ +import * as micro_errors from "@journeyapps-labs/micro-errors"; +import * as defs from "./definitions"; + +export type Schema = { + additionalProperties?: boolean | Schema; + [key: string]: any; +}; + +/** + * Utility function to strip out `additionalProperties` fields from a given JSON-Schema. This can be used + * to make a schema less strict which may be necessary for certain use-cases + */ +export const allowAdditionalProperties = (schema: T): T => { + return Object.keys(schema).reduce((next_schema: any, key) => { + const value = schema[key]; + + if (key === "additionalProperties" && typeof value === "boolean") { + return next_schema; + } + + if (Array.isArray(value)) { + next_schema[key] = value.map((value) => { + if (typeof value !== "object") { + return value; + } + return allowAdditionalProperties(value); + }); + } else if (typeof value === "object") { + next_schema[key] = allowAdditionalProperties(value); + } else { + next_schema[key] = value; + } + + return next_schema; + }, {}); +}; + +/** + * A small utility for validating some data using a MicroValidator. Will return the valid data (typed correctly) or throw + * a validation error + */ +export const validateData = ( + event: any, + validator: defs.MicroValidator, +): T => { + const result = validator.validate(event); + if (!result.valid) { + throw new micro_errors.ValidationError(result.errors); + } + return event; +}; diff --git a/packages/schema/src/validators/index.ts b/packages/schema/src/validators/index.ts new file mode 100644 index 0000000..3829af3 --- /dev/null +++ b/packages/schema/src/validators/index.ts @@ -0,0 +1,3 @@ +export * from "./ts-codec-validator"; +export * from "./schema-validator"; +export * from "./zod-validator"; diff --git a/packages/schema/src/validators/schema-validator.ts b/packages/schema/src/validators/schema-validator.ts new file mode 100644 index 0000000..4d00c7c --- /dev/null +++ b/packages/schema/src/validators/schema-validator.ts @@ -0,0 +1,86 @@ +import * as keywords from "../json-schema/keywords"; +import AjvErrorFormatter from "better-ajv-errors"; +import * as defs from "../definitions"; +import * as utils from "../utils"; +import AJV, * as ajv from "ajv"; + +export class SchemaValidatorError extends Error { + constructor(message: string) { + super(message); + + this.name = this.constructor.name; + } +} + +export type SchemaValidator = defs.MicroValidator; + +export type CreateSchemaValidatorParams = { + ajv?: ajv.Options; + + /** + * Allow making the given schema loosely typed to allow accepting additional properties. This + * is useful in certain scenarios such as accepting kafka events that are going through a + * migration and having additional properties set + */ + allowAdditional?: boolean; + + fail_fast?: boolean; +}; + +/** + * Create a validator from a given JSON-Schema schema object. This makes uses of AJV internally + * to compile a validation function + */ +export const createSchemaValidator = ( + schema: defs.JSONSchema, + params: CreateSchemaValidatorParams = {}, +): SchemaValidator => { + try { + const ajv = new AJV({ + allErrors: !(params.fail_fast ?? false), + keywords: [keywords.BufferNodeType], + ...(params.ajv || {}), + }); + + let processed_schema = schema; + if (params.allowAdditional) { + processed_schema = utils.allowAdditionalProperties(processed_schema); + } + + const validator = ajv.compile(processed_schema); + + return { + toJSONSchema: () => { + return schema; + }, + + validate: (data) => { + const valid = validator(data); + + if (!valid) { + const errors = AjvErrorFormatter( + processed_schema, + data, + validator.errors || [], + { + format: "js", + }, + )?.map((error) => error.error); + + return { + valid: false, + errors: errors || [], + }; + } + + return { + valid: true, + }; + }, + }; + } catch (err) { + // Here we re-throw the error because the original error thrown by AJV has a deep stack that + // obfuscates the location of the error in application code + throw new SchemaValidatorError(err.message); + } +}; diff --git a/packages/schema/src/validators/ts-codec-validator.ts b/packages/schema/src/validators/ts-codec-validator.ts new file mode 100644 index 0000000..8003748 --- /dev/null +++ b/packages/schema/src/validators/ts-codec-validator.ts @@ -0,0 +1,34 @@ +import * as codecs from "@journeyapps-labs/micro-codecs"; +import * as schema_validator from "./schema-validator"; +import * as defs from "../definitions"; +import * as t from "ts-codec"; + +export type TsCodecValidator< + C extends t.AnyCodec, + T extends t.TransformTarget = t.TransformTarget.Encoded, +> = T extends t.TransformTarget.Encoded + ? defs.MicroValidator> + : defs.MicroValidator>; + +type ValidatorOptions = Partial< + Omit +> & { + target?: T; +}; + +/** + * Create a validator from a given ts-codec codec + */ +export const createTsCodecValidator = < + C extends t.AnyCodec, + T extends t.TransformTarget = t.TransformTarget.Encoded, +>( + codec: C, + options?: ValidatorOptions, +): TsCodecValidator => { + const schema = t.generateJSONSchema(codec, { + ...(options || {}), + parsers: [...(options?.parsers ?? []), ...codecs.parsers], + }); + return schema_validator.createSchemaValidator(schema); +}; diff --git a/packages/schema/src/validators/zod-validator.ts b/packages/schema/src/validators/zod-validator.ts new file mode 100644 index 0000000..3fb9b54 --- /dev/null +++ b/packages/schema/src/validators/zod-validator.ts @@ -0,0 +1,33 @@ +import * as defs from "../definitions"; +import * as t from "zod"; + +export type ZodValidator> = defs.MicroValidator< + t.infer +> & { + schema: T; +}; + +/** + * Create a validator from a given Zod schema + * https://github.com/colinhacks/zod + */ +export const createZodValidator = >( + schema: T, +): ZodValidator => { + return { + schema: schema, + validate: (data) => { + const result = schema.safeParse(data); + if (!result.success) { + return { + valid: false, + errors: [JSON.stringify(result.error.format())], + }; + } + + return { + valid: true, + }; + }, + }; +}; diff --git a/packages/schema/tests/__snapshots__/parser.test.ts.snap b/packages/schema/tests/__snapshots__/parser.test.ts.snap new file mode 100644 index 0000000..52d9475 --- /dev/null +++ b/packages/schema/tests/__snapshots__/parser.test.ts.snap @@ -0,0 +1,73 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`schema-tools > it should correctly prune unused definitions 1`] = ` +{ + "definitions": { + "b": { + "properties": { + "prop": { + "type": "string", + }, + }, + "required": [ + "prop", + ], + "type": "object", + }, + "b1": { + "properties": { + "prop": { + "$ref": "#/definitions/b", + }, + }, + "required": [ + "prop", + ], + "type": "object", + }, + "c": { + "properties": { + "prop": { + "$ref": "#/definitions/c", + }, + }, + "required": [ + "prop", + ], + "type": "object", + }, + }, + "properties": { + "a": { + "properties": { + "a": { + "$ref": "#/definitions/b1", + }, + "b": { + "enum": [ + "A", + ], + }, + }, + "type": "object", + }, + "b": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "a": { + "$ref": "#/definitions/c", + }, + }, + "required": [ + "a", + ], + "type": "object", + }, + ], + }, + }, + "type": "object", +} +`; diff --git a/packages/schema/tests/__snapshots__/schema-validation.test.ts.snap b/packages/schema/tests/__snapshots__/schema-validation.test.ts.snap new file mode 100644 index 0000000..5aa01b1 --- /dev/null +++ b/packages/schema/tests/__snapshots__/schema-validation.test.ts.snap @@ -0,0 +1,66 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`json-schema-validation > fails validation for json-schema 1`] = ` +{ + "errors": [ + "/name/b must be equal to one of the allowed values: A", + ], + "valid": false, +} +`; + +exports[`json-schema-validation > fails validation for json-schema 2`] = ` +{ + "errors": [ + "/name must have required property 'a'", + "/b: oneOf must match exactly one schema in oneOf", + "/b/a: type must be number", + ], + "valid": false, +} +`; + +exports[`json-schema-validation > fails validation for json-schema due to additional properties 1`] = ` +{ + "errors": [ + "/name Property c is not expected to be here", + ], + "valid": false, +} +`; + +exports[`json-schema-validation > it correctly validates node types 1`] = ` +{ + "errors": [ + "/a: nodeType should be a buffer", + ], + "valid": false, +} +`; + +exports[`json-schema-validation > it should correctly validate subschemas 1`] = ` +{ + "errors": [ + "/b: type must be string", + ], + "valid": false, +} +`; + +exports[`json-schema-validation > passes json-schema validation with additional properties when allowed 1`] = ` +{ + "valid": true, +} +`; + +exports[`json-schema-validation > passes validation for json-schema 1`] = ` +{ + "valid": true, +} +`; + +exports[`json-schema-validation > passes validation with refs 1`] = ` +{ + "valid": true, +} +`; diff --git a/packages/schema/tests/__snapshots__/ts-codec-validation.test.ts.snap b/packages/schema/tests/__snapshots__/ts-codec-validation.test.ts.snap new file mode 100644 index 0000000..9b26288 --- /dev/null +++ b/packages/schema/tests/__snapshots__/ts-codec-validation.test.ts.snap @@ -0,0 +1,17 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`ts-codec validation > fails validation for runtime codec 1`] = ` +{ + "errors": [ + " must have required property 'surname'", + "/name: type must be string", + "/other/b: const must be equal to constant", + "/tuple/0: type must be string", + "/or: type must be number", + "/or: type must be string", + "/or: anyOf must match a schema in anyOf", + "/complex must have required property 'c'", + ], + "valid": false, +} +`; diff --git a/packages/schema/tests/__snapshots__/utils.test.ts.snap b/packages/schema/tests/__snapshots__/utils.test.ts.snap new file mode 100644 index 0000000..8a0c33d --- /dev/null +++ b/packages/schema/tests/__snapshots__/utils.test.ts.snap @@ -0,0 +1,91 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`utils > allow additional properties in a json schema 1`] = ` +{ + "definitions": { + "c": { + "properties": { + "prop": { + "type": "string", + }, + }, + "required": [ + "prop", + ], + "type": "object", + }, + }, + "properties": { + "b": { + "oneOf": [ + { + "properties": { + "a": { + "type": "number", + }, + }, + "required": [ + "a", + ], + "type": "object", + }, + ], + }, + "d": { + "$ref": "#/definitions/c", + }, + "name": { + "properties": { + "a": { + "type": "string", + }, + "b": { + "enum": [ + "A", + ], + }, + }, + "required": [ + "a", + ], + "type": "object", + }, + }, + "required": [ + "name", + "b", + ], + "type": "object", +} +`; + +exports[`utils > it should only modify additionalProperties if it is a boolean 1`] = ` +{ + "definitions": { + "a": { + "properties": { + "prop": { + "type": "string", + }, + }, + "required": [ + "prop", + ], + "type": "object", + }, + }, + "properties": { + "name": { + "additionalProperties": { + "$ref": "#/definitions/a", + }, + "type": "object", + }, + }, + "required": [ + "name", + "b", + ], + "type": "object", +} +`; diff --git a/packages/schema/tests/__snapshots__/zod-validation.test.ts.snap b/packages/schema/tests/__snapshots__/zod-validation.test.ts.snap new file mode 100644 index 0000000..16d8c7b --- /dev/null +++ b/packages/schema/tests/__snapshots__/zod-validation.test.ts.snap @@ -0,0 +1,16 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`zod-validation > fails validation for runtime codec 1`] = ` +{ + "errors": [ + "{"_errors":[],"name":{"_errors":["Expected string, received number"]},"surname":{"_errors":["Required"]},"other":{"_errors":[],"a":{"_errors":["Required"]},"b":{"_errors":["Invalid literal value, expected \\"optional\\""]}}}", + ], + "valid": false, +} +`; + +exports[`zod-validation > passes validation for runtime codec 1`] = ` +{ + "valid": true, +} +`; diff --git a/packages/schema/tests/fixtures/schema.ts b/packages/schema/tests/fixtures/schema.ts new file mode 100644 index 0000000..fc4519e --- /dev/null +++ b/packages/schema/tests/fixtures/schema.ts @@ -0,0 +1,49 @@ +export default { + definitions: { + c: { + type: "object", + properties: { + prop: { + type: "string", + }, + }, + required: ["prop"], + }, + }, + + type: "object", + properties: { + name: { + type: "object", + properties: { + a: { + type: "string", + }, + b: { + enum: ["A"], + }, + }, + required: ["a"], + additionalProperties: false, + }, + b: { + oneOf: [ + { + type: "object", + properties: { + a: { + type: "number", + }, + }, + required: ["a"], + additionalProperties: false, + }, + ], + }, + d: { + $ref: "#/definitions/c", + }, + }, + required: ["name", "b"], + additionalProperties: false, +}; diff --git a/packages/schema/tests/parser.test.ts b/packages/schema/tests/parser.test.ts new file mode 100644 index 0000000..47b3a50 --- /dev/null +++ b/packages/schema/tests/parser.test.ts @@ -0,0 +1,84 @@ +import { describe, test, expect } from "vitest"; + +import * as micro_schema from "../src"; + +describe("schema-tools", () => { + test("it should correctly prune unused definitions", () => { + const schema = micro_schema.parseJSONSchema({ + definitions: { + // unused, should be stripped out + a: { + type: "object", + properties: { + prop: { + type: "string", + }, + }, + required: ["prop"], + }, + + // extended reference, should be included after walking the full schema + b: { + type: "object", + properties: { + prop: { + type: "string", + }, + }, + required: ["prop"], + }, + b1: { + type: "object", + properties: { + prop: { + $ref: "#/definitions/b", + }, + }, + required: ["prop"], + }, + + // circular reference, should not result in the walker getting stuck + c: { + type: "object", + properties: { + prop: { + $ref: "#/definitions/c", + }, + }, + required: ["prop"], + }, + }, + + type: "object", + properties: { + a: { + type: "object", + properties: { + a: { + $ref: "#/definitions/b1", + }, + b: { + enum: ["A"], + }, + }, + }, + b: { + oneOf: [ + { + type: "object", + properties: { + a: { + $ref: "#/definitions/c", + }, + }, + required: ["a"], + additionalProperties: false, + }, + ], + }, + }, + }); + + expect(schema.compile()).toMatchSnapshot(); + }); +}); diff --git a/packages/schema/tests/schema-validation.test.ts b/packages/schema/tests/schema-validation.test.ts new file mode 100644 index 0000000..5c8ff1a --- /dev/null +++ b/packages/schema/tests/schema-validation.test.ts @@ -0,0 +1,180 @@ +import { describe, test, it, expect } from "vitest"; + +import base_schema from "./fixtures/schema"; +import * as micro_schema from "../src"; + +const base_validator = micro_schema.createSchemaValidator(base_schema); + +describe("json-schema-validation", () => { + test("passes validation for json-schema", () => { + const result = base_validator.validate({ + name: { + a: "1", + b: "A", + }, + b: { + a: 2, + }, + }); + + expect(result).toMatchSnapshot(); + }); + + test("fails validation for json-schema", () => { + const result1 = base_validator.validate({ + name: { + a: "1", + b: "B", + }, + b: { + a: 2, + }, + }); + + expect(result1).toMatchSnapshot(); + + const result2 = base_validator.validate({ + name: {}, + b: { + a: "", + }, + }); + + expect(result2).toMatchSnapshot(); + }); + + test("passes validation with refs", () => { + const result = base_validator.validate({ + name: { + a: "1", + b: "A", + }, + b: { + a: 2, + }, + d: { + prop: "abc", + }, + }); + + expect(result).toMatchSnapshot(); + }); + + test("fails validation for json-schema due to additional properties", () => { + const result = base_validator.validate({ + name: { + a: "1", + b: "A", + c: "additional property", + }, + b: { + a: 2, + }, + }); + + expect(result).toMatchSnapshot(); + }); + + test("passes json-schema validation with additional properties when allowed", () => { + const validator = micro_schema.createSchemaValidator(base_schema, { + allowAdditional: true, + }); + + const result = validator.validate({ + name: { + a: "1", + b: "A", + c: "additional property", + }, + b: { + a: 2, + }, + }); + + expect(result).toMatchSnapshot(); + }); + + const subschema = micro_schema.parseJSONSchema({ + definitions: { + a: { + type: "string", + }, + b: { + type: "object", + properties: { + a: { type: "string" }, + b: { $ref: "#/definitions/a" }, + }, + required: ["b"], + }, + }, + }); + + test("it should correctly validate subschemas", () => { + const validator = subschema.definitions.b.validator(); + + const res1 = validator.validate({ + a: "a", + b: 1, + }); + expect(res1).toMatchSnapshot(); + + const res2 = validator.validate({ + a: "a", + b: "b", + }); + + expect(res2.valid).toBe(true); + }); + + test("it correctly validates node types", () => { + const validator = micro_schema.createSchemaValidator({ + type: "object", + properties: { + a: { + nodeType: "buffer", + }, + b: { + nodeType: "date", + }, + }, + required: ["a"], + }); + + const res = validator.validate({ + a: Buffer.from("123"), + b: new Date(), + }); + expect(res.valid).toBe(true); + + const res2 = validator.validate({ + a: "123", + }); + expect(res2.valid).toBe(false); + expect(res2).toMatchSnapshot(); + }); + + it("should fail to compile invalid node types", () => { + try { + micro_schema.createSchemaValidator({ + type: "object", + properties: { + a: { + nodeType: "Buffer", + }, + b: { + nodeType: "Date", + }, + c: { + nodeType: "unknown", + }, + }, + required: ["a", "b", "c"], + }); + } catch (err) { + expect(err).toBeInstanceOf(micro_schema.SchemaValidatorError); + } + + expect.assertions(1); + }); +}); diff --git a/packages/schema/tests/ts-codec-validation.test.ts b/packages/schema/tests/ts-codec-validation.test.ts new file mode 100644 index 0000000..6315dde --- /dev/null +++ b/packages/schema/tests/ts-codec-validation.test.ts @@ -0,0 +1,103 @@ +import { describe, test, expect } from "vitest"; + +import * as micro_schema from "../src"; +import * as t from "ts-codec"; + +describe("ts-codec validation", () => { + enum Values { + A = "A", + B = "B", + } + + const codec = t.object({ + name: t.string, + surname: t.string, + other: t.object({ + a: t.array(t.string), + b: t.literal("optional").optional(), + }), + tuple: t.tuple([t.string, t.number]), + or: t.number.or(t.string), + enum: t.Enum(Values), + + complex: t + .object({ + a: t.string, + }) + .and( + t.object({ + b: t.number, + }), + ) + .and( + t.object({ + c: t + .object({ + a: t.string, + }) + .and( + t + .object({ + b: t.boolean, + }) + .or( + t.object({ + c: t.number, + }), + ), + ), + }), + ), + }); + + test("passes validation for codec", () => { + const validator = micro_schema.createTsCodecValidator(codec); + + const result = validator.validate({ + name: "a", + surname: "b", + other: { + a: ["nice"], + b: "optional", + }, + tuple: ["string", 1], + or: 1, + enum: Values.A, + + complex: { + a: "", + b: 1, + c: { + a: "", + b: true, + }, + }, + }); + + expect(result.valid).toBe(true); + }); + + test("fails validation for runtime codec", () => { + const validator = micro_schema.createTsCodecValidator(codec); + + const result = validator.validate({ + // @ts-ignore + name: 1, + other: { + a: ["nice"], + // @ts-ignore + b: "op", + }, + // @ts-ignore + tuple: [1, 1], + // @ts-ignore + enum: "c", + // @ts-ignore + or: [], + // @ts-ignore + complex: {}, + }); + + expect(result).toMatchSnapshot(); + }); +}); diff --git a/packages/schema/tests/utils.test.ts b/packages/schema/tests/utils.test.ts new file mode 100644 index 0000000..4f5ba78 --- /dev/null +++ b/packages/schema/tests/utils.test.ts @@ -0,0 +1,41 @@ +import { describe, test, expect } from "vitest"; + +import * as utils from "../src/utils"; +import schema from "./fixtures/schema"; + +describe("utils", () => { + test("allow additional properties in a json schema", () => { + const cleaned_schema = utils.allowAdditionalProperties(schema); + expect(cleaned_schema).toMatchSnapshot(); + }); + + test("it should only modify additionalProperties if it is a boolean", () => { + const cleaned_schema = utils.allowAdditionalProperties({ + definitions: { + a: { + type: "object", + properties: { + prop: { + type: "string", + }, + }, + additionalProperties: false, + required: ["prop"], + }, + }, + + type: "object", + properties: { + name: { + type: "object", + additionalProperties: { + $ref: "#/definitions/a", + }, + }, + }, + required: ["name", "b"], + additionalProperties: false, + }); + expect(cleaned_schema).toMatchSnapshot(); + }); +}); diff --git a/packages/schema/tests/zod-validation.test.ts b/packages/schema/tests/zod-validation.test.ts new file mode 100644 index 0000000..97664a4 --- /dev/null +++ b/packages/schema/tests/zod-validation.test.ts @@ -0,0 +1,45 @@ +import { describe, test, expect } from "vitest"; + +import * as micro_schema from "../src"; +import * as t from "zod"; + +describe("zod-validation", () => { + const schema = t.object({ + name: t.string(), + surname: t.string(), + other: t.object({ + a: t.array(t.string()), + b: t.literal("optional").optional(), + }), + }); + + test("passes validation for runtime codec", () => { + const validator = micro_schema.createZodValidator(schema); + + const result = validator.validate({ + name: "a", + surname: "b", + other: { + a: ["nice"], + b: "optional", + }, + }); + + expect(result).toMatchSnapshot(); + }); + + test("fails validation for runtime codec", () => { + const validator = micro_schema.createZodValidator(schema); + + const result = validator.validate({ + // @ts-ignore + name: 1, + other: { + // @ts-ignore + b: "op", + }, + }); + + expect(result).toMatchSnapshot(); + }); +}); diff --git a/packages/schema/tsconfig.json b/packages/schema/tsconfig.json new file mode 100644 index 0000000..2fbe8b2 --- /dev/null +++ b/packages/schema/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.base", + "compilerOptions": { + "outDir": "dist", + "declarationDir": "dist", + "rootDir": "src", + "resolveJsonModule": true + }, + "include": [ + "./src/**/*.ts", + "./more/schema/**/*.json" + ], + "references": [ + { + "path": "../codecs" + }, + { + "path": "../errors" + } + ] +} diff --git a/packages/streaming/package.json b/packages/streaming/package.json new file mode 100644 index 0000000..bf2db31 --- /dev/null +++ b/packages/streaming/package.json @@ -0,0 +1,29 @@ +{ + "name": "@journeyapps-labs/micro-streaming", + "main": "./dist/index", + "browser": "./dist/web", + "typings": "./dist/index", + "version": "1.0.0", + "repository": "https://github.com/journeyapps-labs/journey-micro", + "files": [ + "dist/**" + ], + "exports": { + "node": "./dist/index.js", + "default": "./dist/web.js" + }, + "scripts": { + "test": "vitest" + }, + "dependencies": { + "@journeyapps-labs/micro-errors": "workspace:^", + "@journeyapps-labs/micro-schema": "workspace:^", + "@types/express": "^4.17.14", + "bson": "^6.7.0" + }, + "devDependencies": { + "@types/lodash": "^4.14.191", + "lodash": "^4.17.21", + "vitest": "^3.2.4" + } +} diff --git a/packages/streaming/src/bson/buffer-array.ts b/packages/streaming/src/bson/buffer-array.ts new file mode 100644 index 0000000..d200b6d --- /dev/null +++ b/packages/streaming/src/bson/buffer-array.ts @@ -0,0 +1,91 @@ +/** + * Read a given `size` number of bytes from a give array of `chunks` as a Buffer. The returned Buffer could be + * larger than the requested size. If the chunks array contains less than the requested size then null is + * returned + */ +export const readBufferFromChunks = (chunks: Buffer[], size: number) => { + let batch = []; + let current_size = 0; + for (const chunk of chunks) { + batch.push(chunk); + current_size += chunk.length; + if (current_size >= size) { + return { + buffer: Buffer.concat(batch), + chunks_read: batch.length, + }; + } + } + return null; +}; + +/** + * Read exactly `size` bytes from a given array of `chunks`, modifying the passed array to remove what + * was read. + * + * If more than `size` is read from the chunks array then the remainder is unshifted back onto the array + */ +export const readBufferFromChunksAndModify = ( + chunks: Buffer[], + size: number, +): Buffer | null => { + const res = readBufferFromChunks(chunks, size); + if (!res) { + return null; + } + + if (res.buffer.length > size) { + chunks.splice(0, res.chunks_read, res.buffer.slice(size)); + return res.buffer.slice(0, size); + } + chunks.splice(0, res.chunks_read); + return res.buffer; +}; + +/** + * Creates a abstraction on top of a compressed set of Buffer chunks that keeps track of the + * entire byte size of the chunks array. + * + * Offers methods to get the byte size, peak at a given amount of data and destructively read + * a given amount of data + */ +export const createReadableBufferArray = () => { + let chunks: Buffer[] = []; + let current_size = 0; + return { + push(...new_chunks: Array) { + const normalized_chunks = new_chunks.map((chunk) => + Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk), + ); + chunks.push(...normalized_chunks); + current_size = new_chunks.reduce((size, chunk) => { + return size + chunk.length; + }, current_size); + }, + read(size: number) { + const buffer = readBufferFromChunksAndModify(chunks, size); + if (buffer) { + current_size -= size; + } + + return buffer; + }, + peek(size: number) { + if (current_size < size) { + return null; + } + + const res = readBufferFromChunks(chunks, 4); + if (!res) { + return null; + } + + return res.buffer; + }, + size() { + return current_size; + }, + }; +}; + +export type ReadableBufferArray = ReturnType; diff --git a/packages/streaming/src/bson/constants.ts b/packages/streaming/src/bson/constants.ts new file mode 100644 index 0000000..1a74274 --- /dev/null +++ b/packages/streaming/src/bson/constants.ts @@ -0,0 +1 @@ +export const TERMINATOR = Buffer.from([0x00, 0x00, 0x00, 0x00]); diff --git a/packages/streaming/src/bson/decoder.ts b/packages/streaming/src/bson/decoder.ts new file mode 100644 index 0000000..e4c7871 --- /dev/null +++ b/packages/streaming/src/bson/decoder.ts @@ -0,0 +1,81 @@ +import * as buffer_array from "./buffer-array"; +import * as stream from "../core/cross-stream"; +import * as constants from "./constants"; +import * as bson from "bson"; + +export type BSONStreamDecoderParams = { + deserialize_options?: bson.DeserializeOptions; + require_terminator?: boolean; + + writableStrategy?: QueuingStrategy; + readableStrategy?: QueuingStrategy; +}; +export const createBSONStreamDecoder = ( + params?: BSONStreamDecoderParams, +) => { + const buffer = buffer_array.createReadableBufferArray(); + let frame_size: null | number = null; + + function* decodeFrames() { + while (true) { + if (frame_size === null) { + frame_size = buffer.peek(4)?.readInt32LE(0) || null; + } + if (frame_size === null) { + break; + } + if (buffer.size() < frame_size) { + break; + } + + const frame = buffer.read(frame_size); + if (!frame) { + break; + } + + frame_size = null; + yield bson.deserialize(frame, { + promoteBuffers: true, + validation: { + utf8: false, + }, + ...(params?.deserialize_options || {}), + }); + } + } + + let writableStrategy = params?.writableStrategy; + if (!writableStrategy) { + writableStrategy = new stream.ByteLengthStrategy({ + highWaterMark: 1024 * 16, + }); + } + + return new stream.Transform( + { + transform(chunk, controller) { + buffer.push(chunk); + for (const frame of decodeFrames()) { + controller.enqueue(frame as T); + } + }, + flush(controller) { + for (const frame of decodeFrames()) { + controller.enqueue(frame as T); + } + + const tail = buffer.peek(4); + if (tail && Buffer.compare(constants.TERMINATOR, tail) === 0) { + return; + } + if (params?.require_terminator === false) { + return; + } + + throw new Error("stream did not complete successfully"); + }, + }, + writableStrategy, + params?.readableStrategy, + ); +}; diff --git a/packages/streaming/src/bson/encoder.ts b/packages/streaming/src/bson/encoder.ts new file mode 100644 index 0000000..69eda45 --- /dev/null +++ b/packages/streaming/src/bson/encoder.ts @@ -0,0 +1,38 @@ +import * as stream from "../core/cross-stream"; +import * as constants from "./constants"; +import * as bson from "bson"; + +export type BSONStreamEncoderParams = { + serialize_options?: bson.SerializeOptions; + sendTerminatorOnEnd?: boolean; + writableStrategy?: QueuingStrategy; + readableStrategy?: QueuingStrategy; +}; + +export const createBSONStreamEncoder = ( + params?: BSONStreamEncoderParams, +) => { + let readableStrategy = params?.readableStrategy; + if (!readableStrategy) { + readableStrategy = new stream.ByteLengthStrategy({ + highWaterMark: 1024 * 16, + }); + } + + return new stream.Transform( + { + transform(chunk, controller) { + controller.enqueue( + Buffer.from(bson.serialize(chunk, params?.serialize_options)), + ); + }, + flush(controller) { + if (params?.sendTerminatorOnEnd ?? true) { + controller.enqueue(constants.TERMINATOR); + } + }, + }, + params?.writableStrategy, + readableStrategy, + ); +}; diff --git a/packages/streaming/src/bson/header.ts b/packages/streaming/src/bson/header.ts new file mode 100644 index 0000000..ca6e634 --- /dev/null +++ b/packages/streaming/src/bson/header.ts @@ -0,0 +1,77 @@ +import * as buffer_array from "./buffer-array"; +import * as bson from "bson"; + +export type DecodedResponse = { + header: T; + stream: AsyncIterable; +}; + +export type ExtractHeaderParams = { + deserialize_options?: bson.DeserializeOptions; +}; +export const extractHeaderFromStream = async ( + input_stream: AsyncIterable, + params?: ExtractHeaderParams, +): Promise> => { + const iterator = input_stream[Symbol.asyncIterator](); + + const buffer = buffer_array.createReadableBufferArray(); + let frame_size: number | null = null; + + async function* resplice( + data: Buffer | null, + iterator: AsyncIterator, + ) { + if (data) { + yield data; + } + + while (true) { + const next = await iterator.next(); + if (next.done) { + return; + } + + yield next.value; + } + } + + while (true) { + const chunk = await iterator.next(); + if (chunk.done) { + throw new Error("Stream did not complete successfully"); + } + + buffer.push(chunk.value); + + if (frame_size === null) { + frame_size = buffer.peek(4)?.readInt32LE(0) || null; + } + if (frame_size === null) { + continue; + } + + const frame = buffer.read(frame_size); + if (!frame) { + continue; + } + + const header = bson.deserialize(frame, { + promoteBuffers: true, + ...(params?.deserialize_options || {}), + }); + + return { + header: header as T, + stream: resplice(buffer.read(buffer.size()), iterator), + }; + } +}; + +export async function* prependHeaderToStream( + header: any, + input_stream: Iterable | AsyncIterable, +) { + yield bson.serialize(header); + yield* input_stream; +} diff --git a/packages/streaming/src/bson/index.ts b/packages/streaming/src/bson/index.ts new file mode 100644 index 0000000..4f7d386 --- /dev/null +++ b/packages/streaming/src/bson/index.ts @@ -0,0 +1,5 @@ +export * from "./buffer-array"; +export * from "./constants"; +export * from "./encoder"; +export * from "./decoder"; +export * from "./header"; diff --git a/packages/streaming/src/core/backpressure.ts b/packages/streaming/src/core/backpressure.ts new file mode 100644 index 0000000..969c76d --- /dev/null +++ b/packages/streaming/src/core/backpressure.ts @@ -0,0 +1,38 @@ +import * as stream from "stream"; + +export type SimpleReadableLike = stream.Readable | stream.Transform; + +/** + * This provides an abstraction for pushing data onto a Readable stream, returning a Promise that + * resolves once any present backpressure is drained. + * + * This is special as it can handle Transform streams in addition to normal Readables. Transform + * streams don't natively expose a method for hooking into the readable sides drain and causes + * problems when building inflating transforms. + * + * The comment at the top of the official node implementation provides more detailed information + * on the problem: https://github.com/nodejs/node/blob/master/lib/internal/streams/transform.js + */ +export const push = async (readable: SimpleReadableLike, data: any) => { + const can_continue = readable.push(data); + if (can_continue) { + return; + } + + // We can't really support transform streams + if (readable instanceof stream.Transform) { + return; + } + + return new Promise((resolve) => { + const errorHandler = () => { + resolve(); + }; + + readable.once("error", errorHandler); + readable.once("drain", () => { + readable.removeListener("error", errorHandler); + resolve(); + }); + }); +}; diff --git a/packages/streaming/src/core/cross-stream.ts b/packages/streaming/src/core/cross-stream.ts new file mode 100644 index 0000000..1361462 --- /dev/null +++ b/packages/streaming/src/core/cross-stream.ts @@ -0,0 +1,19 @@ +import type * as stream from "stream/web"; + +let Readable: typeof stream.ReadableStream; +let Transform: typeof stream.TransformStream; +let ByteLengthStrategy: typeof stream.ByteLengthQueuingStrategy; + +if (typeof window !== "undefined") { + Readable = ReadableStream as any; + Transform = TransformStream as any; + ByteLengthStrategy = + ByteLengthQueuingStrategy as typeof stream.ByteLengthQueuingStrategy; +} else { + const webstream = require("stream/web"); + Readable = webstream.ReadableStream; + Transform = webstream.TransformStream; + ByteLengthStrategy = webstream.ByteLengthQueuingStrategy; +} + +export { Readable, Transform, ByteLengthStrategy }; diff --git a/packages/streaming/src/core/index.ts b/packages/streaming/src/core/index.ts new file mode 100644 index 0000000..a9ba2f9 --- /dev/null +++ b/packages/streaming/src/core/index.ts @@ -0,0 +1,4 @@ +export * from "./backpressure"; +export * from "./transformers"; +export * from "./node-utils"; +export * from "./utils"; diff --git a/packages/streaming/src/core/node-utils.ts b/packages/streaming/src/core/node-utils.ts new file mode 100644 index 0000000..bd3ecfc --- /dev/null +++ b/packages/streaming/src/core/node-utils.ts @@ -0,0 +1,13 @@ +import * as stream from "stream"; + +/** + * Returns a promise that resolves once the given stream finishes. If the stream emits an error then + * then promise will reject + */ +export const wait = (stream: stream.Stream) => { + return new Promise((resolve, reject) => { + stream.on("error", reject); + stream.on("end", resolve); + stream.on("finish", resolve); + }); +}; diff --git a/packages/streaming/src/core/transformers.ts b/packages/streaming/src/core/transformers.ts new file mode 100644 index 0000000..c9b7b01 --- /dev/null +++ b/packages/streaming/src/core/transformers.ts @@ -0,0 +1,106 @@ +/** + * A set of standard helpers for working with iterators. + * + * The majority of the utils included here can be removed if and when the tc39 iterator-helpers proposal lands: + * https://github.com/tc39/proposal-iterator-helpers + */ + +import { StreamLike } from "./utils"; + +export type ExtractStreamElementType> = + T extends Iterable + ? I + : T extends AsyncIterable + ? I + : never; + +/** + * Takes n number of streams (or any AsyncIterators) and returns an AsyncGenerator that + * yields the concatenated output of all input streams + */ +export async function* concat>( + ...sources: S[] +): AsyncGenerator> { + for (const source of sources) { + yield* source as unknown as AsyncGenerator>; + } +} + +/** + * Implements `Array.prototype.map` for iterables + */ +export async function* map( + iterable: StreamLike, + transform: (element: I) => O | Promise, +) { + for await (const element of iterable) { + yield await transform(element); + } +} + +/** + * Implements `Array.prototype.filter` for iterables + */ +export async function* filter( + iterable: StreamLike, + comparator: (element: I) => boolean | Promise, +) { + for await (const element of iterable) { + if (await comparator(element)) { + yield element; + } + } +} + +type ReducedValue = { + __reduced: true; + value: T; +}; +export const reduced = (value: T) => { + return { + __reduced: true, + value, + }; +}; + +const isReducedValue = ( + value: any | ReducedValue, +): value is ReducedValue => { + return "__reduced" in value && value.__reduced; +}; + +/** + * Implements `Array.prototype.reduce` for iterables. + * + * The reducer can return `reduced(accumulator)` to end execution early + */ +export const reduce = async ( + iterable: StreamLike, + reducer: ( + accumulator: A, + element: I, + ) => A | ReducedValue | Promise>, + init: A, +) => { + let accumulator = init; + for await (const element of iterable) { + const result = await reducer(accumulator, element); + if (isReducedValue(result)) { + return result.value; + } + accumulator = result; + } + return accumulator; +}; + +/** + * Drain a given iterables contents into an array. This is kind of like `Array.from` except it works + * with AsyncIterables + */ +export const drain = async (iterator: StreamLike) => { + const data: T[] = []; + for await (const chunk of iterator) { + data.push(chunk); + } + return data; +}; diff --git a/packages/streaming/src/core/utils.ts b/packages/streaming/src/core/utils.ts new file mode 100644 index 0000000..04d1d40 --- /dev/null +++ b/packages/streaming/src/core/utils.ts @@ -0,0 +1,87 @@ +import * as micro_schema from "@journeyapps-labs/micro-schema"; +import * as micro_errors from "@journeyapps-labs/micro-errors"; +import * as cross_stream from "./cross-stream"; +import type * as webstreams from "stream/web"; + +export type StreamLike = Iterable | AsyncIterable; + +/** + * Construct an AsyncIterator from a given ReadableStream. + * + * This is only really intended to be used from browser runtimes or within code intended to + * be used cross-labs. This is because Node ReadableStreams already implement AsyncIterators + */ +export async function* iterableFromReadable( + readable: ReadableStream | webstreams.ReadableStream, +) { + const reader = readable.getReader(); + + try { + while (true) { + const res = await reader.read(); + if (res.done) { + return; + } + yield res.value; + } + } finally { + reader.releaseLock(); + } +} + +/** + * Construct a ReadableStream from a given Iterable or AsyncIterable. + * + * If the given iterable is already a readable then this is a noop + */ +export const readableFrom = ( + iterable: StreamLike, + strategy?: QueuingStrategy, +): webstreams.ReadableStream => { + if (iterable instanceof cross_stream.Readable) { + return iterable; + } + + let resume: (() => void) | undefined; + return new cross_stream.Readable( + { + start(controller) { + void (async function () { + for await (const chunk of iterable) { + controller.enqueue(chunk); + + if (controller.desiredSize != null && controller.desiredSize <= 0) { + await new Promise((resolve) => { + resume = resolve; + }); + } + } + + controller.close(); + })().catch((err) => { + controller.error(err); + }); + }, + async pull() { + resume?.(); + }, + }, + strategy, + ); +}; + +/** + * Yield a generator that validates data flowing through it + */ +export async function* validateDataStream( + iterable: StreamLike, + validator: micro_schema.MicroValidator, +) { + for await (const chunk of iterable) { + const res = validator.validate(chunk); + if (!res.valid) { + throw new micro_errors.ValidationError(res.errors); + } + yield chunk; + } +} diff --git a/packages/streaming/src/index.ts b/packages/streaming/src/index.ts new file mode 100644 index 0000000..7c901f4 --- /dev/null +++ b/packages/streaming/src/index.ts @@ -0,0 +1,4 @@ +export * as compat from "./core/cross-stream"; +export * as middleware from "./middleware"; +export * as bson from "./bson"; +export * from "./core"; diff --git a/packages/streaming/src/middleware/index.ts b/packages/streaming/src/middleware/index.ts new file mode 100644 index 0000000..8421866 --- /dev/null +++ b/packages/streaming/src/middleware/index.ts @@ -0,0 +1,2 @@ +export * from "./streamed-body-parser-v1"; +export * from "./streamed-body-parser-v2"; diff --git a/packages/streaming/src/middleware/streamed-body-parser-v1.ts b/packages/streaming/src/middleware/streamed-body-parser-v1.ts new file mode 100644 index 0000000..8cca5b8 --- /dev/null +++ b/packages/streaming/src/middleware/streamed-body-parser-v1.ts @@ -0,0 +1,62 @@ +import * as querystring from "querystring"; +import * as express from "express"; + +/** + * Parse an incoming request decoding a base64 encoded, JSON formatted `payload` query string + * onto the request body. This is useful in streaming scenarios where the request body is a + * stream that needs to be processed independently from the request 'params' or 'metadata' + * + * For example, the request + * + * POST /a/b?payload=ey... + * + * + * would be decoded into a `body` field on the request without touching the stream + * + * @deprecated + */ +export const streamedRequestBodyParser = ( + req: express.Request, + res: express.Response, + next: express.NextFunction, +) => { + try { + let payload = req.query.payload; + if (!payload) { + return next(); + } + if (typeof payload !== "string") { + return next(); + } + req.body = JSON.parse(Buffer.from(payload, "base64").toString()); + delete req.query.payload; + + next(); + } catch (err) { + next(err); + } +}; + +/** + * Given some payload of data encode it for using as a journey streamed URL payload + * + * This means JSON stringified, base64 url-encoded + * + * @deprecated + */ +export const encodeStreamingPayload = (data: object) => { + return querystring.escape( + Buffer.from(JSON.stringify(data)).toString("base64"), + ); +}; + +/** + * Encode a given payload for streaming and append it as a query string to a given + * URL + * + * @deprecated + */ +export const encodeURLPayload = (url: string, data: object) => { + const payload = encodeStreamingPayload(data); + return `${url}?payload=${payload}`; +}; diff --git a/packages/streaming/src/middleware/streamed-body-parser-v2.ts b/packages/streaming/src/middleware/streamed-body-parser-v2.ts new file mode 100644 index 0000000..4719f33 --- /dev/null +++ b/packages/streaming/src/middleware/streamed-body-parser-v2.ts @@ -0,0 +1,34 @@ +declare module "express" { + interface Request { + stream?: AsyncIterable; + } +} + +import * as stream_headers from "../bson/header"; +import * as express from "express"; + +/** + * Parse an incoming request by decoding the body as a bson stream. This will read the first + * incoming document as the request payload and then passthrough the remaining body as a + * property called `stream` on the request + */ +export const streamedRequestBodyParserV2 = async ( + req: express.Request, + _: express.Response, + next: express.NextFunction, +) => { + try { + if (!req.is("application/*+header")) { + return next(); + } + + const { header, stream } = + await stream_headers.extractHeaderFromStream(req); + + req.stream = stream; + req.body = header; + next(); + } catch (err) { + next(err); + } +}; diff --git a/packages/streaming/src/web.ts b/packages/streaming/src/web.ts new file mode 100644 index 0000000..8eac817 --- /dev/null +++ b/packages/streaming/src/web.ts @@ -0,0 +1,5 @@ +export * as compat from "./core/cross-stream"; +export * as bson from "./bson"; + +export * from "./core/transformers"; +export * from "./core/utils"; diff --git a/packages/streaming/tests/__snapshots__/bson-transform.test.ts.snap b/packages/streaming/tests/__snapshots__/bson-transform.test.ts.snap new file mode 100644 index 0000000..8a738d9 --- /dev/null +++ b/packages/streaming/tests/__snapshots__/bson-transform.test.ts.snap @@ -0,0 +1,83 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`bson-transformer > end-to-end streaming 1`] = ` +[ + { + "a": "b", + }, + { + "c": "d", + }, + { + "e": "f", + }, +] +`; + +exports[`bson-transformer > it successfully deserializes streamed bson data 1`] = ` +[ + { + "a": "b", + }, + { + "c": "d", + }, + { + "e": "f", + }, +] +`; + +exports[`bson-transformer > it successfully serializes objects to bson stream 1`] = ` +{ + "data": [ + 14, + 0, + 0, + 0, + 2, + 97, + 0, + 2, + 0, + 0, + 0, + 98, + 0, + 0, + 14, + 0, + 0, + 0, + 2, + 99, + 0, + 2, + 0, + 0, + 0, + 100, + 0, + 0, + 14, + 0, + 0, + 0, + 2, + 101, + 0, + 2, + 0, + 0, + 0, + 102, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "type": "Buffer", +} +`; diff --git a/packages/streaming/tests/__snapshots__/common.test.ts.snap b/packages/streaming/tests/__snapshots__/common.test.ts.snap new file mode 100644 index 0000000..c49b587 --- /dev/null +++ b/packages/streaming/tests/__snapshots__/common.test.ts.snap @@ -0,0 +1,66 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`common > it should concatenate two streams 1`] = ` +[ + { + "value": 0, + }, + { + "value": 1, + }, + { + "value": 2, + }, + { + "value": 3, + }, + { + "value": 4, + }, + { + "value": 5, + }, + { + "value": 6, + }, + { + "value": 7, + }, + { + "value": 8, + }, + { + "value": 9, + }, + { + "value": 10, + }, + { + "value": 11, + }, + { + "value": 12, + }, + { + "value": 13, + }, + { + "value": 14, + }, + { + "value": 15, + }, + { + "value": 16, + }, + { + "value": 17, + }, + { + "value": 18, + }, + { + "value": 19, + }, +] +`; diff --git a/packages/streaming/tests/bson-transform.test.ts b/packages/streaming/tests/bson-transform.test.ts new file mode 100644 index 0000000..5b36a38 --- /dev/null +++ b/packages/streaming/tests/bson-transform.test.ts @@ -0,0 +1,74 @@ +import { describe, test, expect } from "vitest"; + +import * as micro_streaming from "../src"; +import * as stream from "stream/web"; +import * as bson from "bson"; + +describe("bson-transformer", () => { + test("it successfully reads data from chunks array", () => { + const chunks = Array.from(Array(10).keys()).map((_, i) => Buffer.alloc(i)); + + expect(micro_streaming.bson.readBufferFromChunks(chunks, 50)).toBe(null); + expect( + micro_streaming.bson.readBufferFromChunks(chunks, 5)?.chunks_read, + ).toBe(4); + expect( + micro_streaming.bson.readBufferFromChunks(chunks, 2)?.buffer.length, + ).toBe(3); + }); + + test("it successfully reads data from chunks array, modifying original", () => { + const chunks = Array.from(Array(10).keys()).map((_, i) => Buffer.alloc(i)); + + const read1 = micro_streaming.bson.readBufferFromChunksAndModify(chunks, 1); + expect(read1?.length).toBe(1); + expect(chunks.length).toBe(8); + + const read2 = micro_streaming.bson.readBufferFromChunksAndModify(chunks, 1); + expect(read2?.length).toBe(1); + expect(chunks.length).toBe(8); + expect(chunks[0].length).toBe(1); + }); + + test("it successfully deserializes streamed bson data", async () => { + const source = new stream.ReadableStream({ + start(controller) { + controller.enqueue(bson.serialize({ a: "b" })); + controller.enqueue(bson.serialize({ c: "d" })); + controller.enqueue(bson.serialize({ e: "f" })); + controller.enqueue(micro_streaming.bson.TERMINATOR); + controller.close(); + }, + }); + + const sink = source.pipeThrough( + micro_streaming.bson.createBSONStreamDecoder(), + ); + expect(await micro_streaming.drain(sink)).toMatchSnapshot(); + }); + + test("it successfully serializes objects to bson stream", async () => { + const source = new stream.ReadableStream({ + start(controller) { + controller.enqueue({ a: "b" }); + controller.enqueue({ c: "d" }); + controller.enqueue({ e: "f" }); + controller.close(); + }, + }); + + const sink = source.pipeThrough( + micro_streaming.bson.createBSONStreamEncoder(), + ); + expect(Buffer.concat(await micro_streaming.drain(sink))).toMatchSnapshot(); + }); + + test("end-to-end streaming", async () => { + const output = micro_streaming + .readableFrom([{ a: "b" }, { c: "d" }, { e: "f" }]) + .pipeThrough(micro_streaming.bson.createBSONStreamEncoder()) + .pipeThrough(micro_streaming.bson.createBSONStreamDecoder()); + + expect(await micro_streaming.drain(output)).toMatchSnapshot(); + }); +}); diff --git a/packages/streaming/tests/common.test.ts b/packages/streaming/tests/common.test.ts new file mode 100644 index 0000000..d0322a6 --- /dev/null +++ b/packages/streaming/tests/common.test.ts @@ -0,0 +1,76 @@ +import { describe, test, expect } from "vitest"; + +import * as micro_schema from "@journeyapps-labs/micro-schema"; +import * as micro_errors from "@journeyapps-labs/micro-errors"; +import * as micro_streaming from "../src"; +import * as _ from "lodash"; + +describe("common", () => { + test("it should concatenate two streams", async () => { + async function* one() { + for (let i = 0; i < 10; i++) { + yield { + value: i, + }; + } + } + + async function* two() { + for (let i = 0; i < 10; i++) { + yield { + value: 10 + i, + }; + } + } + + const concat_stream = micro_streaming.concat(one(), two()); + expect(await micro_streaming.drain(concat_stream)).toMatchSnapshot(); + }); + + test("it should validate stream data", async () => { + function* generateLessThan10() { + for (const i of _.range(10)) { + yield { + item: i, + }; + } + } + + function* generateGreaterThan10() { + for (const i of _.range(15)) { + yield { + item: i, + }; + } + } + + const validator: micro_schema.MicroValidator<{ item: number }> = { + validate: (datum) => { + if (datum.item < 10) { + return { + valid: true, + }; + } + + return { + valid: false, + errors: ["not less than 10"], + }; + }, + }; + + const validated_correct = micro_streaming.validateDataStream( + generateLessThan10(), + validator, + ); + await micro_streaming.drain(validated_correct); + + const validated_incorrect = micro_streaming.validateDataStream( + generateGreaterThan10(), + validator, + ); + await expect( + micro_streaming.drain(validated_incorrect), + ).rejects.toThrowError(micro_errors.ValidationError); + }); +}); diff --git a/packages/streaming/tests/header.test.ts b/packages/streaming/tests/header.test.ts new file mode 100644 index 0000000..3995064 --- /dev/null +++ b/packages/streaming/tests/header.test.ts @@ -0,0 +1,78 @@ +import { describe, test, expect } from "vitest"; + +import * as micro_streaming from "../src"; +import * as crypto from "crypto"; +import * as _ from "lodash"; + +describe("bson-stream-header", () => { + test("it should successfully extract the header from a bson stream", async () => { + const bson_stream = micro_streaming + .readableFrom([{ a: "b" }, { c: "d" }]) + .pipeThrough(micro_streaming.bson.createBSONStreamEncoder()); + + const { header, stream: remaining } = + await micro_streaming.bson.extractHeaderFromStream(bson_stream); + expect(header).toEqual({ + a: "b", + }); + + const decoded_stream = micro_streaming + .readableFrom(remaining) + .pipeThrough(micro_streaming.bson.createBSONStreamDecoder()); + expect(await micro_streaming.drain(decoded_stream)).toEqual([ + { + c: "d", + }, + ]); + }); + + test("it should handle a lot of data", async () => { + function* generator() { + for (const i of _.range(20)) { + yield { + data: crypto.randomBytes(1024 * 1024), + }; + } + } + + const bson_stream = micro_streaming + .readableFrom(generator()) + .pipeThrough(micro_streaming.bson.createBSONStreamEncoder()); + + const { header, stream: remaining } = + await micro_streaming.bson.extractHeaderFromStream(bson_stream); + expect(Buffer.isBuffer(header.data)).toBe(true); + + const decoded_stream = await micro_streaming.drain( + micro_streaming + .readableFrom(remaining) + .pipeThrough(micro_streaming.bson.createBSONStreamDecoder()), + ); + expect(decoded_stream.length).toBe(19); + }); + + test("it should properly prepend a header to a bson stream", async () => { + const data = [{ a: "b" }, { c: "d" }]; + const bson_stream = micro_streaming + .readableFrom(data) + .pipeThrough(micro_streaming.bson.createBSONStreamEncoder()); + + const stream_with_header = micro_streaming.bson.prependHeaderToStream( + { + key: "value", + }, + bson_stream, + ); + + const decoded = micro_streaming + .readableFrom(stream_with_header) + .pipeThrough(micro_streaming.bson.createBSONStreamDecoder()); + + expect(await micro_streaming.drain(decoded)).toEqual([ + { + key: "value", + }, + ...data, + ]); + }); +}); diff --git a/packages/streaming/tsconfig.json b/packages/streaming/tsconfig.json new file mode 100644 index 0000000..0e4b9bf --- /dev/null +++ b/packages/streaming/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.base", + "compilerOptions": { + "outDir": "dist", + "declarationDir": "dist", + "rootDir": "src", + "lib": ["DOM", "ES2020"] + }, + "include": ["src"], + "references": [ + { + "path": "../errors" + }, + { + "path": "../schema" + } + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..302d920 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,2223 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@changesets/cli': + specifier: ^2.29.5 + version: 2.29.7(@types/node@24.7.0) + npm-check-updates: + specifier: ^18.0.2 + version: 18.3.1 + prettier: + specifier: 3.6.2 + version: 3.6.2 + typescript: + specifier: ^5.9.2 + version: 5.9.3 + + packages/codecs: + dependencies: + '@types/node': + specifier: ^20.17.6 + version: 20.19.19 + bson: + specifier: ^6.7.0 + version: 6.10.4 + ts-codec: + specifier: ^1.3.0 + version: 1.3.0 + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/node@20.19.19) + + packages/errors: + devDependencies: + '@types/node': + specifier: ^20.17.6 + version: 20.19.19 + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/node@20.19.19) + + packages/schema: + dependencies: + '@apidevtools/json-schema-ref-parser': + specifier: ^9.1.0 + version: 9.1.2 + '@journeyapps-labs/micro-codecs': + specifier: workspace:^ + version: link:../codecs + '@journeyapps-labs/micro-errors': + specifier: workspace:^ + version: link:../errors + ajv: + specifier: ^8.11.2 + version: 8.17.1 + better-ajv-errors: + specifier: ^1.2.0 + version: 1.2.0(ajv@8.17.1) + ts-codec: + specifier: ^1.3.0 + version: 1.3.0 + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/node@24.7.0) + zod: + specifier: ^3.19.1 + version: 3.25.76 + + packages/streaming: + dependencies: + '@journeyapps-labs/micro-errors': + specifier: workspace:^ + version: link:../errors + '@journeyapps-labs/micro-schema': + specifier: workspace:^ + version: link:../schema + '@types/express': + specifier: ^4.17.14 + version: 4.17.23 + bson: + specifier: ^6.7.0 + version: 6.10.4 + devDependencies: + '@types/lodash': + specifier: ^4.14.191 + version: 4.17.20 + lodash: + specifier: ^4.17.21 + version: 4.17.21 + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/node@24.7.0) + +packages: + + '@apidevtools/json-schema-ref-parser@9.1.2': + resolution: {integrity: sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + engines: {node: '>=6.9.0'} + + '@changesets/apply-release-plan@7.0.13': + resolution: {integrity: sha512-BIW7bofD2yAWoE8H4V40FikC+1nNFEKBisMECccS16W1rt6qqhNTBDmIw5HaqmMgtLNz9e7oiALiEUuKrQ4oHg==} + + '@changesets/assemble-release-plan@6.0.9': + resolution: {integrity: sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ==} + + '@changesets/changelog-git@0.2.1': + resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} + + '@changesets/cli@2.29.7': + resolution: {integrity: sha512-R7RqWoaksyyKXbKXBTbT4REdy22yH81mcFK6sWtqSanxUCbUi9Uf+6aqxZtDQouIqPdem2W56CdxXgsxdq7FLQ==} + hasBin: true + + '@changesets/config@3.1.1': + resolution: {integrity: sha512-bd+3Ap2TKXxljCggI0mKPfzCQKeV/TU4yO2h2C6vAihIo8tzseAn2e7klSuiyYYXvgu53zMN1OeYMIQkaQoWnA==} + + '@changesets/errors@0.2.0': + resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} + + '@changesets/get-dependents-graph@2.1.3': + resolution: {integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==} + + '@changesets/get-release-plan@4.0.13': + resolution: {integrity: sha512-DWG1pus72FcNeXkM12tx+xtExyH/c9I1z+2aXlObH3i9YA7+WZEVaiHzHl03thpvAgWTRaH64MpfHxozfF7Dvg==} + + '@changesets/get-version-range-type@0.4.0': + resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} + + '@changesets/git@3.0.4': + resolution: {integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==} + + '@changesets/logger@0.1.1': + resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==} + + '@changesets/parse@0.4.1': + resolution: {integrity: sha512-iwksMs5Bf/wUItfcg+OXrEpravm5rEd9Bf4oyIPL4kVTmJQ7PNDSd6MDYkpSJR1pn7tz/k8Zf2DhTCqX08Ou+Q==} + + '@changesets/pre@2.0.2': + resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==} + + '@changesets/read@0.6.5': + resolution: {integrity: sha512-UPzNGhsSjHD3Veb0xO/MwvasGe8eMyNrR/sT9gR8Q3DhOQZirgKhhXv/8hVsI0QpPjR004Z9iFxoJU6in3uGMg==} + + '@changesets/should-skip-package@0.1.2': + resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==} + + '@changesets/types@4.1.0': + resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==} + + '@changesets/types@6.1.0': + resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==} + + '@changesets/write@0.4.0': + resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + + '@esbuild/aix-ppc64@0.25.10': + resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.10': + resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.10': + resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.10': + resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.10': + resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.10': + resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.10': + resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.10': + resolution: {integrity: sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.10': + resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.10': + resolution: {integrity: sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.10': + resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.10': + resolution: {integrity: sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.10': + resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.10': + resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.10': + resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.10': + resolution: {integrity: sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.10': + resolution: {integrity: sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.10': + resolution: {integrity: sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.10': + resolution: {integrity: sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.10': + resolution: {integrity: sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.10': + resolution: {integrity: sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.10': + resolution: {integrity: sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.10': + resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.10': + resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.10': + resolution: {integrity: sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.10': + resolution: {integrity: sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@humanwhocodes/momoa@2.0.4': + resolution: {integrity: sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==} + engines: {node: '>=10.10.0'} + + '@inquirer/external-editor@1.0.2': + resolution: {integrity: sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jsdevtools/ono@7.1.3': + resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} + + '@manypkg/find-root@1.1.0': + resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} + + '@manypkg/get-packages@1.1.3': + resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@rollup/rollup-android-arm-eabi@4.52.4': + resolution: {integrity: sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.52.4': + resolution: {integrity: sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.52.4': + resolution: {integrity: sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.52.4': + resolution: {integrity: sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.52.4': + resolution: {integrity: sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.52.4': + resolution: {integrity: sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.52.4': + resolution: {integrity: sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.52.4': + resolution: {integrity: sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.52.4': + resolution: {integrity: sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.52.4': + resolution: {integrity: sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.52.4': + resolution: {integrity: sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.52.4': + resolution: {integrity: sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.52.4': + resolution: {integrity: sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.52.4': + resolution: {integrity: sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.52.4': + resolution: {integrity: sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.52.4': + resolution: {integrity: sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.52.4': + resolution: {integrity: sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.52.4': + resolution: {integrity: sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.52.4': + resolution: {integrity: sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.52.4': + resolution: {integrity: sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.52.4': + resolution: {integrity: sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.52.4': + resolution: {integrity: sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==} + cpu: [x64] + os: [win32] + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/chai@5.2.2': + resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/express-serve-static-core@4.19.7': + resolution: {integrity: sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==} + + '@types/express@4.17.23': + resolution: {integrity: sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/lodash@4.17.20': + resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} + + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + + '@types/node@12.20.55': + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + + '@types/node@20.19.19': + resolution: {integrity: sha512-pb1Uqj5WJP7wrcbLU7Ru4QtA0+3kAXrkutGiD26wUKzSMgNNaPARTUDQmElUXp64kh3cWdou3Q0C7qwwxqSFmg==} + + '@types/node@24.7.0': + resolution: {integrity: sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==} + + '@types/qs@6.14.0': + resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/send@0.17.5': + resolution: {integrity: sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==} + + '@types/send@1.2.0': + resolution: {integrity: sha512-zBF6vZJn1IaMpg3xUF25VK3gd3l8zwE0ZLRX7dsQyQi+jp4E8mMDJNGDYnYse+bQhYwWERTxVwHpi3dMOq7RKQ==} + + '@types/serve-static@1.15.9': + resolution: {integrity: sha512-dOTIuqpWLyl3BBXU3maNQsS4A3zuuoYRNIvYSxxhebPfXg2mzWQEPne/nlJ37yOse6uGgR386uTpdsx4D0QZWA==} + + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + better-ajv-errors@1.2.0: + resolution: {integrity: sha512-UW+IsFycygIo7bclP9h5ugkNH8EjCSgqyFB/yQ4Hqqa1OEYDtb0uFIkYE0b6+CjkgJYVM5UKI/pJPxjYe9EZlA==} + engines: {node: '>= 12.13.0'} + peerDependencies: + ajv: 4.11.8 - 8 + + better-path-resolve@1.0.0: + resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} + engines: {node: '>=4'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + bson@6.10.4: + resolution: {integrity: sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==} + engines: {node: '>=16.20.1'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + call-me-maybe@1.0.2: + resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chardet@2.1.0: + resolution: {integrity: sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==} + + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.25.10: + resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==} + engines: {node: '>=18'} + hasBin: true + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.2.2: + resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} + engines: {node: '>=12.0.0'} + + extendable-error@0.1.7: + resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + human-id@4.1.2: + resolution: {integrity: sha512-v/J+4Z/1eIJovEBdlV5TYj1IR+ZiohcYGRY+qN/oC9dAfKzVT023N/Bgw37hrKCoVRBvk3bqyzpr2PP5YeTMSg==} + hasBin: true + + iconv-lite@0.7.0: + resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-subdir@1.2.0: + resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} + engines: {node: '>=4'} + + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + + jsonpointer@5.0.1: + resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} + engines: {node: '>=0.10.0'} + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + magic-string@0.30.19: + resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + npm-check-updates@18.3.1: + resolution: {integrity: sha512-5HwKPq7ybOOA1xB4FZg/1ToZZ5/i93U8m3co1mb3GYZAZPDkcxEFukQTTp/Abym+ZY6ShfrHl45Y0rCcwsNnQA==} + engines: {node: ^18.18.0 || >=20.0.0, npm: '>=8.12.1'} + hasBin: true + + outdent@0.5.0: + resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + + p-filter@2.1.0: + resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} + engines: {node: '>=8'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-map@2.1.0: + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-manager-detector@0.2.11: + resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + read-yaml-file@1.1.0: + resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} + engines: {node: '>=6'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.52.4: + resolution: {integrity: sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + spawndamnit@3.0.1: + resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.9.0: + resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + term-size@2.2.1: + resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} + engines: {node: '>=8'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + ts-codec@1.3.0: + resolution: {integrity: sha512-OOaGvS0UwjyOychFZwjqSm47K65lzTCSup47RDG30crZr2MGnQCHQ13duAI4OcnzuYITNN6JDdS8RrtB0g204Q==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + undici-types@7.14.0: + resolution: {integrity: sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==} + + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@7.1.9: + resolution: {integrity: sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + +snapshots: + + '@apidevtools/json-schema-ref-parser@9.1.2': + dependencies: + '@jsdevtools/ono': 7.1.3 + '@types/json-schema': 7.0.15 + call-me-maybe: 1.0.2 + js-yaml: 4.1.0 + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/runtime@7.28.4': {} + + '@changesets/apply-release-plan@7.0.13': + dependencies: + '@changesets/config': 3.1.1 + '@changesets/get-version-range-type': 0.4.0 + '@changesets/git': 3.0.4 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + detect-indent: 6.1.0 + fs-extra: 7.0.1 + lodash.startcase: 4.4.0 + outdent: 0.5.0 + prettier: 2.8.8 + resolve-from: 5.0.0 + semver: 7.7.2 + + '@changesets/assemble-release-plan@6.0.9': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + semver: 7.7.2 + + '@changesets/changelog-git@0.2.1': + dependencies: + '@changesets/types': 6.1.0 + + '@changesets/cli@2.29.7(@types/node@24.7.0)': + dependencies: + '@changesets/apply-release-plan': 7.0.13 + '@changesets/assemble-release-plan': 6.0.9 + '@changesets/changelog-git': 0.2.1 + '@changesets/config': 3.1.1 + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/get-release-plan': 4.0.13 + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.5 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@changesets/write': 0.4.0 + '@inquirer/external-editor': 1.0.2(@types/node@24.7.0) + '@manypkg/get-packages': 1.1.3 + ansi-colors: 4.1.3 + ci-info: 3.9.0 + enquirer: 2.4.1 + fs-extra: 7.0.1 + mri: 1.2.0 + p-limit: 2.3.0 + package-manager-detector: 0.2.11 + picocolors: 1.1.1 + resolve-from: 5.0.0 + semver: 7.7.2 + spawndamnit: 3.0.1 + term-size: 2.2.1 + transitivePeerDependencies: + - '@types/node' + + '@changesets/config@3.1.1': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/logger': 0.1.1 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + micromatch: 4.0.8 + + '@changesets/errors@0.2.0': + dependencies: + extendable-error: 0.1.7 + + '@changesets/get-dependents-graph@2.1.3': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + picocolors: 1.1.1 + semver: 7.7.2 + + '@changesets/get-release-plan@4.0.13': + dependencies: + '@changesets/assemble-release-plan': 6.0.9 + '@changesets/config': 3.1.1 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.5 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/get-version-range-type@0.4.0': {} + + '@changesets/git@3.0.4': + dependencies: + '@changesets/errors': 0.2.0 + '@manypkg/get-packages': 1.1.3 + is-subdir: 1.2.0 + micromatch: 4.0.8 + spawndamnit: 3.0.1 + + '@changesets/logger@0.1.1': + dependencies: + picocolors: 1.1.1 + + '@changesets/parse@0.4.1': + dependencies: + '@changesets/types': 6.1.0 + js-yaml: 3.14.1 + + '@changesets/pre@2.0.2': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + + '@changesets/read@0.6.5': + dependencies: + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/parse': 0.4.1 + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + p-filter: 2.1.0 + picocolors: 1.1.1 + + '@changesets/should-skip-package@0.1.2': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/types@4.1.0': {} + + '@changesets/types@6.1.0': {} + + '@changesets/write@0.4.0': + dependencies: + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + human-id: 4.1.2 + prettier: 2.8.8 + + '@esbuild/aix-ppc64@0.25.10': + optional: true + + '@esbuild/android-arm64@0.25.10': + optional: true + + '@esbuild/android-arm@0.25.10': + optional: true + + '@esbuild/android-x64@0.25.10': + optional: true + + '@esbuild/darwin-arm64@0.25.10': + optional: true + + '@esbuild/darwin-x64@0.25.10': + optional: true + + '@esbuild/freebsd-arm64@0.25.10': + optional: true + + '@esbuild/freebsd-x64@0.25.10': + optional: true + + '@esbuild/linux-arm64@0.25.10': + optional: true + + '@esbuild/linux-arm@0.25.10': + optional: true + + '@esbuild/linux-ia32@0.25.10': + optional: true + + '@esbuild/linux-loong64@0.25.10': + optional: true + + '@esbuild/linux-mips64el@0.25.10': + optional: true + + '@esbuild/linux-ppc64@0.25.10': + optional: true + + '@esbuild/linux-riscv64@0.25.10': + optional: true + + '@esbuild/linux-s390x@0.25.10': + optional: true + + '@esbuild/linux-x64@0.25.10': + optional: true + + '@esbuild/netbsd-arm64@0.25.10': + optional: true + + '@esbuild/netbsd-x64@0.25.10': + optional: true + + '@esbuild/openbsd-arm64@0.25.10': + optional: true + + '@esbuild/openbsd-x64@0.25.10': + optional: true + + '@esbuild/openharmony-arm64@0.25.10': + optional: true + + '@esbuild/sunos-x64@0.25.10': + optional: true + + '@esbuild/win32-arm64@0.25.10': + optional: true + + '@esbuild/win32-ia32@0.25.10': + optional: true + + '@esbuild/win32-x64@0.25.10': + optional: true + + '@humanwhocodes/momoa@2.0.4': {} + + '@inquirer/external-editor@1.0.2(@types/node@24.7.0)': + dependencies: + chardet: 2.1.0 + iconv-lite: 0.7.0 + optionalDependencies: + '@types/node': 24.7.0 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jsdevtools/ono@7.1.3': {} + + '@manypkg/find-root@1.1.0': + dependencies: + '@babel/runtime': 7.28.4 + '@types/node': 12.20.55 + find-up: 4.1.0 + fs-extra: 8.1.0 + + '@manypkg/get-packages@1.1.3': + dependencies: + '@babel/runtime': 7.28.4 + '@changesets/types': 4.1.0 + '@manypkg/find-root': 1.1.0 + fs-extra: 8.1.0 + globby: 11.1.0 + read-yaml-file: 1.1.0 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@rollup/rollup-android-arm-eabi@4.52.4': + optional: true + + '@rollup/rollup-android-arm64@4.52.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.52.4': + optional: true + + '@rollup/rollup-darwin-x64@4.52.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.52.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.52.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.52.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.52.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.52.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.52.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.52.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.52.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.52.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.52.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.52.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.52.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.52.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.52.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.52.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.52.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.52.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.52.4': + optional: true + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 20.19.19 + + '@types/chai@5.2.2': + dependencies: + '@types/deep-eql': 4.0.2 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 20.19.19 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.8': {} + + '@types/express-serve-static-core@4.19.7': + dependencies: + '@types/node': 20.19.19 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.0 + + '@types/express@4.17.23': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 4.19.7 + '@types/qs': 6.14.0 + '@types/serve-static': 1.15.9 + + '@types/http-errors@2.0.5': {} + + '@types/json-schema@7.0.15': {} + + '@types/lodash@4.17.20': {} + + '@types/mime@1.3.5': {} + + '@types/node@12.20.55': {} + + '@types/node@20.19.19': + dependencies: + undici-types: 6.21.0 + + '@types/node@24.7.0': + dependencies: + undici-types: 7.14.0 + optional: true + + '@types/qs@6.14.0': {} + + '@types/range-parser@1.2.7': {} + + '@types/send@0.17.5': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 20.19.19 + + '@types/send@1.2.0': + dependencies: + '@types/node': 20.19.19 + + '@types/serve-static@1.15.9': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 20.19.19 + '@types/send': 0.17.5 + + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.2 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.4(vite@7.1.9(@types/node@20.19.19))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.19 + optionalDependencies: + vite: 7.1.9(@types/node@20.19.19) + + '@vitest/mocker@3.2.4(vite@7.1.9(@types/node@24.7.0))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.19 + optionalDependencies: + vite: 7.1.9(@types/node@24.7.0) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.19 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-colors@4.1.3: {} + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + array-union@2.1.0: {} + + assertion-error@2.0.1: {} + + better-ajv-errors@1.2.0(ajv@8.17.1): + dependencies: + '@babel/code-frame': 7.27.1 + '@humanwhocodes/momoa': 2.0.4 + ajv: 8.17.1 + chalk: 4.1.2 + jsonpointer: 5.0.1 + leven: 3.1.0 + + better-path-resolve@1.0.0: + dependencies: + is-windows: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + bson@6.10.4: {} + + cac@6.7.14: {} + + call-me-maybe@1.0.2: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chardet@2.1.0: {} + + check-error@2.1.1: {} + + ci-info@3.9.0: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + detect-indent@6.1.0: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + enquirer@2.4.1: + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + + es-module-lexer@1.7.0: {} + + esbuild@0.25.10: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.10 + '@esbuild/android-arm': 0.25.10 + '@esbuild/android-arm64': 0.25.10 + '@esbuild/android-x64': 0.25.10 + '@esbuild/darwin-arm64': 0.25.10 + '@esbuild/darwin-x64': 0.25.10 + '@esbuild/freebsd-arm64': 0.25.10 + '@esbuild/freebsd-x64': 0.25.10 + '@esbuild/linux-arm': 0.25.10 + '@esbuild/linux-arm64': 0.25.10 + '@esbuild/linux-ia32': 0.25.10 + '@esbuild/linux-loong64': 0.25.10 + '@esbuild/linux-mips64el': 0.25.10 + '@esbuild/linux-ppc64': 0.25.10 + '@esbuild/linux-riscv64': 0.25.10 + '@esbuild/linux-s390x': 0.25.10 + '@esbuild/linux-x64': 0.25.10 + '@esbuild/netbsd-arm64': 0.25.10 + '@esbuild/netbsd-x64': 0.25.10 + '@esbuild/openbsd-arm64': 0.25.10 + '@esbuild/openbsd-x64': 0.25.10 + '@esbuild/openharmony-arm64': 0.25.10 + '@esbuild/sunos-x64': 0.25.10 + '@esbuild/win32-arm64': 0.25.10 + '@esbuild/win32-ia32': 0.25.10 + '@esbuild/win32-x64': 0.25.10 + + esprima@4.0.1: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + expect-type@1.2.2: {} + + extendable-error@0.1.7: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-uri@3.1.0: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fsevents@2.3.3: + optional: true + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + graceful-fs@4.2.11: {} + + has-flag@4.0.0: {} + + human-id@4.1.2: {} + + iconv-lite@0.7.0: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-subdir@1.2.0: + dependencies: + better-path-resolve: 1.0.0 + + is-windows@1.0.2: {} + + isexe@2.0.0: {} + + js-tokens@4.0.0: {} + + js-tokens@9.0.1: {} + + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + json-schema-traverse@1.0.0: {} + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + + jsonpointer@5.0.1: {} + + leven@3.1.0: {} + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + lodash.startcase@4.4.0: {} + + lodash@4.17.21: {} + + loupe@3.2.1: {} + + magic-string@0.30.19: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mri@1.2.0: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + npm-check-updates@18.3.1: {} + + outdent@0.5.0: {} + + p-filter@2.1.0: + dependencies: + p-map: 2.1.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-map@2.1.0: {} + + p-try@2.2.0: {} + + package-manager-detector@0.2.11: + dependencies: + quansync: 0.2.11 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-type@4.0.0: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pify@4.0.1: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prettier@2.8.8: {} + + prettier@3.6.2: {} + + quansync@0.2.11: {} + + queue-microtask@1.2.3: {} + + read-yaml-file@1.1.0: + dependencies: + graceful-fs: 4.2.11 + js-yaml: 3.14.1 + pify: 4.0.1 + strip-bom: 3.0.0 + + require-from-string@2.0.2: {} + + resolve-from@5.0.0: {} + + reusify@1.1.0: {} + + rollup@4.52.4: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.52.4 + '@rollup/rollup-android-arm64': 4.52.4 + '@rollup/rollup-darwin-arm64': 4.52.4 + '@rollup/rollup-darwin-x64': 4.52.4 + '@rollup/rollup-freebsd-arm64': 4.52.4 + '@rollup/rollup-freebsd-x64': 4.52.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.52.4 + '@rollup/rollup-linux-arm-musleabihf': 4.52.4 + '@rollup/rollup-linux-arm64-gnu': 4.52.4 + '@rollup/rollup-linux-arm64-musl': 4.52.4 + '@rollup/rollup-linux-loong64-gnu': 4.52.4 + '@rollup/rollup-linux-ppc64-gnu': 4.52.4 + '@rollup/rollup-linux-riscv64-gnu': 4.52.4 + '@rollup/rollup-linux-riscv64-musl': 4.52.4 + '@rollup/rollup-linux-s390x-gnu': 4.52.4 + '@rollup/rollup-linux-x64-gnu': 4.52.4 + '@rollup/rollup-linux-x64-musl': 4.52.4 + '@rollup/rollup-openharmony-arm64': 4.52.4 + '@rollup/rollup-win32-arm64-msvc': 4.52.4 + '@rollup/rollup-win32-ia32-msvc': 4.52.4 + '@rollup/rollup-win32-x64-gnu': 4.52.4 + '@rollup/rollup-win32-x64-msvc': 4.52.4 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safer-buffer@2.1.2: {} + + semver@7.7.2: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + signal-exit@4.1.0: {} + + slash@3.0.0: {} + + source-map-js@1.2.1: {} + + spawndamnit@3.0.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + sprintf-js@1.0.3: {} + + stackback@0.0.2: {} + + std-env@3.9.0: {} + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom@3.0.0: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + term-size@2.2.1: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + ts-codec@1.3.0: {} + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + undici-types@7.14.0: + optional: true + + universalify@0.1.2: {} + + vite-node@3.2.4(@types/node@20.19.19): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.1.9(@types/node@20.19.19) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite-node@3.2.4(@types/node@24.7.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.1.9(@types/node@24.7.0) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@7.1.9(@types/node@20.19.19): + dependencies: + esbuild: 0.25.10 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.52.4 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 20.19.19 + fsevents: 2.3.3 + + vite@7.1.9(@types/node@24.7.0): + dependencies: + esbuild: 0.25.10 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.52.4 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.7.0 + fsevents: 2.3.3 + + vitest@3.2.4(@types/node@20.19.19): + dependencies: + '@types/chai': 5.2.2 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.1.9(@types/node@20.19.19)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.2.2 + magic-string: 0.30.19 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.1.9(@types/node@20.19.19) + vite-node: 3.2.4(@types/node@20.19.19) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.19.19 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vitest@3.2.4(@types/node@24.7.0): + dependencies: + '@types/chai': 5.2.2 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.1.9(@types/node@24.7.0)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.2.2 + magic-string: 0.30.19 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.1.9(@types/node@24.7.0) + vite-node: 3.2.4(@types/node@24.7.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.7.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + zod@3.25.76: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..3b32839 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: +- packages/* diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..a8bcdfa --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "declaration": true, + "composite": true, + "lib": ["ESNext"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "target": "ES2020", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "useUnknownInCatchVariables": false + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..56f1166 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "files": [], + "references": [ + { + "path": "./packages/codecs" + }, + { + "path": "./packages/errors" + }, + { + "path": "./packages/streaming" + }, + { + "path": "./packages/schema" + } + ] +} \ No newline at end of file From 9b3985ea935885cd0de7d0840b1b320ba9588197 Mon Sep 17 00:00:00 2001 From: Dylan Vorster Date: Tue, 7 Oct 2025 12:14:13 -0600 Subject: [PATCH 2/5] prettier --- .prettierrc | 8 ++ packages/codecs/src/codecs.ts | 58 ++++---- packages/codecs/src/index.ts | 4 +- packages/codecs/src/parsers.ts | 119 +++++++---------- packages/codecs/tests/parsers.test.ts | 28 ++-- packages/errors/src/errors.ts | 40 +++--- packages/errors/src/index.ts | 4 +- packages/errors/src/utils.ts | 2 +- packages/errors/tests/errors.test.ts | 30 ++--- packages/schema/src/better-ajv-errors.d.ts | 10 +- packages/schema/src/definitions.ts | 4 +- packages/schema/src/index.ts | 10 +- packages/schema/src/json-schema/keywords.ts | 16 +-- packages/schema/src/json-schema/parser.ts | 39 +++--- packages/schema/src/utils.ts | 15 +-- packages/schema/src/validators/index.ts | 6 +- .../schema/src/validators/schema-validator.ts | 31 ++--- .../src/validators/ts-codec-validator.ts | 27 ++-- .../schema/src/validators/zod-validator.ts | 18 +-- packages/schema/tests/fixtures/schema.ts | 46 +++---- packages/schema/tests/parser.test.ts | 72 +++++----- .../schema/tests/schema-validation.test.ts | 126 +++++++++--------- .../schema/tests/ts-codec-validation.test.ts | 68 +++++----- packages/schema/tests/utils.test.ts | 36 ++--- packages/schema/tests/zod-validation.test.ts | 30 ++--- packages/streaming/src/bson/buffer-array.ts | 13 +- packages/streaming/src/bson/decoder.ts | 24 ++-- packages/streaming/src/bson/encoder.ts | 20 ++- packages/streaming/src/bson/header.ts | 22 ++- packages/streaming/src/bson/index.ts | 10 +- packages/streaming/src/core/backpressure.ts | 8 +- packages/streaming/src/core/cross-stream.ts | 9 +- packages/streaming/src/core/index.ts | 8 +- packages/streaming/src/core/node-utils.ts | 8 +- packages/streaming/src/core/transformers.ts | 37 ++--- packages/streaming/src/core/utils.ts | 23 ++-- packages/streaming/src/index.ts | 8 +- packages/streaming/src/middleware/index.ts | 4 +- .../src/middleware/streamed-body-parser-v1.ts | 18 +-- .../src/middleware/streamed-body-parser-v2.ts | 13 +- packages/streaming/src/web.ts | 8 +- .../streaming/tests/bson-transform.test.ts | 54 ++++---- packages/streaming/tests/common.test.ts | 44 +++--- packages/streaming/tests/header.test.ts | 50 +++---- 44 files changed, 555 insertions(+), 673 deletions(-) create mode 100644 .prettierrc diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..dd651a5 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "useTabs": false, + "printWidth": 120, + "trailingComma": "none" +} diff --git a/packages/codecs/src/codecs.ts b/packages/codecs/src/codecs.ts index fc38fa3..144fe41 100644 --- a/packages/codecs/src/codecs.ts +++ b/packages/codecs/src/codecs.ts @@ -1,19 +1,19 @@ -import * as t from "ts-codec"; -import * as bson from "bson"; +import * as t from 'ts-codec'; +import * as bson from 'bson'; export const buffer = t.codec( - "Buffer", + 'Buffer', (buffer) => { if (!Buffer.isBuffer(buffer)) { throw new t.TransformError([`Expected buffer but got ${typeof buffer}`]); } - return buffer.toString("base64"); + return buffer.toString('base64'); }, - (buffer) => Buffer.from(buffer, "base64"), + (buffer) => Buffer.from(buffer, 'base64') ); export const date = t.codec( - "Date", + 'Date', (date) => { if (!(date instanceof Date)) { throw new t.TransformError([`Expected Date but got ${typeof date}`]); @@ -26,18 +26,16 @@ export const date = t.codec( throw new t.TransformError([`Invalid date`]); } return parsed; - }, + } ); const assertObjectId = (value: any) => { if (!bson.ObjectId.isValid(value)) { - throw new t.TransformError([ - `Expected an ObjectId but got ${typeof value}`, - ]); + throw new t.TransformError([`Expected an ObjectId but got ${typeof value}`]); } }; export const ObjectId = t.codec( - "ObjectId", + 'ObjectId', (id) => { assertObjectId(id); return id.toHexString(); @@ -45,11 +43,11 @@ export const ObjectId = t.codec( (id) => { assertObjectId(id); return new bson.ObjectId(id); - }, + } ); const assertObjectWithField = (field: string, data: any) => { - if (typeof data !== "object") { + if (typeof data !== 'object') { throw new t.TransformError([`Expected an object but got ${typeof data}`]); } if (!(field in data)) { @@ -57,30 +55,30 @@ const assertObjectWithField = (field: string, data: any) => { } }; export const ResourceId = t.codec<{ _id: bson.ObjectId }, { id: string }>( - "ResourceId", + 'ResourceId', (data) => { - assertObjectWithField("_id", data); + assertObjectWithField('_id', data); return { - id: ObjectId.encode(data._id), + id: ObjectId.encode(data._id) }; }, (data) => { - assertObjectWithField("id", data); + assertObjectWithField('id', data); return { - _id: ObjectId.decode(data.id), + _id: ObjectId.decode(data.id) }; - }, + } ); export const Timestamps = t.object({ created_at: date, - updated_at: date, + updated_at: date }); export const Resource = ResourceId.and(Timestamps); export const QueryFilter = t.object({ - exists: t.boolean, + exists: t.boolean }); export const makeQueryFilter = (type: t.AnyCodec) => { @@ -88,21 +86,15 @@ export const makeQueryFilter = (type: t.AnyCodec) => { }; export const FilterProperties = ( - type: T, + type: T ): t.Codec< { - [K in keyof t.Encoded]?: - | t.Ix[K] - | t.Ix[K][] - | t.Ix; + [K in keyof t.Encoded]?: t.Ix[K] | t.Ix[K][] | t.Ix; }, { - [K in keyof t.Encoded]?: - | t.Ox[K] - | t.Ox[K][] - | t.Ox; + [K in keyof t.Encoded]?: t.Ox[K] | t.Ox[K][] | t.Ox; }, - "FilterProperties" + 'FilterProperties' > => { let codecs = new Map(); @@ -123,7 +115,7 @@ export const FilterProperties = ( } t.object({ - test: t.string, + test: t.string }); // @ts-ignore @@ -131,6 +123,6 @@ export const FilterProperties = ( Array.from(codecs.keys()).reduce((prev: any, cur) => { prev[cur] = makeQueryFilter(codecs.get(cur)!); return prev; - }, {}), + }, {}) ); }; diff --git a/packages/codecs/src/index.ts b/packages/codecs/src/index.ts index b003005..b086af5 100644 --- a/packages/codecs/src/index.ts +++ b/packages/codecs/src/index.ts @@ -1,2 +1,2 @@ -export * from "./parsers"; -export * from "./codecs"; +export * from './parsers'; +export * from './codecs'; diff --git a/packages/codecs/src/parsers.ts b/packages/codecs/src/parsers.ts index e3b9968..038636e 100644 --- a/packages/codecs/src/parsers.ts +++ b/packages/codecs/src/parsers.ts @@ -1,77 +1,60 @@ -import * as codecs from "./codecs"; -import * as t from "ts-codec"; +import * as codecs from './codecs'; +import * as t from 'ts-codec'; -export const ObjectIdParser = t.createParser( - codecs.ObjectId._tag, - (_, { target }) => { - switch (target) { - case t.TransformTarget.Encoded: { - return { type: "string" }; - } - case t.TransformTarget.Decoded: { - return { bsonType: "ObjectId" }; - } +export const ObjectIdParser = t.createParser(codecs.ObjectId._tag, (_, { target }) => { + switch (target) { + case t.TransformTarget.Encoded: { + return { type: 'string' }; } - }, -); + case t.TransformTarget.Decoded: { + return { bsonType: 'ObjectId' }; + } + } +}); -export const ResourceIdParser = t.createParser( - codecs.ResourceId._tag, - (_, { target }) => { - switch (target) { - case t.TransformTarget.Encoded: { - return { - type: "object", - properties: { - id: { type: "string" }, - }, - required: ["id"], - }; - } - case t.TransformTarget.Decoded: { - return { - type: "object", - properties: { - _id: { bsonType: "ObjectId" }, - }, - required: ["_id"], - }; - } +export const ResourceIdParser = t.createParser(codecs.ResourceId._tag, (_, { target }) => { + switch (target) { + case t.TransformTarget.Encoded: { + return { + type: 'object', + properties: { + id: { type: 'string' } + }, + required: ['id'] + }; + } + case t.TransformTarget.Decoded: { + return { + type: 'object', + properties: { + _id: { bsonType: 'ObjectId' } + }, + required: ['_id'] + }; } - }, -); + } +}); -export const DateParser = t.createParser( - codecs.date._tag, - (_, { target }) => { - switch (target) { - case t.TransformTarget.Encoded: { - return { type: "string" }; - } - case t.TransformTarget.Decoded: { - return { nodeType: "date" }; - } +export const DateParser = t.createParser(codecs.date._tag, (_, { target }) => { + switch (target) { + case t.TransformTarget.Encoded: { + return { type: 'string' }; } - }, -); + case t.TransformTarget.Decoded: { + return { nodeType: 'date' }; + } + } +}); -export const BufferParser = t.createParser( - codecs.buffer._tag, - (_, { target }) => { - switch (target) { - case t.TransformTarget.Encoded: { - return { type: "string" }; - } - case t.TransformTarget.Decoded: { - return { nodeType: "buffer" }; - } +export const BufferParser = t.createParser(codecs.buffer._tag, (_, { target }) => { + switch (target) { + case t.TransformTarget.Encoded: { + return { type: 'string' }; + } + case t.TransformTarget.Decoded: { + return { nodeType: 'buffer' }; } - }, -); + } +}); -export const parsers = [ - ObjectIdParser, - ResourceIdParser, - DateParser, - BufferParser, -]; +export const parsers = [ObjectIdParser, ResourceIdParser, DateParser, BufferParser]; diff --git a/packages/codecs/tests/parsers.test.ts b/packages/codecs/tests/parsers.test.ts index 50032c6..a8249d2 100644 --- a/packages/codecs/tests/parsers.test.ts +++ b/packages/codecs/tests/parsers.test.ts @@ -1,49 +1,49 @@ -import { describe, it, expect } from "vitest"; +import { describe, it, expect } from 'vitest'; -import * as codecs from "../src"; -import * as t from "ts-codec"; +import * as codecs from '../src'; +import * as t from 'ts-codec'; const generate = (codec: t.AnyCodec) => { const encoded = t.generateJSONSchema(codec, { parsers: codecs.parsers, - target: t.TransformTarget.Encoded, + target: t.TransformTarget.Encoded }); const decoded = t.generateJSONSchema(codec, { parsers: codecs.parsers, - target: t.TransformTarget.Decoded, + target: t.TransformTarget.Decoded }); return { encoded, decoded }; }; -describe("parsers", () => { - it("should correctly generate date schemas", () => { +describe('parsers', () => { + it('should correctly generate date schemas', () => { expect(generate(codecs.date)).toMatchSnapshot(); }); - it("should correctly generate buffer schemas", () => { + it('should correctly generate buffer schemas', () => { expect(generate(codecs.buffer)).toMatchSnapshot(); }); - it("should correctly generate ObjectId schemas", () => { + it('should correctly generate ObjectId schemas', () => { expect(generate(codecs.ObjectId)).toMatchSnapshot(); }); - it("should correctly generate ResourceId schemas", () => { + it('should correctly generate ResourceId schemas', () => { expect(generate(codecs.ResourceId)).toMatchSnapshot(); }); - it("should correctly generate Filterable schema from Object", () => { + it('should correctly generate Filterable schema from Object', () => { const filterObject1 = t.object({ - test1: t.string, + test1: t.string }); const filterObject2 = t.object({ - test2: t.number, + test2: t.number }); const filterObject3 = t.object({ - test1: t.string, + test1: t.string }); const filterObject4 = filterObject1.and(filterObject2).and(filterObject3); diff --git a/packages/errors/src/errors.ts b/packages/errors/src/errors.ts index 0b5207b..ba9c10c 100644 --- a/packages/errors/src/errors.ts +++ b/packages/errors/src/errors.ts @@ -13,9 +13,9 @@ */ export enum ErrorSeverity { - INFO = "info", - WARNING = "warning", - ERROR = "error", + INFO = 'info', + WARNING = 'warning', + ERROR = 'error' } export type ErrorData = { @@ -78,7 +78,7 @@ export class JourneyError extends Error { } toJSON(): ErrorData { - if (process.env.NODE_ENV !== "production") { + if (process.env.NODE_ENV !== 'production') { return this.errorData; } return { @@ -89,7 +89,7 @@ export class JourneyError extends Error { details: this.errorData.details, trace_id: this.errorData.trace_id, severity: this.errorData.severity, - origin: this.errorData.origin, + origin: this.errorData.origin }; } @@ -99,46 +99,46 @@ export class JourneyError extends Error { } export class ValidationError extends JourneyError { - static CODE = "VALIDATION_ERROR"; + static CODE = 'VALIDATION_ERROR'; constructor(errors: any) { super({ code: ValidationError.CODE, status: 400, - description: "Validation failed", + description: 'Validation failed', details: JSON.stringify(errors), - report: false, + report: false }); } } export class AuthorizationError extends JourneyError { - static CODE = "AUTHORIZATION"; + static CODE = 'AUTHORIZATION'; constructor(errors: any) { super({ code: AuthorizationError.CODE, status: 401, - description: "Authorization failed", - details: errors, + description: 'Authorization failed', + details: errors }); } } export class InternalServerError extends JourneyError { - static CODE = "INTERNAL_SERVER_ERROR"; + static CODE = 'INTERNAL_SERVER_ERROR'; constructor(err: Error) { super({ code: InternalServerError.CODE, severity: ErrorSeverity.ERROR, status: 500, - description: "Something went wrong", + description: 'Something went wrong', details: err.message, - stack: process.env.NODE_ENV !== "production" ? err.stack : undefined, + stack: process.env.NODE_ENV !== 'production' ? err.stack : undefined }); } } export class ResourceNotFound extends JourneyError { - static CODE = "RESOURCE_NOT_FOUND"; + static CODE = 'RESOURCE_NOT_FOUND'; /** * @deprecated Use the (resource, id) constructor instead. @@ -152,23 +152,23 @@ export class ResourceNotFound extends JourneyError { super({ code: ResourceNotFound.CODE, status: 404, - description: "The requested resource does not exist on this server", + description: 'The requested resource does not exist on this server', details: `The resource ${combinedId} does not exist on this server`, - severity: ErrorSeverity.INFO, + severity: ErrorSeverity.INFO }); } } export class ResourceConflict extends JourneyError { - static CODE = "RESOURCE_CONFLICT"; + static CODE = 'RESOURCE_CONFLICT'; constructor(details: string) { super({ code: ResourceConflict.CODE, status: 409, - description: "The specified resource already exists on this server", + description: 'The specified resource already exists on this server', details: details, - severity: ErrorSeverity.INFO, + severity: ErrorSeverity.INFO }); } } diff --git a/packages/errors/src/index.ts b/packages/errors/src/index.ts index b330f3d..37810c0 100644 --- a/packages/errors/src/index.ts +++ b/packages/errors/src/index.ts @@ -1,2 +1,2 @@ -export * from "./errors"; -export * from "./utils"; +export * from './errors'; +export * from './utils'; diff --git a/packages/errors/src/utils.ts b/packages/errors/src/utils.ts index 55e8ebd..cbee7fb 100644 --- a/packages/errors/src/utils.ts +++ b/packages/errors/src/utils.ts @@ -1,4 +1,4 @@ -import { ErrorData, JourneyError } from "./errors"; +import { ErrorData, JourneyError } from './errors'; export const isJourneyError = (err: any): err is JourneyError => { const matches = err instanceof JourneyError || err.is_journey_error; diff --git a/packages/errors/tests/errors.test.ts b/packages/errors/tests/errors.test.ts index aaae3de..d3ad058 100644 --- a/packages/errors/tests/errors.test.ts +++ b/packages/errors/tests/errors.test.ts @@ -1,27 +1,27 @@ -import { describe, test, expect } from "vitest"; +import { describe, test, expect } from 'vitest'; -import * as micro_errors from "../src"; +import * as micro_errors from '../src'; class CustomJourneyError extends micro_errors.JourneyError { constructor() { super({ - code: "CUSTOM_JOURNEY_ERROR", - description: "This is a custom error", - details: "this is some more detailed information", + code: 'CUSTOM_JOURNEY_ERROR', + description: 'This is a custom error', + details: 'this is some more detailed information' }); } } -describe("errors", () => { - test("it should respond to instanceof checks", () => { +describe('errors', () => { + test('it should respond to instanceof checks', () => { const error = new CustomJourneyError(); expect(error instanceof Error).toBe(true); expect(error instanceof micro_errors.JourneyError).toBe(true); - expect(error.name).toBe("CustomJourneyError"); + expect(error.name).toBe('CustomJourneyError'); }); - test("it should serialize properly", () => { + test('it should serialize properly', () => { const error = new CustomJourneyError(); // The error stack will contain host specific path information. We only care about the header @@ -33,19 +33,15 @@ describe("errors", () => { expect(`${error}`.startsWith(initial)).toBe(true); }); - test("utilities should properly match a journey error", () => { - const standard_error = new Error("non-journey error"); + test('utilities should properly match a journey error', () => { + const standard_error = new Error('non-journey error'); const error = new CustomJourneyError(); expect(micro_errors.isJourneyError(standard_error)).toBe(false); expect(micro_errors.isJourneyError(error)).toBe(true); - expect(micro_errors.matchesErrorCode(error, "CUSTOM_JOURNEY_ERROR")).toBe( - true, - ); - expect( - micro_errors.matchesErrorCode(standard_error, "CUSTOM_JOURNEY_ERROR"), - ).toBe(false); + expect(micro_errors.matchesErrorCode(error, 'CUSTOM_JOURNEY_ERROR')).toBe(true); + expect(micro_errors.matchesErrorCode(standard_error, 'CUSTOM_JOURNEY_ERROR')).toBe(false); expect(micro_errors.getErrorData(error)).toMatchSnapshot(); expect(micro_errors.getErrorData(standard_error)).toBe(undefined); diff --git a/packages/schema/src/better-ajv-errors.d.ts b/packages/schema/src/better-ajv-errors.d.ts index 1f64d8e..0a8a5a6 100644 --- a/packages/schema/src/better-ajv-errors.d.ts +++ b/packages/schema/src/better-ajv-errors.d.ts @@ -1,5 +1,5 @@ -declare module "better-ajv-errors" { - import type { ErrorObject } from "ajv"; +declare module 'better-ajv-errors' { + import type { ErrorObject } from 'ajv'; export interface IOutputError { start: { line: number; column: number; offset: number }; @@ -10,7 +10,7 @@ declare module "better-ajv-errors" { } export interface IInputOptions { - format?: "cli" | "js"; + format?: 'cli' | 'js'; indent?: number | null; /** Raw JSON used when highlighting error location */ @@ -21,6 +21,6 @@ declare module "better-ajv-errors" { schema: S, data: T, errors: Array, - options?: Options, - ): Options extends { format: "js" } ? Array : string; + options?: Options + ): Options extends { format: 'js' } ? Array : string; } diff --git a/packages/schema/src/definitions.ts b/packages/schema/src/definitions.ts index 03ce3a7..e293df3 100644 --- a/packages/schema/src/definitions.ts +++ b/packages/schema/src/definitions.ts @@ -12,9 +12,7 @@ export type ValidationLeft = { errors: T; }; -export type ValidationResponse = - | ValidationLeft - | IValidationRight; +export type ValidationResponse = ValidationLeft | IValidationRight; export type MicroValidator = { validate: (data: T) => ValidationResponse; diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts index b2d817d..a6f21a1 100644 --- a/packages/schema/src/index.ts +++ b/packages/schema/src/index.ts @@ -1,6 +1,6 @@ -export * from "./json-schema/keywords"; -export * from "./json-schema/parser"; +export * from './json-schema/keywords'; +export * from './json-schema/parser'; -export * from "./definitions"; -export * from "./validators"; -export * from "./utils"; +export * from './definitions'; +export * from './validators'; +export * from './utils'; diff --git a/packages/schema/src/json-schema/keywords.ts b/packages/schema/src/json-schema/keywords.ts index 1f8011d..06ee104 100644 --- a/packages/schema/src/json-schema/keywords.ts +++ b/packages/schema/src/json-schema/keywords.ts @@ -1,27 +1,27 @@ -import * as ajv from "ajv"; +import * as ajv from 'ajv'; export const BufferNodeType: ajv.KeywordDefinition = { - keyword: "nodeType", + keyword: 'nodeType', metaSchema: { - type: "string", - enum: ["buffer", "date"], + type: 'string', + enum: ['buffer', 'date'] }, error: { message: ({ schemaCode }) => { return ajv.str`should be a ${schemaCode}`; - }, + } }, code(context) { switch (context.schema) { - case "buffer": { + case 'buffer': { return context.fail(ajv._`!Buffer.isBuffer(${context.data})`); } - case "date": { + case 'date': { return context.fail(ajv._`!(${context.data} instanceof Date)`); } default: { context.fail(ajv._`true`); } } - }, + } }; diff --git a/packages/schema/src/json-schema/parser.ts b/packages/schema/src/json-schema/parser.ts index 5906870..38f4593 100644 --- a/packages/schema/src/json-schema/parser.ts +++ b/packages/schema/src/json-schema/parser.ts @@ -1,5 +1,5 @@ -import * as schema_validator from "../validators/schema-validator"; -import * as defs from "../definitions"; +import * as schema_validator from '../validators/schema-validator'; +import * as defs from '../definitions'; /** * Recursively walk a given schema resolving a list of refs that are actively used in some way by the @@ -11,11 +11,7 @@ import * as defs from "../definitions"; * * We don't use this here as it resolves to a Promise, which we want to avoid for this tool */ -export const findUsedRefs = ( - schema: any, - definitions = schema.definitions, - cache: string[] = [], -): string[] => { +export const findUsedRefs = (schema: any, definitions = schema.definitions, cache: string[] = []): string[] => { if (Array.isArray(schema)) { return schema .map((subschema) => { @@ -24,19 +20,19 @@ export const findUsedRefs = ( .flat(); } - if (typeof schema === "object") { + if (typeof schema === 'object') { return Object.keys(schema).reduce((used: string[], key) => { const value = schema[key]; - if (key === "$ref") { + if (key === '$ref') { if (cache.includes(value)) { return used; } cache.push(value); - const subschema = definitions[value.replace("#/definitions/", "")]; + const subschema = definitions[value.replace('#/definitions/', '')]; used.push(value, ...findUsedRefs(subschema, definitions, cache)); return used; } - if (key === "definitions") { + if (key === 'definitions') { return used; } return used.concat(findUsedRefs(value, definitions, cache)); @@ -51,10 +47,7 @@ export const findUsedRefs = ( * definition keys that are referenced in some way, either directly or indirectly, by the * root schema */ -export const pruneDefinitions = ( - definitions: Record, - refs: string[], -) => { +export const pruneDefinitions = (definitions: Record, refs: string[]) => { return Object.keys(definitions).reduce((pruned: Record, key) => { if (refs.includes(`#/definitions/${key}`)) { pruned[key] = definitions[key]; @@ -65,7 +58,7 @@ export const pruneDefinitions = ( export type CompilerFunction = () => defs.JSONSchema; export type ValidatorFunction = ( - params?: schema_validator.CreateSchemaValidatorParams, + params?: schema_validator.CreateSchemaValidatorParams ) => schema_validator.SchemaValidator; export type Compilers = { @@ -88,14 +81,14 @@ export type WithCompilers = Compilers & { */ export const parseJSONSchema = ( schema: T, - definitions = schema.definitions, + definitions = schema.definitions ): WithCompilers => { return new Proxy(schema, { get(target: any, prop) { const compile: CompilerFunction = () => { const schema = { definitions: definitions, - ...target, + ...target }; if (!schema.definitions) { @@ -105,17 +98,17 @@ export const parseJSONSchema = ( const used = findUsedRefs(schema); return { ...schema, - definitions: pruneDefinitions(schema.definitions, used), + definitions: pruneDefinitions(schema.definitions, used) }; }; const validator: ValidatorFunction = (options) => { return schema_validator.createSchemaValidator(compile(), options); }; - if (prop === "compile") { + if (prop === 'compile') { return compile; } - if (prop === "validator") { + if (prop === 'validator') { return validator; } @@ -124,11 +117,11 @@ export const parseJSONSchema = ( if (Array.isArray(subschema)) { return subschema; } - if (typeof subschema !== "object") { + if (typeof subschema !== 'object') { return subschema; } return parseJSONSchema(subschema, definitions); - }, + } }); }; diff --git a/packages/schema/src/utils.ts b/packages/schema/src/utils.ts index 1882b36..47dd2ad 100644 --- a/packages/schema/src/utils.ts +++ b/packages/schema/src/utils.ts @@ -1,5 +1,5 @@ -import * as micro_errors from "@journeyapps-labs/micro-errors"; -import * as defs from "./definitions"; +import * as micro_errors from '@journeyapps-labs/micro-errors'; +import * as defs from './definitions'; export type Schema = { additionalProperties?: boolean | Schema; @@ -14,18 +14,18 @@ export const allowAdditionalProperties = (schema: T): T => { return Object.keys(schema).reduce((next_schema: any, key) => { const value = schema[key]; - if (key === "additionalProperties" && typeof value === "boolean") { + if (key === 'additionalProperties' && typeof value === 'boolean') { return next_schema; } if (Array.isArray(value)) { next_schema[key] = value.map((value) => { - if (typeof value !== "object") { + if (typeof value !== 'object') { return value; } return allowAdditionalProperties(value); }); - } else if (typeof value === "object") { + } else if (typeof value === 'object') { next_schema[key] = allowAdditionalProperties(value); } else { next_schema[key] = value; @@ -39,10 +39,7 @@ export const allowAdditionalProperties = (schema: T): T => { * A small utility for validating some data using a MicroValidator. Will return the valid data (typed correctly) or throw * a validation error */ -export const validateData = ( - event: any, - validator: defs.MicroValidator, -): T => { +export const validateData = (event: any, validator: defs.MicroValidator): T => { const result = validator.validate(event); if (!result.valid) { throw new micro_errors.ValidationError(result.errors); diff --git a/packages/schema/src/validators/index.ts b/packages/schema/src/validators/index.ts index 3829af3..e9db392 100644 --- a/packages/schema/src/validators/index.ts +++ b/packages/schema/src/validators/index.ts @@ -1,3 +1,3 @@ -export * from "./ts-codec-validator"; -export * from "./schema-validator"; -export * from "./zod-validator"; +export * from './ts-codec-validator'; +export * from './schema-validator'; +export * from './zod-validator'; diff --git a/packages/schema/src/validators/schema-validator.ts b/packages/schema/src/validators/schema-validator.ts index 4d00c7c..270dc4c 100644 --- a/packages/schema/src/validators/schema-validator.ts +++ b/packages/schema/src/validators/schema-validator.ts @@ -1,8 +1,8 @@ -import * as keywords from "../json-schema/keywords"; -import AjvErrorFormatter from "better-ajv-errors"; -import * as defs from "../definitions"; -import * as utils from "../utils"; -import AJV, * as ajv from "ajv"; +import * as keywords from '../json-schema/keywords'; +import AjvErrorFormatter from 'better-ajv-errors'; +import * as defs from '../definitions'; +import * as utils from '../utils'; +import AJV, * as ajv from 'ajv'; export class SchemaValidatorError extends Error { constructor(message: string) { @@ -33,13 +33,13 @@ export type CreateSchemaValidatorParams = { */ export const createSchemaValidator = ( schema: defs.JSONSchema, - params: CreateSchemaValidatorParams = {}, + params: CreateSchemaValidatorParams = {} ): SchemaValidator => { try { const ajv = new AJV({ allErrors: !(params.fail_fast ?? false), keywords: [keywords.BufferNodeType], - ...(params.ajv || {}), + ...(params.ajv || {}) }); let processed_schema = schema; @@ -58,25 +58,20 @@ export const createSchemaValidator = ( const valid = validator(data); if (!valid) { - const errors = AjvErrorFormatter( - processed_schema, - data, - validator.errors || [], - { - format: "js", - }, - )?.map((error) => error.error); + const errors = AjvErrorFormatter(processed_schema, data, validator.errors || [], { + format: 'js' + })?.map((error) => error.error); return { valid: false, - errors: errors || [], + errors: errors || [] }; } return { - valid: true, + valid: true }; - }, + } }; } catch (err) { // Here we re-throw the error because the original error thrown by AJV has a deep stack that diff --git a/packages/schema/src/validators/ts-codec-validator.ts b/packages/schema/src/validators/ts-codec-validator.ts index 8003748..72b42f5 100644 --- a/packages/schema/src/validators/ts-codec-validator.ts +++ b/packages/schema/src/validators/ts-codec-validator.ts @@ -1,34 +1,27 @@ -import * as codecs from "@journeyapps-labs/micro-codecs"; -import * as schema_validator from "./schema-validator"; -import * as defs from "../definitions"; -import * as t from "ts-codec"; +import * as codecs from '@journeyapps-labs/micro-codecs'; +import * as schema_validator from './schema-validator'; +import * as defs from '../definitions'; +import * as t from 'ts-codec'; export type TsCodecValidator< C extends t.AnyCodec, - T extends t.TransformTarget = t.TransformTarget.Encoded, -> = T extends t.TransformTarget.Encoded - ? defs.MicroValidator> - : defs.MicroValidator>; + T extends t.TransformTarget = t.TransformTarget.Encoded +> = T extends t.TransformTarget.Encoded ? defs.MicroValidator> : defs.MicroValidator>; -type ValidatorOptions = Partial< - Omit -> & { +type ValidatorOptions = Partial> & { target?: T; }; /** * Create a validator from a given ts-codec codec */ -export const createTsCodecValidator = < - C extends t.AnyCodec, - T extends t.TransformTarget = t.TransformTarget.Encoded, ->( +export const createTsCodecValidator = ( codec: C, - options?: ValidatorOptions, + options?: ValidatorOptions ): TsCodecValidator => { const schema = t.generateJSONSchema(codec, { ...(options || {}), - parsers: [...(options?.parsers ?? []), ...codecs.parsers], + parsers: [...(options?.parsers ?? []), ...codecs.parsers] }); return schema_validator.createSchemaValidator(schema); }; diff --git a/packages/schema/src/validators/zod-validator.ts b/packages/schema/src/validators/zod-validator.ts index 3fb9b54..c83658d 100644 --- a/packages/schema/src/validators/zod-validator.ts +++ b/packages/schema/src/validators/zod-validator.ts @@ -1,9 +1,7 @@ -import * as defs from "../definitions"; -import * as t from "zod"; +import * as defs from '../definitions'; +import * as t from 'zod'; -export type ZodValidator> = defs.MicroValidator< - t.infer -> & { +export type ZodValidator> = defs.MicroValidator> & { schema: T; }; @@ -11,9 +9,7 @@ export type ZodValidator> = defs.MicroValidator< * Create a validator from a given Zod schema * https://github.com/colinhacks/zod */ -export const createZodValidator = >( - schema: T, -): ZodValidator => { +export const createZodValidator = >(schema: T): ZodValidator => { return { schema: schema, validate: (data) => { @@ -21,13 +17,13 @@ export const createZodValidator = >( if (!result.success) { return { valid: false, - errors: [JSON.stringify(result.error.format())], + errors: [JSON.stringify(result.error.format())] }; } return { - valid: true, + valid: true }; - }, + } }; }; diff --git a/packages/schema/tests/fixtures/schema.ts b/packages/schema/tests/fixtures/schema.ts index fc4519e..3a64ec7 100644 --- a/packages/schema/tests/fixtures/schema.ts +++ b/packages/schema/tests/fixtures/schema.ts @@ -1,49 +1,49 @@ export default { definitions: { c: { - type: "object", + type: 'object', properties: { prop: { - type: "string", - }, + type: 'string' + } }, - required: ["prop"], - }, + required: ['prop'] + } }, - type: "object", + type: 'object', properties: { name: { - type: "object", + type: 'object', properties: { a: { - type: "string", + type: 'string' }, b: { - enum: ["A"], - }, + enum: ['A'] + } }, - required: ["a"], - additionalProperties: false, + required: ['a'], + additionalProperties: false }, b: { oneOf: [ { - type: "object", + type: 'object', properties: { a: { - type: "number", - }, + type: 'number' + } }, - required: ["a"], - additionalProperties: false, - }, - ], + required: ['a'], + additionalProperties: false + } + ] }, d: { - $ref: "#/definitions/c", - }, + $ref: '#/definitions/c' + } }, - required: ["name", "b"], - additionalProperties: false, + required: ['name', 'b'], + additionalProperties: false }; diff --git a/packages/schema/tests/parser.test.ts b/packages/schema/tests/parser.test.ts index 47b3a50..73c54ef 100644 --- a/packages/schema/tests/parser.test.ts +++ b/packages/schema/tests/parser.test.ts @@ -1,82 +1,82 @@ -import { describe, test, expect } from "vitest"; +import { describe, test, expect } from 'vitest'; -import * as micro_schema from "../src"; +import * as micro_schema from '../src'; -describe("schema-tools", () => { - test("it should correctly prune unused definitions", () => { +describe('schema-tools', () => { + test('it should correctly prune unused definitions', () => { const schema = micro_schema.parseJSONSchema({ definitions: { // unused, should be stripped out a: { - type: "object", + type: 'object', properties: { prop: { - type: "string", - }, + type: 'string' + } }, - required: ["prop"], + required: ['prop'] }, // extended reference, should be included after walking the full schema b: { - type: "object", + type: 'object', properties: { prop: { - type: "string", - }, + type: 'string' + } }, - required: ["prop"], + required: ['prop'] }, b1: { - type: "object", + type: 'object', properties: { prop: { - $ref: "#/definitions/b", - }, + $ref: '#/definitions/b' + } }, - required: ["prop"], + required: ['prop'] }, // circular reference, should not result in the walker getting stuck c: { - type: "object", + type: 'object', properties: { prop: { - $ref: "#/definitions/c", - }, + $ref: '#/definitions/c' + } }, - required: ["prop"], - }, + required: ['prop'] + } }, - type: "object", + type: 'object', properties: { a: { - type: "object", + type: 'object', properties: { a: { - $ref: "#/definitions/b1", + $ref: '#/definitions/b1' }, b: { - enum: ["A"], - }, - }, + enum: ['A'] + } + } }, b: { oneOf: [ { - type: "object", + type: 'object', properties: { a: { - $ref: "#/definitions/c", - }, + $ref: '#/definitions/c' + } }, - required: ["a"], - additionalProperties: false, - }, - ], - }, - }, + required: ['a'], + additionalProperties: false + } + ] + } + } }); expect(schema.compile()).toMatchSnapshot(); diff --git a/packages/schema/tests/schema-validation.test.ts b/packages/schema/tests/schema-validation.test.ts index 5c8ff1a..0a48344 100644 --- a/packages/schema/tests/schema-validation.test.ts +++ b/packages/schema/tests/schema-validation.test.ts @@ -1,34 +1,34 @@ -import { describe, test, it, expect } from "vitest"; +import { describe, test, it, expect } from 'vitest'; -import base_schema from "./fixtures/schema"; -import * as micro_schema from "../src"; +import base_schema from './fixtures/schema'; +import * as micro_schema from '../src'; const base_validator = micro_schema.createSchemaValidator(base_schema); -describe("json-schema-validation", () => { - test("passes validation for json-schema", () => { +describe('json-schema-validation', () => { + test('passes validation for json-schema', () => { const result = base_validator.validate({ name: { - a: "1", - b: "A", + a: '1', + b: 'A' }, b: { - a: 2, - }, + a: 2 + } }); expect(result).toMatchSnapshot(); }); - test("fails validation for json-schema", () => { + test('fails validation for json-schema', () => { const result1 = base_validator.validate({ name: { - a: "1", - b: "B", + a: '1', + b: 'B' }, b: { - a: 2, - }, + a: 2 + } }); expect(result1).toMatchSnapshot(); @@ -36,59 +36,59 @@ describe("json-schema-validation", () => { const result2 = base_validator.validate({ name: {}, b: { - a: "", - }, + a: '' + } }); expect(result2).toMatchSnapshot(); }); - test("passes validation with refs", () => { + test('passes validation with refs', () => { const result = base_validator.validate({ name: { - a: "1", - b: "A", + a: '1', + b: 'A' }, b: { - a: 2, + a: 2 }, d: { - prop: "abc", - }, + prop: 'abc' + } }); expect(result).toMatchSnapshot(); }); - test("fails validation for json-schema due to additional properties", () => { + test('fails validation for json-schema due to additional properties', () => { const result = base_validator.validate({ name: { - a: "1", - b: "A", - c: "additional property", + a: '1', + b: 'A', + c: 'additional property' }, b: { - a: 2, - }, + a: 2 + } }); expect(result).toMatchSnapshot(); }); - test("passes json-schema validation with additional properties when allowed", () => { + test('passes json-schema validation with additional properties when allowed', () => { const validator = micro_schema.createSchemaValidator(base_schema, { - allowAdditional: true, + allowAdditional: true }); const result = validator.validate({ name: { - a: "1", - b: "A", - c: "additional property", + a: '1', + b: 'A', + c: 'additional property' }, b: { - a: 2, - }, + a: 2 + } }); expect(result).toMatchSnapshot(); @@ -97,79 +97,79 @@ describe("json-schema-validation", () => { const subschema = micro_schema.parseJSONSchema({ definitions: { a: { - type: "string", + type: 'string' }, b: { - type: "object", + type: 'object', properties: { - a: { type: "string" }, - b: { $ref: "#/definitions/a" }, + a: { type: 'string' }, + b: { $ref: '#/definitions/a' } }, - required: ["b"], - }, - }, + required: ['b'] + } + } }); - test("it should correctly validate subschemas", () => { + test('it should correctly validate subschemas', () => { const validator = subschema.definitions.b.validator(); const res1 = validator.validate({ - a: "a", - b: 1, + a: 'a', + b: 1 }); expect(res1).toMatchSnapshot(); const res2 = validator.validate({ - a: "a", - b: "b", + a: 'a', + b: 'b' }); expect(res2.valid).toBe(true); }); - test("it correctly validates node types", () => { + test('it correctly validates node types', () => { const validator = micro_schema.createSchemaValidator({ - type: "object", + type: 'object', properties: { a: { - nodeType: "buffer", + nodeType: 'buffer' }, b: { - nodeType: "date", - }, + nodeType: 'date' + } }, - required: ["a"], + required: ['a'] }); const res = validator.validate({ - a: Buffer.from("123"), - b: new Date(), + a: Buffer.from('123'), + b: new Date() }); expect(res.valid).toBe(true); const res2 = validator.validate({ - a: "123", + a: '123' }); expect(res2.valid).toBe(false); expect(res2).toMatchSnapshot(); }); - it("should fail to compile invalid node types", () => { + it('should fail to compile invalid node types', () => { try { micro_schema.createSchemaValidator({ - type: "object", + type: 'object', properties: { a: { - nodeType: "Buffer", + nodeType: 'Buffer' }, b: { - nodeType: "Date", + nodeType: 'Date' }, c: { - nodeType: "unknown", - }, + nodeType: 'unknown' + } }, - required: ["a", "b", "c"], + required: ['a', 'b', 'c'] }); } catch (err) { expect(err).toBeInstanceOf(micro_schema.SchemaValidatorError); diff --git a/packages/schema/tests/ts-codec-validation.test.ts b/packages/schema/tests/ts-codec-validation.test.ts index 6315dde..ef6b871 100644 --- a/packages/schema/tests/ts-codec-validation.test.ts +++ b/packages/schema/tests/ts-codec-validation.test.ts @@ -1,12 +1,12 @@ -import { describe, test, expect } from "vitest"; +import { describe, test, expect } from 'vitest'; -import * as micro_schema from "../src"; -import * as t from "ts-codec"; +import * as micro_schema from '../src'; +import * as t from 'ts-codec'; -describe("ts-codec validation", () => { +describe('ts-codec validation', () => { enum Values { - A = "A", - B = "B", + A = 'A', + B = 'B' } const codec = t.object({ @@ -14,7 +14,7 @@ describe("ts-codec validation", () => { surname: t.string, other: t.object({ a: t.array(t.string), - b: t.literal("optional").optional(), + b: t.literal('optional').optional() }), tuple: t.tuple([t.string, t.number]), or: t.number.or(t.string), @@ -22,80 +22,80 @@ describe("ts-codec validation", () => { complex: t .object({ - a: t.string, + a: t.string }) .and( t.object({ - b: t.number, - }), + b: t.number + }) ) .and( t.object({ c: t .object({ - a: t.string, + a: t.string }) .and( t .object({ - b: t.boolean, + b: t.boolean }) .or( t.object({ - c: t.number, - }), - ), - ), - }), - ), + c: t.number + }) + ) + ) + }) + ) }); - test("passes validation for codec", () => { + test('passes validation for codec', () => { const validator = micro_schema.createTsCodecValidator(codec); const result = validator.validate({ - name: "a", - surname: "b", + name: 'a', + surname: 'b', other: { - a: ["nice"], - b: "optional", + a: ['nice'], + b: 'optional' }, - tuple: ["string", 1], + tuple: ['string', 1], or: 1, enum: Values.A, complex: { - a: "", + a: '', b: 1, c: { - a: "", - b: true, - }, - }, + a: '', + b: true + } + } }); expect(result.valid).toBe(true); }); - test("fails validation for runtime codec", () => { + test('fails validation for runtime codec', () => { const validator = micro_schema.createTsCodecValidator(codec); const result = validator.validate({ // @ts-ignore name: 1, other: { - a: ["nice"], + a: ['nice'], // @ts-ignore - b: "op", + b: 'op' }, // @ts-ignore tuple: [1, 1], // @ts-ignore - enum: "c", + enum: 'c', // @ts-ignore or: [], // @ts-ignore - complex: {}, + complex: {} }); expect(result).toMatchSnapshot(); diff --git a/packages/schema/tests/utils.test.ts b/packages/schema/tests/utils.test.ts index 4f5ba78..4979648 100644 --- a/packages/schema/tests/utils.test.ts +++ b/packages/schema/tests/utils.test.ts @@ -1,40 +1,40 @@ -import { describe, test, expect } from "vitest"; +import { describe, test, expect } from 'vitest'; -import * as utils from "../src/utils"; -import schema from "./fixtures/schema"; +import * as utils from '../src/utils'; +import schema from './fixtures/schema'; -describe("utils", () => { - test("allow additional properties in a json schema", () => { +describe('utils', () => { + test('allow additional properties in a json schema', () => { const cleaned_schema = utils.allowAdditionalProperties(schema); expect(cleaned_schema).toMatchSnapshot(); }); - test("it should only modify additionalProperties if it is a boolean", () => { + test('it should only modify additionalProperties if it is a boolean', () => { const cleaned_schema = utils.allowAdditionalProperties({ definitions: { a: { - type: "object", + type: 'object', properties: { prop: { - type: "string", - }, + type: 'string' + } }, additionalProperties: false, - required: ["prop"], - }, + required: ['prop'] + } }, - type: "object", + type: 'object', properties: { name: { - type: "object", + type: 'object', additionalProperties: { - $ref: "#/definitions/a", - }, - }, + $ref: '#/definitions/a' + } + } }, - required: ["name", "b"], - additionalProperties: false, + required: ['name', 'b'], + additionalProperties: false }); expect(cleaned_schema).toMatchSnapshot(); }); diff --git a/packages/schema/tests/zod-validation.test.ts b/packages/schema/tests/zod-validation.test.ts index 97664a4..d37c2f5 100644 --- a/packages/schema/tests/zod-validation.test.ts +++ b/packages/schema/tests/zod-validation.test.ts @@ -1,34 +1,34 @@ -import { describe, test, expect } from "vitest"; +import { describe, test, expect } from 'vitest'; -import * as micro_schema from "../src"; -import * as t from "zod"; +import * as micro_schema from '../src'; +import * as t from 'zod'; -describe("zod-validation", () => { +describe('zod-validation', () => { const schema = t.object({ name: t.string(), surname: t.string(), other: t.object({ a: t.array(t.string()), - b: t.literal("optional").optional(), - }), + b: t.literal('optional').optional() + }) }); - test("passes validation for runtime codec", () => { + test('passes validation for runtime codec', () => { const validator = micro_schema.createZodValidator(schema); const result = validator.validate({ - name: "a", - surname: "b", + name: 'a', + surname: 'b', other: { - a: ["nice"], - b: "optional", - }, + a: ['nice'], + b: 'optional' + } }); expect(result).toMatchSnapshot(); }); - test("fails validation for runtime codec", () => { + test('fails validation for runtime codec', () => { const validator = micro_schema.createZodValidator(schema); const result = validator.validate({ @@ -36,8 +36,8 @@ describe("zod-validation", () => { name: 1, other: { // @ts-ignore - b: "op", - }, + b: 'op' + } }); expect(result).toMatchSnapshot(); diff --git a/packages/streaming/src/bson/buffer-array.ts b/packages/streaming/src/bson/buffer-array.ts index d200b6d..4f7c933 100644 --- a/packages/streaming/src/bson/buffer-array.ts +++ b/packages/streaming/src/bson/buffer-array.ts @@ -12,7 +12,7 @@ export const readBufferFromChunks = (chunks: Buffer[], size: number) => { if (current_size >= size) { return { buffer: Buffer.concat(batch), - chunks_read: batch.length, + chunks_read: batch.length }; } } @@ -25,10 +25,7 @@ export const readBufferFromChunks = (chunks: Buffer[], size: number) => { * * If more than `size` is read from the chunks array then the remainder is unshifted back onto the array */ -export const readBufferFromChunksAndModify = ( - chunks: Buffer[], - size: number, -): Buffer | null => { +export const readBufferFromChunksAndModify = (chunks: Buffer[], size: number): Buffer | null => { const res = readBufferFromChunks(chunks, size); if (!res) { return null; @@ -54,9 +51,7 @@ export const createReadableBufferArray = () => { let current_size = 0; return { push(...new_chunks: Array) { - const normalized_chunks = new_chunks.map((chunk) => - Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk), - ); + const normalized_chunks = new_chunks.map((chunk) => (Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))); chunks.push(...normalized_chunks); current_size = new_chunks.reduce((size, chunk) => { return size + chunk.length; @@ -84,7 +79,7 @@ export const createReadableBufferArray = () => { }, size() { return current_size; - }, + } }; }; diff --git a/packages/streaming/src/bson/decoder.ts b/packages/streaming/src/bson/decoder.ts index e4c7871..1c16ce1 100644 --- a/packages/streaming/src/bson/decoder.ts +++ b/packages/streaming/src/bson/decoder.ts @@ -1,7 +1,7 @@ -import * as buffer_array from "./buffer-array"; -import * as stream from "../core/cross-stream"; -import * as constants from "./constants"; -import * as bson from "bson"; +import * as buffer_array from './buffer-array'; +import * as stream from '../core/cross-stream'; +import * as constants from './constants'; +import * as bson from 'bson'; export type BSONStreamDecoderParams = { deserialize_options?: bson.DeserializeOptions; @@ -10,9 +10,7 @@ export type BSONStreamDecoderParams = { writableStrategy?: QueuingStrategy; readableStrategy?: QueuingStrategy; }; -export const createBSONStreamDecoder = ( - params?: BSONStreamDecoderParams, -) => { +export const createBSONStreamDecoder = (params?: BSONStreamDecoderParams) => { const buffer = buffer_array.createReadableBufferArray(); let frame_size: null | number = null; @@ -37,9 +35,9 @@ export const createBSONStreamDecoder = ( yield bson.deserialize(frame, { promoteBuffers: true, validation: { - utf8: false, + utf8: false }, - ...(params?.deserialize_options || {}), + ...(params?.deserialize_options || {}) }); } } @@ -47,7 +45,7 @@ export const createBSONStreamDecoder = ( let writableStrategy = params?.writableStrategy; if (!writableStrategy) { writableStrategy = new stream.ByteLengthStrategy({ - highWaterMark: 1024 * 16, + highWaterMark: 1024 * 16 }); } @@ -72,10 +70,10 @@ export const createBSONStreamDecoder = ( return; } - throw new Error("stream did not complete successfully"); - }, + throw new Error('stream did not complete successfully'); + } }, writableStrategy, - params?.readableStrategy, + params?.readableStrategy ); }; diff --git a/packages/streaming/src/bson/encoder.ts b/packages/streaming/src/bson/encoder.ts index 69eda45..b67701e 100644 --- a/packages/streaming/src/bson/encoder.ts +++ b/packages/streaming/src/bson/encoder.ts @@ -1,6 +1,6 @@ -import * as stream from "../core/cross-stream"; -import * as constants from "./constants"; -import * as bson from "bson"; +import * as stream from '../core/cross-stream'; +import * as constants from './constants'; +import * as bson from 'bson'; export type BSONStreamEncoderParams = { serialize_options?: bson.SerializeOptions; @@ -9,30 +9,26 @@ export type BSONStreamEncoderParams = { readableStrategy?: QueuingStrategy; }; -export const createBSONStreamEncoder = ( - params?: BSONStreamEncoderParams, -) => { +export const createBSONStreamEncoder = (params?: BSONStreamEncoderParams) => { let readableStrategy = params?.readableStrategy; if (!readableStrategy) { readableStrategy = new stream.ByteLengthStrategy({ - highWaterMark: 1024 * 16, + highWaterMark: 1024 * 16 }); } return new stream.Transform( { transform(chunk, controller) { - controller.enqueue( - Buffer.from(bson.serialize(chunk, params?.serialize_options)), - ); + controller.enqueue(Buffer.from(bson.serialize(chunk, params?.serialize_options))); }, flush(controller) { if (params?.sendTerminatorOnEnd ?? true) { controller.enqueue(constants.TERMINATOR); } - }, + } }, params?.writableStrategy, - readableStrategy, + readableStrategy ); }; diff --git a/packages/streaming/src/bson/header.ts b/packages/streaming/src/bson/header.ts index ca6e634..09ab136 100644 --- a/packages/streaming/src/bson/header.ts +++ b/packages/streaming/src/bson/header.ts @@ -1,5 +1,5 @@ -import * as buffer_array from "./buffer-array"; -import * as bson from "bson"; +import * as buffer_array from './buffer-array'; +import * as bson from 'bson'; export type DecodedResponse = { header: T; @@ -11,17 +11,14 @@ export type ExtractHeaderParams = { }; export const extractHeaderFromStream = async ( input_stream: AsyncIterable, - params?: ExtractHeaderParams, + params?: ExtractHeaderParams ): Promise> => { const iterator = input_stream[Symbol.asyncIterator](); const buffer = buffer_array.createReadableBufferArray(); let frame_size: number | null = null; - async function* resplice( - data: Buffer | null, - iterator: AsyncIterator, - ) { + async function* resplice(data: Buffer | null, iterator: AsyncIterator) { if (data) { yield data; } @@ -39,7 +36,7 @@ export const extractHeaderFromStream = async ( while (true) { const chunk = await iterator.next(); if (chunk.done) { - throw new Error("Stream did not complete successfully"); + throw new Error('Stream did not complete successfully'); } buffer.push(chunk.value); @@ -58,20 +55,17 @@ export const extractHeaderFromStream = async ( const header = bson.deserialize(frame, { promoteBuffers: true, - ...(params?.deserialize_options || {}), + ...(params?.deserialize_options || {}) }); return { header: header as T, - stream: resplice(buffer.read(buffer.size()), iterator), + stream: resplice(buffer.read(buffer.size()), iterator) }; } }; -export async function* prependHeaderToStream( - header: any, - input_stream: Iterable | AsyncIterable, -) { +export async function* prependHeaderToStream(header: any, input_stream: Iterable | AsyncIterable) { yield bson.serialize(header); yield* input_stream; } diff --git a/packages/streaming/src/bson/index.ts b/packages/streaming/src/bson/index.ts index 4f7d386..a11cf3b 100644 --- a/packages/streaming/src/bson/index.ts +++ b/packages/streaming/src/bson/index.ts @@ -1,5 +1,5 @@ -export * from "./buffer-array"; -export * from "./constants"; -export * from "./encoder"; -export * from "./decoder"; -export * from "./header"; +export * from './buffer-array'; +export * from './constants'; +export * from './encoder'; +export * from './decoder'; +export * from './header'; diff --git a/packages/streaming/src/core/backpressure.ts b/packages/streaming/src/core/backpressure.ts index 969c76d..8c3dad7 100644 --- a/packages/streaming/src/core/backpressure.ts +++ b/packages/streaming/src/core/backpressure.ts @@ -1,4 +1,4 @@ -import * as stream from "stream"; +import * as stream from 'stream'; export type SimpleReadableLike = stream.Readable | stream.Transform; @@ -29,9 +29,9 @@ export const push = async (readable: SimpleReadableLike, data: any) => { resolve(); }; - readable.once("error", errorHandler); - readable.once("drain", () => { - readable.removeListener("error", errorHandler); + readable.once('error', errorHandler); + readable.once('drain', () => { + readable.removeListener('error', errorHandler); resolve(); }); }); diff --git a/packages/streaming/src/core/cross-stream.ts b/packages/streaming/src/core/cross-stream.ts index 1361462..4eb4b77 100644 --- a/packages/streaming/src/core/cross-stream.ts +++ b/packages/streaming/src/core/cross-stream.ts @@ -1,16 +1,15 @@ -import type * as stream from "stream/web"; +import type * as stream from 'stream/web'; let Readable: typeof stream.ReadableStream; let Transform: typeof stream.TransformStream; let ByteLengthStrategy: typeof stream.ByteLengthQueuingStrategy; -if (typeof window !== "undefined") { +if (typeof window !== 'undefined') { Readable = ReadableStream as any; Transform = TransformStream as any; - ByteLengthStrategy = - ByteLengthQueuingStrategy as typeof stream.ByteLengthQueuingStrategy; + ByteLengthStrategy = ByteLengthQueuingStrategy as typeof stream.ByteLengthQueuingStrategy; } else { - const webstream = require("stream/web"); + const webstream = require('stream/web'); Readable = webstream.ReadableStream; Transform = webstream.TransformStream; ByteLengthStrategy = webstream.ByteLengthQueuingStrategy; diff --git a/packages/streaming/src/core/index.ts b/packages/streaming/src/core/index.ts index a9ba2f9..dc1f8f2 100644 --- a/packages/streaming/src/core/index.ts +++ b/packages/streaming/src/core/index.ts @@ -1,4 +1,4 @@ -export * from "./backpressure"; -export * from "./transformers"; -export * from "./node-utils"; -export * from "./utils"; +export * from './backpressure'; +export * from './transformers'; +export * from './node-utils'; +export * from './utils'; diff --git a/packages/streaming/src/core/node-utils.ts b/packages/streaming/src/core/node-utils.ts index bd3ecfc..e658818 100644 --- a/packages/streaming/src/core/node-utils.ts +++ b/packages/streaming/src/core/node-utils.ts @@ -1,4 +1,4 @@ -import * as stream from "stream"; +import * as stream from 'stream'; /** * Returns a promise that resolves once the given stream finishes. If the stream emits an error then @@ -6,8 +6,8 @@ import * as stream from "stream"; */ export const wait = (stream: stream.Stream) => { return new Promise((resolve, reject) => { - stream.on("error", reject); - stream.on("end", resolve); - stream.on("finish", resolve); + stream.on('error', reject); + stream.on('end', resolve); + stream.on('finish', resolve); }); }; diff --git a/packages/streaming/src/core/transformers.ts b/packages/streaming/src/core/transformers.ts index c9b7b01..07177e9 100644 --- a/packages/streaming/src/core/transformers.ts +++ b/packages/streaming/src/core/transformers.ts @@ -5,22 +5,16 @@ * https://github.com/tc39/proposal-iterator-helpers */ -import { StreamLike } from "./utils"; +import { StreamLike } from './utils'; export type ExtractStreamElementType> = - T extends Iterable - ? I - : T extends AsyncIterable - ? I - : never; + T extends Iterable ? I : T extends AsyncIterable ? I : never; /** * Takes n number of streams (or any AsyncIterators) and returns an AsyncGenerator that * yields the concatenated output of all input streams */ -export async function* concat>( - ...sources: S[] -): AsyncGenerator> { +export async function* concat>(...sources: S[]): AsyncGenerator> { for (const source of sources) { yield* source as unknown as AsyncGenerator>; } @@ -29,10 +23,7 @@ export async function* concat>( /** * Implements `Array.prototype.map` for iterables */ -export async function* map( - iterable: StreamLike, - transform: (element: I) => O | Promise, -) { +export async function* map(iterable: StreamLike, transform: (element: I) => O | Promise) { for await (const element of iterable) { yield await transform(element); } @@ -41,10 +32,7 @@ export async function* map( /** * Implements `Array.prototype.filter` for iterables */ -export async function* filter( - iterable: StreamLike, - comparator: (element: I) => boolean | Promise, -) { +export async function* filter(iterable: StreamLike, comparator: (element: I) => boolean | Promise) { for await (const element of iterable) { if (await comparator(element)) { yield element; @@ -59,14 +47,12 @@ type ReducedValue = { export const reduced = (value: T) => { return { __reduced: true, - value, + value }; }; -const isReducedValue = ( - value: any | ReducedValue, -): value is ReducedValue => { - return "__reduced" in value && value.__reduced; +const isReducedValue = (value: any | ReducedValue): value is ReducedValue => { + return '__reduced' in value && value.__reduced; }; /** @@ -76,11 +62,8 @@ const isReducedValue = ( */ export const reduce = async ( iterable: StreamLike, - reducer: ( - accumulator: A, - element: I, - ) => A | ReducedValue | Promise>, - init: A, + reducer: (accumulator: A, element: I) => A | ReducedValue | Promise>, + init: A ) => { let accumulator = init; for await (const element of iterable) { diff --git a/packages/streaming/src/core/utils.ts b/packages/streaming/src/core/utils.ts index 04d1d40..e9cf282 100644 --- a/packages/streaming/src/core/utils.ts +++ b/packages/streaming/src/core/utils.ts @@ -1,7 +1,7 @@ -import * as micro_schema from "@journeyapps-labs/micro-schema"; -import * as micro_errors from "@journeyapps-labs/micro-errors"; -import * as cross_stream from "./cross-stream"; -import type * as webstreams from "stream/web"; +import * as micro_schema from '@journeyapps-labs/micro-schema'; +import * as micro_errors from '@journeyapps-labs/micro-errors'; +import * as cross_stream from './cross-stream'; +import type * as webstreams from 'stream/web'; export type StreamLike = Iterable | AsyncIterable; @@ -11,9 +11,7 @@ export type StreamLike = Iterable | AsyncIterable; * This is only really intended to be used from browser runtimes or within code intended to * be used cross-labs. This is because Node ReadableStreams already implement AsyncIterators */ -export async function* iterableFromReadable( - readable: ReadableStream | webstreams.ReadableStream, -) { +export async function* iterableFromReadable(readable: ReadableStream | webstreams.ReadableStream) { const reader = readable.getReader(); try { @@ -36,7 +34,7 @@ export async function* iterableFromReadable( */ export const readableFrom = ( iterable: StreamLike, - strategy?: QueuingStrategy, + strategy?: QueuingStrategy ): webstreams.ReadableStream => { if (iterable instanceof cross_stream.Readable) { return iterable; @@ -64,19 +62,16 @@ export const readableFrom = ( }, async pull() { resume?.(); - }, + } }, - strategy, + strategy ); }; /** * Yield a generator that validates data flowing through it */ -export async function* validateDataStream( - iterable: StreamLike, - validator: micro_schema.MicroValidator, -) { +export async function* validateDataStream(iterable: StreamLike, validator: micro_schema.MicroValidator) { for await (const chunk of iterable) { const res = validator.validate(chunk); if (!res.valid) { diff --git a/packages/streaming/src/index.ts b/packages/streaming/src/index.ts index 7c901f4..ad35b15 100644 --- a/packages/streaming/src/index.ts +++ b/packages/streaming/src/index.ts @@ -1,4 +1,4 @@ -export * as compat from "./core/cross-stream"; -export * as middleware from "./middleware"; -export * as bson from "./bson"; -export * from "./core"; +export * as compat from './core/cross-stream'; +export * as middleware from './middleware'; +export * as bson from './bson'; +export * from './core'; diff --git a/packages/streaming/src/middleware/index.ts b/packages/streaming/src/middleware/index.ts index 8421866..fc01127 100644 --- a/packages/streaming/src/middleware/index.ts +++ b/packages/streaming/src/middleware/index.ts @@ -1,2 +1,2 @@ -export * from "./streamed-body-parser-v1"; -export * from "./streamed-body-parser-v2"; +export * from './streamed-body-parser-v1'; +export * from './streamed-body-parser-v2'; diff --git a/packages/streaming/src/middleware/streamed-body-parser-v1.ts b/packages/streaming/src/middleware/streamed-body-parser-v1.ts index 8cca5b8..3e009a4 100644 --- a/packages/streaming/src/middleware/streamed-body-parser-v1.ts +++ b/packages/streaming/src/middleware/streamed-body-parser-v1.ts @@ -1,5 +1,5 @@ -import * as querystring from "querystring"; -import * as express from "express"; +import * as querystring from 'querystring'; +import * as express from 'express'; /** * Parse an incoming request decoding a base64 encoded, JSON formatted `payload` query string @@ -15,20 +15,16 @@ import * as express from "express"; * * @deprecated */ -export const streamedRequestBodyParser = ( - req: express.Request, - res: express.Response, - next: express.NextFunction, -) => { +export const streamedRequestBodyParser = (req: express.Request, res: express.Response, next: express.NextFunction) => { try { let payload = req.query.payload; if (!payload) { return next(); } - if (typeof payload !== "string") { + if (typeof payload !== 'string') { return next(); } - req.body = JSON.parse(Buffer.from(payload, "base64").toString()); + req.body = JSON.parse(Buffer.from(payload, 'base64').toString()); delete req.query.payload; next(); @@ -45,9 +41,7 @@ export const streamedRequestBodyParser = ( * @deprecated */ export const encodeStreamingPayload = (data: object) => { - return querystring.escape( - Buffer.from(JSON.stringify(data)).toString("base64"), - ); + return querystring.escape(Buffer.from(JSON.stringify(data)).toString('base64')); }; /** diff --git a/packages/streaming/src/middleware/streamed-body-parser-v2.ts b/packages/streaming/src/middleware/streamed-body-parser-v2.ts index 4719f33..9cecea0 100644 --- a/packages/streaming/src/middleware/streamed-body-parser-v2.ts +++ b/packages/streaming/src/middleware/streamed-body-parser-v2.ts @@ -1,11 +1,11 @@ -declare module "express" { +declare module 'express' { interface Request { stream?: AsyncIterable; } } -import * as stream_headers from "../bson/header"; -import * as express from "express"; +import * as stream_headers from '../bson/header'; +import * as express from 'express'; /** * Parse an incoming request by decoding the body as a bson stream. This will read the first @@ -15,15 +15,14 @@ import * as express from "express"; export const streamedRequestBodyParserV2 = async ( req: express.Request, _: express.Response, - next: express.NextFunction, + next: express.NextFunction ) => { try { - if (!req.is("application/*+header")) { + if (!req.is('application/*+header')) { return next(); } - const { header, stream } = - await stream_headers.extractHeaderFromStream(req); + const { header, stream } = await stream_headers.extractHeaderFromStream(req); req.stream = stream; req.body = header; diff --git a/packages/streaming/src/web.ts b/packages/streaming/src/web.ts index 8eac817..e511714 100644 --- a/packages/streaming/src/web.ts +++ b/packages/streaming/src/web.ts @@ -1,5 +1,5 @@ -export * as compat from "./core/cross-stream"; -export * as bson from "./bson"; +export * as compat from './core/cross-stream'; +export * as bson from './bson'; -export * from "./core/transformers"; -export * from "./core/utils"; +export * from './core/transformers'; +export * from './core/utils'; diff --git a/packages/streaming/tests/bson-transform.test.ts b/packages/streaming/tests/bson-transform.test.ts index 5b36a38..25d8822 100644 --- a/packages/streaming/tests/bson-transform.test.ts +++ b/packages/streaming/tests/bson-transform.test.ts @@ -1,23 +1,19 @@ -import { describe, test, expect } from "vitest"; +import { describe, test, expect } from 'vitest'; -import * as micro_streaming from "../src"; -import * as stream from "stream/web"; -import * as bson from "bson"; +import * as micro_streaming from '../src'; +import * as stream from 'stream/web'; +import * as bson from 'bson'; -describe("bson-transformer", () => { - test("it successfully reads data from chunks array", () => { +describe('bson-transformer', () => { + test('it successfully reads data from chunks array', () => { const chunks = Array.from(Array(10).keys()).map((_, i) => Buffer.alloc(i)); expect(micro_streaming.bson.readBufferFromChunks(chunks, 50)).toBe(null); - expect( - micro_streaming.bson.readBufferFromChunks(chunks, 5)?.chunks_read, - ).toBe(4); - expect( - micro_streaming.bson.readBufferFromChunks(chunks, 2)?.buffer.length, - ).toBe(3); + expect(micro_streaming.bson.readBufferFromChunks(chunks, 5)?.chunks_read).toBe(4); + expect(micro_streaming.bson.readBufferFromChunks(chunks, 2)?.buffer.length).toBe(3); }); - test("it successfully reads data from chunks array, modifying original", () => { + test('it successfully reads data from chunks array, modifying original', () => { const chunks = Array.from(Array(10).keys()).map((_, i) => Buffer.alloc(i)); const read1 = micro_streaming.bson.readBufferFromChunksAndModify(chunks, 1); @@ -30,42 +26,38 @@ describe("bson-transformer", () => { expect(chunks[0].length).toBe(1); }); - test("it successfully deserializes streamed bson data", async () => { + test('it successfully deserializes streamed bson data', async () => { const source = new stream.ReadableStream({ start(controller) { - controller.enqueue(bson.serialize({ a: "b" })); - controller.enqueue(bson.serialize({ c: "d" })); - controller.enqueue(bson.serialize({ e: "f" })); + controller.enqueue(bson.serialize({ a: 'b' })); + controller.enqueue(bson.serialize({ c: 'd' })); + controller.enqueue(bson.serialize({ e: 'f' })); controller.enqueue(micro_streaming.bson.TERMINATOR); controller.close(); - }, + } }); - const sink = source.pipeThrough( - micro_streaming.bson.createBSONStreamDecoder(), - ); + const sink = source.pipeThrough(micro_streaming.bson.createBSONStreamDecoder()); expect(await micro_streaming.drain(sink)).toMatchSnapshot(); }); - test("it successfully serializes objects to bson stream", async () => { + test('it successfully serializes objects to bson stream', async () => { const source = new stream.ReadableStream({ start(controller) { - controller.enqueue({ a: "b" }); - controller.enqueue({ c: "d" }); - controller.enqueue({ e: "f" }); + controller.enqueue({ a: 'b' }); + controller.enqueue({ c: 'd' }); + controller.enqueue({ e: 'f' }); controller.close(); - }, + } }); - const sink = source.pipeThrough( - micro_streaming.bson.createBSONStreamEncoder(), - ); + const sink = source.pipeThrough(micro_streaming.bson.createBSONStreamEncoder()); expect(Buffer.concat(await micro_streaming.drain(sink))).toMatchSnapshot(); }); - test("end-to-end streaming", async () => { + test('end-to-end streaming', async () => { const output = micro_streaming - .readableFrom([{ a: "b" }, { c: "d" }, { e: "f" }]) + .readableFrom([{ a: 'b' }, { c: 'd' }, { e: 'f' }]) .pipeThrough(micro_streaming.bson.createBSONStreamEncoder()) .pipeThrough(micro_streaming.bson.createBSONStreamDecoder()); diff --git a/packages/streaming/tests/common.test.ts b/packages/streaming/tests/common.test.ts index d0322a6..920e582 100644 --- a/packages/streaming/tests/common.test.ts +++ b/packages/streaming/tests/common.test.ts @@ -1,16 +1,16 @@ -import { describe, test, expect } from "vitest"; +import { describe, test, expect } from 'vitest'; -import * as micro_schema from "@journeyapps-labs/micro-schema"; -import * as micro_errors from "@journeyapps-labs/micro-errors"; -import * as micro_streaming from "../src"; -import * as _ from "lodash"; +import * as micro_schema from '@journeyapps-labs/micro-schema'; +import * as micro_errors from '@journeyapps-labs/micro-errors'; +import * as micro_streaming from '../src'; +import * as _ from 'lodash'; -describe("common", () => { - test("it should concatenate two streams", async () => { +describe('common', () => { + test('it should concatenate two streams', async () => { async function* one() { for (let i = 0; i < 10; i++) { yield { - value: i, + value: i }; } } @@ -18,7 +18,7 @@ describe("common", () => { async function* two() { for (let i = 0; i < 10; i++) { yield { - value: 10 + i, + value: 10 + i }; } } @@ -27,11 +27,11 @@ describe("common", () => { expect(await micro_streaming.drain(concat_stream)).toMatchSnapshot(); }); - test("it should validate stream data", async () => { + test('it should validate stream data', async () => { function* generateLessThan10() { for (const i of _.range(10)) { yield { - item: i, + item: i }; } } @@ -39,7 +39,7 @@ describe("common", () => { function* generateGreaterThan10() { for (const i of _.range(15)) { yield { - item: i, + item: i }; } } @@ -48,29 +48,21 @@ describe("common", () => { validate: (datum) => { if (datum.item < 10) { return { - valid: true, + valid: true }; } return { valid: false, - errors: ["not less than 10"], + errors: ['not less than 10'] }; - }, + } }; - const validated_correct = micro_streaming.validateDataStream( - generateLessThan10(), - validator, - ); + const validated_correct = micro_streaming.validateDataStream(generateLessThan10(), validator); await micro_streaming.drain(validated_correct); - const validated_incorrect = micro_streaming.validateDataStream( - generateGreaterThan10(), - validator, - ); - await expect( - micro_streaming.drain(validated_incorrect), - ).rejects.toThrowError(micro_errors.ValidationError); + const validated_incorrect = micro_streaming.validateDataStream(generateGreaterThan10(), validator); + await expect(micro_streaming.drain(validated_incorrect)).rejects.toThrowError(micro_errors.ValidationError); }); }); diff --git a/packages/streaming/tests/header.test.ts b/packages/streaming/tests/header.test.ts index 3995064..3cec74d 100644 --- a/packages/streaming/tests/header.test.ts +++ b/packages/streaming/tests/header.test.ts @@ -1,19 +1,18 @@ -import { describe, test, expect } from "vitest"; +import { describe, test, expect } from 'vitest'; -import * as micro_streaming from "../src"; -import * as crypto from "crypto"; -import * as _ from "lodash"; +import * as micro_streaming from '../src'; +import * as crypto from 'crypto'; +import * as _ from 'lodash'; -describe("bson-stream-header", () => { - test("it should successfully extract the header from a bson stream", async () => { +describe('bson-stream-header', () => { + test('it should successfully extract the header from a bson stream', async () => { const bson_stream = micro_streaming - .readableFrom([{ a: "b" }, { c: "d" }]) + .readableFrom([{ a: 'b' }, { c: 'd' }]) .pipeThrough(micro_streaming.bson.createBSONStreamEncoder()); - const { header, stream: remaining } = - await micro_streaming.bson.extractHeaderFromStream(bson_stream); + const { header, stream: remaining } = await micro_streaming.bson.extractHeaderFromStream(bson_stream); expect(header).toEqual({ - a: "b", + a: 'b' }); const decoded_stream = micro_streaming @@ -21,16 +20,16 @@ describe("bson-stream-header", () => { .pipeThrough(micro_streaming.bson.createBSONStreamDecoder()); expect(await micro_streaming.drain(decoded_stream)).toEqual([ { - c: "d", - }, + c: 'd' + } ]); }); - test("it should handle a lot of data", async () => { + test('it should handle a lot of data', async () => { function* generator() { for (const i of _.range(20)) { yield { - data: crypto.randomBytes(1024 * 1024), + data: crypto.randomBytes(1024 * 1024) }; } } @@ -39,29 +38,24 @@ describe("bson-stream-header", () => { .readableFrom(generator()) .pipeThrough(micro_streaming.bson.createBSONStreamEncoder()); - const { header, stream: remaining } = - await micro_streaming.bson.extractHeaderFromStream(bson_stream); + const { header, stream: remaining } = await micro_streaming.bson.extractHeaderFromStream(bson_stream); expect(Buffer.isBuffer(header.data)).toBe(true); const decoded_stream = await micro_streaming.drain( - micro_streaming - .readableFrom(remaining) - .pipeThrough(micro_streaming.bson.createBSONStreamDecoder()), + micro_streaming.readableFrom(remaining).pipeThrough(micro_streaming.bson.createBSONStreamDecoder()) ); expect(decoded_stream.length).toBe(19); }); - test("it should properly prepend a header to a bson stream", async () => { - const data = [{ a: "b" }, { c: "d" }]; - const bson_stream = micro_streaming - .readableFrom(data) - .pipeThrough(micro_streaming.bson.createBSONStreamEncoder()); + test('it should properly prepend a header to a bson stream', async () => { + const data = [{ a: 'b' }, { c: 'd' }]; + const bson_stream = micro_streaming.readableFrom(data).pipeThrough(micro_streaming.bson.createBSONStreamEncoder()); const stream_with_header = micro_streaming.bson.prependHeaderToStream( { - key: "value", + key: 'value' }, - bson_stream, + bson_stream ); const decoded = micro_streaming @@ -70,9 +64,9 @@ describe("bson-stream-header", () => { expect(await micro_streaming.drain(decoded)).toEqual([ { - key: "value", + key: 'value' }, - ...data, + ...data ]); }); }); From 0f4dcacbe5884af7a3c731a532f791dcd89e6275 Mon Sep 17 00:00:00 2001 From: Dylan Vorster Date: Tue, 7 Oct 2025 12:15:02 -0600 Subject: [PATCH 3/5] readme --- README.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d278435..90cd311 100644 --- a/README.md +++ b/README.md @@ -1 +1,15 @@ -# micro +# JourneyApps Labs Micro + +Micro-service framework + +## Release + +### Dev release + +1. Ensure a changeset has been created `pnpm changeset` +2. Run https://github.com/journeyapps-labs/common/actions/workflows/dev-packages.yaml the workflow manually on the branch you need + +### Production release + +1. Ensure a changeset has been created `pnpm changeset` +2. Merge PR and then merge the versions PR which gets created. \ No newline at end of file From 09503335d8728723ffd0668cd6ad702677470430 Mon Sep 17 00:00:00 2001 From: Dylan Vorster Date: Tue, 7 Oct 2025 12:15:51 -0600 Subject: [PATCH 4/5] packages --- packages/codecs/package.json | 2 +- packages/errors/package.json | 2 +- packages/schema/package.json | 2 +- packages/streaming/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/codecs/package.json b/packages/codecs/package.json index 17dde0c..536e86f 100644 --- a/packages/codecs/package.json +++ b/packages/codecs/package.json @@ -2,7 +2,7 @@ "name": "@journeyapps-labs/micro-codecs", "main": "./dist/index", "typings": "./dist/index", - "version": "1.0.0", + "version": "0.0.1", "repository": "https://github.com/journeyapps-labs/journey-micro", "files": [ "dist/**" diff --git a/packages/errors/package.json b/packages/errors/package.json index 349efd6..f79c4e9 100644 --- a/packages/errors/package.json +++ b/packages/errors/package.json @@ -2,7 +2,7 @@ "name": "@journeyapps-labs/micro-errors", "main": "./dist/index", "typings": "./dist/index", - "version": "1.0.0", + "version": "0.0.1", "repository": "https://github.com/journeyapps-labs/journey-micro", "files": [ "dist/**" diff --git a/packages/schema/package.json b/packages/schema/package.json index 6e8cf35..c395e87 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -2,7 +2,7 @@ "name": "@journeyapps-labs/micro-schema", "main": "./dist/index", "typings": "./dist/index", - "version": "1.0.0", + "version": "0.0.1", "repository": "https://github.com/journeyapps-labs/journey-micro", "scripts": { "test": "vitest" diff --git a/packages/streaming/package.json b/packages/streaming/package.json index bf2db31..5393c3f 100644 --- a/packages/streaming/package.json +++ b/packages/streaming/package.json @@ -3,7 +3,7 @@ "main": "./dist/index", "browser": "./dist/web", "typings": "./dist/index", - "version": "1.0.0", + "version": "0.0.1", "repository": "https://github.com/journeyapps-labs/journey-micro", "files": [ "dist/**" From 3b6c50263f9376d74729693ea1b5a27c69aefd19 Mon Sep 17 00:00:00 2001 From: Dylan Vorster Date: Tue, 7 Oct 2025 12:16:21 -0600 Subject: [PATCH 5/5] changeset --- .changeset/dull-parrots-sip.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/dull-parrots-sip.md diff --git a/.changeset/dull-parrots-sip.md b/.changeset/dull-parrots-sip.md new file mode 100644 index 0000000..9871617 --- /dev/null +++ b/.changeset/dull-parrots-sip.md @@ -0,0 +1,8 @@ +--- +'@journeyapps-labs/micro-streaming': major +'@journeyapps-labs/micro-codecs': major +'@journeyapps-labs/micro-errors': major +'@journeyapps-labs/micro-schema': major +--- + +Initial publish