Skip to content

Commit b14d877

Browse files
authored
Merge pull request #21 from aligent/release/1.1.1
Release/1.1.1
2 parents 8e2cf64 + dd388d1 commit b14d877

File tree

10 files changed

+585
-285
lines changed

10 files changed

+585
-285
lines changed

package.json

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@aligent/bigcommerce-api",
3-
"version": "1.1.0",
3+
"version": "1.1.1",
44
"type": "module",
55
"scripts": {
66
"test": "tsd",
@@ -50,21 +50,22 @@
5050
"@aligent/ts-code-standards": "^4.0.2",
5151
"@types/node": "^22.16.5",
5252
"diff": "^8.0.2",
53-
"eslint": "^9.31.0",
54-
"openapi-typescript": "^7.8.0",
53+
"eslint": "^9.34.0",
54+
"openapi-typescript": "^7.9.1",
5555
"prettier": "^3.6.2",
5656
"rimraf": "^6.0.1",
5757
"simple-git": "^3.28.0",
5858
"ts-morph": "^26.0.0",
59-
"tsd": "^0.32.0",
59+
"tsd": "^0.33.0",
6060
"tshy": "^3.0.2",
61-
"typescript": "^5.8.3",
61+
"typescript": "^5.9.2",
62+
"typescript-eslint": "^8.41.0",
6263
"uuid": "^11.1.0"
6364
},
6465
"dependencies": {
6566
"form-data": "^4.0.4",
6667
"node-fetch": "^3.3.2",
6768
"query-string": "^9.2.2"
6869
},
69-
"packageManager": "[email protected].2"
70+
"packageManager": "[email protected].4"
7071
}

src/internal/operation.ts

Lines changed: 89 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@ import { Agent as HttpsAgent } from 'https';
88
import fetch, { Response as FetchResponse } from 'node-fetch';
99
import qs from 'query-string';
1010

11-
// Represents HTTP methods supported by the API
11+
/**
12+
* @description HTTP methods supported by the API
13+
*/
1214
export type RequestMethod = 'DELETE' | 'GET' | 'PATCH' | 'POST' | 'PUT';
1315

14-
// Represents a complete API request with its endpoint and parameters
16+
/**
17+
* @description Represents a complete API request with its endpoint and parameters
18+
*/
1519
export type Request<
1620
ReqLine extends RequestLine = RequestLine,
1721
Params extends Parameters = Parameters,
@@ -20,23 +24,37 @@ export type Request<
2024
readonly parameters: Params;
2125
};
2226

23-
// Represents an API endpoint in the format "METHOD /path"
24-
// e.g., "GET /products" or "POST /orders"
27+
/**
28+
* @description Represents an API endpoint in the format "METHOD /path"
29+
* e.g., "GET /products" or "POST /orders"
30+
*/
2531
export type RequestLine = `${RequestMethod} ${string}`;
2632

27-
// Represents all possible parameter types that can be sent with a request:
28-
// - path: URL path parameters (e.g., /products/{id})
29-
// - query: URL query parameters
30-
// - body: Request body data
31-
// - header: Custom HTTP headers
33+
/**
34+
* @description Represents all possible parameter types that can be sent with a request:
35+
*/
3236
export type Parameters = {
37+
/**
38+
* @description URL path parameters (e.g., /products/{id})
39+
*/
3340
readonly path?: Record<string, any>;
41+
/**
42+
* @description URL query parameters
43+
*/
3444
readonly query?: any;
45+
/**
46+
* @description Request body data
47+
*/
3548
readonly body?: any;
49+
/**
50+
* @description Custom HTTP headers
51+
*/
3652
readonly header?: Record<string, any>;
3753
};
3854

