feat: add RequestBody, QueryParam and FormParam

This commit is contained in:
George He
2024-01-10 15:29:55 +08:00
parent 490b2c1a06
commit eede456566
8 changed files with 485 additions and 19 deletions

47
package-lock.json generated
View File

@@ -10938,6 +10938,14 @@
"integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==",
"dev": true
},
"node_modules/decode-uri-component": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz",
"integrity": "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==",
"engines": {
"node": ">=14.16"
}
},
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
@@ -13568,6 +13576,17 @@
"node": ">=8"
}
},
"node_modules/filter-obj": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz",
"integrity": "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==",
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/finalhandler": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
@@ -21088,6 +21107,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/query-string": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-8.1.0.tgz",
"integrity": "sha512-BFQeWxJOZxZGix7y+SByG3F36dA0AbTy9o6pSmKFcFz7DAj0re9Frkty3saBn3nHo3D0oZJ/+rx3r8H8r8Jbpw==",
"dependencies": {
"decode-uri-component": "^0.4.1",
"filter-obj": "^5.1.0",
"split-on-first": "^3.0.0"
},
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
@@ -22558,6 +22593,17 @@
"spdx-ranges": "^2.0.0"
}
},
"node_modules/split-on-first": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz",
"integrity": "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
@@ -24955,6 +25001,7 @@
"oauth-1.0a": "^2.2.6",
"papaparse": "^5.4.1",
"prettier": "2.4.1",
"query-string": "^8.1.0",
"shell-quote": "^1.8.1",
"swagger-ui-dist": "5.0.0-alpha.6",
"tough-cookie": "^4.1.3",

View File

@@ -81,6 +81,7 @@
"oauth-1.0a": "^2.2.6",
"papaparse": "^5.4.1",
"prettier": "2.4.1",
"query-string": "^8.1.0",
"shell-quote": "^1.8.1",
"swagger-ui-dist": "5.0.0-alpha.6",
"tough-cookie": "^4.1.3",

View File

