Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"packages/openapi-express-viewer": "4.1.0",
"packages/openapi-helpers": "5.1.0",
"packages/openapi-typed-request-handler": "0.1.0",
"packages/openapi-typed-request-sender": "0.1.0",
"packages/semantic-conventions": "1.0.0",
"packages/tracing": "1.0.0",
"packages/tracing-utils": "1.0.0",
Expand Down
8 changes: 1 addition & 7 deletions packages/openapi-helpers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@
"description": "A package that provides utilities for working with openapi files",
"type": "commonjs",
"exports": {
"./requestSender": {
"types": "./dist/requestSender/requestSender.d.ts",
"default": "./dist/requestSender/requestSender.js"
},
"./generators": {
"types": "./dist/generator/index.d.ts",
"default": "./dist/generator/index.js"
Expand All @@ -24,9 +20,7 @@
"prepack": "turbo run build",
"check-dist": "publint && attw --profile node16 --pack .",
"knip": "knip --directory ../.. --workspace packages/openapi-helpers",
"api:check": "pnpm run sender:api:check && pnpm run generators:api:check",
"sender:api": "api-extractor run --local --verbose --config ./api-extractor.sender.json",
"sender:api:check": "api-extractor run --verbose --config ./api-extractor.sender.json",
"api:check": "pnpm run generators:api:check",
"generators:api": "api-extractor run --local --verbose --config ./api-extractor.generators.json",
"generators:api:check": "api-extractor run --verbose --config ./api-extractor.generators.json",
"generate:test:types": "pnpm run build && node dist/generator/generateTypes.mjs tests/openapi3.yaml tests/types.d.ts"
Expand Down
28 changes: 28 additions & 0 deletions packages/openapi-typed-request-sender/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# openapi-typed-request-sender

Supertest-based testing utilities that provide full type safety and autocomplete based on OpenAPI specifications.

## Installation

```bash
npm install --save-dev @map-colonies/openapi-typed-request-sender supertest
```

## Usage

```typescript
import { createRequestSender, expectResponseStatusFactory } from '@map-colonies/openapi-typed-request-sender';
import type { paths, operations } from './types.d.ts';

const requestSender = await createRequestSender<paths, operations>('openapi.yaml', expressApp);

// Call API by operation name!
const response = await requestSender.getUser({ pathParams: { id: '123' } });

// Type-safe status assertion
const expectResponseStatus = expectResponseStatusFactory(expect);
expectResponseStatus(response, 200);

// response.body is now correctly typed for status 200
console.log(response.body.name);
```
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"extends": "../../api-extractor.json",
"mainEntryPointFilePath": "dist/requestSender/requestSender.d.ts",
"mainEntryPointFilePath": "dist/index.d.ts",
"apiReport": {
"enabled": true,
"reportFileName": "sender.<unscopedPackageName>"
"reportFileName": "openapi-typed-request-sender.api.md"
}
}
4 changes: 4 additions & 0 deletions packages/openapi-typed-request-sender/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import baseConfig from '@map-colonies/eslint-config/ts-base';
import { defineConfig } from 'eslint/config';

export default defineConfig(baseConfig, { ignores: ['vitest.config.ts'] });
59 changes: 59 additions & 0 deletions packages/openapi-typed-request-sender/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"name": "@map-colonies/openapi-typed-request-sender",
"version": "0.1.0",
"description": "Supertest-based testing utilities for OpenAPI-defined APIs",
"type": "commonjs",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"scripts": {
"test": "vitest run",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"prebuild": "pnpm run clean",
"build": "tsc --project tsconfig.build.json",
"clean": "rimraf dist",
"prepack": "turbo run build",
"check-dist": "publint && attw --profile node16 --pack .",
"api:check": "api-extractor run --verbose --config ./api-extractor.json",
"api": "api-extractor run --local --verbose --config ./api-extractor.json"
},
"repository": "github:MapColonies/infra-packages",
"files": [
"dist/**/*"
],
"publishConfig": {
"access": "public"
},
"engines": {
"node": ">=24"
},
"dependencies": {
"oas-normalize": "^15.0.0",
"ts-essentials": "^10.1.1"
},
"peerDependencies": {
"@types/express": "^4.17.21",
"openapi-typescript": "^7.4.1",
"supertest": "^7.0.0"
},
"devDependencies": {
"@map-colonies/eslint-config": "workspace:^",
"@map-colonies/tsconfig": "workspace:^",
"@types/node": "catalog:",
"@types/supertest": "catalog:",
"body-parser": "2.2.2",
"@types/body-parser": "1.19.6",
"express": "5.2.1",
"eslint": "catalog:",
"rimraf": "catalog:",
"typescript": "catalog:",
"vitest-config": "workspace:^",
"vitest": "catalog:",
"@microsoft/api-extractor": "catalog:",
"openapi-types": "12.1.3"
}
}
31 changes: 31 additions & 0 deletions packages/openapi-typed-request-sender/src/common/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { WritableKeys } from 'ts-essentials';

