feat: move objects to the sdk folder

This commit is contained in:
George He
2024-01-12 15:43:55 +08:00
parent a1fe7fb514
commit 58066828d5
11 changed files with 2458 additions and 0 deletions

View File

@@ -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');
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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';
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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}
}

View File

@@ -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;
}
}

View File

@@ -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}"`);
}

View File

@@ -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;
}
}

View File

@@ -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';
}
}