39-
// Represents an API response with status code and optional body
55+
/**
56+
* @description Represents an API response with status code and optional body
57+
*/
4058
export type Response = {
4159
readonly status: number | string;
4260
readonly body?: any;
@@ -45,14 +63,20 @@ export type Response = {
4563
// Namespace for response-related type utilities
4664
export namespace Response {
4765
type SuccessStatus = 200 | 201 | 204;
66+
67+
/**
68+
* @description Extracts and formats the possible success responses for an operation
69+
*/
4870
export type Success<T extends Response | Operation> = T extends Operation
4971
? Success<T['response']>
5072
: T extends { status: SuccessStatus }
5173
? T
5274
: never;
5375
}
5476

55-
// Represents a complete API operation with its parameters and expected response
77+
/**
78+
* @description Represents a complete API operation with its parameters and expected response
79+
*/
5680
export type Operation = {
5781
readonly parameters: Request['parameters'];
5882
readonly response: Response;
@@ -61,43 +85,59 @@ export type Operation = {
6185
// Namespace for operation-related type utilities
6286
// TODO: MI-199 - determine if this is still required
6387
export namespace Operation {
64-
// Extracts the minimal required input parameters for an operation
88+
/**
89+
* @description Extracts the minimal required input parameters for an operation
90+
*/
6591
export type MinimalInput<Op extends Operation> = InputParameters<Op['parameters']>;
6692

67-
// Transforms API parameters based on their type:
68-
// - query parameters are made optional (Partial)
69-
// - header parameters have Accept and Content-Type removed since they're handled by the client
70-
// - all other parameters (path, body) are kept as-is
71-
// TODO: MI-199 should we be making params partial?
72-
type TransformParam<
93+
/**
94+
* @description Makes properties optional if they're on the 'query' object
95+
*/
96+
type MakeQueryParamsOptional<
7397
OpParams extends Operation['parameters'],
7498
K extends keyof OpParams,
7599
> = K extends 'query' ? Partial<OpParams[K]> : OpParams[K];
76100

77-
// Transforms operation parameters to make certain fields optional
101+
/**
102+
* @description Makes properties optional if they can be empty objects
103+
*/
104+
type MakeEmptyObjectOptional<T> = {
105+
readonly [K in keyof T as {} extends T[K] ? K : never]?: T[K];
106+
} & {
107+
readonly [K in keyof T as {} extends T[K] ? never : K]: T[K];
108+
};
109+
110+
/**
111+
* @description Prepares request parameters by making query params and any possibly empty params optional
112+
*/
78113
type InputParameters<OpParams extends Operation['parameters']> = MakeEmptyObjectOptional<{
79-
[K in keyof OpParams]: TransformParam<OpParams, K>;
114+
[K in keyof OpParams]: MakeQueryParamsOptional<OpParams, K>;
80115
}>;
81116
}
82117

83-
// Maps request lines to their corresponding operations
118+
/**
119+
* @description Maps request lines to their corresponding operations
120+
*/
84121
export type OperationIndex = Record<string, Operation>;
85122

86123
// Namespace for operation index utilities
87124
export namespace OperationIndex {
88-
// Filters operations to only include those with optional parameters
125+
/**
126+
* @description Filters operations to only include those with optional parameters
127+
*/
89128
export type FilterOptionalParams<Ops extends OperationIndex> = {
90129
[K in keyof Ops as {} extends Ops[K]['parameters'] ? K : never]: Ops[K];
91130
};
92131
}
93132

94-
// Utility type that makes properties optional if they can be empty objects
95-
type MakeEmptyObjectOptional<T> = {
96-
readonly [K in keyof T as {} extends T[K] ? K : never]?: T[K];
97-
} & {
98-
readonly [K in keyof T as {} extends T[K] ? never : K]: T[K];
99-
};
100-
133+
/**
134+
* @description Resolves the path for a request
135+
* @example
136+
* ```ts
137+
* resolvePath('/products/{id}', { id: 123 })
138+
* // returns '/products/123'
139+
* ```
140+
*/
101141
export function resolvePath(parameterizedPath: string, pathParams: Record<string, any>): string {
102142
return parameterizedPath
103143
.split('/')
@@ -116,10 +156,14 @@ export function resolvePath(parameterizedPath: string, pathParams: Record<string
116156
.join('/');
117157
}
118158

119-
// Transport function type that handles making the actual API requests
159+
/**
160+
* @description Transport function type that handles making the actual API requests
161+
*/
120162
export type Transport = (requestLine: string, params?: Parameters) => Promise<Response>;
121163

122-
// Configuration options for the fetch-based transport
164+
/**
165+
* @description Configuration options for the fetch-based transport
166+
*/
123167
export type FetchTransportOptions = {
124168
readonly baseUrl: string;
125169
readonly headers: Record<string, string>;
@@ -165,6 +209,20 @@ const defaultRetryConfig: Exclude<FetchTransportOptions['retry'], boolean | unde
165209
},
166210
};
167211

212+
/**
213+
* @description Creates a fetch transport function
214+
* @example
215+
* ```ts
216+
* const transport = fetchTransport({
217+
* baseUrl: 'https://api.bigcommerce.com/stores/1234567890/v3',
218+
* headers: {
219+
* 'X-Auth-Token': '1234567890',
220+
* },
221+
* });
222+
* ```
223+
* @param options - The options for the fetch transport
224+
* @returns
225+
*/
168226
export function fetchTransport(options: FetchTransportOptions): Transport {
169227
const { agent, baseUrl, headers, retry } = options;
170228

Lines changed: 89 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,110 @@
1-
// TECH DEBT: Work out if these eslint rules are reasonable in this context
2-
/* eslint-disable @typescript-eslint/no-empty-object-type */
3-
/* eslint-disable @typescript-eslint/no-explicit-any */
41
import type { RequestMethod } from '../operation.js';
2+
import type { Flatten, IsOptional } from '../type-utils.js';
53

64
// Takes an OpenAPI paths specification and converts it into our internal operation index format,
75
// removing any undefined endpoints and flattening the paths into a single type
6+
/**
7+
* @description Converts an OpenAPI paths specification into our internal operation index format
8+
* - REST operations are combined with their parent path e.g. 'GET /path/a'
9+
* - Parameters are kept, or defaulted to an empty object if not present
10+
* - Response and Request objects are reformatted with generics
11+
*
12+
* Only application/json request and response bodies are included
13+
*
14+
* @example
15+
* ```ts
16+
* type A = InferOperationIndex<{
17+
* '/path/a': {
18+
* put: {
19+
* parameters: { a: string };
20+
* requestBody: { content: { 'application/json': { b: string } } };
21+
* };
22+
* };
23+
* }>;
24+
* // A is {
25+
* // 'PUT /path/a': RequiredRequestBody<{
26+
* // a: string;
27+
* // },
28+
* // unknown,
29+
* // {
30+
* // b: string;
31+
* // }>
32+
* // }
33+
*/
834
export type InferOperationIndex<PathsSpec> = Flatten<{
935
[PathStr in keyof PathsSpec & string]: PathOperationIndex<PathStr, PathsSpec[PathStr]>;
1036
}>;
1137

12-
// Takes a record of types and flattens them into a single intersection type
13-
// This combines all the operations from different paths into one type
14-
type Flatten<T extends Record<string, any>> = UnionToIntersection<T[keyof T]>;
15-
type UnionToIntersection<T> = (T extends any ? (x: T) => any : never) extends (x: infer R) => any
16-
? R
17-
: never;
18-
19-
// Converts an OpenAPI 3.0 path specification into our operation index format
20-
// Currently this only extracts type details for application/json request bodies
38+
/**
39+
* @description Formats the OpenAPI parameters, responses, and responseBody specs at a given path
40+
* in preparation for flattening the paths in to a single index type
41+
*
42+
* @see InferOperationIndex for more information
43+
*/
2144
type PathOperationIndex<Path extends string, PathSpec> = {
22-
[K in keyof PathSpec as K extends RequestMethodLc
23-
? `${Uppercase<K>} ${Path}`
24-
: never]: PathSpec[K] extends { parameters?: infer Params; responses?: infer Responses }
25-
? PathSpec[K] extends {
26-
requestBody: { content: { 'application/json': infer RequestBody } };
45+
[K in keyof PathSpec as PathKey<K, Path>]: PathSpec[K] extends {
46+
parameters?: infer Params;
47+
responses?: infer Responses;
48+
}
49+
? {
50+
parameters: Params & ParamsRequestBody<PathSpec[K]>;
51+
response: ResponseUnion<Responses>;
2752
}
28-
? {
29-
readonly parameters: (unknown extends Params ? {} : Params) & {
30-
body: RequestBody;
31-
};
32-
readonly response: Response<Responses>;
33-
}
34-
: {
35-
readonly parameters: unknown extends Params ? {} : Params;
36-
readonly response: Response<Responses>;
37-
}
3853
: never;
3954
};
4055

41-
// Lowercase version of our RequestMethod type
42-
// Used for matching HTTP methods in OpenAPI specs which are typically lowercase
43-
type RequestMethodLc = Lowercase<RequestMethod>;
56+
/**
57+
* @description Combines the REST operation method with the endpoint path to form a unique key
58+
* @example
59+
* ```ts
60+
* type A = PathKey<'get', '/path/a'>
61+
* // A is 'GET /path/a'
62+
*/
63+
type PathKey<K, Path extends string> =
64+
K extends Lowercase<RequestMethod> ? `${Uppercase<K>} ${Path}` : never;
4465

45-
// Converts OpenAPI response specifications into our internal response format
46-
type Response<ResponsesSpec> = {
66+
/**
67+
* @description Converts OpenAPI response specifications into a union of possible responses
68+
* Only responses with a 'application/json' content type are included
69+
*
70+
* @example
71+
* ```ts
72+
* type A = Response<{
73+
* 200: { content: { 'application/json': { a: string } } };
74+
* 204: { content: { 'application/json': {} } };
75+
* }>;
76+
* // A is { status: 200, body: { a: string } } | { status: 204, body: {} }
77+
* ```
78+
*/
79+
type ResponseUnion<ResponsesSpec> = {
4780
[Status in keyof ResponsesSpec]: {
4881
status: Status;
4982
body: ResponsesSpec[Status] extends { content: { 'application/json': infer ResponseBody } }
5083
? ResponseBody
5184
: never;
5285
};
5386
}[keyof ResponsesSpec];
87+
88+
/**
89+
* @description Extracts the request body from an OpenAPI specification
90+
* - Keeps the optional or required nature of the request body from the original spec
91+
* - Only includes 'application/json' content type request bodies
92+
* - Returns a type with no body property if the original spec has no request body property (e.g. GET requests)
93+
*
94+
* @example
95+
* ```ts
96+
* type A = ParamsRequestBody<{
97+
* requestBody: { content: { 'application/json': { a: string } } };
98+
* }>;
99+
* // A is { body: { a: string } }
100+
* ```
101+
*/
102+
type ParamsRequestBody<PathSpec> = PathSpec extends {
103+
requestBody?: { content: { 'application/json': infer RequestBody } };
104+
}
105+
? unknown extends RequestBody
106+
? unknown
107+
: IsOptional<PathSpec, 'requestBody'> extends true
108+
? Partial<{ body: RequestBody }>
109+
: { body: RequestBody }
110+
: never;

0 commit comments

Comments
 (0)