diff --git a/README.md b/README.md index b3159b4..585704c 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,11 @@ Node.js + Express (TypeScript) service that will power the StreamPay API gateway npm run build && npm start ``` -API will be at `http://localhost:3001` (or `PORT` env). Try `GET /health` and `GET /api/streams`. +API will be at `http://localhost:3001` (or `PORT` env). + +- **Health Check**: `GET /health` +- **Streams API**: `GET /api/v1/streams` +- **OpenAPI Spec**: `GET /api/openapi.json` ## Indexer webhook ingestion diff --git a/lint.txt b/lint.txt new file mode 100644 index 0000000..e69de29 diff --git a/lint2.txt b/lint2.txt new file mode 100644 index 0000000..e69de29 diff --git a/lint_fixed.txt b/lint_fixed.txt new file mode 100644 index 0000000..e10bdb1 --- /dev/null +++ b/lint_fixed.txt @@ -0,0 +1,12 @@ + +C:\Users\ADMIN\Desktop\remmy-drips\StreamPay-Backend\src\api\v1\streams.test.ts + 11:102 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any + 46:101 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any + +C:\Users\ADMIN\Desktop\remmy-drips\StreamPay-Backend\src\repositories\streamRepository.test.ts + 18:35 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any + 19:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any + 25:27 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any + +Γ£û 5 problems (5 errors, 0 warnings) + diff --git a/lint_output.txt b/lint_output.txt new file mode 100644 index 0000000..01487f8 Binary files /dev/null and b/lint_output.txt differ diff --git a/lint_results.json b/lint_results.json new file mode 100644 index 0000000..e83be2f Binary files /dev/null and b/lint_results.json differ diff --git a/package-lock.json b/package-lock.json index 5f8be52..4e5f0a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,13 @@ "name": "streampay-backend", "version": "0.1.0", "dependencies": { + "@asteasolutions/zod-to-openapi": "^8.5.0", "cors": "^2.8.5", "dotenv": "^17.3.1", "drizzle-orm": "^0.45.1", "express": "^4.21.0", "pg": "^8.20.0", + "prom-client": "^15.1.3", "zod": "^4.3.6" }, "devDependencies": { @@ -38,6 +40,18 @@ "node": ">=18" } }, + "node_modules/@asteasolutions/zod-to-openapi": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-8.5.0.tgz", + "integrity": "sha512-SABbKiObg5dLRiTFnqiW1WWwGcg1BJfmHtT2asIBnBHg6Smy/Ms2KHc650+JI4Hw7lSkdiNebEGXpwoxfben8Q==", + "license": "MIT", + "dependencies": { + "openapi3-ts": "^4.1.2" + }, + "peerDependencies": { + "zod": "^4.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -2473,7 +2487,7 @@ "version": "22.19.15", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true, "dependencies": { @@ -2484,7 +2498,7 @@ "version": "8.20.0", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true, "dependencies": { @@ -6315,6 +6329,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openapi3-ts": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.5.0.tgz", + "integrity": "sha512-jaL+HgTq2Gj5jRcfdutgRGLosCy/hT8sQf6VOy+P+g36cZOjI1iukdPnijC+4CmeRzg/jEllJUboEic2FhxhtQ==", + "license": "MIT", + "dependencies": { + "yaml": "^2.8.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -8341,7 +8364,7 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/unpipe": { @@ -8542,6 +8565,21 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -8599,6 +8637,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 00b9375..219b9ac 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,13 @@ "node": ">=18" }, "dependencies": { + "@asteasolutions/zod-to-openapi": "^8.5.0", "cors": "^2.8.5", "dotenv": "^17.3.1", "drizzle-orm": "^0.45.1", "express": "^4.21.0", "pg": "^8.20.0", + "prom-client": "^15.1.3", "zod": "^4.3.6" }, "devDependencies": { diff --git a/src/api/v1/__snapshots__/openapi.test.ts.snap b/src/api/v1/__snapshots__/openapi.test.ts.snap new file mode 100644 index 0000000..d82434d --- /dev/null +++ b/src/api/v1/__snapshots__/openapi.test.ts.snap @@ -0,0 +1,387 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`OpenAPI Specification should match the snapshot 1`] = ` +{ + "components": { + "parameters": {}, + "schemas": { + "Error": { + "properties": { + "error": { + "description": "Error message", + "example": "Invalid stream ID format", + "type": "string", + }, + }, + "required": [ + "error", + ], + "type": "object", + }, + "Health": { + "properties": { + "service": { + "example": "streampay-backend", + "type": "string", + }, + "status": { + "example": "ok", + "type": "string", + }, + "timestamp": { + "example": "2024-03-24T20:00:00Z", + "type": "string", + }, + }, + "required": [ + "status", + "service", + "timestamp", + ], + "type": "object", + }, + "Stream": { + "properties": { + "accruedEstimate": { + "description": "Estimated accrued amount since last settlement", + "example": "0.05", + "type": "string", + }, + "createdAt": { + "anyOf": [ + { + "format": "date-time", + "type": "string", + }, + { + "type": "string", + }, + ], + "description": "Creation timestamp", + "example": "2024-03-24T19:00:00Z", + }, + "endTime": { + "anyOf": [ + { + "format": "date-time", + "type": "string", + }, + { + "type": "string", + }, + { + "nullable": true, + }, + ], + "description": "End time of the stream (optional)", + "example": "2024-12-24T20:00:00Z", + }, + "id": { + "description": "Unique identifier for the stream", + "example": "550e8400-e29b-411d-a716-446655440000", + "format": "uuid", + "type": "string", + }, + "lastSettledAt": { + "anyOf": [ + { + "format": "date-time", + "type": "string", + }, + { + "type": "string", + }, + ], + "description": "Last time the stream was settled", + "example": "2024-03-24T20:05:00Z", + }, + "payer": { + "description": "Address of the payer", + "example": "0x1234567890123456789012345678901234567890", + "type": "string", + }, + "ratePerSecond": { + "description": "Payment rate per second", + "example": "0.0001", + "type": "string", + }, + "recipient": { + "description": "Address of the recipient", + "example": "0x0987654321098765432109876543210987654321", + "type": "string", + }, + "startTime": { + "anyOf": [ + { + "format": "date-time", + "type": "string", + }, + { + "type": "string", + }, + ], + "description": "Start time of the stream", + "example": "2024-03-24T20:00:00Z", + }, + "status": { + "description": "Status of the payment stream", + "enum": [ + "active", + "paused", + "cancelled", + "completed", + ], + "example": "active", + "type": "string", + }, + "totalAmount": { + "description": "Total amount allocated for the stream", + "example": "100.0", + "type": "string", + }, + "updatedAt": { + "anyOf": [ + { + "format": "date-time", + "type": "string", + }, + { + "type": "string", + }, + ], + "description": "Last update timestamp", + "example": "2024-03-24T20:00:00Z", + }, + }, + "required": [ + "id", + "payer", + "recipient", + "status", + "ratePerSecond", + "startTime", + "endTime", + "totalAmount", + "lastSettledAt", + "createdAt", + "updatedAt", + ], + "type": "object", + }, + "StreamList": { + "properties": { + "limit": { + "description": "Number of streams returned in this batch", + "example": 20, + "type": "number", + }, + "offset": { + "description": "Offset for pagination", + "example": 0, + "type": "number", + }, + "streams": { + "items": { + "$ref": "#/components/schemas/Stream", + }, + "type": "array", + }, + "total": { + "description": "Total number of streams matching the criteria", + "example": 100, + "type": "number", + }, + }, + "required": [ + "streams", + "total", + "limit", + "offset", + ], + "type": "object", + }, + }, + }, + "info": { + "description": "API for managing payment streams, metering, and settlement.", + "title": "StreamPay API", + "version": "1.0.0", + }, + "openapi": "3.0.0", + "paths": { + "/api/v1/streams": { + "get": { + "description": "Get a list of payment streams", + "parameters": [ + { + "description": "Filter by payer address", + "in": "query", + "name": "payer", + "required": false, + "schema": { + "description": "Filter by payer address", + "type": "string", + }, + }, + { + "description": "Filter by recipient address", + "in": "query", + "name": "recipient", + "required": false, + "schema": { + "description": "Filter by recipient address", + "type": "string", + }, + }, + { + "description": "Status of the payment stream", + "in": "query", + "name": "status", + "required": false, + "schema": { + "description": "Status of the payment stream", + "enum": [ + "active", + "paused", + "cancelled", + "completed", + ], + "example": "active", + "type": "string", + }, + }, + { + "description": "Number of records to return", + "in": "query", + "name": "limit", + "required": false, + "schema": { + "description": "Number of records to return", + "example": "20", + "type": "string", + }, + }, + { + "description": "Number of records to skip", + "in": "query", + "name": "offset", + "required": false, + "schema": { + "description": "Number of records to skip", + "example": "0", + "type": "string", + }, + }, + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StreamList", + }, + }, + }, + "description": "List of streams", + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error", + }, + }, + }, + "description": "Internal server error", + }, + }, + "summary": "List Streams", + }, + }, + "/api/v1/streams/{id}": { + "get": { + "description": "Get details of a specific payment stream", + "parameters": [ + { + "description": "The unique identifier of the stream", + "in": "path", + "name": "id", + "required": true, + "schema": { + "description": "The unique identifier of the stream", + "format": "uuid", + "type": "string", + }, + }, + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Stream", + }, + }, + }, + "description": "Stream details", + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error", + }, + }, + }, + "description": "Invalid stream ID format", + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error", + }, + }, + }, + "description": "Stream not found", + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error", + }, + }, + }, + "description": "Internal server error", + }, + }, + "summary": "Get Stream", + }, + }, + "/health": { + "get": { + "description": "Get service health status", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Health", + }, + }, + }, + "description": "Service is healthy", + }, + }, + "summary": "Health Check", + }, + }, + }, + "servers": [ + { + "url": "/", + }, + ], +} +`; diff --git a/src/api/v1/openapi.test.ts b/src/api/v1/openapi.test.ts new file mode 100644 index 0000000..43e4aee --- /dev/null +++ b/src/api/v1/openapi.test.ts @@ -0,0 +1,25 @@ +import request from "supertest"; +import app from "../../index"; +import { generateOpenApi } from "./openapi"; + +describe("OpenAPI Specification", () => { + it("should serve the OpenAPI JSON at /api/openapi.json", async () => { + const response = await request(app).get("/api/openapi.json"); + expect(response.status).toBe(200); + expect(response.body.openapi).toBe("3.0.0"); + expect(response.body.info.title).toBe("StreamPay API"); + }); + + it("should include health and stream paths", () => { + const spec = generateOpenApi(); + expect(spec.paths).toHaveProperty("/health"); + expect(spec.paths).toHaveProperty("/api/v1/streams"); + expect(spec.paths).toHaveProperty("/api/v1/streams/{id}"); + }); + + it("should match the snapshot", () => { + const spec = generateOpenApi(); + // We normalize some fields if necessary, but here we just snapshot + expect(spec).toMatchSnapshot(); + }); +}); diff --git a/src/api/v1/openapi.ts b/src/api/v1/openapi.ts new file mode 100644 index 0000000..dc26e02 --- /dev/null +++ b/src/api/v1/openapi.ts @@ -0,0 +1,131 @@ +import { OpenApiGeneratorV3, OpenAPIRegistry } from "@asteasolutions/zod-to-openapi"; +import { z } from "zod"; +import { + StreamSchema, + StreamListSchema, + ErrorSchema, + HealthSchema, + StreamStatusSchema, +} from "./schemas"; + +export const registry = new OpenAPIRegistry(); + +// Register Schemas +registry.register("Stream", StreamSchema); +registry.register("StreamList", StreamListSchema); +registry.register("Error", ErrorSchema); +registry.register("Health", HealthSchema); + +// GET /health +registry.registerPath({ + method: "get", + path: "/health", + description: "Get service health status", + summary: "Health Check", + responses: { + 200: { + description: "Service is healthy", + content: { + "application/json": { + schema: HealthSchema, + }, + }, + }, + }, +}); + +// GET /api/v1/streams +registry.registerPath({ + method: "get", + path: "/api/v1/streams", + description: "Get a list of payment streams", + summary: "List Streams", + request: { + query: z.object({ + payer: z.string().optional().openapi({ description: "Filter by payer address" }), + recipient: z.string().optional().openapi({ description: "Filter by recipient address" }), + status: StreamStatusSchema.optional(), + limit: z.string().optional().openapi({ description: "Number of records to return", example: "20" }), + offset: z.string().optional().openapi({ description: "Number of records to skip", example: "0" }), + }), + }, + responses: { + 200: { + description: "List of streams", + content: { + "application/json": { + schema: StreamListSchema, + }, + }, + }, + 500: { + description: "Internal server error", + content: { + "application/json": { + schema: ErrorSchema, + }, + }, + }, + }, +}); + +// GET /api/v1/streams/{id} +registry.registerPath({ + method: "get", + path: "/api/v1/streams/{id}", + description: "Get details of a specific payment stream", + summary: "Get Stream", + request: { + params: z.object({ + id: z.string().uuid().openapi({ description: "The unique identifier of the stream" }), + }), + }, + responses: { + 200: { + description: "Stream details", + content: { + "application/json": { + schema: StreamSchema, + }, + }, + }, + 400: { + description: "Invalid stream ID format", + content: { + "application/json": { + schema: ErrorSchema, + }, + }, + }, + 404: { + description: "Stream not found", + content: { + "application/json": { + schema: ErrorSchema, + }, + }, + }, + 500: { + description: "Internal server error", + content: { + "application/json": { + schema: ErrorSchema, + }, + }, + }, + }, +}); + +export function generateOpenApi() { + const generator = new OpenApiGeneratorV3(registry.definitions); + + return generator.generateDocument({ + openapi: "3.0.0", + info: { + version: "1.0.0", + title: "StreamPay API", + description: "API for managing payment streams, metering, and settlement.", + }, + servers: [{ url: "/" }], + }); +} diff --git a/src/api/v1/schemas.ts b/src/api/v1/schemas.ts new file mode 100644 index 0000000..d39e13c --- /dev/null +++ b/src/api/v1/schemas.ts @@ -0,0 +1,86 @@ +import { z } from "zod"; +import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi"; + +extendZodWithOpenApi(z); + +export const StreamStatusSchema = z.enum(["active", "paused", "cancelled", "completed"]).openapi({ + description: "Status of the payment stream", + example: "active", +}); + +export const StreamSchema = z.object({ + id: z.string().uuid().openapi({ + description: "Unique identifier for the stream", + example: "550e8400-e29b-411d-a716-446655440000", + }), + payer: z.string().openapi({ + description: "Address of the payer", + example: "0x1234567890123456789012345678901234567890", + }), + recipient: z.string().openapi({ + description: "Address of the recipient", + example: "0x0987654321098765432109876543210987654321", + }), + status: StreamStatusSchema, + ratePerSecond: z.string().openapi({ + description: "Payment rate per second", + example: "0.0001", + }), + startTime: z.date().or(z.string()).openapi({ + description: "Start time of the stream", + example: "2024-03-24T20:00:00Z", + }), + endTime: z.date().or(z.string()).nullable().openapi({ + description: "End time of the stream (optional)", + example: "2024-12-24T20:00:00Z", + }), + totalAmount: z.string().openapi({ + description: "Total amount allocated for the stream", + example: "100.0", + }), + lastSettledAt: z.date().or(z.string()).openapi({ + description: "Last time the stream was settled", + example: "2024-03-24T20:05:00Z", + }), + accruedEstimate: z.string().optional().openapi({ + description: "Estimated accrued amount since last settlement", + example: "0.05", + }), + createdAt: z.date().or(z.string()).openapi({ + description: "Creation timestamp", + example: "2024-03-24T19:00:00Z", + }), + updatedAt: z.date().or(z.string()).openapi({ + description: "Last update timestamp", + example: "2024-03-24T20:00:00Z", + }), +}).openapi("Stream"); + +export const StreamListSchema = z.object({ + streams: z.array(StreamSchema), + total: z.number().openapi({ + description: "Total number of streams matching the criteria", + example: 100, + }), + limit: z.number().openapi({ + description: "Number of streams returned in this batch", + example: 20, + }), + offset: z.number().openapi({ + description: "Offset for pagination", + example: 0, + }), +}).openapi("StreamList"); + +export const ErrorSchema = z.object({ + error: z.string().openapi({ + description: "Error message", + example: "Invalid stream ID format", + }), +}).openapi("Error"); + +export const HealthSchema = z.object({ + status: z.string().openapi({ example: "ok" }), + service: z.string().openapi({ example: "streampay-backend" }), + timestamp: z.string().openapi({ example: "2024-03-24T20:00:00Z" }), +}).openapi("Health"); diff --git a/src/api/v1/streams.test.ts b/src/api/v1/streams.test.ts index d473a4a..60aea1e 100644 --- a/src/api/v1/streams.test.ts +++ b/src/api/v1/streams.test.ts @@ -8,7 +8,8 @@ describe("Stream API Routes", () => { it("should return 200 and the stream when found", async () => { const mockStream = { id: validId, payer: "p1", accruedEstimate: "10.5" }; - const spy = jest.spyOn(StreamRepository.prototype, "findById").mockResolvedValue(mockStream as never); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const spy = jest.spyOn(StreamRepository.prototype, "findById").mockResolvedValue(mockStream as any); const response = await request(app).get(`/api/v1/streams/${validId}`); @@ -43,7 +44,8 @@ describe("Stream API Routes", () => { limit: 20, offset: 0, }; - const spy = jest.spyOn(StreamRepository.prototype, "findAll").mockResolvedValue(mockResult as never); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const spy = jest.spyOn(StreamRepository.prototype, "findAll").mockResolvedValue(mockResult as any); const response = await request(app).get("/api/v1/streams?payer=p1"); diff --git a/src/index.ts b/src/index.ts index aaeb355..52483ea 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,9 @@ import cors from "cors"; import express, { Request, Response } from "express"; -import v1Router from "./api/v1/router"; +import streamRoutes from "./api/v1/streams"; +import { generateOpenApi } from "./api/v1/openapi"; +import { metricsHandler, metricsMiddleware } from "./metrics/prometheus"; import indexerWebhookRouter from "./routes/webhooks/indexer"; @@ -17,14 +19,26 @@ app.get("/metrics", metricsHandler); app.use(metricsMiddleware); app.use(cors()); -app.use("/webhooks/indexer", express.raw({ type: "application/json" }), indexerWebhookRouter); +app.use( + "/webhooks/indexer", + express.raw({ type: "application/json" }), + indexerWebhookRouter, +); app.use(express.json()); app.get("/health", (_req: Request, res: Response) => { - res.json({ status: "ok", service: "streampay-backend", timestamp: new Date().toISOString() }); + res.json({ + status: "ok", + service: "streampay-backend", + timestamp: new Date().toISOString(), + }); +}); + +app.get("/api/openapi.json", (_req: Request, res: Response) => { + res.json(generateOpenApi()); }); -app.use("/api/v1", v1Router); +app.use("/api/v1/streams", streamRoutes); if (require.main === module) { app.listen(PORT, () => { diff --git a/src/indexerWebhook.test.ts b/src/indexerWebhook.test.ts index 54e73a7..d0d73ca 100644 --- a/src/indexerWebhook.test.ts +++ b/src/indexerWebhook.test.ts @@ -1,8 +1,7 @@ import crypto from "crypto"; - import request from "supertest"; - import app from "./index"; + import { eventIngestionService } from "./services/eventIngestionService"; const secret = "test-indexer-secret"; @@ -111,7 +110,7 @@ describe("POST /webhooks/indexer", () => { }); it("rejects malformed JSON even with a valid signature", async () => { - const body = "{\"eventId\":"; + const body = '{"eventId":'; const res = await request(app) .post("/webhooks/indexer") diff --git a/src/repositories/streamRepository.test.ts b/src/repositories/streamRepository.test.ts index 93e008e..748bb0d 100644 --- a/src/repositories/streamRepository.test.ts +++ b/src/repositories/streamRepository.test.ts @@ -15,14 +15,16 @@ describe("StreamRepository", () => { jest.clearAllMocks(); }); - const createMockQuery = (value: T) => { - const query = { + const createMockQuery = (value: unknown) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const query: any = { from: jest.fn().mockReturnThis(), where: jest.fn().mockReturnThis(), orderBy: jest.fn().mockReturnThis(), limit: jest.fn().mockReturnThis(), offset: jest.fn().mockReturnThis(), - then: (onfulfilled: (value: T) => unknown) => Promise.resolve(value).then(onfulfilled), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + then: (onfulfilled: any) => Promise.resolve(value).then(onfulfilled), }; return query; };