mirror of
https://github.com/Kong/insomnia.git
synced 2026-04-21 22:57:59 -04:00
feat: move objects to the sdk folder
This commit is contained in:
@@ -0,0 +1,336 @@
|
||||
import { Property } from './base';
|
||||
import { Variable, VariableList } from './variables';
|
||||
|
||||
const AuthTypes = new Set([
|
||||
'noauth',
|
||||
'basic',
|
||||
'bearer',
|
||||
'jwt',
|
||||
'digest',
|
||||
'oauth1',
|
||||
'oauth2',
|
||||
'hawk',
|
||||
'awsv4',
|
||||
'ntlm',
|
||||
'apikey',
|
||||
'edgegrid',
|
||||
'asap',
|
||||
]);
|
||||
|
||||
export interface AuthOption {
|
||||
type: string;
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
// 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[];
|
||||
jwt?: AuthOption[];
|
||||
digest?: AuthOption[];
|
||||
oauth1?: AuthOption[];
|
||||
oauth2?: AuthOption[];
|
||||
hawk?: AuthOption[];
|
||||
awsv4?: AuthOption[];
|
||||
ntlm?: AuthOption[];
|
||||
apikey?: AuthOption[];
|
||||
edgegrid?: AuthOption[];
|
||||
asap?: AuthOption[];
|
||||
}
|
||||
|
||||
function rawOptionsToVariables(options: VariableList<Variable> | Variable[] | object, targetType?: string): VariableList<Variable>[] {
|
||||
if (VariableList.isVariableList(options)) {
|
||||
return [options as VariableList<Variable>];
|
||||
} else if ('type' in options) { // object
|
||||
const optsObj = options as AuthOptions;
|
||||
const optsVarLists = Object.entries(optsObj)
|
||||
.filter(optsObjEntry => optsObjEntry[0] === targetType)
|
||||
.map(optsEntry => {
|
||||
return new VariableList(
|
||||
undefined,
|
||||
optsEntry.map(opt => new Variable({
|
||||
key: opt.key,
|
||||
value: opt.value,
|
||||
type: opt.type,
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
||||
return optsVarLists;
|
||||
} else if ('length' in options) { // array
|
||||
return [new VariableList(undefined, options)];
|
||||
}
|
||||
|
||||
throw Error('options is not valid: it must be VariableList<Variable> | Variable[] | object');
|
||||
}
|
||||
|
||||
export class RequestAuth extends Property {
|
||||
private type: string;
|
||||
private authOptions: Map<string, VariableList<Variable>> = new Map();
|
||||
|
||||
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 AuthOptions;
|
||||
Object.entries(optsObj)
|
||||
.filter(optsObjEntry => optsObjEntry[0] !== 'type')
|
||||
.map(optsEntry => {
|
||||
return {
|
||||
type: optsEntry[0],
|
||||
options: new VariableList(
|
||||
undefined,
|
||||
optsEntry.map(opt => new Variable({
|
||||
key: opt.key,
|
||||
value: opt.value,
|
||||
type: opt.type,
|
||||
})),
|
||||
),
|
||||
};
|
||||
})
|
||||
.forEach(authOpts => {
|
||||
this.authOptions.set(authOpts.type, authOpts.options);
|
||||
});
|
||||
|
||||
this._parent = parent;
|
||||
}
|
||||
|
||||
static isValidType(authType: string) {
|
||||
return AuthTypes.has(authType);
|
||||
}
|
||||
|
||||
clear(type: string) {
|
||||
if (RequestAuth.isValidType(type)) {
|
||||
this.authOptions.delete(type);
|
||||
}
|
||||
}
|
||||
|
||||
parameters() {
|
||||
return this.authOptions.get(this.type);
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const obj: AuthOptions = { type: this.type };
|
||||
const authOption = this.authOptions.get(this.type);
|
||||
if (!authOption) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
const authOptionJSON = authOption.map(optValue => optValue.toJSON(), {});
|
||||
|
||||
switch (this.type) {
|
||||
case 'basic':
|
||||
obj.basic = authOptionJSON;
|
||||
break;
|
||||
case 'bearer':
|
||||
obj.bearer = authOptionJSON;
|
||||
break;
|
||||
case 'jwt':
|
||||
obj.jwt = authOptionJSON;
|
||||
break;
|
||||
case 'digest':
|
||||
obj.digest = authOptionJSON;
|
||||
break;
|
||||
case 'oauth1':
|
||||
obj.oauth1 = authOptionJSON;
|
||||
break;
|
||||
case 'oauth2':
|
||||
obj.oauth2 = authOptionJSON;
|
||||
break;
|
||||
case 'hawk':
|
||||
obj.hawk = authOptionJSON;
|
||||
break;
|
||||
case 'awsv4':
|
||||
obj.awsv4 = authOptionJSON;
|
||||
break;
|
||||
case 'ntlm':
|
||||
obj.ntlm = authOptionJSON;
|
||||
break;
|
||||
case 'apikey':
|
||||
obj.apikey = authOptionJSON;
|
||||
break;
|
||||
case 'edgegrid':
|
||||
obj.edgegrid = authOptionJSON;
|
||||
break;
|
||||
case 'asap':
|
||||
obj.asap = authOptionJSON;
|
||||
break;
|
||||
default: // noauth, no op
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
update(options: VariableList<Variable> | Variable[] | object, type?: string) {
|
||||
const currentType = type ? type : this.type;
|
||||
const authOpts = rawOptionsToVariables(options, currentType);
|
||||
if (authOpts.length > 0) {
|
||||
this.authOptions.set(currentType, authOpts[0]);
|
||||
} else {
|
||||
throw Error('no valid RequestAuth options is found');
|
||||
}
|
||||
}
|
||||
|
||||
use(type: string, options: VariableList<Variable> | Variable[] | object) {
|
||||
if (!RequestAuth.isValidType(type)) {
|
||||
throw Error(`invalid type (${type}), it must be noauth | basic | bearer | jwt | digest | oauth1 | oauth2 | hawk | awsv4 | ntlm | apikey | edgegrid | asap.`);
|
||||
}
|
||||
|
||||
const authOpts = rawOptionsToVariables(options, type);
|
||||
if (authOpts.length > 0) {
|
||||
this.type = type;
|
||||
this.authOptions.set(type, authOpts[0]);
|
||||
} else {
|
||||
throw Error('no valid RequestAuth options is found');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
import clone from 'clone';
|
||||
import equal from 'deep-equal';
|
||||
|
||||
export interface JSONer {
|
||||
toJSON: () => object;
|
||||
}
|
||||
|
||||
export class PropertyBase {
|
||||
public kind = 'PropertyBase';
|
||||
protected description: string;
|
||||
protected _parent: PropertyBase | undefined = undefined;
|
||||
|
||||
constructor(def: { description: string }) {
|
||||
this.description = def.description;
|
||||
}
|
||||
|
||||
// static propertyIsMeta(_value: any, _key: string) {
|
||||
// // always return false because no meta is defined in Insomnia
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// static propertyUnprefixMeta(_value, _key) {
|
||||
// // no meta key is enabled
|
||||
// // so no op here
|
||||
// }
|
||||
|
||||
static toJSON(obj: JSONer) {
|
||||
return obj.toJSON();
|
||||
}
|
||||
|
||||
meta() {
|
||||
return {};
|
||||
};
|
||||
|
||||
parent() {
|
||||
return this._parent;
|
||||
}
|
||||
|
||||
forEachParent(_options: { withRoot?: boolean }, iterator: (obj: PropertyBase) => boolean) {
|
||||
const currentParent = this.parent();
|
||||
if (!currentParent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const queue: PropertyBase[] = [currentParent];
|
||||
const parents: PropertyBase[] = [];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const ancester = queue.shift();
|
||||
if (!ancester) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: check options
|
||||
const cloned = clone(ancester);
|
||||
const keepIterating = iterator(cloned);
|
||||
parents.push(cloned);
|
||||
if (!keepIterating) {
|
||||
break;
|
||||
}
|
||||
|
||||
const olderAncester = ancester.parent();
|
||||
if (olderAncester) {
|
||||
queue.push(olderAncester);
|
||||
}
|
||||
}
|
||||
|
||||
return parents;
|
||||
}
|
||||
|
||||
findInParents(property: string, customizer?: (ancester: PropertyBase) => boolean): PropertyBase | undefined {
|
||||
const currentParent = this.parent();
|
||||
if (!currentParent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const queue: PropertyBase[] = [currentParent];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const ancester = queue.shift();
|
||||
if (!ancester) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: check options
|
||||
const cloned = clone(ancester);
|
||||
const hasProperty = Object.keys(cloned.meta()).includes(property);
|
||||
if (!hasProperty) {
|
||||
// keep traversing until parent has the property
|
||||
// no op
|
||||
} else {
|
||||
if (customizer) {
|
||||
if (customizer(cloned)) {
|
||||
// continue until customizer returns a truthy value
|
||||
return cloned;
|
||||
}
|
||||
} else {
|
||||
// customizer is not specified, stop at the first parent that contains the property
|
||||
return cloned;
|
||||
}
|
||||
}
|
||||
|
||||
const olderAncester = ancester.parent();
|
||||
if (olderAncester) {
|
||||
queue.push(olderAncester);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const entriesToExport = Object
|
||||
.entries(this)
|
||||
.filter((kv: [string, any]) =>
|
||||
typeof kv[1] !== 'function' && typeof kv[1] !== 'undefined'
|
||||
);
|
||||
|
||||
return Object.fromEntries(entriesToExport);
|
||||
}
|
||||
|
||||
toObject() {
|
||||
return this.toJSON();
|
||||
}
|
||||
|
||||
toString() {
|
||||
return JSON.stringify(this.toJSON());
|
||||
}
|
||||
}
|
||||
|
||||
export class Property extends PropertyBase {
|
||||
id?: string;
|
||||
name?: string;
|
||||
disabled?: boolean;
|
||||
info?: object;
|
||||
|
||||
constructor(def?: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
disabled?: boolean;
|
||||
info?: object;
|
||||
}) {
|
||||
super({ description: 'Property' });
|
||||
this.id = def?.id || '';
|
||||
this.name = def?.name || '';
|
||||
this.disabled = def?.disabled || false;
|
||||
this.info = def?.info || {};
|
||||
}
|
||||
|
||||
// static replaceSubstitutions(_str: string, _variables: object): string {
|
||||
// // TODO: unsupported
|
||||
// return '';
|
||||
// }
|
||||
|
||||
// static replaceSubstitutionsIn(obj: string, variables: object): object {
|
||||
// // TODO: unsupported
|
||||
// return {};
|
||||
// }
|
||||
|
||||
describe(content: string, typeName: string) {
|
||||
this.kind = typeName;
|
||||
this.description = content;
|
||||
}
|
||||
}
|
||||
|
||||
export class PropertyList<T> {
|
||||
kind: string = 'PropertyList';
|
||||
protected _parent: PropertyList<T> | undefined = undefined;
|
||||
protected list: T[] = [];
|
||||
|
||||
constructor(
|
||||
// public readonly typeClass: { new(...arg: any): T },
|
||||
// public readonly parent: string,
|
||||
populate: T[],
|
||||
) {
|
||||
this.list = populate;
|
||||
}
|
||||
|
||||
static isPropertyList(obj: object) {
|
||||
return 'kind' in obj && obj.kind === 'PropertyList';
|
||||
}
|
||||
|
||||
add(item: T) {
|
||||
this.list.push(item);
|
||||
}
|
||||
|
||||
all() {
|
||||
return new Map(
|
||||
this.list.map(
|
||||
pp => [pp.id, pp.toJSON()]
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
append(item: T) {
|
||||
// it doesn't move item to the end of list for avoiding side effect
|
||||
this.add(item);
|
||||
}
|
||||
|
||||
assimilate(source: T[] | PropertyList<T>, prune?: boolean) {
|
||||
// it doesn't update values from a source list
|
||||
if (prune) {
|
||||
this.clear();
|
||||
}
|
||||
if ('list' in source) { // it is PropertyList<T>
|
||||
this.list.push(...source.list);
|
||||
} else {
|
||||
this.list.push(...source);
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.list = [];
|
||||
}
|
||||
|
||||
count() {
|
||||
return this.list.length;
|
||||
}
|
||||
|
||||
each(iterator: (item: T) => void, context: object) {
|
||||
interface Iterator {
|
||||
context?: object;
|
||||
(item: T): void;
|
||||
}
|
||||
const it: Iterator = iterator;
|
||||
it.context = context;
|
||||
|
||||
this.list.forEach(it);
|
||||
}
|
||||
|
||||
// TODO: unsupported
|
||||
// eachParent(iterator, contextopt) {}
|
||||
|
||||
filter(rule: (item: T) => boolean, context: object) {
|
||||
interface Iterator {
|
||||
context?: object;
|
||||
(item: T): boolean;
|
||||
}
|
||||
const it: Iterator = rule;
|
||||
it.context = context;
|
||||
|
||||
return this.list.filter(it);
|
||||
}
|
||||
|
||||
// TODO: support returning {Item|ItemGroup}
|
||||
find(rule: (item: T) => boolean, context?: object) {
|
||||
interface Finder {
|
||||
context?: object;
|
||||
(item: T): boolean;
|
||||
}
|
||||
const finder: Finder = rule;
|
||||
finder.context = context;
|
||||
|
||||
return this.list.find(finder);
|
||||
}
|
||||
|
||||
// it does not return underlying type of the item because they are not supported
|
||||
get(key: string) {
|
||||
return this.one(key);
|
||||
}
|
||||
|
||||
// TODO: value is not used as its usage is unknown
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
has(item: T, _value: any) {
|
||||
return this.indexOf(item) >= 0;
|
||||
}
|
||||
|
||||
idx(index: number) {
|
||||
if (index <= this.list.length - 1) {
|
||||
return this.list[index];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
indexOf(item: string | T) {
|
||||
for (let i = 0; i < this.list.length; i++) {
|
||||
if (typeof item === 'string') {
|
||||
if (item === this.list[i].id) {
|
||||
return i;
|
||||
}
|
||||
} else {
|
||||
if (equal(item, this.list[i])) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
insert(item: T, before?: number) {
|
||||
if (before && before <= this.list.length - 1) {
|
||||
this.list = [...this.list.slice(0, before), item, ...this.list.slice(before)];
|
||||
} else {
|
||||
this.append(item);
|
||||
}
|
||||
}
|
||||
|
||||
insertAfter(item: T, after?: number) {
|
||||
if (after && after <= this.list.length - 1) {
|
||||
this.list = [...this.list.slice(0, after + 1), item, ...this.list.slice(after + 1)];
|
||||
} else {
|
||||
this.append(item);
|
||||
}
|
||||
}
|
||||
|
||||
map(iterator: (item: T) => any, context: object) {
|
||||
interface Iterator {
|
||||
context?: object;
|
||||
(item: T): any;
|
||||
}
|
||||
const it: Iterator = iterator;
|
||||
it.context = context;
|
||||
|
||||
return this.list.map(it);
|
||||
}
|
||||
|
||||
one(id: string) {
|
||||
for (let i = this.list.length - 1; i >= 0; i--) {
|
||||
if (this.list[i].id === id) {
|
||||
return this.list[i];
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
populate(items: T[]) {
|
||||
this.list = [...this.list, ...items];
|
||||
}
|
||||
|
||||
prepend(item: T) {
|
||||
this.list = [item, ...this.list];
|
||||
}
|
||||
|
||||
reduce(iterator: ((acc: any, item: T) => any), accumulator: any, context: object) {
|
||||
interface Iterator {
|
||||
context?: object;
|
||||
(acc: any, item: T): any;
|
||||
}
|
||||
const it: Iterator = iterator;
|
||||
it.context = context;
|
||||
|
||||
this.list.reduce(it, accumulator);
|
||||
}
|
||||
|
||||
remove(predicate: T | ((item: T) => boolean), context: object) {
|
||||
if (typeof predicate === 'function') {
|
||||
this.list = this.filter(predicate, context);
|
||||
} else {
|
||||
this.list = this.filter(item => equal(predicate, item), context);
|
||||
}
|
||||
}
|
||||
|
||||
repopulate(items: T[]) {
|
||||
this.clear();
|
||||
this.populate(items);
|
||||
}
|
||||
|
||||
// unsupportd as _postman_propertyIndexKey is not supported
|
||||
// toObject(excludeDisabled?: boolean, caseSensitive?: boolean, multiValue?: boolean, sanitizeKeys?: boolean) {
|
||||
// const itemObjects = this.list
|
||||
// .filter(item => {
|
||||
// if (excludeDisabled) {
|
||||
// return !item.disabled;
|
||||
// }
|
||||
// return true;
|
||||
// })
|
||||
// .map(item => {
|
||||
// return item.toJSON();
|
||||
// });
|
||||
// }
|
||||
|
||||
toString() {
|
||||
const itemStrs = this.list.map(item => item.toString());
|
||||
return `[${itemStrs.join(',')}]`;
|
||||
}
|
||||
|
||||
upsert(item: T): boolean {
|
||||
const itemIdx = this.indexOf(item);
|
||||
if (itemIdx >= 0) {
|
||||
this.list = [...this.list.splice(0, itemIdx), item, ...this.list.splice(itemIdx + 1)];
|
||||
return false;
|
||||
}
|
||||
|
||||
this.add(item);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Property } from './base';
|
||||
import { UrlMatchPattern, UrlMatchPatternList } from './urls';
|
||||
|
||||
export interface CertificateOptions {
|
||||
name?: string;
|
||||
matches?: string[];
|
||||
key?: object;
|
||||
cert?: object;
|
||||
passphrase?: string;
|
||||
pfx?: object; // PFX or PKCS12 Certificate
|
||||
}
|
||||
|
||||
export class Certificate extends Property {
|
||||
name?: string;
|
||||
matches?: UrlMatchPatternList<UrlMatchPattern>;
|
||||
key?: object;
|
||||
cert?: object;
|
||||
passphrase?: string;
|
||||
pfx?: object; // PFX or PKCS12 Certificate
|
||||
|
||||
constructor(options: CertificateOptions) {
|
||||
super();
|
||||
this.kind = 'Certificate';
|
||||
this.name = options.name;
|
||||
this.matches = new UrlMatchPatternList(
|
||||
undefined,
|
||||
options.matches ?
|
||||
options.matches.map(matchStr => new UrlMatchPattern(matchStr)) :
|
||||
[],
|
||||
);
|
||||
this.key = options.key;
|
||||
this.cert = options.cert;
|
||||
this.passphrase = options.passphrase;
|
||||
this.pfx = options.pfx;
|
||||
}
|
||||
|
||||
static isCertificate(obj: object) {
|
||||
return 'kind' in obj && obj.kind === 'Certificate';
|
||||
}
|
||||
|
||||
canApplyTo(url: string) {
|
||||
return this.matches ? this.matches.test(url) : false;
|
||||
}
|
||||
|
||||
update(options: CertificateOptions) {
|
||||
this.name = options.name;
|
||||
this.matches = new UrlMatchPatternList(
|
||||
undefined,
|
||||
options.matches ?
|
||||
options.matches.map(matchStr => new UrlMatchPattern(matchStr)) :
|
||||
[],
|
||||
);
|
||||
this.key = options.key;
|
||||
this.cert = options.cert;
|
||||
this.passphrase = options.passphrase;
|
||||
this.pfx = options.pfx;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import { Property, PropertyList } from './base';
|
||||
|
||||
export interface CookieOptions {
|
||||
key: string;
|
||||
value: string;
|
||||
expires?: Date | string;
|
||||
maxAge?: Number;
|
||||
domain?: string;
|
||||
path?: string;
|
||||
secure?: Boolean;
|
||||
httpOnly?: Boolean;
|
||||
hostOnly?: Boolean;
|
||||
session?: Boolean;
|
||||
extensions?: { key: string; value: string }[];
|
||||
}
|
||||
|
||||
export class Cookie extends Property {
|
||||
private def: object;
|
||||
|
||||
constructor(cookieDef: CookieOptions | string) {
|
||||
super();
|
||||
this.kind = 'Cookie';
|
||||
this.description = 'Cookie';
|
||||
|
||||
if (typeof cookieDef === 'string') {
|
||||
this.def = Cookie.parse(cookieDef);
|
||||
} else {
|
||||
this.def = cookieDef;
|
||||
}
|
||||
}
|
||||
|
||||
static isCookie(obj: Property) {
|
||||
return obj.kind === 'Cookie';
|
||||
}
|
||||
|
||||
static parse(cookieStr: string) {
|
||||
const parts = cookieStr.split(';');
|
||||
|
||||
const def: CookieOptions = { key: '', value: '' };
|
||||
const extensions: { key: string; value: string }[] = [];
|
||||
|
||||
parts.forEach((part, i) => {
|
||||
const kvParts = part.split('=');
|
||||
const key = kvParts[0];
|
||||
|
||||
if (i === 0) {
|
||||
const value = kvParts.length > 1 ? kvParts[1] : '';
|
||||
def.key, def.value = key, value;
|
||||
} else {
|
||||
switch (key) {
|
||||
case 'Expires':
|
||||
// TODO: it should be timestamp
|
||||
const expireVal = kvParts.length > 1 ? kvParts[1] : '0';
|
||||
def.expires = expireVal;
|
||||
break;
|
||||
case 'Max-Age':
|
||||
let maxAgeVal = 0;
|
||||
if (kvParts.length > 1) {
|
||||
maxAgeVal = parseInt(kvParts[1], 10);
|
||||
}
|
||||
def.maxAge = maxAgeVal;
|
||||
break;
|
||||
case 'Domain':
|
||||
const domainVal = kvParts.length > 1 ? kvParts[1] : '';
|
||||
def.domain = domainVal;
|
||||
break;
|
||||
case 'Path':
|
||||
const pathVal = kvParts.length > 1 ? kvParts[1] : '';
|
||||
def.path = pathVal;
|
||||
break;
|
||||
case 'Secure':
|
||||
def.secure = true;
|
||||
break;
|
||||
case 'HttpOnly':
|
||||
def.httpOnly = true;
|
||||
break;
|
||||
case 'HostOnly':
|
||||
def.hostOnly = true;
|
||||
break;
|
||||
case 'Session':
|
||||
def.session = true;
|
||||
break;
|
||||
default:
|
||||
const value = kvParts.length > 1 ? kvParts[1] : '';
|
||||
extensions.push({ key, value });
|
||||
def.extensions = extensions;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return def;
|
||||
}
|
||||
|
||||
static stringify(cookie: Cookie) {
|
||||
return cookie.toString();
|
||||
}
|
||||
|
||||
static unparseSingle(cookie: Cookie) {
|
||||
return cookie.toString();
|
||||
}
|
||||
|
||||
// TODO: support PropertyList
|
||||
static unparse(cookies: Cookie[]) {
|
||||
const cookieStrs = cookies.map(cookie => cookie.toString());
|
||||
return cookieStrs.join(';');
|
||||
}
|
||||
|
||||
toString = () => {
|
||||
// Reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
|
||||
const cookieDef = this.def as CookieOptions;
|
||||
const kvPair = `${cookieDef.key}=${cookieDef.value};`;
|
||||
const expires = cookieDef.expires ? `Expires=${cookieDef.expires?.toString()};` : '';
|
||||
const maxAge = cookieDef.maxAge ? `Max-Age=${cookieDef.maxAge};` : '';
|
||||
const domain = cookieDef.domain ? `Domain=${cookieDef.domain};` : '';
|
||||
const path = cookieDef.path ? `Path=${cookieDef.path};` : '';
|
||||
const secure = cookieDef.secure ? 'Secure;' : '';
|
||||
const httpOnly = cookieDef.httpOnly ? 'HttpOnly;' : '';
|
||||
// TODO: SameSite, Partitioned is not suported
|
||||
|
||||
const hostOnly = cookieDef.hostOnly ? 'HostOnly;' : '';
|
||||
const session = cookieDef.session ? 'Session;' : '';
|
||||
|
||||
// TODO: extension key may be conflict with pre-defined keys
|
||||
const extensions = cookieDef.extensions ?
|
||||
cookieDef.extensions
|
||||
.map((kv: { key: string; value: string }) => `${kv.key}=${kv.value}`)
|
||||
.join(';') : ''; // the last field doesn't have ';'
|
||||
|
||||
return `${kvPair} ${expires} ${maxAge} ${domain} ${path} ${secure} ${httpOnly} ${hostOnly} ${session} ${extensions}`;
|
||||
};
|
||||
|
||||
valueOf = () => {
|
||||
return (this.def as CookieOptions).value;
|
||||
};
|
||||
}
|
||||
|
||||
export class CookieList extends PropertyList<Cookie> {
|
||||
kind: string = 'CookieList';
|
||||
cookies: Cookie[];
|
||||
|
||||
constructor(parent: CookieList | undefined, cookies: Cookie[]) {
|
||||
super(
|
||||
// Cookie, parent.toString()
|
||||
cookies
|
||||
);
|
||||
this._parent = parent;
|
||||
this.cookies = cookies;
|
||||
}
|
||||
|
||||
static isCookieList(obj: object) {
|
||||
return 'kind' in obj && obj.kind === 'CookieList';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { Property, PropertyList } from './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 = '';
|
||||
key: string;
|
||||
value: string;
|
||||
|
||||
constructor(
|
||||
opts: HeaderOptions | string,
|
||||
name?: string, // if it is defined, it overrides 'key' (not 'name')
|
||||
) {
|
||||
super();
|
||||
|
||||
if (typeof opts === 'string') {
|
||||
const obj = Header.parseSingle(opts);
|
||||
this.key = obj.key;
|
||||
this.value = obj.value;
|
||||
} else {
|
||||
this.id = opts.id ? opts.id : '';
|
||||
this.key = opts.key ? opts.key : '';
|
||||
this.name = name ? name : (opts.name ? opts.name : '');
|
||||
this.value = opts.value ? opts.value : '';
|
||||
this.type = opts.type ? opts.type : '';
|
||||
this.disabled = opts ? opts.disabled : false;
|
||||
}
|
||||
}
|
||||
|
||||
static create(input?: { key: string; value: string } | string, name?: string): Header {
|
||||
return new Header(input || { key: '', value: '' }, name);
|
||||
}
|
||||
|
||||
static isHeader(obj: object) {
|
||||
return 'kind' in obj && obj.kind === 'Header';
|
||||
}
|
||||
|
||||
// example: 'Content-Type: application/json\nUser-Agent: MyClientLibrary/2.0\n'
|
||||
static parse(headerString: string): { key: string; value: string }[] {
|
||||
return headerString
|
||||
.split('\n')
|
||||
.map(kvPart => Header.parseSingle(kvPart));
|
||||
}
|
||||
|
||||
static parseSingle(headerStr: string): { key: string; value: string } {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
|
||||
// the first colon is the separator
|
||||
const separatorPos = headerStr.indexOf(':');
|
||||
const key = headerStr.slice(0, separatorPos);
|
||||
const value = headerStr.slice(separatorPos + 1);
|
||||
|
||||
return { key, value };
|
||||
}
|
||||
|
||||
static unparse(headers: { key: string; value: string }[] | PropertyList<Header>, separator?: string): string {
|
||||
const headerStrs = headers.map(
|
||||
header => this.unparseSingle(header), {}
|
||||
);
|
||||
|
||||
return headerStrs.join(separator || '\n');
|
||||
}
|
||||
|
||||
static unparseSingle(header: { key: string; value: string } | Header): string {
|
||||
// both PropertyList and object contains 'key' and 'value'
|
||||
return `${header.key}: ${header.value}`;
|
||||
}
|
||||
|
||||
update(newHeader: { key: string; value: string }) {
|
||||
this.key = newHeader.key;
|
||||
this.value = newHeader.value;
|
||||
}
|
||||
|
||||
valueOf() {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
|
||||
export class HeaderList<T extends Header> extends PropertyList<T> {
|
||||
constructor(parent: PropertyList<T> | undefined, populate: T[]) {
|
||||
super(populate);
|
||||
this._parent = parent;
|
||||
}
|
||||
|
||||
static isHeaderList(obj: any) {
|
||||
return 'kind' in obj && obj.kind === 'HeaderList';
|
||||
}
|
||||
|
||||
// unsupported
|
||||
// eachParent(iterator, contextopt)
|
||||
// toObject(excludeDisabledopt, nullable, caseSensitiveopt, nullable, multiValueopt, nullable, sanitizeKeysopt) → {Object}
|
||||
|
||||
contentSize(): number {
|
||||
return this.list
|
||||
.map(header => header.toString())
|
||||
.map(headerStr => headerStr.length) // TODO: handle special characters
|
||||
.reduce((totalSize, headerSize) => totalSize + headerSize, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { configure, type ConfigureOptions, type Environment as NunjuncksEnv } from 'nunjucks';
|
||||
|
||||
class Intepolator {
|
||||
private engine: NunjuncksEnv;
|
||||
|
||||
constructor(config: ConfigureOptions) {
|
||||
this.engine = configure(config);
|
||||
}
|
||||
|
||||
render = (template: string, context: object) => {
|
||||
// TODO: handle timeout
|
||||
// TODO: support plugin?
|
||||
return this.engine.renderString(template, context);
|
||||
};
|
||||
}
|
||||
|
||||
const intepolator = new Intepolator({
|
||||
autoescape: false,
|
||||
// Don't escape HTML
|
||||
throwOnUndefined: true,
|
||||
// Strict mode
|
||||
tags: {
|
||||
blockStart: '{%',
|
||||
blockEnd: '%}',
|
||||
variableStart: '{{',
|
||||
variableEnd: '}}',
|
||||
commentStart: '{#',
|
||||
commentEnd: '#}',
|
||||
},
|
||||
});
|
||||
|
||||
export function getIntepolator() {
|
||||
return intepolator;
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import { Property, PropertyList } from './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;
|
||||
|
||||
host: string;
|
||||
match: string;
|
||||
port: number;
|
||||
tunnel: boolean;
|
||||
authenticate: boolean;
|
||||
username: string;
|
||||
password: string;
|
||||
|
||||
static authenticate: boolean;
|
||||
// static bypass: UrlMatchPatternList;
|
||||
static host: string;
|
||||
static match: string;
|
||||
static password: string;
|
||||
static port: number;
|
||||
static tunnel: boolean; // unsupported
|
||||
static username: string;
|
||||
|
||||
static {
|
||||
ProxyConfig.authenticate = false;
|
||||
// ProxyConfig.bypass: UrlMatchPatternList;
|
||||
ProxyConfig.host = '';
|
||||
ProxyConfig.match = '';
|
||||
ProxyConfig.password = '';
|
||||
ProxyConfig.port = 0;
|
||||
ProxyConfig.tunnel = false;
|
||||
ProxyConfig.username = '';
|
||||
}
|
||||
|
||||
constructor(def: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
type?: string;
|
||||
|
||||
match: string;
|
||||
host: string;
|
||||
port: number;
|
||||
tunnel: boolean;
|
||||
disabled?: boolean;
|
||||
authenticate: boolean;
|
||||
username: string;
|
||||
password: string;
|
||||
|
||||
}) {
|
||||
super();
|
||||
|
||||
this.id = def.id ? def.id : '';
|
||||
this.name = def.name ? def.name : '';
|
||||
this.type = def.type ? def.type : '';
|
||||
this.disabled = def.disabled ? def.disabled : false;
|
||||
|
||||
this.host = def.host;
|
||||
this.match = def.match;
|
||||
this.port = def.port;
|
||||
this.tunnel = def.tunnel;
|
||||
this.authenticate = def.authenticate;
|
||||
this.username = def.username;
|
||||
this.password = def.password;
|
||||
}
|
||||
|
||||
static isProxyConfig(obj: object) {
|
||||
return 'kind' in obj && obj.kind === 'ProxyConfig';
|
||||
}
|
||||
|
||||
// TODO: should not read from match?
|
||||
getProtocols(): string[] {
|
||||
// match field example: 'http+https://example.com/*'
|
||||
const protoSeparator = this.match.indexOf('://');
|
||||
if (protoSeparator <= 0 || protoSeparator >= this.match.length) {
|
||||
return []; // invalid match value
|
||||
}
|
||||
|
||||
return this.match
|
||||
.slice(0, protoSeparator)
|
||||
.split('+');
|
||||
}
|
||||
|
||||
getProxyUrl(): string {
|
||||
const protos = this.getProtocols();
|
||||
// TODO: how to pick up a protocol?
|
||||
if (protos.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// http://proxy_username:proxy_password@proxy.com:8080
|
||||
if (this.authenticate) {
|
||||
return `protos[0]://${this.username}:${this.password}@${this.host}:${this.port}`;
|
||||
}
|
||||
return `protos[0]://${this.host}:${this.port}`;
|
||||
}
|
||||
|
||||
// TODO: unsupported yet
|
||||
// test(urlStropt)
|
||||
|
||||
update(options: {
|
||||
host: string;
|
||||
match: string;
|
||||
port: number;
|
||||
tunnel: boolean;
|
||||
authenticate: boolean;
|
||||
username: string;
|
||||
password: string;
|
||||
}) {
|
||||
this.host = options.host;
|
||||
this.match = options.match;
|
||||
this.port = options.port;
|
||||
this.tunnel = options.tunnel;
|
||||
this.authenticate = options.authenticate;
|
||||
this.username = options.username;
|
||||
this.password = options.password;
|
||||
}
|
||||
|
||||
updateProtocols(protocols: string[]) {
|
||||
const protoSeparator = this.match.indexOf('://');
|
||||
if (protoSeparator <= 0 || protoSeparator >= this.match.length) {
|
||||
return; // invalid match value
|
||||
}
|
||||
|
||||
this.match = protocols.join('+') + this.match.slice(protoSeparator);
|
||||
}
|
||||
}
|
||||
|
||||
// myProxyConfig = new ProxyConfigList({}, [
|
||||
// {match: 'https://example.com/*', host: 'proxy.com', port: 8080, tunnel: true},
|
||||
// {match: 'http+https://example2.com/*', host: 'proxy2.com'},
|
||||
// ]);
|
||||
|
||||
export class ProxyConfigList<T extends ProxyConfig> extends PropertyList<T> {
|
||||
constructor(parent: PropertyList<T> | undefined, populate: T[]) {
|
||||
super(populate);
|
||||
this._parent = parent;
|
||||
}
|
||||
|
||||
static isProxyConfigList(obj: any) {
|
||||
return 'kind' in obj && obj.kind === 'ProxyConfigList';
|
||||
}
|
||||
|
||||
// TODO: need support URL at first
|
||||
// resolve(url?: URL) {
|
||||
// return {
|
||||
// host: string;
|
||||
// match: string;
|
||||
// port: number;
|
||||
// tunnel: boolean;
|
||||
// authenticate: boolean;
|
||||
// username: string;
|
||||
// password: string;
|
||||
// }
|
||||
// }
|
||||
|
||||
// toObject(excludeDisabledopt, nullable, caseSensitiveopt, nullable, multiValueopt, nullable, sanitizeKeysopt) → {Object}
|
||||
}
|
||||
@@ -0,0 +1,574 @@
|
||||
import { RESPONSE_CODE_REASONS } from '../../../common/constants';
|
||||
import { AuthOptions, RequestAuth } from './auth';
|
||||
import { Property, PropertyBase, PropertyList } from './base';
|
||||
import { CertificateOptions } from './certificates';
|
||||
import { Certificate } from './certificates';
|
||||
import { Cookie, CookieList, CookieOptions } from './cookies';
|
||||
import { HeaderOptions } from './headers';
|
||||
import { Header, HeaderList } from './headers';
|
||||
import { ProxyConfig, ProxyConfigOptions } from './proxy-configs';
|
||||
import { QueryParam, Url } from './urls';
|
||||
import { Variable, VariableList } from './variables';
|
||||
|
||||
// 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 }[];
|
||||
}
|
||||
|
||||
export interface ResponseContentInfo {
|
||||
mimeType: string;
|
||||
mimeFormat: string;
|
||||
charset: string;
|
||||
fileExtension: string;
|
||||
fileName: string;
|
||||
contentType: string;
|
||||
}
|
||||
|
||||
class FormParam {
|
||||
key: string;
|
||||
value: string;
|
||||
|
||||
constructor(options: { key: string; value: string }) {
|
||||
this.key = options.key;
|
||||
this.value = options.value;
|
||||
}
|
||||
|
||||
// TODO
|
||||
// (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;
|
||||
}
|
||||
}
|
||||
|
||||
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>; // 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 interface RequestSize {
|
||||
body: number;
|
||||
header: number;
|
||||
total: number;
|
||||
source: string;
|
||||
}
|
||||
|
||||
export class Request extends Property {
|
||||
kind: string = 'Request';
|
||||
|
||||
url: Url;
|
||||
method: string;
|
||||
headers: HeaderList<Header>;
|
||||
body?: RequestBody;
|
||||
auth: RequestAuth;
|
||||
proxy: ProxyConfig;
|
||||
certificate?: Certificate;
|
||||
|
||||
constructor(options: RequestOptions) {
|
||||
super();
|
||||
|
||||
this.url = typeof options.url === 'string' ? new Url(options.url) : options.url;
|
||||
this.method = options.method;
|
||||
this.headers = new HeaderList(
|
||||
undefined,
|
||||
options.header.map(header => new Header(header)),
|
||||
);
|
||||
this.body = new RequestBody(options.body);
|
||||
this.auth = new RequestAuth(options.auth);
|
||||
this.proxy = new ProxyConfig(options.proxy);
|
||||
this.certificate = new Certificate(options.certificate);
|
||||
}
|
||||
|
||||
static isRequest(obj: object) {
|
||||
return 'kind' in obj && obj.kind === 'Request';
|
||||
}
|
||||
|
||||
addHeader(header: Header | object) {
|
||||
if (Header.isHeader(header)) {
|
||||
const headerInstance = header as Header;
|
||||
this.headers.add(headerInstance);
|
||||
} else if ('key' in header && 'value' in header) {
|
||||
const headerInstance = new Header(header);
|
||||
this.headers.add(headerInstance);
|
||||
} else {
|
||||
throw Error('header must be Header | object');
|
||||
}
|
||||
}
|
||||
|
||||
addQueryParams(params: QueryParam[] | string) {
|
||||
this.url.addQueryParams(params);
|
||||
}
|
||||
|
||||
authorizeUsing(authType: string | AuthOptions, options?: VariableList<Variable>) {
|
||||
const selectedAuth = typeof authType === 'string' ? authType : authType.type;
|
||||
this.auth.use(selectedAuth, options || {});
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new Request({
|
||||
url: this.url,
|
||||
method: this.method,
|
||||
header: this.headers.map(header => header.toJSON(), {}),
|
||||
body: {
|
||||
mode: this.body?.mode,
|
||||
file: this.body?.file,
|
||||
formdata: this.body?.formdata?.map(formParam => formParam.toJSON(), {}),
|
||||
graphql: this.body?.graphql,
|
||||
raw: this.body?.raw,
|
||||
urlencoded: this.body?.urlencoded?.map(queryParam => queryParam.toJSON(), {}),
|
||||
},
|
||||
auth: this.auth.toJSON(),
|
||||
proxy: {
|
||||
match: this.proxy.match,
|
||||
host: this.proxy.host,
|
||||
port: this.proxy.port,
|
||||
tunnel: this.proxy.tunnel,
|
||||
disabled: this.proxy.disabled,
|
||||
authenticate: this.proxy.authenticate,
|
||||
username: this.proxy.username,
|
||||
password: this.proxy.password,
|
||||
},
|
||||
certificate: {
|
||||
name: this.certificate?.name,
|
||||
matches: this.certificate?.matches?.map(match => match.toString(), {}),
|
||||
key: this.certificate?.key,
|
||||
cert: this.certificate?.cert,
|
||||
passphrase: this.certificate?.passphrase,
|
||||
pfx: this.certificate?.pfx,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
forEachHeader(callback: (header: Header, context?: object) => void) {
|
||||
this.headers.each(callback, {});
|
||||
}
|
||||
|
||||
getHeaders(options?: {
|
||||
ignoreCase: boolean;
|
||||
enabled: boolean;
|
||||
multiValue: boolean;
|
||||
sanitizeKeys: boolean;
|
||||
}) {
|
||||
// merge headers with same key into an array
|
||||
const headerMap = new Map<string, string[]>();
|
||||
this.headers.each(header => {
|
||||
const enabled = options?.enabled ? !header.disabled : true;
|
||||
const sanitized = options?.sanitizeKeys ? !!header.value : true;
|
||||
const hasName = !!header.key;
|
||||
|
||||
if (!enabled || !sanitized || !hasName) {
|
||||
return;
|
||||
}
|
||||
|
||||
header.key = options?.ignoreCase ? header.key?.toLocaleLowerCase() : header.key;
|
||||
|
||||
if (headerMap.has(header.key)) {
|
||||
const existingHeader = headerMap.get(header.key) || [];
|
||||
headerMap.set(header.key, [...existingHeader, header.value]);
|
||||
} else {
|
||||
headerMap.set(header.key, [header.value]);
|
||||
}
|
||||
}, {});
|
||||
|
||||
const obj: Record<string, string[] | string> = {};
|
||||
Array.from(headerMap.entries())
|
||||
.forEach(headerEntry => {
|
||||
obj[headerEntry[0]] = headerEntry[1];
|
||||
});
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
removeHeader(toRemove: string | Header, options: { ignoreCase: boolean }) {
|
||||
const filteredHeaders = this.headers.filter(
|
||||
header => {
|
||||
if (!header.key) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof toRemove === 'string') {
|
||||
return options.ignoreCase ?
|
||||
header.key.toLocaleLowerCase() !== toRemove.toLocaleLowerCase() :
|
||||
header.key !== toRemove;
|
||||
} else if ('name' in toRemove) {
|
||||
if (!toRemove.key) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return options.ignoreCase ?
|
||||
header.key.toLocaleLowerCase() !== toRemove.key.toLocaleLowerCase() :
|
||||
header.key !== toRemove.key;
|
||||
} else {
|
||||
throw Error('type of the "toRemove" must be: string | Header');
|
||||
}
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
this.headers = new HeaderList(undefined, filteredHeaders);
|
||||
}
|
||||
|
||||
removeQueryParams(params: string | string[] | QueryParam[]) {
|
||||
this.url.removeQueryParams(params);
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// size() → { Object }
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
url: this.url,
|
||||
method: this.method,
|
||||
header: this.headers.map(header => header.toJSON(), {}),
|
||||
body: {
|
||||
mode: this.body?.mode,
|
||||
file: this.body?.file,
|
||||
formdata: this.body?.formdata?.map(formParam => formParam.toJSON(), {}),
|
||||
graphql: this.body?.graphql,
|
||||
raw: this.body?.raw,
|
||||
urlencoded: this.body?.urlencoded?.map(queryParam => queryParam.toJSON(), {}),
|
||||
},
|
||||
auth: this.auth.toJSON(),
|
||||
proxy: {
|
||||
match: this.proxy.match,
|
||||
host: this.proxy.host,
|
||||
port: this.proxy.port,
|
||||
tunnel: this.proxy.tunnel,
|
||||
disabled: this.proxy.disabled,
|
||||
authenticate: this.proxy.authenticate,
|
||||
username: this.proxy.username,
|
||||
password: this.proxy.password,
|
||||
},
|
||||
certificate: {
|
||||
name: this.certificate?.name,
|
||||
matches: this.certificate?.matches?.map(match => match.toString(), {}),
|
||||
key: this.certificate?.key,
|
||||
cert: this.certificate?.cert,
|
||||
passphrase: this.certificate?.passphrase,
|
||||
pfx: this.certificate?.pfx,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
update(options: RequestOptions) {
|
||||
this.url = typeof options.url === 'string' ? new Url(options.url) : options.url;
|
||||
this.method = options.method;
|
||||
this.headers = new HeaderList(
|
||||
undefined,
|
||||
options.header.map(header => new Header(header)),
|
||||
);
|
||||
this.body = new RequestBody(options.body);
|
||||
this.auth = new RequestAuth(options.auth);
|
||||
this.proxy = new ProxyConfig(options.proxy);
|
||||
this.certificate = new Certificate(options.certificate);
|
||||
}
|
||||
|
||||
upsertHeader(header: HeaderOptions) {
|
||||
// remove keys with same name
|
||||
this.headers = new HeaderList(
|
||||
undefined,
|
||||
this.headers
|
||||
.filter(
|
||||
existingHeader => existingHeader.key !== header.key,
|
||||
{},
|
||||
)
|
||||
);
|
||||
|
||||
// append new
|
||||
this.headers.append(new Header(header));
|
||||
}
|
||||
}
|
||||
|
||||
export interface ResponseOptions {
|
||||
code: number;
|
||||
reason?: string;
|
||||
header?: HeaderOptions[];
|
||||
cookie?: CookieOptions[];
|
||||
body?: string;
|
||||
// originally stream's type is ‘Buffer | ArrayBuffer’, but it should work in both browser and node
|
||||
stream?: ArrayBuffer;
|
||||
responseTime: number;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export class Response extends Property {
|
||||
kind: string = 'Response';
|
||||
|
||||
body: string;
|
||||
code: number;
|
||||
cookies: CookieList;
|
||||
headers: HeaderList<Header>;
|
||||
// originalRequest: Request;
|
||||
responseTime: number;
|
||||
status: string;
|
||||
stream?: ArrayBuffer;
|
||||
|
||||
constructor(options: ResponseOptions) {
|
||||
super();
|
||||
|
||||
this.body = options.body || '';
|
||||
this.code = options.code;
|
||||
this.cookies = new CookieList(
|
||||
undefined,
|
||||
options.cookie?.map(cookie => new Cookie(cookie)) || [],
|
||||
);
|
||||
this.headers = new HeaderList(
|
||||
undefined,
|
||||
options.header?.map(headerOpt => new Header(headerOpt)) || [],
|
||||
);
|
||||
// TODO: how to init request?
|
||||
// this.originalRequest = options.originalRequest;
|
||||
this.responseTime = options.responseTime;
|
||||
this.status = RESPONSE_CODE_REASONS[options.code];
|
||||
this.stream = options.stream;
|
||||
}
|
||||
|
||||
// TODO: accurate type of response should be given
|
||||
static createFromNode(
|
||||
response: {
|
||||
body: string;
|
||||
headers: HeaderOptions[];
|
||||
statusCode: number;
|
||||
statusMessage: string;
|
||||
elapsedTime: number;
|
||||
},
|
||||
cookies: CookieOptions[],
|
||||
) {
|
||||
// TODO: revisit this converting
|
||||
const buf = new ArrayBuffer(response.body ? response.body.length * 2 : 0); // 2 bytes for each char
|
||||
const bufView = new Uint16Array(buf);
|
||||
const strLen = response.body.length;
|
||||
for (let i = 0; i < strLen; i++) {
|
||||
bufView[i] = response.body.charCodeAt(i);
|
||||
}
|
||||
|
||||
return new Response({
|
||||
cookie: cookies,
|
||||
body: response.body.toString(),
|
||||
stream: bufView.buffer,
|
||||
header: response.headers,
|
||||
code: response.statusCode,
|
||||
status: response.statusMessage,
|
||||
responseTime: response.elapsedTime,
|
||||
});
|
||||
}
|
||||
|
||||
static isResponse(obj: object) {
|
||||
return 'kind' in obj && obj.kind === 'Response';
|
||||
}
|
||||
|
||||
contentInfo(): ResponseContentInfo {
|
||||
const contentType = this.headers.get('Content-Type');
|
||||
// TODO:
|
||||
// const contentDisposition = this.headers.get('Content-Disposition');
|
||||
// const mimeInfo = getMimeInfo(contentType, response.stream || response.body);
|
||||
// let fileName = getFileNameFromDispositionHeader(contentDisposition);
|
||||
const mimeInfo = {
|
||||
extension: '',
|
||||
mimeType: '',
|
||||
mimeFormat: '',
|
||||
charset: '',
|
||||
contentType: '',
|
||||
};
|
||||
let fileName = '';
|
||||
const fileExtension = mimeInfo.extension;
|
||||
|
||||
if (!fileName) {
|
||||
fileName = 'response';
|
||||
fileName += `.${fileExtension}` || '';
|
||||
}
|
||||
|
||||
// contentType = mimeInfo.contentType || contentType;
|
||||
|
||||
return {
|
||||
mimeType: mimeInfo.mimeType,
|
||||
mimeFormat: mimeInfo.mimeFormat,
|
||||
charset: mimeInfo.charset,
|
||||
fileExtension: fileExtension,
|
||||
fileName: fileName,
|
||||
contentType: contentType?.valueOf() || 'text/plain',
|
||||
};
|
||||
}
|
||||
|
||||
dataURI() {
|
||||
const contentInfo = this.contentInfo();
|
||||
const bodyInBase64 = this.stream || this.body;
|
||||
if (!bodyInBase64) {
|
||||
throw Error('dataURI() failed as response body is not defined');
|
||||
}
|
||||
|
||||
return `data:${contentInfo.contentType};baseg4, <base64-encoded-body>`;
|
||||
}
|
||||
|
||||
// need a library for this
|
||||
// json(reviver?, strict?) {
|
||||
|
||||
// }
|
||||
|
||||
// jsonp(reviver?, strict?) {
|
||||
|
||||
// }
|
||||
|
||||
reason() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// size(): number {
|
||||
|
||||
// }
|
||||
|
||||
text() {
|
||||
return this.body;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as uuid from 'uuid';
|
||||
|
||||
const builtinModules = new Map<string, any>([
|
||||
['uuid', uuid],
|
||||
]);
|
||||
|
||||
const nodeModules = new Map<string, any>([]);
|
||||
|
||||
export function require(moduleName: string) {
|
||||
if (builtinModules.has(moduleName)) {
|
||||
return builtinModules.get(moduleName);
|
||||
}
|
||||
|
||||
if (nodeModules.has(moduleName)) {
|
||||
// invoke main.js
|
||||
}
|
||||
|
||||
throw Error(`no module is found for "${moduleName}"`);
|
||||
}
|
||||
@@ -0,0 +1,563 @@
|
||||
import queryString from 'query-string';
|
||||
|
||||
import { Property, PropertyBase, PropertyList } from './base';
|
||||
import { Variable, VariableList } from './variables';
|
||||
|
||||
// export class QueryParam extends Property {
|
||||
// key: string = '';
|
||||
// value: string = '';
|
||||
|
||||
// constructor(options: {
|
||||
// id?: string;
|
||||
// name?: string;
|
||||
// key: string;
|
||||
// value: string;
|
||||
// }) {
|
||||
// super();
|
||||
|
||||
// this.id = options.id ? options.id : '';
|
||||
// this.name = options.name ? options.name : '';
|
||||
// this.key = options.key;
|
||||
// this.value = options.value;
|
||||
// }
|
||||
|
||||
// // TODO: improve following fields
|
||||
// static _postman_propertyAllowsMultipleValues: boolean = true;
|
||||
// static _postman_propertyIndexKey: string = 'formData';
|
||||
|
||||
// // parse a form data string into an array of objects, where each object contains a key and a value.
|
||||
// static parse(query: string): { key: string; value: string }[] {
|
||||
// try {
|
||||
// const keyValues = JSON.parse(query);
|
||||
// return keyValues.filter((keyValue: object) => {
|
||||
// if (!('key' in keyValue) || !('value' in keyValue)) {
|
||||
// console.error('ignored some formdata as "key" or "value" is not found in it');
|
||||
// return false;
|
||||
// }
|
||||
// return true;
|
||||
// });
|
||||
// } catch (e) {
|
||||
// console.error(`failed to parse QueryParams: ${e.message}`);
|
||||
// return [];
|
||||
// }
|
||||
// }
|
||||
|
||||
// valueOf() {
|
||||
// return this.value;
|
||||
// }
|
||||
// }
|
||||
|
||||
export 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 interface UrlObject {
|
||||
auth: {
|
||||
username: string;
|
||||
password: string;
|
||||
} | undefined;
|
||||
hash: string;
|
||||
host: string[];
|
||||
path: string[];
|
||||
port: string;
|
||||
protocol: string;
|
||||
query: { key: string; value: string }[];
|
||||
variables: { key: string; value: string }[];
|
||||
}
|
||||
|
||||
export class Url extends PropertyBase {
|
||||
kind: string = 'Url';
|
||||
|
||||
auth?: { username: string; password: string };
|
||||
hash?: string;
|
||||
host: string[];
|
||||
path?: string[];
|
||||
port?: string;
|
||||
protocol?: string;
|
||||
query: PropertyList<QueryParam>;
|
||||
variables: VariableList<Variable>;
|
||||
|
||||
// TODO: user could pass anything
|
||||
constructor(def: {
|
||||
auth?: { username: string; password: string }; // TODO: should be related to RequestAuth
|
||||
hash?: string;
|
||||
host: string[];
|
||||
path?: string[];
|
||||
port?: string;
|
||||
protocol: string;
|
||||
query: PropertyList<QueryParam>;
|
||||
variables: VariableList<Variable>;
|
||||
} | string) {
|
||||
super({ description: 'Url' });
|
||||
|
||||
if (typeof def === 'string') {
|
||||
const urlObj = Url.parse(def);
|
||||
|
||||
if (urlObj) {
|
||||
this.auth = urlObj.auth;
|
||||
this.hash = urlObj.hash;
|
||||
this.host = urlObj.host;
|
||||
this.path = urlObj.path;
|
||||
this.port = urlObj.port;
|
||||
this.protocol = urlObj.protocol;
|
||||
|
||||
const queryList = urlObj.query ? urlObj.query.map(kvObj => new QueryParam(kvObj)) : [];
|
||||
this.query = new PropertyList<QueryParam>(queryList);
|
||||
const varList = urlObj.variables ?
|
||||
urlObj.variables
|
||||
.map((kvObj: { key: string; value: string }) => new Variable(kvObj)) :
|
||||
[];
|
||||
this.variables = new VariableList(undefined, varList);
|
||||
} else {
|
||||
throw Error(`url is invalid: ${def}`); // TODO:
|
||||
}
|
||||
} else {
|
||||
this.auth = def.auth ? { username: def.auth.username, password: def.auth.password } : undefined;
|
||||
this.hash = def.hash ? def.hash : '';
|
||||
this.host = def.host;
|
||||
this.path = def.path ? def.path : [];
|
||||
this.port = def.port ? def.port : '';
|
||||
this.protocol = def.protocol ? def.protocol : '';
|
||||
this.query = def.query ? def.query : new PropertyList<QueryParam>([]);
|
||||
this.variables = def.variables ? def.variables : new VariableList(undefined, new Array<Variable>());
|
||||
}
|
||||
}
|
||||
|
||||
static isUrl(obj: object) {
|
||||
return 'kind' in obj && obj.kind === 'Url';
|
||||
}
|
||||
|
||||
static parse(urlStr: string): UrlObject | undefined {
|
||||
if (!URL.canParse(urlStr)) {
|
||||
console.error(`invalid URL string ${urlStr}`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const url = new URL(urlStr);
|
||||
const query = Array.from(url.searchParams.entries())
|
||||
.map((kv: [string, string]) => {
|
||||
return { key: kv[0], value: kv[1] };
|
||||
});
|
||||
|
||||
return {
|
||||
auth: url.username !== '' ? { // TODO: make it compatible with RequestAuth
|
||||
username: url.username,
|
||||
password: url.password,
|
||||
} : undefined,
|
||||
hash: url.hash,
|
||||
host: url.hostname.split('/'),
|
||||
path: url.pathname.split('/'),
|
||||
port: url.port,
|
||||
protocol: url.protocol, // e.g. https:
|
||||
query,
|
||||
variables: [],
|
||||
};
|
||||
}
|
||||
|
||||
addQueryParams(params: { key: string; value: string }[] | string) {
|
||||
let queryParams: { key: string; value: string }[];
|
||||
|
||||
if (typeof params === 'string') {
|
||||
queryParams = QueryParam.parse(params);
|
||||
} else {
|
||||
queryParams = params;
|
||||
}
|
||||
|
||||
queryParams.forEach((param: { key: string; value: string }) => {
|
||||
this.query.append(new QueryParam({ key: param.key, value: param.value }));
|
||||
});
|
||||
}
|
||||
|
||||
getHost() {
|
||||
return this.host.join('/');
|
||||
}
|
||||
|
||||
getPath(unresolved?: boolean) {
|
||||
const path = this.path ? this.path.join('/') : '';
|
||||
|
||||
if (unresolved) {
|
||||
return '/' + path;
|
||||
}
|
||||
// TODO: handle variables
|
||||
return path;
|
||||
}
|
||||
|
||||
getPathWithQuery() {
|
||||
const path = this.path ? this.path.join('/') : '';
|
||||
const pathHasPrefix = path.startsWith('/') ? path : '/' + path;
|
||||
|
||||
const query = this.query
|
||||
.map(param => `${param.key}=${param.value}`, {})
|
||||
.join('&');
|
||||
|
||||
return `${pathHasPrefix}?${query}`;
|
||||
}
|
||||
|
||||
getQueryString() {
|
||||
return this.query
|
||||
.map(queryParam => (`${queryParam.key}=${queryParam.key}`), {})
|
||||
.join('&');
|
||||
}
|
||||
|
||||
// TODO:
|
||||
getRemote(forcePort?: boolean) {
|
||||
const host = this.host.join('/');
|
||||
if (forcePort) {
|
||||
const port = this.protocol && (this.protocol === 'https:') ? 443 : 80; // TODO: support ws, gql, grpc
|
||||
return `${host}:${port}`;
|
||||
}
|
||||
return this.port ? `${host}:${this.port}` : `${host}`;
|
||||
}
|
||||
|
||||
removeQueryParams(params: QueryParam[] | string[] | string) {
|
||||
if (typeof params === 'string') {
|
||||
// it is a string
|
||||
this.query = new PropertyList(
|
||||
this.query.filter(queryParam => queryParam.key === params, {})
|
||||
);
|
||||
} else if (params.length > 0) {
|
||||
let toBeRemoved: Set<string>;
|
||||
|
||||
if (typeof params[0] === 'string') {
|
||||
// it is a string[]
|
||||
toBeRemoved = new Set(params as string[]);
|
||||
} else {
|
||||
// it is a QueryParam[]
|
||||
toBeRemoved = new Set(
|
||||
(params as QueryParam[])
|
||||
.map(param => param.key)
|
||||
);
|
||||
}
|
||||
|
||||
this.query = new PropertyList(
|
||||
this.query.filter(queryParam => !toBeRemoved.has(queryParam.key), {})
|
||||
);
|
||||
} else {
|
||||
console.error('failed to remove query params: unknown params type, only supports QueryParam[], string[] or string');
|
||||
}
|
||||
}
|
||||
|
||||
toString(forceProtocol?: boolean) {
|
||||
const queryStr = this.query
|
||||
.map(param => `${param.key}=${param.value}`, {})
|
||||
.join('&');
|
||||
|
||||
const auth = this.auth ? `${this.auth.username}:${this.auth.password}@` : '';
|
||||
if (forceProtocol) {
|
||||
return `${this.protocol}//${auth}${this.host}:${this.port}${this.path}?${queryStr}#${this.hash}`; // TODO: improve auth
|
||||
}
|
||||
return `${auth}${this.host}:${this.port}${this.path}?${queryStr}#${this.hash}`;
|
||||
}
|
||||
|
||||
update(url: string | object) {
|
||||
// user could pass anything in script
|
||||
|
||||
if (typeof url === 'string') {
|
||||
const urlObj = Url.parse(url);
|
||||
if (urlObj) {
|
||||
this.auth = urlObj.auth ? {
|
||||
username: urlObj.auth.username,
|
||||
password: urlObj.auth.password,
|
||||
} : {
|
||||
username: '',
|
||||
password: '',
|
||||
};
|
||||
this.hash = urlObj.hash ? urlObj.hash : '';
|
||||
this.host = urlObj.host ? urlObj.host : [];
|
||||
this.path = urlObj.path ? urlObj.path : [];
|
||||
this.port = urlObj.port ? urlObj.port : '';
|
||||
this.protocol = urlObj.protocol ? urlObj.protocol : '';
|
||||
this.query = urlObj.query ?
|
||||
new PropertyList(urlObj.query.map(kv => new QueryParam(kv))) :
|
||||
new PropertyList<QueryParam>([]);
|
||||
// TODO: update variables
|
||||
// this.variables = new VariableList(undefined, new Array<Variable>());
|
||||
} else {
|
||||
throw Error(`failed to parse url: ${url}`);
|
||||
}
|
||||
} else {
|
||||
if ('auth' in url && typeof url.auth === 'object' && url.auth) {
|
||||
if ('username' in url.auth
|
||||
&& typeof url.auth.username === 'string'
|
||||
&& 'password' in url.auth
|
||||
&& typeof url.auth.password === 'string'
|
||||
) {
|
||||
this.auth = { username: url.auth.username, password: url.auth.password };
|
||||
} else {
|
||||
console.error('the auth field must have "username" and "password" fields');
|
||||
}
|
||||
}
|
||||
|
||||
if ('hash' in url && typeof url.hash === 'string') {
|
||||
this.hash = url.hash;
|
||||
} else {
|
||||
this.hash = '';
|
||||
}
|
||||
|
||||
if ('host' in url && Array.isArray(url.host)) {
|
||||
const isStringArray = url.host.length > 0 ? typeof url.host[0] === 'string' : true;
|
||||
if (isStringArray) {
|
||||
this.host = url.host;
|
||||
} else {
|
||||
console.error('type of "host" is invalid');
|
||||
}
|
||||
} else {
|
||||
this.host = [];
|
||||
}
|
||||
|
||||
if ('path' in url && Array.isArray(url.path)) {
|
||||
const isStringArray = url.path.length > 0 ? typeof url.path[0] === 'string' : true;
|
||||
if (isStringArray) {
|
||||
this.path = url.path;
|
||||
} else {
|
||||
console.error('type of "path" is invalid');
|
||||
}
|
||||
} else {
|
||||
this.path = [];
|
||||
}
|
||||
|
||||
this.port = 'port' in url && url.port && typeof url.port === 'string' ? url.port : '';
|
||||
this.protocol = 'protocol' in url && url.protocol && typeof url.protocol === 'string' ? url.protocol : '';
|
||||
|
||||
if ('query' in url && Array.isArray(url.query)) {
|
||||
const queryParams = url.query
|
||||
.filter(obj => 'key' in obj && 'value' in obj)
|
||||
.map(kv => new QueryParam(kv));
|
||||
|
||||
this.query = new PropertyList(queryParams);
|
||||
}
|
||||
|
||||
// TODO: update variables
|
||||
// this.variables = new VariableList(undefined, new Array<Variable>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UrlMatchPattern implements chrome extension match patterns: https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns
|
||||
export class UrlMatchPattern extends Property {
|
||||
// scheme
|
||||
scheme: 'http:' | 'https:' | '*' | 'file:';
|
||||
// host
|
||||
// About wildcard:
|
||||
// If you use a wildcard in the host pattern
|
||||
// it must be the first or only character, and it must be followed by a period (.) or forward slash (/).
|
||||
host: string;
|
||||
// path:
|
||||
// Must contain at least a forward slash
|
||||
// The slash by itself matches any path.
|
||||
path: string;
|
||||
|
||||
private port: string;
|
||||
|
||||
// Special cases: https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns#special
|
||||
// "<all_urls>"
|
||||
// "file:///"
|
||||
// "http://localhost/*"
|
||||
// It doesn't support match patterns for top Level domains (TLD).
|
||||
|
||||
constructor(pattern: string) {
|
||||
super();
|
||||
|
||||
const patternObj = UrlMatchPattern.parseAndValidate(pattern);
|
||||
this.scheme = patternObj.scheme;
|
||||
this.host = patternObj.host.join('/');
|
||||
this.path = patternObj.path.join('/');
|
||||
this.port = patternObj.port;
|
||||
}
|
||||
|
||||
static parseAndValidate(pattern: string): {
|
||||
scheme: 'http:' | 'https:' | '*' | 'file:';
|
||||
host: string[];
|
||||
path: string[];
|
||||
port: string;
|
||||
} {
|
||||
// TODO: validate the pattern
|
||||
const urlObj = Url.parse(pattern);
|
||||
|
||||
if (!urlObj || urlObj.host.length === 0) {
|
||||
throw Error(`match pattern (${pattern}) is invalid and failed to parse`);
|
||||
}
|
||||
|
||||
if (urlObj.protocol !== 'http:' && urlObj.protocol !== 'https:' && urlObj.protocol !== '*' && urlObj.protocol !== 'file:') {
|
||||
throw Error(`scheme (${urlObj.protocol}) is invalid and failed to parse`);
|
||||
}
|
||||
|
||||
return { scheme: urlObj.protocol, host: urlObj.host, path: urlObj.path, port: `${urlObj.port}` };
|
||||
}
|
||||
|
||||
static readonly MATCH_ALL_URLS: string = '<all_urls>';
|
||||
static pattern: string | undefined = undefined; // TODO: its usage is unknown
|
||||
static readonly PROTOCOL_DELIMITER: string = '+';
|
||||
|
||||
// TODO: the url can not start with -
|
||||
private readonly starRegPattern = '[a-zA-Z0-9\-]*';
|
||||
|
||||
getProtocols(): string[] {
|
||||
switch (this.scheme) {
|
||||
case 'http:':
|
||||
return ['http'];
|
||||
case 'https:':
|
||||
return ['https'];
|
||||
case '*':
|
||||
return ['http', 'https'];
|
||||
case 'file:':
|
||||
return ['file'];
|
||||
default:
|
||||
throw `invalid scheme ${this.scheme}`;
|
||||
}
|
||||
}
|
||||
|
||||
test(urlStr: string) {
|
||||
const urlObj = Url.parse(urlStr);
|
||||
if (!urlObj) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.testProtocol(urlObj.protocol)
|
||||
&& this.testHost(urlObj.host.join('/'))
|
||||
&& this.testPort(urlObj.port, urlObj.protocol)
|
||||
&& this.testPath(urlObj.path.join('/'));
|
||||
}
|
||||
|
||||
testHost(host: string) {
|
||||
const hostRegPattern = new RegExp(this.host.replace('*', this.starRegPattern), 'ig');
|
||||
return hostRegPattern.test(host);
|
||||
}
|
||||
|
||||
testPath(path: string) {
|
||||
const pathRegPattern = new RegExp(this.path.replace('*', this.starRegPattern), 'ig');
|
||||
return pathRegPattern.test(path);
|
||||
}
|
||||
|
||||
// TODO: it is confused to verify both port and protocol
|
||||
// testPort verifies both port and protocol, but not the relationship between them
|
||||
testPort(port: string, protocol: string) {
|
||||
if (!this.testProtocol(protocol)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const portRegPattern = new RegExp(this.port.replace('*', this.starRegPattern), 'ig');
|
||||
if (!portRegPattern.test(port)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
testProtocol(protocol: string) {
|
||||
switch (protocol) {
|
||||
case 'http:':
|
||||
return this.scheme === 'http:' || this.scheme === '*';
|
||||
case 'https:':
|
||||
return this.scheme === 'https:' || this.scheme === '*';
|
||||
case '*':
|
||||
return this.scheme === 'http:' || this.scheme === 'https:' || this.scheme === '*';
|
||||
case 'file:':
|
||||
return this.scheme === 'file:';
|
||||
default:
|
||||
throw `invalid scheme ${protocol}`;
|
||||
}
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `${this.scheme}//${this.host}${this.path}`;
|
||||
}
|
||||
|
||||
update(pattern: string) {
|
||||
const patternObj = UrlMatchPattern.parseAndValidate(pattern);
|
||||
this.scheme = patternObj.scheme;
|
||||
this.host = patternObj.host.join('/');
|
||||
this.path = patternObj.path.join('/');
|
||||
this.port = patternObj.port;
|
||||
}
|
||||
}
|
||||
|
||||
export class UrlMatchPatternList<T extends UrlMatchPattern> extends PropertyList<T> {
|
||||
kind: string = 'UrlMatchPatternList';
|
||||
|
||||
constructor(parent: PropertyList<T> | undefined, populate: T[]) {
|
||||
super(populate);
|
||||
this._parent = parent;
|
||||
}
|
||||
|
||||
static isUrlMatchPatternList(obj: any) {
|
||||
return 'kind' in obj && obj.kind === 'UrlMatchPatternList';
|
||||
}
|
||||
|
||||
// TODO: unsupported yet
|
||||
// toObject(excludeDisabledopt, nullable, caseSensitiveopt, nullable, multiValueopt, nullable, sanitizeKeysopt) → {Object}
|
||||
|
||||
test(urlStr: string) {
|
||||
return this
|
||||
.filter(matchPattern => matchPattern.test(urlStr), {})
|
||||
.length > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Property, PropertyList } from './base';
|
||||
|
||||
export class Variable extends Property {
|
||||
key: string;
|
||||
value: any;
|
||||
type: string;
|
||||
kind: string = 'Variable';
|
||||
|
||||
constructor(def?: {
|
||||
id?: string;
|
||||
key: string;
|
||||
name?: string;
|
||||
value: string;
|
||||
type?: string;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
super();
|
||||
|
||||
this.id = def ? def.id : '';
|
||||
this.key = def ? def.key : '';
|
||||
this.name = def ? def.name : '';
|
||||
this.value = def ? def.value : '';
|
||||
this.type = def && def.type ? def.type : 'Variable';
|
||||
this.disabled = def ? def.disabled : false;
|
||||
}
|
||||
|
||||
// unknown usage and unsupported
|
||||
// static readonly types() => {
|
||||
// }
|
||||
|
||||
// cast typecasts a value to the Variable.types of this Variable.
|
||||
cast(value: any) {
|
||||
if ('kind' in value && value.kind === 'Variable') {
|
||||
return value.value;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
set(value: any) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
export class VariableList<T extends Variable> extends PropertyList<T> {
|
||||
kind: string = 'VariableList';
|
||||
|
||||
constructor(parent: PropertyList<T> | undefined, populate: T[]) {
|
||||
super(populate);
|
||||
this._parent = parent;
|
||||
}
|
||||
|
||||
static isVariableList(obj: any) {
|
||||
return 'kind' in obj && obj.kind === 'VariableList';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user