diff --git a/BREAKINGCHANGES.md b/BREAKINGCHANGES.md index 03adfae1..cbca9b8d 100644 --- a/BREAKINGCHANGES.md +++ b/BREAKINGCHANGES.md @@ -1,3 +1,3 @@ -1. `auth()` cannot be directly compared with a relation anymore -2. `update` and `delete` policy rejection throws `NotFoundError` -3. non-optional to-one relation doesn't automatically filter parent read when evaluating access policies +1. `update` and `delete` policy rejection throws `NotFoundError` +1. `check()` ORM api has been removed +1. non-optional to-one relation doesn't automatically filter parent read when evaluating access policies diff --git a/TODO.md b/TODO.md index 49ae0537..f472535a 100644 --- a/TODO.md +++ b/TODO.md @@ -86,7 +86,7 @@ - [ ] DbNull vs JsonNull - [ ] Migrate to tsdown - [x] @default validation - - [ ] Benchmark + - [x] Benchmark - [x] Plugin - [x] Post-mutation hooks should be called after transaction is committed - [x] TypeDef and mixin diff --git a/packages/common-helpers/src/index.ts b/packages/common-helpers/src/index.ts index 5b63ae85..9a3b3678 100644 --- a/packages/common-helpers/src/index.ts +++ b/packages/common-helpers/src/index.ts @@ -1,6 +1,7 @@ export * from './is-plain-object'; export * from './lower-case-first'; export * from './param-case'; +export * from './safe-json-stringify'; export * from './sleep'; export * from './tiny-invariant'; export * from './upper-case-first'; diff --git a/packages/common-helpers/src/safe-json-stringify.ts b/packages/common-helpers/src/safe-json-stringify.ts new file mode 100644 index 00000000..0a49d685 --- /dev/null +++ b/packages/common-helpers/src/safe-json-stringify.ts @@ -0,0 +1,12 @@ +/** + * A safe JSON stringify that handles bigint values. + */ +export function safeJSONStringify(value: unknown) { + return JSON.stringify(value, (_, v) => { + if (typeof v === 'bigint') { + return v.toString(); + } else { + return v; + } + }); +} diff --git a/packages/language/package.json b/packages/language/package.json index b6653419..b5c31a50 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -13,6 +13,7 @@ "build": "pnpm langium:generate && tsc --noEmit && tsup-node", "watch": "tsup-node --watch", "lint": "eslint src --ext ts", + "test": "vitest run", "langium:generate": "langium generate", "langium:generate:production": "langium generate --mode=production", "pack": "pnpm pack" diff --git a/packages/language/src/constants.ts b/packages/language/src/constants.ts index 973880a4..a39b4913 100644 --- a/packages/language/src/constants.ts +++ b/packages/language/src/constants.ts @@ -1,5 +1,5 @@ /** - * Supported Prisma db providers + * Supported db providers */ export const SUPPORTED_PROVIDERS = [ 'sqlite', diff --git a/packages/plugins/policy/src/policy-handler.ts b/packages/plugins/policy/src/policy-handler.ts index c90bb563..4a845a50 100644 --- a/packages/plugins/policy/src/policy-handler.ts +++ b/packages/plugins/policy/src/policy-handler.ts @@ -104,7 +104,7 @@ export class PolicyHandler extends OperationNodeTransf if (constCondition === true) { needCheckPreCreate = false; } else if (constCondition === false) { - throw new RejectedByPolicyError(mutationModel); + throw new RejectedByPolicyError(mutationModel, RejectedByPolicyReason.NO_ACCESS); } } @@ -621,7 +621,7 @@ export class PolicyHandler extends OperationNodeTransf const result = await proceed(preCreateCheck); if (!result.rows[0]?.$condition) { - throw new RejectedByPolicyError(model); + throw new RejectedByPolicyError(model, RejectedByPolicyReason.NO_ACCESS); } } diff --git a/packages/runtime/package.json b/packages/runtime/package.json index c4f65761..ba989200 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -64,7 +64,7 @@ "dependencies": { "@paralleldrive/cuid2": "^2.2.2", "@zenstackhq/common-helpers": "workspace:*", - "decimal.js": "^10.4.3", + "decimal.js": "catalog:", "json-stable-stringify": "^1.3.0", "nanoid": "^5.0.9", "toposort": "^2.0.2", diff --git a/packages/runtime/src/client/crud/dialects/postgresql.ts b/packages/runtime/src/client/crud/dialects/postgresql.ts index 2aec3a67..a1fe784a 100644 --- a/packages/runtime/src/client/crud/dialects/postgresql.ts +++ b/packages/runtime/src/client/crud/dialects/postgresql.ts @@ -111,7 +111,12 @@ export class PostgresCrudDialect extends BaseCrudDiale } private transformOutputBytes(value: unknown) { - return Buffer.isBuffer(value) ? Uint8Array.from(value) : value; + return Buffer.isBuffer(value) + ? Uint8Array.from(value) + : // node-pg encode bytea as hex string prefixed with \x when embedded in JSON + typeof value === 'string' && value.startsWith('\\x') + ? Uint8Array.from(Buffer.from(value.slice(2), 'hex')) + : value; } override buildRelationSelection( diff --git a/packages/runtime/src/client/crud/validator/index.ts b/packages/runtime/src/client/crud/validator/index.ts index 1e32a865..0c997ec1 100644 --- a/packages/runtime/src/client/crud/validator/index.ts +++ b/packages/runtime/src/client/crud/validator/index.ts @@ -232,6 +232,7 @@ export class InputValidator { const { error, data } = schema.safeParse(args); if (error) { throw new InputValidationError( + model, `Invalid ${operation} args for model "${model}": ${formatError(error)}`, error, ); diff --git a/packages/runtime/src/client/errors.ts b/packages/runtime/src/client/errors.ts index c1be626a..89ef02f2 100644 --- a/packages/runtime/src/client/errors.ts +++ b/packages/runtime/src/client/errors.ts @@ -7,7 +7,11 @@ export class ZenStackError extends Error {} * Error thrown when input validation fails. */ export class InputValidationError extends ZenStackError { - constructor(message: string, cause?: unknown) { + constructor( + public readonly model: string, + message: string, + cause?: unknown, + ) { super(message, { cause }); } } @@ -30,7 +34,10 @@ export class InternalError extends ZenStackError {} * Error thrown when an entity is not found. */ export class NotFoundError extends ZenStackError { - constructor(model: string, details?: string) { + constructor( + public readonly model: string, + details?: string, + ) { super(`Entity not found for model "${model}"${details ? `: ${details}` : ''}`); } } diff --git a/packages/runtime/src/client/helpers/schema-db-pusher.ts b/packages/runtime/src/client/helpers/schema-db-pusher.ts index a666b4d8..52fc462b 100644 --- a/packages/runtime/src/client/helpers/schema-db-pusher.ts +++ b/packages/runtime/src/client/helpers/schema-db-pusher.ts @@ -294,9 +294,16 @@ export class SchemaDbPusher { (cb) => { if (fieldDef.relation?.onDelete) { cb = cb.onDelete(this.mapCascadeAction(fieldDef.relation.onDelete)); + } else if (fieldDef.optional) { + cb = cb.onDelete('set null'); + } else { + cb = cb.onDelete('restrict'); } + if (fieldDef.relation?.onUpdate) { cb = cb.onUpdate(this.mapCascadeAction(fieldDef.relation.onUpdate)); + } else { + cb = cb.onUpdate('cascade'); } return cb; }, diff --git a/packages/runtime/src/client/query-utils.ts b/packages/runtime/src/client/query-utils.ts index 9f97305b..6bd51435 100644 --- a/packages/runtime/src/client/query-utils.ts +++ b/packages/runtime/src/client/query-utils.ts @@ -315,16 +315,6 @@ export function ensureArray(value: T | T[]): T[] { } } -export function safeJSONStringify(value: unknown) { - return JSON.stringify(value, (_, v) => { - if (typeof v === 'bigint') { - return v.toString(); - } else { - return v; - } - }); -} - export function extractIdFields(entity: any, schema: SchemaDef, model: string) { const idFields = requireIdFields(schema, model); return extractFields(entity, idFields); diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 7a03329a..5c688f47 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -47,7 +47,7 @@ "devDependencies": { "@zenstackhq/eslint-config": "workspace:*", "@zenstackhq/typescript-config": "workspace:*", - "decimal.js": "^10.4.3", + "decimal.js": "catalog:", "kysely": "catalog:" } } diff --git a/packages/server/eslint.config.js b/packages/server/eslint.config.js new file mode 100644 index 00000000..5698b991 --- /dev/null +++ b/packages/server/eslint.config.js @@ -0,0 +1,4 @@ +import config from '@zenstackhq/eslint-config/base.js'; + +/** @type {import("eslint").Linter.Config} */ +export default config; diff --git a/packages/server/package.json b/packages/server/package.json new file mode 100644 index 00000000..84e79aea --- /dev/null +++ b/packages/server/package.json @@ -0,0 +1,79 @@ +{ + "name": "@zenstackhq/server", + "version": "3.0.0-beta.12", + "description": "ZenStack automatic CRUD API handlers and server adapters", + "type": "module", + "scripts": { + "build": "tsc --noEmit && tsup-node", + "watch": "tsup-node --watch", + "lint": "eslint src --ext ts", + "test": "vitest run", + "pack": "pnpm pack" + }, + "keywords": [ + "fastify", + "express", + "nextjs", + "sveltekit", + "nuxtjs", + "elysia", + "tanstack-start" + ], + "author": "ZenStack Team", + "license": "MIT", + "files": [ + "dist" + ], + "exports": { + "./package.json": { + "import": "./package.json", + "require": "./package.json" + }, + "./api": { + "import": { + "types": "./dist/api.d.ts", + "default": "./dist/api.js" + }, + "require": { + "types": "./dist/api.d.cts", + "default": "./dist/api.cjs" + } + }, + "./express": { + "import": { + "types": "./dist/express.d.ts", + "default": "./dist/express.js" + }, + "require": { + "types": "./dist/express.d.cts", + "default": "./dist/express.cjs" + } + } + }, + "dependencies": { + "@zenstackhq/common-helpers": "workspace:*", + "@zenstackhq/runtime": "workspace:*", + "decimal.js": "catalog:", + "superjson": "^2.2.3", + "ts-pattern": "catalog:" + }, + "devDependencies": { + "@types/body-parser": "^1.19.6", + "@types/express": "^5.0.0", + "@types/supertest": "^6.0.3", + "@zenstackhq/eslint-config": "workspace:*", + "@zenstackhq/testtools": "workspace:*", + "@zenstackhq/typescript-config": "workspace:*", + "@zenstackhq/vitest-config": "workspace:*", + "body-parser": "^2.2.0", + "supertest": "^7.1.4" + }, + "peerDependencies": { + "express": "^5.0.0" + }, + "peerDependenciesMeta": { + "express": { + "optional": true + } + } +} diff --git a/packages/server/src/api/index.ts b/packages/server/src/api/index.ts new file mode 100644 index 00000000..b415bd06 --- /dev/null +++ b/packages/server/src/api/index.ts @@ -0,0 +1 @@ +export { RPCApiHandler } from './rpc'; diff --git a/packages/server/src/api/rpc/index.ts b/packages/server/src/api/rpc/index.ts new file mode 100644 index 00000000..28cb017b --- /dev/null +++ b/packages/server/src/api/rpc/index.ts @@ -0,0 +1,244 @@ +import { lowerCaseFirst, safeJSONStringify } from '@zenstackhq/common-helpers'; +import { + InputValidationError, + NotFoundError, + RejectedByPolicyError, + ZenStackError, + type ClientContract, +} from '@zenstackhq/runtime'; +import type { SchemaDef } from '@zenstackhq/runtime/schema'; +import SuperJSON from 'superjson'; +import type { ApiHandler, LogConfig, RequestContext, Response } from '../../types'; +import { log, registerCustomSerializers } from '../utils'; + +registerCustomSerializers(); + +/** + * Options for {@link RPCApiHandler} + */ +export type RPCApiHandlerOptions = { + schema: Schema; + log?: LogConfig; +}; + +/** + * RPC style API request handler that mirrors the ZenStackClient API + */ +export class RPCApiHandler implements ApiHandler { + constructor(private readonly options: RPCApiHandlerOptions) {} + + get schema(): Schema { + return this.options.schema; + } + + async handleRequest({ client, method, path, query, requestBody }: RequestContext): Promise { + const parts = path.split('/').filter((p) => !!p); + const op = parts.pop(); + let model = parts.pop(); + + if (parts.length !== 0 || !op || !model) { + return this.makeBadInputErrorResponse('invalid request path'); + } + + model = lowerCaseFirst(model); + method = method.toUpperCase(); + let args: unknown; + let resCode = 200; + + switch (op) { + case 'create': + case 'createMany': + case 'createManyAndReturn': + case 'upsert': + if (method !== 'POST') { + return this.makeBadInputErrorResponse('invalid request method, only POST is supported'); + } + if (!requestBody) { + return this.makeBadInputErrorResponse('missing request body'); + } + + args = requestBody; + resCode = 201; + break; + + case 'findFirst': + case 'findUnique': + case 'findMany': + case 'aggregate': + case 'groupBy': + case 'count': + if (method !== 'GET') { + return this.makeBadInputErrorResponse('invalid request method, only GET is supported'); + } + try { + args = query?.['q'] + ? this.unmarshalQ(query['q'] as string, query['meta'] as string | undefined) + : {}; + } catch { + return this.makeBadInputErrorResponse('invalid "q" query parameter'); + } + break; + + case 'update': + case 'updateMany': + case 'updateManyAndReturn': + if (method !== 'PUT' && method !== 'PATCH') { + return this.makeBadInputErrorResponse('invalid request method, only PUT or PATCH are supported'); + } + if (!requestBody) { + return this.makeBadInputErrorResponse('missing request body'); + } + + args = requestBody; + break; + + case 'delete': + case 'deleteMany': + if (method !== 'DELETE') { + return this.makeBadInputErrorResponse('invalid request method, only DELETE is supported'); + } + try { + args = query?.['q'] + ? this.unmarshalQ(query['q'] as string, query['meta'] as string | undefined) + : {}; + } catch (err) { + return this.makeBadInputErrorResponse( + err instanceof Error ? err.message : 'invalid "q" query parameter', + ); + } + break; + + default: + return this.makeBadInputErrorResponse('invalid operation: ' + op); + } + + const { result: processedArgs, error } = await this.processRequestPayload(args); + if (error) { + return this.makeBadInputErrorResponse(error); + } + + try { + if (!this.isValidModel(client, model)) { + return this.makeBadInputErrorResponse(`unknown model name: ${model}`); + } + + log( + this.options.log, + 'debug', + () => `handling "${model}.${op}" request with args: ${safeJSONStringify(processedArgs)}`, + ); + + const clientResult = await (client as any)[model][op](processedArgs); + let responseBody: any = { data: clientResult }; + + // superjson serialize response + if (clientResult) { + const { json, meta } = SuperJSON.serialize(clientResult); + responseBody = { data: json }; + if (meta) { + responseBody.meta = { serialization: meta }; + } + } + + const response = { status: resCode, body: responseBody }; + log( + this.options.log, + 'debug', + () => `sending response for "${model}.${op}" request: ${safeJSONStringify(response)}`, + ); + return response; + } catch (err) { + log(this.options.log, 'error', `error occurred when handling "${model}.${op}" request`, err); + if (err instanceof ZenStackError) { + return this.makeZenStackErrorResponse(err); + } else { + return this.makeGenericErrorResponse(err); + } + } + } + + private isValidModel(client: ClientContract, model: string) { + return Object.keys(client.$schema.models).some((m) => lowerCaseFirst(m) === lowerCaseFirst(model)); + } + + private makeBadInputErrorResponse(message: string) { + const resp = { + status: 400, + body: { error: { message } }, + }; + log(this.options.log, 'debug', () => `sending error response: ${safeJSONStringify(resp)}`); + return resp; + } + + private makeGenericErrorResponse(err: unknown) { + const resp = { + status: 500, + body: { error: { message: (err as Error).message || 'unknown error' } }, + }; + log(this.options.log, 'debug', () => `sending error response: ${safeJSONStringify(resp)}`); + return resp; + } + + private makeZenStackErrorResponse(err: ZenStackError) { + let status = 400; + const error: any = { message: err.message }; + if (err.cause && err.cause instanceof Error) { + error.cause = err.cause.message; + } + + if (err instanceof NotFoundError) { + status = 404; + error.model = err.model; + } else if (err instanceof InputValidationError) { + status = 422; + error.rejectedByValidation = true; + error.model = err.model; + } else if (err instanceof RejectedByPolicyError) { + status = 403; + error.rejectedByPolicy = true; + error.rejectReason = err.reason; + error.model = err.model; + } + + const resp = { status, body: { error } }; + log(this.options.log, 'debug', () => `sending error response: ${safeJSONStringify(resp)}`); + return resp; + } + + private async processRequestPayload(args: any) { + const { meta, ...rest } = args; + if (meta?.serialization) { + try { + // superjson deserialization + args = SuperJSON.deserialize({ json: rest, meta: meta.serialization }); + } catch (err) { + return { result: undefined, error: `failed to deserialize request payload: ${(err as Error).message}` }; + } + } + return { result: args, error: undefined }; + } + + private unmarshalQ(value: string, meta: string | undefined) { + let parsedValue: any; + try { + parsedValue = JSON.parse(value); + } catch { + throw new Error('invalid "q" query parameter'); + } + + if (meta) { + let parsedMeta: any; + try { + parsedMeta = JSON.parse(meta); + } catch { + throw new Error('invalid "meta" query parameter'); + } + + if (parsedMeta.serialization) { + return SuperJSON.deserialize({ json: parsedValue, meta: parsedMeta.serialization }); + } + } + + return parsedValue; + } +} diff --git a/packages/server/src/api/utils.ts b/packages/server/src/api/utils.ts new file mode 100644 index 00000000..a31a50a8 --- /dev/null +++ b/packages/server/src/api/utils.ts @@ -0,0 +1,50 @@ +import { Decimal } from 'decimal.js'; +import SuperJSON from 'superjson'; +import { match } from 'ts-pattern'; +import type { LogConfig, LogLevel } from '../types'; + +export function log(logger: LogConfig | undefined, level: LogLevel, message: string | (() => string), error?: unknown) { + if (!logger) { + return; + } + + const getMessage = typeof message === 'function' ? message : () => message; + + if (typeof logger === 'function') { + logger(level, getMessage(), error); + } else if (logger.includes(level)) { + const logFn = match(level) + .with('debug', () => console.debug) + .with('info', () => console.info) + .with('warn', () => console.warn) + .with('error', () => console.error) + .exhaustive(); + logFn(`@zenstackhq/server: [${level}] ${getMessage()}${error ? `\n${error}` : ''}`); + } +} + +/** + * Registers custom superjson serializers. + */ +export function registerCustomSerializers() { + SuperJSON.registerCustom( + { + isApplicable: (v): v is Decimal => Decimal.isDecimal(v), + serialize: (v) => v.toJSON(), + deserialize: (v) => new Decimal(v), + }, + 'Decimal', + ); + + // `Buffer` is not available in edge runtime + if (globalThis.Buffer) { + SuperJSON.registerCustom( + { + isApplicable: (v): v is Buffer => Buffer.isBuffer(v), + serialize: (v) => v.toString('base64'), + deserialize: (v) => Buffer.from(v, 'base64'), + }, + 'Bytes', + ); + } +} diff --git a/packages/server/src/express/index.ts b/packages/server/src/express/index.ts new file mode 100644 index 00000000..35fc53b7 --- /dev/null +++ b/packages/server/src/express/index.ts @@ -0,0 +1 @@ +export { ZenStackMiddleware, type MiddlewareOptions } from './middleware'; diff --git a/packages/server/src/express/middleware.ts b/packages/server/src/express/middleware.ts new file mode 100644 index 00000000..27cc9347 --- /dev/null +++ b/packages/server/src/express/middleware.ts @@ -0,0 +1,82 @@ +import type { ClientContract } from '@zenstackhq/runtime'; +import type { SchemaDef } from '@zenstackhq/runtime/schema'; +import type { Handler, Request, Response } from 'express'; +import type { ApiHandler } from '../types'; + +/** + * Express middleware options + */ +export interface MiddlewareOptions { + apiHandler: ApiHandler; + + /** + * Callback for getting a ZenStackClient for the given request + */ + getClient: (req: Request, res: Response) => ClientContract | Promise>; + + /** + * Controls if the middleware directly sends a response. If set to false, + * the response is stored in the `res.locals` object and then the middleware + * calls the `next()` function to pass the control to the next middleware. + * Subsequent middleware or request handlers need to make sure to send + * a response. + * + * Defaults to true; + */ + sendResponse?: boolean; +} + +/** + * Creates an Express middleware for handling CRUD requests. + */ +const factory = (options: MiddlewareOptions): Handler => { + const requestHandler = options.apiHandler; + + return async (request, response, next) => { + const client = await options.getClient(request, response); + const { sendResponse } = options; + + if (sendResponse === false && !client) { + throw new Error('unable to get ZenStackClient from request context'); + } + + if (!client) { + return response.status(500).json({ message: 'unable to get ZenStackClient from request context' }); + } + + // express converts query parameters with square brackets into object + // e.g.: filter[foo]=bar is parsed to { filter: { foo: 'bar' } } + // we need to revert this behavior and reconstruct params from original URL + const url = request.protocol + '://' + request.get('host') + request.originalUrl; + const searchParams = new URL(url).searchParams; + const query = Object.fromEntries(searchParams); + + try { + const r = await requestHandler.handleRequest({ + method: request.method, + path: request.path, + query, + requestBody: request.body, + client, + }); + if (sendResponse === false) { + // attach response and pass control to the next middleware + response.locals['zenstack'] = { + status: r.status, + body: r.body, + }; + return next(); + } + return response.status(r.status).json(r.body); + } catch (err) { + if (sendResponse === false) { + throw err; + } + return response.status(500).json({ message: `An unhandled error occurred: ${err}` }); + } + }; +}; + +export default factory; + +export { factory as ZenStackMiddleware }; diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts new file mode 100644 index 00000000..66ef7774 --- /dev/null +++ b/packages/server/src/types.ts @@ -0,0 +1,77 @@ +import type { ClientContract } from '@zenstackhq/runtime'; +import type { SchemaDef } from '@zenstackhq/runtime/schema'; + +/** + * Log levels + */ +export type LogLevel = 'debug' | 'info' | 'warn' | 'error'; + +/** + * Logger function + */ +export type Logger = (level: LogLevel, message: string, error?: unknown) => void; + +/** + * Log configuration + */ +export type LogConfig = ReadonlyArray | Logger; + +/** + * API request context + */ +export type RequestContext = { + /** + * The ZenStackClient instance + */ + client: ClientContract; + + /** + * The HTTP method + */ + method: string; + + /** + * The request endpoint path (excluding any prefix) + */ + path: string; + + /** + * The query parameters + */ + query?: Record; + + /** + * The request body object + */ + requestBody?: unknown; +}; + +/** + * API response + */ +export type Response = { + /** + * HTTP status code + */ + status: number; + + /** + * Response body + */ + body: unknown; +}; + +/** + * Framework-agnostic API handler. + */ +export interface ApiHandler { + /** + * The schema associated with this handler. + */ + get schema(): Schema; + + /** + * Handle an API request. + */ + handleRequest(context: RequestContext): Promise; +} diff --git a/packages/server/test/adapter/express.test.ts b/packages/server/test/adapter/express.test.ts new file mode 100644 index 00000000..7926754d --- /dev/null +++ b/packages/server/test/adapter/express.test.ts @@ -0,0 +1,176 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import bodyParser from 'body-parser'; +import express from 'express'; +import request from 'supertest'; +import { describe, expect, it } from 'vitest'; +import { RPCApiHandler } from '../../src/api'; +import { ZenStackMiddleware } from '../../src/express'; +import { makeUrl, schema } from '../utils'; + +describe('Express adapter tests - rpc handler', () => { + it('works with simple requests', async () => { + const client = await createPolicyTestClient(schema); + const rawClient = client.$unuseAll(); + + const app = express(); + app.use(bodyParser.json()); + app.use( + '/api', + ZenStackMiddleware({ + apiHandler: new RPCApiHandler({ schema: client.$schema }), + getClient: () => rawClient, + }), + ); + + let r = await request(app).get(makeUrl('/api/post/findMany', { where: { id: { equals: '1' } } })); + expect(r.status).toBe(200); + expect(r.body.data).toHaveLength(0); + + r = await request(app) + .post('/api/user/create') + .send({ + include: { posts: true }, + data: { + id: 'user1', + email: 'user1@abc.com', + posts: { + create: [ + { title: 'post1', published: true, viewCount: 1 }, + { title: 'post2', published: false, viewCount: 2 }, + ], + }, + }, + }); + + expect(r.status).toBe(201); + const data = r.body.data; + expect(data).toEqual( + expect.objectContaining({ + email: 'user1@abc.com', + posts: expect.arrayContaining([ + expect.objectContaining({ title: 'post1' }), + expect.objectContaining({ title: 'post2' }), + ]), + }), + ); + + r = await request(app).get(makeUrl('/api/post/findMany')); + expect(r.status).toBe(200); + expect(r.body.data).toHaveLength(2); + + r = await request(app).get(makeUrl('/api/post/findMany', { where: { viewCount: { gt: 1 } } })); + expect(r.status).toBe(200); + expect(r.body.data).toHaveLength(1); + + r = await request(app) + .put('/api/user/update') + .send({ where: { id: 'user1' }, data: { email: 'user1@def.com' } }); + expect(r.status).toBe(200); + expect(r.body.data.email).toBe('user1@def.com'); + + r = await request(app).get(makeUrl('/api/post/count', { where: { viewCount: { gt: 1 } } })); + expect(r.status).toBe(200); + expect(r.body.data).toBe(1); + + r = await request(app).get(makeUrl('/api/post/aggregate', { _sum: { viewCount: true } })); + expect(r.status).toBe(200); + expect(r.body.data._sum.viewCount).toBe(3); + + r = await request(app).get(makeUrl('/api/post/groupBy', { by: ['published'], _sum: { viewCount: true } })); + expect(r.status).toBe(200); + expect(r.body.data).toEqual( + expect.arrayContaining([ + expect.objectContaining({ published: true, _sum: { viewCount: 1 } }), + expect.objectContaining({ published: false, _sum: { viewCount: 2 } }), + ]), + ); + + r = await request(app).delete(makeUrl('/api/user/deleteMany', { where: { id: 'user1' } })); + expect(r.status).toBe(200); + expect(r.body.data.count).toBe(1); + }); +}); + +// describe('Express adapter tests - rest handler', () => { +// it('run middleware', async () => { +// const { prisma, zodSchemas, modelMeta } = await loadSchema(schema); + +// const app = express(); +// app.use(bodyParser.json()); +// app.use( +// '/api', +// ZenStackMiddleware({ +// getPrisma: () => prisma, +// modelMeta, +// zodSchemas, +// handler: RESTAPIHandler({ endpoint: 'http://localhost/api' }), +// }), +// ); + +// let r = await request(app).get(makeUrl('/api/post/1')); +// expect(r.status).toBe(404); + +// r = await request(app) +// .post('/api/user') +// .send({ +// data: { +// type: 'user', +// attributes: { +// id: 'user1', +// email: 'user1@abc.com', +// }, +// }, +// }); +// expect(r.status).toBe(201); +// expect(r.body).toMatchObject({ +// jsonapi: { version: '1.1' }, +// data: { type: 'user', id: 'user1', attributes: { email: 'user1@abc.com' } }, +// }); + +// r = await request(app).get('/api/user?filter[id]=user1'); +// expect(r.body.data).toHaveLength(1); + +// r = await request(app).get('/api/user?filter[id]=user2'); +// expect(r.body.data).toHaveLength(0); + +// r = await request(app).get('/api/user?filter[id]=user1&filter[email]=xyz'); +// expect(r.body.data).toHaveLength(0); + +// r = await request(app) +// .put('/api/user/user1') +// .send({ data: { type: 'user', attributes: { email: 'user1@def.com' } } }); +// expect(r.status).toBe(200); +// expect(r.body.data.attributes.email).toBe('user1@def.com'); + +// r = await request(app).delete(makeUrl('/api/user/user1')); +// expect(r.status).toBe(200); +// expect(await prisma.user.findMany()).toHaveLength(0); +// }); +// }); + +// describe('Express adapter tests - rest handler with custom middleware', () => { +// it('run middleware', async () => { +// const { prisma, zodSchemas, modelMeta } = await loadSchema(schema); + +// const app = express(); +// app.use(bodyParser.json()); +// app.use( +// '/api', +// ZenStackMiddleware({ +// getPrisma: () => prisma, +// modelMeta, +// zodSchemas, +// handler: RESTAPIHandler({ endpoint: 'http://localhost/api' }), +// sendResponse: false, +// }), +// ); + +// app.use((req, res) => { +// res.status(res.locals.status).json({ message: res.locals.body }); +// }); + +// const r = await request(app).get(makeUrl('/api/post/1')); +// expect(r.status).toBe(404); +// expect(r.body.message).toHaveProperty('errors'); +// }); +// }); diff --git a/packages/server/test/api/rpc.test.ts b/packages/server/test/api/rpc.test.ts new file mode 100644 index 00000000..532d4ca2 --- /dev/null +++ b/packages/server/test/api/rpc.test.ts @@ -0,0 +1,527 @@ +import { ClientContract } from '@zenstackhq/runtime'; +import { SchemaDef } from '@zenstackhq/runtime/schema'; +import { createPolicyTestClient, createTestClient } from '@zenstackhq/testtools'; +import Decimal from 'decimal.js'; +import SuperJSON from 'superjson'; +import { beforeAll, describe, expect, it } from 'vitest'; +import { RPCApiHandler } from '../../src/api'; +import { schema } from '../utils'; + +describe('RPC API Handler Tests', () => { + let client: ClientContract; + let rawClient: ClientContract; + + beforeAll(async () => { + client = await createPolicyTestClient(schema); + rawClient = client.$unuseAll(); + }); + + it('crud', async () => { + const handleRequest = makeHandler(); + + let r = await handleRequest({ + method: 'get', + path: '/post/findMany', + client: rawClient, + }); + expect(r.status).toBe(200); + expect(r.data).toHaveLength(0); + + r = await handleRequest({ + method: 'post', + path: '/user/create', + query: {}, + requestBody: { + include: { posts: true }, + data: { + id: 'user1', + email: 'user1@abc.com', + posts: { + create: [ + { title: 'post1', published: true, viewCount: 1 }, + { title: 'post2', published: false, viewCount: 2 }, + ], + }, + }, + }, + client: rawClient, + }); + expect(r.status).toBe(201); + expect(r.data).toEqual( + expect.objectContaining({ + email: 'user1@abc.com', + posts: expect.arrayContaining([ + expect.objectContaining({ title: 'post1' }), + expect.objectContaining({ title: 'post2' }), + ]), + }), + ); + + r = await handleRequest({ + method: 'get', + path: '/post/findMany', + client: rawClient, + }); + expect(r.status).toBe(200); + expect(r.data).toHaveLength(2); + + r = await handleRequest({ + method: 'get', + path: '/post/findMany', + query: { q: JSON.stringify({ where: { viewCount: { gt: 1 } } }) }, + client: rawClient, + }); + expect(r.status).toBe(200); + expect(r.data).toHaveLength(1); + + r = await handleRequest({ + method: 'put', + path: '/user/update', + requestBody: { where: { id: 'user1' }, data: { email: 'user1@def.com' } }, + client: rawClient, + }); + expect(r.status).toBe(200); + expect(r.data.email).toBe('user1@def.com'); + + r = await handleRequest({ + method: 'get', + path: '/post/count', + query: { q: JSON.stringify({ where: { viewCount: { gt: 1 } } }) }, + client: rawClient, + }); + expect(r.status).toBe(200); + expect(r.data).toBe(1); + + r = await handleRequest({ + method: 'get', + path: '/post/aggregate', + query: { q: JSON.stringify({ _sum: { viewCount: true } }) }, + client: rawClient, + }); + expect(r.status).toBe(200); + expect(r.data._sum.viewCount).toBe(3); + + r = await handleRequest({ + method: 'get', + path: '/post/groupBy', + query: { q: JSON.stringify({ by: ['published'], _sum: { viewCount: true } }) }, + client: rawClient, + }); + expect(r.status).toBe(200); + expect(r.data).toEqual( + expect.arrayContaining([ + expect.objectContaining({ published: true, _sum: { viewCount: 1 } }), + expect.objectContaining({ published: false, _sum: { viewCount: 2 } }), + ]), + ); + + r = await handleRequest({ + method: 'delete', + path: '/user/deleteMany', + query: { q: JSON.stringify({ where: { id: 'user1' } }) }, + client: rawClient, + }); + expect(r.status).toBe(200); + expect(r.data.count).toBe(1); + }); + + it('pagination and ordering', async () => { + const handleRequest = makeHandler(); + + // Clean up any existing data first + await rawClient.post.deleteMany(); + await rawClient.user.deleteMany(); + + // Create test data + await rawClient.user.create({ + data: { + id: 'user1', + email: 'user1@abc.com', + posts: { + create: [ + { id: '1', title: 'A Post', published: true, viewCount: 5 }, + { id: '2', title: 'B Post', published: true, viewCount: 3 }, + { id: '3', title: 'C Post', published: true, viewCount: 7 }, + { id: '4', title: 'D Post', published: true, viewCount: 1 }, + { id: '5', title: 'E Post', published: true, viewCount: 9 }, + ], + }, + }, + }); + + // Test orderBy with title ascending + let r = await handleRequest({ + method: 'get', + path: '/post/findMany', + query: { q: JSON.stringify({ orderBy: { title: 'asc' } }) }, + client: rawClient, + }); + expect(r.status).toBe(200); + expect(r.data).toHaveLength(5); + expect(r.data[0].title).toBe('A Post'); + expect(r.data[4].title).toBe('E Post'); + + // Test orderBy with viewCount descending + r = await handleRequest({ + method: 'get', + path: '/post/findMany', + query: { q: JSON.stringify({ orderBy: { viewCount: 'desc' } }) }, + client: rawClient, + }); + expect(r.status).toBe(200); + expect(r.data[0].viewCount).toBe(9); + expect(r.data[4].viewCount).toBe(1); + + // Test multiple orderBy + r = await handleRequest({ + method: 'get', + path: '/post/findMany', + query: { q: JSON.stringify({ orderBy: [{ published: 'desc' }, { title: 'asc' }] }) }, + client: rawClient, + }); + expect(r.status).toBe(200); + expect(r.data[0].title).toBe('A Post'); + + // Test take (limit) + r = await handleRequest({ + method: 'get', + path: '/post/findMany', + query: { q: JSON.stringify({ take: 3 }) }, + client: rawClient, + }); + expect(r.status).toBe(200); + expect(r.data).toHaveLength(3); + + // Test skip (offset) + r = await handleRequest({ + method: 'get', + path: '/post/findMany', + query: { q: JSON.stringify({ skip: 2, take: 2 }) }, + client: rawClient, + }); + expect(r.status).toBe(200); + expect(r.data).toHaveLength(2); + + // Test skip and take with orderBy + r = await handleRequest({ + method: 'get', + path: '/post/findMany', + query: { q: JSON.stringify({ orderBy: { title: 'asc' }, skip: 1, take: 3 }) }, + client: rawClient, + }); + expect(r.status).toBe(200); + expect(r.data).toHaveLength(3); + expect(r.data[0].title).toBe('B Post'); + expect(r.data[2].title).toBe('D Post'); + + // Test cursor-based pagination + r = await handleRequest({ + method: 'get', + path: '/post/findMany', + query: { q: JSON.stringify({ orderBy: { id: 'asc' }, take: 2 }) }, + client: rawClient, + }); + expect(r.status).toBe(200); + expect(r.data).toHaveLength(2); + const lastId = r.data[1].id; + + // Get next page using cursor + r = await handleRequest({ + method: 'get', + path: '/post/findMany', + query: { q: JSON.stringify({ orderBy: { id: 'asc' }, take: 2, skip: 1, cursor: { id: lastId } }) }, + client: rawClient, + }); + expect(r.status).toBe(200); + expect(r.data).toHaveLength(2); + expect(r.data[0].id).toBe('3'); + expect(r.data[1].id).toBe('4'); + + // Clean up + await rawClient.post.deleteMany(); + await rawClient.user.deleteMany(); + }); + + it('policy violation', async () => { + // Clean up any existing data first + await rawClient.post.deleteMany(); + await rawClient.user.deleteMany(); + + await rawClient.user.create({ + data: { + id: '1', + email: 'user1@abc.com', + posts: { create: { id: '1', title: 'post1', published: true } }, + }, + }); + + const handleRequest = makeHandler(); + + let r = await handleRequest({ + method: 'post', + path: '/post/create', + requestBody: { + data: { id: '2', title: 'post2', authorId: '1', published: false }, + }, + client, + }); + expect(r.status).toBe(403); + expect(r.error.rejectedByPolicy).toBe(true); + expect(r.error.model).toBe('Post'); + expect(r.error.rejectReason).toBe('no-access'); + + r = await handleRequest({ + method: 'put', + path: '/post/update', + requestBody: { + where: { id: '1' }, + data: { title: 'post2' }, + }, + client, + }); + expect(r.status).toBe(404); + expect(r.error.model).toBe('Post'); + }); + + it('validation error', async () => { + const handleRequest = makeHandler(); + + let r = await handleRequest({ + method: 'get', + path: '/post/findUnique', + client: rawClient, + }); + expect(r.status).toBe(422); + expect(r.error.message).toContain('Validation error'); + expect(r.error.message).toContain('where'); + + r = await handleRequest({ + method: 'post', + path: '/post/create', + requestBody: { data: {} }, + client: rawClient, + }); + expect(r.status).toBe(422); + expect(r.error.message).toContain('Validation error'); + expect(r.error.message).toContain('data'); + + r = await handleRequest({ + method: 'post', + path: '/user/create', + requestBody: { data: { email: 'hello' } }, + client: rawClient, + }); + expect(r.status).toBe(422); + expect(r.error.message).toContain('Validation error'); + expect(r.error.message).toContain('email'); + }); + + it('invalid path or args', async () => { + const handleRequest = makeHandler(); + let r = await handleRequest({ + method: 'get', + path: '/post/', + client: rawClient, + }); + expect(r.status).toBe(400); + expect(r.error.message).toContain('invalid request path'); + + r = await handleRequest({ + method: 'get', + path: '/post/findMany/abc', + client: rawClient, + }); + expect(r.status).toBe(400); + expect(r.error.message).toContain('invalid request path'); + + r = await handleRequest({ + method: 'get', + path: '/post/findUnique', + query: { q: 'abc' }, + client: rawClient, + }); + expect(r.status).toBe(400); + expect(r.error.message).toContain('invalid "q" query parameter'); + + r = await handleRequest({ + method: 'delete', + path: '/post/deleteMany', + query: { q: 'abc' }, + client: rawClient, + }); + expect(r.status).toBe(400); + expect(r.error.message).toContain('invalid "q" query parameter'); + }); + + it('field types', async () => { + const schema = ` + model Foo { + id Int @id + + string String + int Int + bigInt BigInt + date DateTime + float Float + decimal Decimal + boolean Boolean + bytes Bytes + bars Bar[] + } + + + model Bar { + id Int @id @default(autoincrement()) + bytes Bytes + foo Foo @relation(fields: [fooId], references: [id]) + fooId Int @unique + } + `; + + const handleRequest = makeHandler(); + const client = await createTestClient(schema, { provider: 'postgresql' }); + + const decimalValue = new Decimal('0.046875'); + const bigIntValue = BigInt(534543543534); + const dateValue = new Date(); + const bytesValue = new Uint8Array([1, 2, 3, 4]); + const barBytesValue = new Uint8Array([7, 8, 9]); + + const createData = { + string: 'string', + int: 123, + bigInt: bigIntValue, + date: dateValue, + float: 1.23, + decimal: decimalValue, + boolean: true, + bytes: bytesValue, + bars: { + create: { bytes: barBytesValue }, + }, + }; + + const serialized = SuperJSON.serialize({ + include: { bars: true }, + data: { id: 1, ...createData }, + }); + + let r = await handleRequest({ + method: 'post', + path: '/foo/create', + query: {}, + client, + requestBody: { + ...(serialized.json as any), + meta: { serialization: serialized.meta }, + }, + }); + expect(r.status).toBe(201); + expect(r.meta).toBeTruthy(); + const data: any = SuperJSON.deserialize({ json: r.data, meta: r.meta.serialization }); + expect(typeof data.bigInt).toBe('bigint'); + expect(data.bytes).toBeInstanceOf(Uint8Array); + expect(data.date instanceof Date).toBeTruthy(); + expect(Decimal.isDecimal(data.decimal)).toBeTruthy(); + expect(data.bars[0].bytes).toBeInstanceOf(Uint8Array); + + // find with filter not found + const serializedQ = SuperJSON.serialize({ + where: { + bigInt: { + gt: bigIntValue, + }, + }, + }); + r = await handleRequest({ + method: 'get', + path: '/foo/findFirst', + query: { + q: JSON.stringify(serializedQ.json), + meta: JSON.stringify({ serialization: serializedQ.meta }), + }, + client, + }); + expect(r.status).toBe(200); + expect(r.data).toBeNull(); + + // find with filter found + const serializedQ1 = SuperJSON.serialize({ + where: { + bigInt: bigIntValue, + }, + }); + r = await handleRequest({ + method: 'get', + path: '/foo/findFirst', + query: { + q: JSON.stringify(serializedQ1.json), + meta: JSON.stringify({ serialization: serializedQ1.meta }), + }, + client, + }); + expect(r.status).toBe(200); + expect(r.data).toBeTruthy(); + + // find with filter found + const serializedQ2 = SuperJSON.serialize({ + where: { + bars: { + some: { + bytes: barBytesValue, + }, + }, + }, + }); + r = await handleRequest({ + method: 'get', + path: '/foo/findFirst', + query: { + q: JSON.stringify(serializedQ2.json), + meta: JSON.stringify({ serialization: serializedQ2.meta }), + }, + client, + }); + expect(r.status).toBe(200); + expect(r.data).toBeTruthy(); + + // find with filter not found + const serializedQ3 = SuperJSON.serialize({ + where: { + bars: { + none: { + bytes: barBytesValue, + }, + }, + }, + }); + r = await handleRequest({ + method: 'get', + path: '/foo/findFirst', + query: { + q: JSON.stringify(serializedQ3.json), + meta: JSON.stringify({ serialization: serializedQ3.meta }), + }, + client, + }); + expect(r.status).toBe(200); + expect(r.data).toBeNull(); + }); + + function makeHandler() { + const handler = new RPCApiHandler({ schema: client.$schema }); + return async (args: any) => { + const r = await handler.handleRequest({ + ...args, + url: new URL(`http://localhost/${args.path}`), + }); + return { + status: r.status, + body: r.body as any, + data: (r.body as any).data, + error: (r.body as any).error, + meta: (r.body as any).meta, + }; + }; + } +}); diff --git a/packages/server/test/utils.ts b/packages/server/test/utils.ts new file mode 100644 index 00000000..674a35c7 --- /dev/null +++ b/packages/server/test/utils.ts @@ -0,0 +1,33 @@ +import superjson from 'superjson'; + +export const schema = ` +model User { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + email String @unique @email + posts Post[] + + @@allow('all', auth() == this) + @@allow('create,read', true) +} + +model Post { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + title String + author User? @relation(fields: [authorId], references: [id]) + authorId String? + published Boolean @default(false) + publishedAt DateTime? + viewCount Int @default(0) + + @@allow('all', author == auth()) + @@allow('read', published) +} +`; + +export function makeUrl(path: string, q?: object, useSuperJson = false) { + return q ? `${path}?q=${encodeURIComponent(useSuperJson ? superjson.stringify(q) : JSON.stringify(q))}` : path; +} diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json new file mode 100644 index 00000000..8ef64682 --- /dev/null +++ b/packages/server/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@zenstackhq/typescript-config/base.json", + "include": ["src/**/*.ts"] +} diff --git a/packages/server/tsup.config.ts b/packages/server/tsup.config.ts new file mode 100644 index 00000000..231bf08c --- /dev/null +++ b/packages/server/tsup.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: { + api: 'src/api/index.ts', + express: 'src/express/index.ts', + }, + outDir: 'dist', + splitting: false, + sourcemap: true, + clean: true, + dts: true, + format: ['cjs', 'esm'], +}); diff --git a/packages/server/vitest.config.ts b/packages/server/vitest.config.ts new file mode 100644 index 00000000..75a9f709 --- /dev/null +++ b/packages/server/vitest.config.ts @@ -0,0 +1,4 @@ +import base from '@zenstackhq/vitest-config/base'; +import { defineConfig, mergeConfig } from 'vitest/config'; + +export default mergeConfig(base, defineConfig({})); diff --git a/packages/testtools/src/client.ts b/packages/testtools/src/client.ts index 27133f83..de819363 100644 --- a/packages/testtools/src/client.ts +++ b/packages/testtools/src/client.ts @@ -197,9 +197,8 @@ function getTestDbName(provider: string) { if (provider === 'sqlite') { return './test.db'; } - const testName = expect.getState().currentTestName; + const testName = expect.getState().currentTestName ?? 'unnamed'; const testPath = expect.getState().testPath ?? ''; - invariant(testName); // digest test name const digest = createHash('md5') .update(testName + testPath) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aa32c4a2..bfe83846 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,6 +15,9 @@ catalogs: better-sqlite3: specifier: ^12.2.0 version: 12.2.0 + decimal.js: + specifier: ^10.4.3 + version: 10.4.3 kysely: specifier: ^0.27.6 version: 0.27.6 @@ -317,7 +320,7 @@ importers: specifier: 'catalog:' version: 12.2.0 decimal.js: - specifier: ^10.4.3 + specifier: 'catalog:' version: 10.4.3 json-stable-stringify: specifier: ^1.3.0 @@ -403,12 +406,61 @@ importers: specifier: workspace:* version: link:../config/typescript-config decimal.js: - specifier: ^10.4.3 + specifier: 'catalog:' version: 10.4.3 kysely: specifier: 'catalog:' version: 0.27.6 + packages/server: + dependencies: + '@zenstackhq/common-helpers': + specifier: workspace:* + version: link:../common-helpers + '@zenstackhq/runtime': + specifier: workspace:* + version: link:../runtime + decimal.js: + specifier: 'catalog:' + version: 10.4.3 + express: + specifier: ^5.0.0 + version: 5.1.0 + superjson: + specifier: ^2.2.3 + version: 2.2.3 + ts-pattern: + specifier: 'catalog:' + version: 5.7.1 + devDependencies: + '@types/body-parser': + specifier: ^1.19.6 + version: 1.19.6 + '@types/express': + specifier: ^5.0.0 + version: 5.0.3 + '@types/supertest': + specifier: ^6.0.3 + version: 6.0.3 + '@zenstackhq/eslint-config': + specifier: workspace:* + version: link:../config/eslint-config + '@zenstackhq/testtools': + specifier: workspace:* + version: link:../testtools + '@zenstackhq/typescript-config': + specifier: workspace:* + version: link:../config/typescript-config + '@zenstackhq/vitest-config': + specifier: workspace:* + version: link:../config/vitest-config + body-parser: + specifier: ^2.2.0 + version: 2.2.0 + supertest: + specifier: ^7.1.4 + version: 7.1.4 + packages/tanstack-query: dependencies: '@tanstack/react-query': @@ -560,7 +612,7 @@ importers: specifier: 'catalog:' version: 12.2.0 decimal.js: - specifier: ^10.4.3 + specifier: 'catalog:' version: 10.4.3 kysely: specifier: 'catalog:' @@ -588,7 +640,7 @@ importers: specifier: workspace:* version: link:../../packages/testtools decimal.js: - specifier: ^10.4.3 + specifier: 'catalog:' version: 10.4.3 devDependencies: '@zenstackhq/cli': @@ -1235,9 +1287,18 @@ packages: '@types/better-sqlite3@7.6.13': resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==} + '@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/cookiejar@2.1.5': + resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -1247,9 +1308,24 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/express-serve-static-core@5.1.0': + resolution: {integrity: sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==} + + '@types/express@5.0.3': + resolution: {integrity: sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==} + + '@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/methods@1.1.4': + resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} + + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + '@types/node@20.17.24': resolution: {integrity: sha512-d7fGCyB96w9BnWQrOsJtpyiSaBcAYYr75bnK6ZRjDbql2cGLj/3GsL5OYmLPNq76l7Gf2q4Rv9J2o6h5CrD9sA==} @@ -1259,12 +1335,33 @@ packages: '@types/pluralize@0.0.33': resolution: {integrity: sha512-JOqsl+ZoCpP4e8TDke9W79FDcSgPAR0l6pixx2JHkhnRjvShyYiAYw2LVsnA7K08Y6DeOnaU6ujmENO4os/cYg==} + '@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/semver@7.7.0': resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==} + '@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==} + '@types/sql.js@1.4.9': resolution: {integrity: sha512-ep8b36RKHlgWPqjNG9ToUrPiwkhwh0AEzy883mO5Xnd+cL6VBH1EvSjBAAuxLUFF2Vn/moE3Me6v9E1Lo+48GQ==} + '@types/superagent@8.1.9': + resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==} + + '@types/supertest@6.0.3': + resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==} + '@types/tmp@0.2.6': resolution: {integrity: sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==} @@ -1366,6 +1463,10 @@ packages: '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1405,10 +1506,16 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1425,6 +1532,10 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + body-parser@2.2.0: + resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + engines: {node: '>=18'} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -1447,6 +1558,10 @@ packages: peerDependencies: esbuild: '>=0.18' + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + c12@3.1.0: resolution: {integrity: sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==} peerDependencies: @@ -1532,6 +1647,10 @@ packages: resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==} engines: {node: '>=0.1.90'} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + commander@11.0.0: resolution: {integrity: sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==} engines: {node: '>=16'} @@ -1544,6 +1663,9 @@ packages: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} + component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1557,6 +1679,29 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + content-disposition@1.0.0: + resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + engines: {node: '>= 0.6'} + + cookiejar@2.1.4: + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + + copy-anything@4.0.5: + resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} + engines: {node: '>=18'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1602,6 +1747,14 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + destr@2.0.5: resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} @@ -1609,6 +1762,9 @@ packages: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} + dezalgo@1.0.4: + resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + dotenv@16.6.1: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} @@ -1620,6 +1776,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + effect@3.16.12: resolution: {integrity: sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==} @@ -1633,6 +1792,10 @@ packages: resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} engines: {node: '>=14'} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} @@ -1651,6 +1814,10 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + esbuild@0.23.1: resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} engines: {node: '>=18'} @@ -1661,6 +1828,9 @@ packages: engines: {node: '>=18'} hasBin: true + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -1710,6 +1880,10 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} @@ -1718,6 +1892,10 @@ packages: resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==} engines: {node: '>=12.0.0'} + express@5.1.0: + resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + engines: {node: '>= 18'} + exsolve@1.0.7: resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} @@ -1738,6 +1916,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -1760,6 +1941,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@2.1.0: + resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} + engines: {node: '>= 0.8'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -1778,6 +1963,22 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + engines: {node: '>= 6'} + + formidable@3.5.4: + resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==} + engines: {node: '>=14.0.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -1856,14 +2057,30 @@ packages: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + https-proxy-agent@5.0.0: resolution: {integrity: sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==} engines: {node: '>= 6'} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.7.0: + resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -1889,6 +2106,10 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1909,10 +2130,17 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} + is-what@5.5.0: + resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} + engines: {node: '>=18'} + isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -2034,14 +2262,47 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.1: + resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + engines: {node: '>= 0.6'} + + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -2104,6 +2365,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + node-abi@3.73.0: resolution: {integrity: sha512-z8iYzQGBu35ZkTQ9mtR8RqugJZ9RCLn8fv3d7LsgDBzOijGQP3RdKTX4LA7LXw03ZhU5z0l4xfhIMgSES31+cg==} engines: {node: '>=10'} @@ -2120,6 +2385,10 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} @@ -2130,6 +2399,10 @@ packages: ohash@2.0.11: resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -2163,6 +2436,10 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -2179,6 +2456,9 @@ packages: resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} engines: {node: 20 || >=22} + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -2337,6 +2617,10 @@ packages: typescript: optional: true + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + pump@3.0.2: resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} @@ -2347,12 +2631,28 @@ packages: pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} railroad-diagrams@1.0.0: resolution: {integrity: sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==} + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.1: + resolution: {integrity: sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==} + engines: {node: '>= 0.10'} + rc9@2.1.2: resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} @@ -2396,21 +2696,39 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + 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 + send@1.2.0: + resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + engines: {node: '>= 18'} + + serve-static@2.2.0: + resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} + engines: {node: '>= 18'} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2419,6 +2737,22 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -2454,6 +2788,10 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + std-env@3.9.0: resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} @@ -2492,6 +2830,18 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true + superagent@10.2.3: + resolution: {integrity: sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==} + engines: {node: '>=14.18.0'} + + superjson@2.2.3: + resolution: {integrity: sha512-ay3d+LW/S6yppKoTz3Bq4mG0xrS5bFwfWEBmQfbC7lt5wmtk+Obq0TxVuA9eYRirBTQb1K3eEpBRHMQEo0WyVw==} + engines: {node: '>=16'} + + supertest@7.1.4: + resolution: {integrity: sha512-tjLPs7dVyqgItVFirHYqe2T+MfWc2VOBQ8QFKKbWTA3PU7liZR8zoSpAi/C1k1ilm9RsXIKYf197oap9wXGVYg==} + engines: {node: '>=14.18.0'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -2543,6 +2893,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + toposort@2.0.2: resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} @@ -2635,6 +2989,10 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + typescript-eslint@8.34.1: resolution: {integrity: sha512-XjS+b6Vg9oT1BaIUfkW3M3LvqZE++rbzAMEHuccCfO/YkP43ha6w3jTEMilQxMF92nVOYCcdjv1ZUhAa1D/0ow==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2661,6 +3019,10 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -2671,6 +3033,10 @@ packages: resolution: {integrity: sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==} hasBin: true + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -3256,18 +3622,48 @@ snapshots: dependencies: '@types/node': 20.17.24 + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 20.17.24 + '@types/chai@5.2.2': dependencies: '@types/deep-eql': 4.0.2 + '@types/connect@3.4.38': + dependencies: + '@types/node': 20.17.24 + + '@types/cookiejar@2.1.5': {} + '@types/deep-eql@4.0.2': {} '@types/emscripten@1.40.1': {} '@types/estree@1.0.8': {} + '@types/express-serve-static-core@5.1.0': + dependencies: + '@types/node': 20.17.24 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.0 + + '@types/express@5.0.3': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.0 + '@types/serve-static': 1.15.9 + + '@types/http-errors@2.0.5': {} + '@types/json-schema@7.0.15': {} + '@types/methods@1.1.4': {} + + '@types/mime@1.3.5': {} + '@types/node@20.17.24': dependencies: undici-types: 6.19.8 @@ -3280,13 +3676,44 @@ snapshots: '@types/pluralize@0.0.33': {} + '@types/qs@6.14.0': {} + + '@types/range-parser@1.2.7': {} + '@types/semver@7.7.0': {} + '@types/send@0.17.5': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 20.17.24 + + '@types/send@1.2.0': + dependencies: + '@types/node': 20.17.24 + + '@types/serve-static@1.15.9': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 20.17.24 + '@types/send': 0.17.5 + '@types/sql.js@1.4.9': dependencies: '@types/emscripten': 1.40.1 '@types/node': 20.17.24 + '@types/superagent@8.1.9': + dependencies: + '@types/cookiejar': 2.1.5 + '@types/methods': 1.1.4 + '@types/node': 20.17.24 + form-data: 4.0.4 + + '@types/supertest@6.0.3': + dependencies: + '@types/methods': 1.1.4 + '@types/superagent': 8.1.9 + '@types/tmp@0.2.6': {} '@types/toposort@2.0.7': {} @@ -3431,6 +3858,11 @@ snapshots: loupe: 3.1.4 tinyrainbow: 2.0.0 + accepts@2.0.0: + dependencies: + mime-types: 3.0.1 + negotiator: 1.0.0 + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -3464,8 +3896,12 @@ snapshots: argparse@2.0.1: {} + asap@2.0.6: {} + assertion-error@2.0.1: {} + asynckit@0.4.0: {} + balanced-match@1.0.2: {} base64-js@1.5.1: {} @@ -3485,6 +3921,20 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + body-parser@2.2.0: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.1 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 3.0.1 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -3512,6 +3962,8 @@ snapshots: esbuild: 0.25.5 load-tsconfig: 0.2.5 + bytes@3.1.2: {} + c12@3.1.0: dependencies: chokidar: 4.0.3 @@ -3605,12 +4057,18 @@ snapshots: colors@1.4.0: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + commander@11.0.0: {} commander@4.1.1: {} commander@8.3.0: {} + component-emitter@1.3.1: {} + concat-map@0.0.1: {} confbox@0.1.8: {} @@ -3619,6 +4077,22 @@ snapshots: consola@3.4.2: {} + content-disposition@1.0.0: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.1: {} + + cookiejar@2.1.4: {} + + copy-anything@4.0.5: + dependencies: + is-what: 5.5.0 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -3655,10 +4129,19 @@ snapshots: defu@6.1.4: {} + delayed-stream@1.0.0: {} + + depd@2.0.0: {} + destr@2.0.5: {} detect-libc@2.0.3: {} + dezalgo@1.0.4: + dependencies: + asap: 2.0.6 + wrappy: 1.0.2 + dotenv@16.6.1: {} dunder-proto@1.0.1: @@ -3669,6 +4152,8 @@ snapshots: eastasianwidth@0.2.0: {} + ee-first@1.1.1: {} + effect@3.16.12: dependencies: '@standard-schema/spec': 1.0.0 @@ -3680,6 +4165,8 @@ snapshots: empathic@2.0.0: {} + encodeurl@2.0.0: {} + end-of-stream@1.4.4: dependencies: once: 1.4.0 @@ -3694,6 +4181,13 @@ snapshots: dependencies: es-errors: 1.3.0 + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + esbuild@0.23.1: optionalDependencies: '@esbuild/aix-ppc64': 0.23.1 @@ -3749,6 +4243,8 @@ snapshots: '@esbuild/win32-ia32': 0.25.5 '@esbuild/win32-x64': 0.25.5 + escape-html@1.0.3: {} + escape-string-regexp@4.0.0: {} eslint-scope@8.4.0: @@ -3824,10 +4320,44 @@ snapshots: esutils@2.0.3: {} + etag@1.8.1: {} + expand-template@2.0.3: {} expect-type@1.2.1: {} + express@5.1.0: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.0 + content-disposition: 1.0.0 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.2.2 + debug: 4.4.1 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.0 + fresh: 2.0.0 + http-errors: 2.0.0 + merge-descriptors: 2.0.0 + mime-types: 3.0.1 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.0 + serve-static: 2.2.0 + statuses: 2.0.1 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + exsolve@1.0.7: {} fast-check@3.23.2: @@ -3848,6 +4378,8 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-safe-stringify@2.1.1: {} + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -3866,6 +4398,17 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@2.1.0: + dependencies: + debug: 4.4.1 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -3889,6 +4432,24 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + form-data@4.0.4: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + formidable@3.5.4: + dependencies: + '@paralleldrive/cuid2': 2.2.2 + dezalgo: 1.0.4 + once: 1.4.0 + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + fs-constants@1.0.0: {} fs-extra@11.1.1: @@ -3981,10 +4542,22 @@ snapshots: has-symbols@1.1.0: {} + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + hasown@2.0.2: dependencies: function-bind: 1.1.2 + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + https-proxy-agent@5.0.0: dependencies: agent-base: 6.0.2 @@ -3992,6 +4565,14 @@ snapshots: transitivePeerDependencies: - supports-color + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.7.0: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -4009,6 +4590,8 @@ snapshots: ini@1.3.8: {} + ipaddr.js@1.9.1: {} + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -4021,8 +4604,12 @@ snapshots: is-number@7.0.0: {} + is-promise@4.0.0: {} + is-unicode-supported@0.1.0: {} + is-what@5.5.0: {} + isarray@2.0.5: {} isexe@2.0.0: {} @@ -4140,13 +4727,33 @@ snapshots: math-intrinsics@1.1.0: {} + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + merge2@1.4.1: {} + methods@1.1.2: {} + micromatch@4.0.8: dependencies: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.1: + dependencies: + mime-db: 1.54.0 + + mime@2.6.0: {} + mimic-fn@2.1.0: {} mimic-response@3.1.0: {} @@ -4202,6 +4809,8 @@ snapshots: natural-compare@1.4.0: {} + negotiator@1.0.0: {} + node-abi@3.73.0: dependencies: semver: 7.7.2 @@ -4218,12 +4827,18 @@ snapshots: object-assign@4.1.1: {} + object-inspect@1.13.4: {} + object-keys@1.1.1: {} obuf@1.1.2: {} ohash@2.0.11: {} + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -4269,6 +4884,8 @@ snapshots: dependencies: callsites: 3.1.0 + parseurl@1.3.3: {} + path-exists@4.0.0: {} path-key@3.1.1: {} @@ -4283,6 +4900,8 @@ snapshots: lru-cache: 11.1.0 minipass: 7.1.2 + path-to-regexp@8.3.0: {} + pathe@2.0.3: {} pathval@2.0.0: {} @@ -4423,6 +5042,11 @@ snapshots: transitivePeerDependencies: - magicast + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + pump@3.0.2: dependencies: end-of-stream: 1.4.4 @@ -4432,10 +5056,27 @@ snapshots: pure-rand@6.1.0: {} + qs@6.13.0: + dependencies: + side-channel: 1.1.0 + + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + queue-microtask@1.2.3: {} railroad-diagrams@1.0.0: {} + range-parser@1.2.1: {} + + raw-body@3.0.1: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.7.0 + unpipe: 1.0.0 + rc9@2.1.2: dependencies: defu: 6.1.4 @@ -4497,14 +5138,51 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.44.0 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.1 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 safe-buffer@5.2.1: {} + safer-buffer@2.1.2: {} + semver@7.7.2: {} + send@1.2.0: + dependencies: + debug: 4.4.1 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.0 + mime-types: 3.0.1 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.0: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.0 + transitivePeerDependencies: + - supports-color + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -4514,12 +5192,42 @@ snapshots: gopd: 1.2.0 has-property-descriptors: 1.0.2 + setprototypeof@1.2.0: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 shebang-regex@3.0.0: {} + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} signal-exit@3.0.7: {} @@ -4546,6 +5254,8 @@ snapshots: stackback@0.0.2: {} + statuses@2.0.1: {} + std-env@3.9.0: {} string-width@4.2.3: @@ -4590,6 +5300,31 @@ snapshots: pirates: 4.0.7 ts-interface-checker: 0.1.13 + superagent@10.2.3: + dependencies: + component-emitter: 1.3.1 + cookiejar: 2.1.4 + debug: 4.4.1 + fast-safe-stringify: 2.1.1 + form-data: 4.0.4 + formidable: 3.5.4 + methods: 1.1.2 + mime: 2.6.0 + qs: 6.13.0 + transitivePeerDependencies: + - supports-color + + superjson@2.2.3: + dependencies: + copy-anything: 4.0.5 + + supertest@7.1.4: + dependencies: + methods: 1.1.2 + superagent: 10.2.3 + transitivePeerDependencies: + - supports-color + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -4640,6 +5375,8 @@ snapshots: dependencies: is-number: 7.0.0 + toidentifier@1.0.1: {} + toposort@2.0.2: {} tr46@1.0.1: @@ -4734,6 +5471,12 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.1 + typescript-eslint@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3): dependencies: '@typescript-eslint/eslint-plugin': 8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) @@ -4754,6 +5497,8 @@ snapshots: universalify@2.0.1: {} + unpipe@1.0.0: {} + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -4762,6 +5507,8 @@ snapshots: uuid@11.0.5: {} + vary@1.1.2: {} + vite-node@3.2.4(@types/node@20.17.24)(jiti@2.4.2)(tsx@4.20.3)(yaml@2.8.0): dependencies: cac: 6.7.14 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9dc25b4c..4eb5466d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -16,3 +16,4 @@ catalog: 'zod-validation-error': ^4.0.1 'better-sqlite3': ^12.2.0 'pg': ^8.13.1 + 'decimal.js': '^10.4.3' diff --git a/tests/e2e/package.json b/tests/e2e/package.json index 9a895dbc..6af6ca2b 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -20,7 +20,7 @@ "@zenstackhq/sdk": "workspace:*", "@zenstackhq/testtools": "workspace:*", "better-sqlite3": "catalog:", - "decimal.js": "^10.4.3", + "decimal.js": "catalog:", "kysely": "catalog:", "ulid": "^3.0.0", "uuid": "^11.0.5" diff --git a/tests/regression/package.json b/tests/regression/package.json index 268d3ceb..3b8e0ba2 100644 --- a/tests/regression/package.json +++ b/tests/regression/package.json @@ -9,7 +9,7 @@ }, "dependencies": { "@zenstackhq/testtools": "workspace:*", - "decimal.js": "^10.4.3" + "decimal.js": "catalog:" }, "devDependencies": { "@zenstackhq/cli": "workspace:*",