type HasContent<T> = [T] extends [{ content: any }] ? T['content']['application/json'] : never;

export type AddIfNotNever<T, U> = [U] extends [never] ? T : T & U;
export type PickWritable<T extends NonNullable<unknown>> = Pick<T, WritableKeys<T>>;

export type Methods = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head' | 'options' | 'trace';

export type OperationsTemplate = Record<string, any>;

export type PathsTemplate = Record<
string,
{
parameters: {
query?: any;
header?: any;
path?: any;
cookie?: any;
};
} & {
[key in Methods]?: OperationsTemplate;
}
>;

export type ResponseObjectToFlat<T> = [T] extends [{ responses: any }]
? {
[res in keyof T['responses']]: { status: res; body: HasContent<T['responses'][res]> };
}[keyof T['responses']]
: never;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image

update the example

File renamed without changes.
3 changes: 3 additions & 0 deletions packages/openapi-typed-request-sender/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './requestSender.js';
export * from './types.js';
export * from './expect.js';
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ import type express from 'express';
import oasNormalize from 'oas-normalize';
import type { OmitProperties } from 'ts-essentials';
import type { OpenAPIV3 } from 'openapi-types';
import type { PathsTemplate, Methods, OperationsTemplate } from '../common/types';
import { expectResponseStatusFactory } from './expect';
import type { ExpectResponseStatus } from './expect';
import type { PathsTemplate, Methods, OperationsTemplate } from './common/types';
import type { PathRequestOptions, RequestOptions, OperationsNames, RequestSender, RequestReturn, RequestSenderOptions } from './types';

function sendRequest<
Expand Down Expand Up @@ -96,8 +94,6 @@ function getOperationsPathAndMethod<Paths extends PathsTemplate, Operations exte
return result as Record<OperationsNames<Operations>, { path: keyof Paths; method: Methods }>;
}

export { RequestSender, expectResponseStatusFactory, ExpectResponseStatus };

/**
* Creates a request sender object that can be used to send fake HTTP requests using supertest based on an OpenAPI specification.
* The openapi types should be generated using the openapi-typescript package.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { OmitProperties, OptionalKeys, Prettify, RequiredKeys } from 'ts-essentials';
import type * as supertest from 'supertest';
import type { AddIfNotNever, OperationsTemplate, PathsTemplate, PickWritable } from '../common/types';
import type { AddIfNotNever, OperationsTemplate, PathsTemplate, PickWritable, ResponseObjectToFlat } from './common/types';

/**
* Configuration options for the request sender.
Expand All @@ -21,14 +21,6 @@ interface Headers {
headers?: Record<string, string>;
}

type HasContent<T> = [T] extends [{ content: any }] ? T['content']['application/json'] : never;

export type ResponseObjectToFlat<T> = [T] extends [{ responses: any }]
? {
[res in keyof T['responses']]: { status: res; body: HasContent<T['responses'][res]> };
}[keyof T['responses']]
: never;

type PathParamsObj<T> = [T] extends [{ parameters: { path: NonNullable<any> } }] ? { pathParams: T['parameters']['path'] } : never;

type QueryParamsObj<T> = [T] extends [{ parameters: { query?: NonNullable<any> } }]
Expand Down
Loading
Loading