@@ -23,7 +23,152 @@ export interface AuthOption {
value: string;
}
export interface RawAuthOptions {
// export interface AuthBasic {
// password: string;
// username: string;
// id: string;
// }
// export interface AuthBearer {
// token: string;
// id: string;
// }
// export interface AuthJWT {
// secret: string;
// algorithm: string;
// isSecretBase64Encoded: boolean;
// payload: string; // e.g. "{}"
// addTokenTo: string;
// headerPrefix: string;
// queryParamKey: string;
// header: string; // e.g. "{}"
// id: string;
// }
// export interface AuthDigest {
// opaque: string;
// clientNonce: string;
// nonceCount: string;
// qop: string;
// nonce: string;
// realm: string;
// password: string;
// username: string;
// algorithm: string;
// id: string;
// }
// export interface AuthOAuth1 {
// addEmptyParamsToSign: boolean;
// includeBodyHash: boolean;
// realm: string;
// nonce: string;
// timestamp: string;
// verifier: string;
// callback: string;
// tokenSecret: string;
// token: string;
// consumerSecret: string;
// consumerKey: string;
// signatureMethod: string; // "HMAC-SHA1"
// version: string;
// addParamsToHeader: string;
// id: string;
// }
// export interface OAuth2Param {
// key: string;
// value: string;
// enabled: boolean;
// send_as: string; // it follows exising naming
// }
// export interface AuthOAuth2 {
// accessToken: string;
// refreshRequestParams: OAuth2Param[];
// tokenRequestParams: OAuth2Param[];
// authRequestParams: OAuth2Param[];
// refreshTokenUrl: string;
// state: string;
// scope: string;
// clientSecret: string;
// clientId: string;
// tokenName: string;
// addTokenTo: string;
// id: string;
// }
// export interface AuthHAWK {
// includePayloadHash: boolean;
// timestamp: string;
// delegation: string;
// app: string;
// extraData: string;
// nonce: string;
// user: string;
// authKey: string;
// authId: string;
// algorithm: string;
// id: string;
// }
// export interface AuthAWSV4 {
// sessionToken: string;
// service: string;
// region: string;
// secretKey: string;
// accessKey: string;
// id: string;
// }
// export interface AuthNTLM {
// workstation: string;
// domain: string;
// password: string;
// username: string;
// id: string;
// }
// export interface AuthAPIKey {
// key: string;
// value: string;
// id: string;
// }
// export interface AuthEdgegrid {
// headersToSign: string;
// baseURL: string;
// timestamp: string;
// nonce: string;
// clientSecret: string;
// clientToken: string;
// accessToken: string;
// id: string;
// }
// export interface AuthASAP {
// exp: string; // expiry
// claims: string; // e.g., { "additional claim": "claim value" }
// sub: string; // subject
// privateKey: string; // private key
// kid: string; // key id
// aud: string; // audience
// iss: string; // issuer
// alg: string; // e.g., RS256
// id: string;
// }
// function AuthMethodToParams(authMethod: AuthNoAuth | AuthBasic | AuthBasic | AuthBearer | AuthJWT | AuthDigest | AuthOAuth1 | AuthOAuth2 | AuthHAWK | AuthAWSV4 | AuthNTLM | AuthAPIKey | AuthEdgegrid | AuthASAP) {
// return Object.entries(authMethod).
// map(entry => ({
// type: 'any',
// key: entry[0],
// value: entry[1],
// }));
// }
export interface AuthOptions {
type: string;
basic?: AuthOption[];
bearer?: AuthOption[];
@@ -43,7 +188,7 @@ function rawOptionsToVariables(options: VariableList<Variable> | Variable[] | ob
if (VariableList.isVariableList(options)) {
return [options as VariableList<Variable>];
} else if ('type' in options) { // object
const optsObj = options as RawAuthOptions;
const optsObj = options as AuthOptions;
const optsVarLists = Object.entries(optsObj)
.filter(optsObjEntry => optsObjEntry[0] === targetType)
.map(optsEntry => {
@@ -69,14 +214,14 @@ export class RequestAuth extends Property {
private type: string;
private authOptions: Map<string, VariableList<Variable>> = new Map();
constructor(options: RawAuthOptions, parent?: Property) {
constructor(options: AuthOptions, parent?: Property) {
super();
if (!RequestAuth.isValidType(options.type)) {
throw Error(`invalid auth type ${options.type}`);
}
this.type = options.type;
const optsObj = options as RawAuthOptions;
const optsObj = options as AuthOptions;
Object.entries(optsObj)
.filter(optsObjEntry => optsObjEntry[0] !== 'type')
.map(optsEntry => {
@@ -114,7 +259,7 @@ export class RequestAuth extends Property {
}
toJSON() {
const obj: RawAuthOptions = { type: this.type };
const obj: AuthOptions = { type: this.type };
const authOption = this.authOptions.get(this.type);
if (!authOption) {
return obj;

View File

@@ -163,7 +163,7 @@ export class Property extends PropertyBase {
}
}
export class PropertyList<T extends Property> {
export class PropertyList<T> {
kind: string = 'PropertyList';
protected _parent: PropertyList<T> | undefined = undefined;
protected list: T[] = [];

View File

@@ -1,7 +1,7 @@
import { Property } from './object-base';
import { UrlMatchPattern, UrlMatchPatternList } from './object-urls';
export interface RawCertificateOptions {
export interface CertificateOptions {
name?: string;
matches?: string[];
key?: object;
@@ -18,7 +18,7 @@ export class Certificate extends Property {
passphrase?: string;
pfx?: object; // PFX or PKCS12 Certificate
constructor(options: RawCertificateOptions) {
constructor(options: CertificateOptions) {
super();
this.kind = 'Certificate';
this.name = options.name;
@@ -42,7 +42,7 @@ export class Certificate extends Property {
return this.matches ? this.matches.test(url) : false;
}
update(options: RawCertificateOptions) {
update(options: CertificateOptions) {
this.name = options.name;
this.matches = new UrlMatchPatternList(
undefined,

View File

@@ -1,5 +1,14 @@
import { Property, PropertyList } from './object-base';
export interface HeaderOptions {
id?: string;
name?: string;
type?: string;
disabled?: boolean;
key: string;
value: string;
}
export class Header extends Property {
kind: string = 'Header';
type: string = '';
@@ -7,14 +16,7 @@ export class Header extends Property {
value: string;
constructor(
opts: {
id?: string;
name?: string;
type?: string;
disabled?: boolean;
key: string;
value: string;
} | string,
opts: HeaderOptions | string,
name?: string, // if it is defined, it overrides 'key' (not 'name')
) {
super();

View File

@@ -1,5 +1,16 @@
import { Property, PropertyList } from './object-base';
export interface ProxyConfigOptions {
match: string;
host: string;
port: number;
tunnel: boolean;
disabled?: boolean;
authenticate: boolean;
username: string;
password: string;
}
export class ProxyConfig extends Property {
kind: string = 'ProxyConfig';
type: string;
@@ -36,12 +47,12 @@ export class ProxyConfig extends Property {
id?: string;
name?: string;
type?: string;
disabled?: boolean;
host: string;
match: string;
host: string;
port: number;
tunnel: boolean;
disabled?: boolean;
authenticate: boolean;
username: string;
password: string;

View File

@@ -0,0 +1,260 @@
import queryString from 'query-string';
import { AuthOptions } from './object-auth';
import { Property, PropertyBase, PropertyList } from './object-base';
import { CertificateOptions } from './object-certificates';
import { HeaderOptions } from './object-headers';
import { ProxyConfigOptions } from './object-proxy-configs';
import { Url } from './object-urls';
// export type RequestBodyMode =
// file string
// formdata string
// graphql string
// raw string
// urlencoded string
export type RequestBodyMode = undefined | 'formdata' | 'urlencoded' | 'raw' | 'file' | 'graphql';
export interface RequestBodyOptions {
mode: RequestBodyMode;
file?: string;
formdata?: { key: string; value: string }[];
graphql?: object;
raw?: string;
urlencoded?: { key: string; value: string }[] | string;
}
class FormParam {
key: string;
value: string;
constructor(options: { key: string; value: string }) {
this.key = options.key;
this.value = options.value;
}
// (static) _postman_propertyAllowsMultipleValues :Boolean
// (static) _postman_propertyIndexKey :String
// not implemented either
// static parse(_: FormParam) {
// throw Error('unimplemented yet');
// }
toJSON() {
return { key: this.key, value: this.value };
}
toString() {
return `${this.key}=${this.value}`; // validate key, value contains '='
}
valueOf() {
return this.value;
}
}
class QueryParam extends Property {
key: string;
value: string;
constructor(options: { key: string; value: string } | string) {
super();
if (typeof options === 'string') {
try {
const optionsObj = JSON.parse(options);
// validate key and value fields
this.key = optionsObj.key;
this.value = optionsObj.value;
} catch (e) {
throw Error(`invalid QueryParam options ${e}`);
}
} else if (typeof options === 'object' && ('key' in options) && ('value' in options)) {
this.key = options.key;
this.value = options.value;
} else {
throw Error('unknown options for new QueryParam');
}
}
// TODO:
// (static) _postman_propertyAllowsMultipleValues :Boolean
// (static) _postman_propertyIndexKey :String
static parse(queryStr: string) {
// this may not always be executed in the browser
return queryString.parse(queryStr);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
static parseSingle(param: string, _idx?: number, _all?: string[]) {
// it seems that _idx and _all are not useful
return queryString.parse(param);
}
static unparse(params: object) {
return Object.entries(params)
.map(entry => `${entry[0]}=${entry[1] || ''}`)
.join('&');
}
static unparseSingle(obj: { key: string; value: string }) {
if ('key' in obj && 'value' in obj) {
// TODO: validate and unescape
return `${obj.key}=${obj.value}`;
}
return {};
}
toString() {
return `${this.key}=${this.value}`; // validate key, value contains '='
}
update(param: string | { key: string; value: string }) {
if (typeof param === 'string') {
const paramObj = QueryParam.parse(param);
this.key = typeof paramObj.key === 'string' ? paramObj.key : '';
this.value = typeof paramObj.value === 'string' ? paramObj.value : '';
} else if ('key' in param && 'value' in param) {
this.key = param.key;
this.value = param.value;
} else {
throw Error('the param for update must be: string | { key: string; value: string }');
}
}
}
export class RequestBody extends PropertyBase {
mode: RequestBodyMode; // type of request data
file?: string; // It can be a file path (when used with Node.js) or a unique ID (when used with the browser).
formdata?: PropertyList<FormParam>;
graphql?: object; // raw graphql data
options?: object; // request body options
raw?: string; // raw body
urlencoded?: PropertyList<QueryParam> | string; // URL encoded body params
constructor(opts: RequestBodyOptions) {
super({ description: '' });
this.file = opts.file;
this.formdata = opts.formdata ?
new PropertyList(
opts.formdata.
map(formParamObj => new FormParam({ key: formParamObj.key, value: formParamObj.value }))
) :
undefined;
this.graphql = opts.graphql;
this.mode = opts.mode;
// this.options = opts.options;
this.raw = opts.raw;
if (typeof opts.urlencoded === 'string') {
const queryParamObj = QueryParam.parse(opts.urlencoded);
this.urlencoded = opts.urlencoded ?
new PropertyList(
Object.entries(queryParamObj)
.map(entry => ({ key: entry[0], value: JSON.stringify(entry[1]) }))
.map(kv => new QueryParam(kv)),
) :
undefined;
} else {
// TODO: validate key, value in each entry
this.urlencoded = opts.urlencoded ?
new PropertyList(
opts.urlencoded
.map(entry => ({ key: entry.key, value: entry.value }))
.map(kv => new QueryParam(kv)),
) :
undefined;
}
}
isEmpty() {
switch (this.mode) {
case 'formdata':
return this.formdata == null;
case 'urlencoded':
return this.urlencoded == null;
case 'raw':
return this.raw == null;
case 'file':
return this.file == null;
case 'graphql':
return this.graphql == null;
default:
throw Error(`mode (${this.mode}) is unexpected`);
}
}
toString() {
try {
switch (this.mode) {
case 'formdata':
return this.formdata ? this.formdata?.toString() : '';
case 'urlencoded':
return this.urlencoded ? this.urlencoded.toString() : '';
case 'raw':
return this.raw ? this.raw.toString() : '';
case 'file':
return this.file || ''; // TODO: check file id or file content
case 'graphql':
return this.graphql ? JSON.stringify(this.graphql) : '';
default:
throw Error(`mode (${this.mode}) is unexpected`);
}
} catch (e) {
return '';
}
}
update(opts: RequestBodyOptions) {
this.file = opts.file;
this.formdata = opts.formdata ?
new PropertyList(
opts.formdata.
map(formParamObj => new FormParam({ key: formParamObj.key, value: formParamObj.value }))
) :
undefined;
this.graphql = opts.graphql;
this.mode = opts.mode;
// this.options = opts.options;
this.raw = opts.raw;
if (typeof opts.urlencoded === 'string') {
const queryParamObj = QueryParam.parse(opts.urlencoded);
this.urlencoded = opts.urlencoded ?
new PropertyList(
Object.entries(queryParamObj)
.map(entry => ({ key: entry[0], value: JSON.stringify(entry[1]) }))
.map(kv => new QueryParam(kv)),
) :
undefined;
} else {
// TODO: validate key, value in each entry
this.urlencoded = opts.urlencoded ?
new PropertyList(
opts.urlencoded
.map(entry => ({ key: entry.key, value: JSON.stringify(entry.value) }))
.map(kv => new QueryParam(kv)),
) :
undefined;
}
}
}
export interface RequestOptions {
url: string | Url;
method: string;
header: HeaderOptions;
body: RequestBodyOptions;
auth: AuthOptions;
proxy: ProxyConfigOptions;
certificate: CertificateOptions;
}
export class Request extends Property {
}