From b6ec5482a7ca08973dfe9cc3bd3f5a8ed41fc2b7 Mon Sep 17 00:00:00 2001 From: Ryjen1 Date: Wed, 25 Feb 2026 14:23:52 +0100 Subject: [PATCH 1/4] chore: add eslint and prettier --- .prettierignore | 3 ++ .prettierrc.json | 8 ++++ README.md | 19 +++++--- eslint.config.js | 57 ++++++++++++++++++++++++ eslintrc.json | 17 -------- package-lock.json | 107 +++++++++++++++++++++++++++++++++++++++++++--- package.json | 8 +++- src/index.test.ts | 3 +- src/index.ts | 2 +- 9 files changed, 191 insertions(+), 33 deletions(-) create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 eslint.config.js delete mode 100644 eslintrc.json diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..1502ec2 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +dist +node_modules + diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..d64eabc --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,8 @@ +{ + "singleQuote": true, + "semi": true, + "tabWidth": 2, + "printWidth": 80, + "trailingComma": "es5" +} + diff --git a/README.md b/README.md index 956d585..b0fa4bb 100644 --- a/README.md +++ b/README.md @@ -30,11 +30,20 @@ API gateway, usage metering, and billing services for the Callora API marketplac ## Scripts -| Command | Description | -|----------------|--------------------------------| -| `npm run dev` | Run with tsx watch (no build) | -| `npm run build`| Compile TypeScript to `dist/` | -| `npm start` | Run compiled `dist/index.js` | +| Command | Description | +| ------------------ | ------------------------------------ | +| `npm run dev` | Run with tsx watch (no build) | +| `npm run build` | Compile TypeScript to `dist/` | +| `npm start` | Run compiled `dist/index.js` | +| `npm run lint` | Run ESLint on `src/` | +| `npm run lint:fix` | Auto-fix ESLint issues | +| `npm run format` | Format `src/` + README with Prettier | + +## Linting and formatting + +- `npm run lint` runs ESLint on `src/` +- `npm run lint:fix` applies ESLint auto-fixes +- `npm run format` formats files with Prettier ## Project layout diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..070350b --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,57 @@ +import tsParser from '@typescript-eslint/parser'; +import tsPlugin from '@typescript-eslint/eslint-plugin'; +import prettierPlugin from 'eslint-plugin-prettier'; +import prettierConfig from 'eslint-config-prettier'; +import eslintJs from '@eslint/js'; + +const globals = { + console: 'readonly', + process: 'readonly', + Buffer: 'readonly', + setImmediate: 'readonly', + clearImmediate: 'readonly', + setInterval: 'readonly', + clearInterval: 'readonly', + setTimeout: 'readonly', + clearTimeout: 'readonly', + describe: 'readonly', + it: 'readonly', + test: 'readonly', + expect: 'readonly', + beforeAll: 'readonly', + afterAll: 'readonly', + beforeEach: 'readonly', + afterEach: 'readonly', + jest: 'readonly', +}; + +export default [ + { + ignores: ['dist/**', 'node_modules/**'], + }, + eslintJs.configs.recommended, + { + files: ['src/**/*.ts'], + languageOptions: { + parser: tsParser, + ecmaVersion: 2022, + sourceType: 'module', + globals, + }, + plugins: { + '@typescript-eslint': tsPlugin, + prettier: prettierPlugin, + }, + rules: { + ...tsPlugin.configs.recommended.rules, + ...prettierConfig.rules, + 'prettier/prettier': [ + 'error', + { + singleQuote: true, + semi: true, + }, + ], + }, + }, +]; diff --git a/eslintrc.json b/eslintrc.json deleted file mode 100644 index b28655a..0000000 --- a/eslintrc.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "env": { - "node": true, - "es2022": true, - "jest": true - }, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "sourceType": "module" - }, - "plugins": ["@typescript-eslint"], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended" - ], - "ignorePatterns": ["dist", "node_modules"] -} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 6984af8..61db13c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "express": "^4.18.2" }, "devDependencies": { + "@eslint/js": "^9.0.0", "@types/express": "^4.17.21", "@types/jest": "^30.0.0", "@types/node": "^20.10.0", @@ -18,7 +19,10 @@ "@typescript-eslint/eslint-plugin": "^8.56.1", "@typescript-eslint/parser": "^8.56.1", "eslint": "^10.0.2", + "eslint-config-prettier": "^10.0.0", + "eslint-plugin-prettier": "^5.2.1", "jest": "^30.2.0", + "prettier": "^3.5.0", "supertest": "^7.2.2", "ts-jest": "^29.4.6", "tsx": "^4.7.0", @@ -56,7 +60,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1163,6 +1166,19 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@eslint/js": { + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz", + "integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, "node_modules/@eslint/object-schema": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.2.tgz", @@ -2742,7 +2758,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3058,7 +3073,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3730,7 +3744,6 @@ "integrity": "sha512-uYixubwmqJZH+KLVYIVKY1JQt7tysXhtj21WSvjcSmU5SVNzMus1bgLe+pAt816yQ8opKfheVVoPLqvVMGejYw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -3781,6 +3794,53 @@ } } }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz", + "integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.12" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, "node_modules/eslint-scope": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.1.tgz", @@ -4073,6 +4133,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -4836,7 +4903,6 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -6138,6 +6204,35 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/pretty-format": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", @@ -6959,7 +7054,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7158,7 +7252,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 1dc3f49..42f5664 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "build": "tsc", "start": "node dist/index.js", "dev": "tsx watch src/index.ts", - "lint": "eslint . --ext .ts", + "lint": "eslint \"src/**/*.ts\"", + "lint:fix": "npm run lint -- --fix", + "format": "prettier --write \"src/**/*.ts\" \"README.md\"", "typecheck": "tsc --noEmit", "test": "jest --runInBand" }, @@ -14,6 +16,7 @@ "express": "^4.18.2" }, "devDependencies": { + "@eslint/js": "^9.0.0", "@types/express": "^4.17.21", "@types/jest": "^30.0.0", "@types/node": "^20.10.0", @@ -21,7 +24,10 @@ "@typescript-eslint/eslint-plugin": "^8.56.1", "@typescript-eslint/parser": "^8.56.1", "eslint": "^10.0.2", + "eslint-config-prettier": "^10.0.0", + "eslint-plugin-prettier": "^5.2.1", "jest": "^30.2.0", + "prettier": "^3.5.0", "supertest": "^7.2.2", "ts-jest": "^29.4.6", "tsx": "^4.7.0", diff --git a/src/index.test.ts b/src/index.test.ts index 73a05dc..e7b8c11 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,5 +1,4 @@ import request from 'supertest'; -import express from 'express'; import app from './index'; describe('Health API', () => { @@ -8,4 +7,4 @@ describe('Health API', () => { expect(response.status).toBe(200); expect(response.body.status).toBe('ok'); }); -}); \ No newline at end of file +}); diff --git a/src/index.ts b/src/index.ts index c40217b..07ea34c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,4 +23,4 @@ if (process.env.NODE_ENV !== 'test') { }); } -export default app; \ No newline at end of file +export default app; From eed4ff0f5c235036ad56dc20fefa2fffbaad416f Mon Sep 17 00:00:00 2001 From: Ryjen1 Date: Wed, 25 Feb 2026 14:28:39 +0100 Subject: [PATCH 2/4] chore: make jest config cjs --- jest.config.js => jest.config.cjs | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename jest.config.js => jest.config.cjs (100%) diff --git a/jest.config.js b/jest.config.cjs similarity index 100% rename from jest.config.js rename to jest.config.cjs From fbf5b9abe71f45057b924e649dc4219237106ff2 Mon Sep 17 00:00:00 2001 From: Ryjen1 Date: Wed, 25 Feb 2026 14:42:25 +0100 Subject: [PATCH 3/4] docs: add OpenAPI spec and swagger ui --- README.md | 2 + src/index.ts | 30 +++++++++++++ src/openapi.ts | 111 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 143 insertions(+) create mode 100644 src/openapi.ts diff --git a/README.md b/README.md index b0fa4bb..d9d638c 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ API gateway, usage metering, and billing services for the Callora API marketplac - Health check: `GET /api/health` - Placeholder routes: `GET /api/apis`, `GET /api/usage` +- OpenAPI spec: `GET /api/openapi.json` +- Swagger UI: `GET /api/docs` - JSON body parsing; ready to add auth, metering, and contract calls ## Local setup diff --git a/src/index.ts b/src/index.ts index 07ea34c..f9c6f84 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,40 @@ import express from 'express'; +import { openApiSpec } from './openapi.js'; const app = express(); const PORT = process.env.PORT ?? 3000; app.use(express.json()); +app.get('/api/openapi.json', (_req, res) => { + res.json(openApiSpec); +}); + +app.get('/api/docs', (_req, res) => { + res.type('html').send(` + + + + + Callora Backend API Docs + + + +
+ + + +`); +}); + app.get('/api/health', (_req, res) => { res.json({ status: 'ok', service: 'callora-backend' }); }); diff --git a/src/openapi.ts b/src/openapi.ts new file mode 100644 index 0000000..18e5766 --- /dev/null +++ b/src/openapi.ts @@ -0,0 +1,111 @@ +export const openApiSpec = { + openapi: '3.0.3', + info: { + title: 'Callora Backend API', + version: '0.0.1', + description: + 'API gateway, usage metering, and billing services for the Callora API marketplace.', + }, + servers: [{ url: 'http://localhost:3000' }], + components: { + securitySchemes: { + bearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }, + }, + schemas: { + HealthResponse: { + type: 'object', + additionalProperties: false, + properties: { + status: { type: 'string', example: 'ok' }, + service: { type: 'string', example: 'callora-backend' }, + }, + required: ['status', 'service'], + }, + ApisResponse: { + type: 'object', + additionalProperties: false, + properties: { + apis: { + type: 'array', + items: { type: 'string' }, + example: [], + }, + }, + required: ['apis'], + }, + UsageResponse: { + type: 'object', + additionalProperties: false, + properties: { + calls: { type: 'number', example: 0 }, + period: { type: 'string', example: 'current' }, + }, + required: ['calls', 'period'], + }, + ErrorResponse: { + type: 'object', + additionalProperties: false, + properties: { + error: { type: 'string' }, + message: { type: 'string' }, + }, + required: ['error', 'message'], + }, + }, + }, + paths: { + '/api/health': { + get: { + summary: 'Health check', + tags: ['System'], + responses: { + 200: { + description: 'Service is healthy', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/HealthResponse' }, + }, + }, + }, + }, + }, + }, + '/api/apis': { + get: { + summary: 'List APIs (placeholder)', + tags: ['APIs'], + responses: { + 200: { + description: 'Returns available APIs (currently placeholder)', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/ApisResponse' }, + }, + }, + }, + }, + }, + }, + '/api/usage': { + get: { + summary: 'Get usage (placeholder)', + tags: ['Usage'], + responses: { + 200: { + description: 'Returns usage summary (currently placeholder)', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/UsageResponse' }, + }, + }, + }, + }, + }, + }, + }, +} as const; + From 561d415e576539f431cfedf80002bbc4bdb38876 Mon Sep 17 00:00:00 2001 From: Ryjen1 Date: Wed, 25 Feb 2026 14:48:06 +0100 Subject: [PATCH 4/4] chore: fix build and jest for ESM --- jest.config.cjs | 21 ++++++++++++++++++--- package.json | 2 +- src/openapi.ts | 1 - tsconfig.json | 3 ++- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/jest.config.cjs b/jest.config.cjs index 779b71c..dbc5d87 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -1,6 +1,21 @@ /** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { - preset: 'ts-jest', testEnvironment: 'node', - testMatch: ['**/?(*.)+(spec|test).ts'] -}; \ No newline at end of file + testMatch: ['**/?(*.)+(spec|test).ts'], + extensionsToTreatAsEsm: ['.ts'], + transform: { + '^.+\\.ts$': [ + 'ts-jest', + { + useESM: true, + isolatedModules: true, + diagnostics: { + ignoreCodes: [151002], + }, + }, + ], + }, + moduleNameMapper: { + '^(\\.{1,2}/.*)\\.js$': '$1', + }, +}; diff --git a/package.json b/package.json index 42f5664..9ea54e0 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "lint:fix": "npm run lint -- --fix", "format": "prettier --write \"src/**/*.ts\" \"README.md\"", "typecheck": "tsc --noEmit", - "test": "jest --runInBand" + "test": "NODE_OPTIONS=--experimental-vm-modules jest --runInBand" }, "dependencies": { "express": "^4.18.2" diff --git a/src/openapi.ts b/src/openapi.ts index 18e5766..b81c6a7 100644 --- a/src/openapi.ts +++ b/src/openapi.ts @@ -108,4 +108,3 @@ export const openApiSpec = { }, }, } as const; - diff --git a/tsconfig.json b/tsconfig.json index a00d463..fe86a8a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,5 +10,6 @@ "types": ["node", "jest"], "skipLibCheck": true }, - "include": ["src"] + "include": ["src"], + "exclude": ["src/**/*.test.ts", "src/**/*.spec.ts"] }