projects/wms-framework/src/lib/dataService/dataServices.ts
Unsupported class
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { catchError } from 'rxjs/operators';
import { Debugger } from '../diagnostics';
import {
ClassType,
ObservableCollection,
Exception,
ArgumentException,
ArgumentNullException,
InvalidOperationException,
Uri,
SimpleDictionary,
ISimpleDictionary,
iuElementAt,
ReadonlyCollection,
} from '../baseframework';
import { AsyncCallback, IAsyncResult } from '../wcfserviceinvocationsupport';
import { xml2json } from '../utils/XmlUtils';
import { OperationResponse } from './OperationResponse';
import { DataServiceResponse } from './dataServiceResponse';
import { EntityXmlSerializer } from './entityXmlSerializer';
import { EntityObject } from './dataServiceInterfaces';
import {
DataServiceDataType,
DataServiceEntityState,
SaveChangeOptions,
TrackingMode,
} from './dataServiceEnums';
import { SaveOperationResponse } from './saveOperationResponse';
import { DSEntitySet } from './entitySet';
import { FunctionParamInfo } from './FunctionParamInfo';
import { Link } from './link';
/**
* Defines the info associated to the EntitySet decoration
*/
export interface DataServiceEntitySetMetadata {
/**
* Name of the entity set
*
* @type {string}
* @memberof DataServiceEntitySetMetadata
*/
setName: string;
/**
* Name of the entity object
*
* @type {string}
* @memberof DataServiceEntitySetMetadata
*/
entityName: string;
/**
* namespace of the entity set. Used to send informationto server.
*
* @type {string}
* @memberof DataServiceEntitySetMetadata
*/
namespace: string;
}
/**
* Defines the info associated to the data service key decoration
*/
export interface DataServiceKeyMetadata {
/**
* Name of the fields used as key
*/
keys: string[];
}
let currentDataServiceId: number = 1;
export function resetDataServiceIdCounter() {
currentDataServiceId = 1;
}
/**
* Registrates the EntitySet class decorator.
* @param entitySetMetadata the {@link EntitySetMetadata} object containing this class set information
*/
export function DataServiceEntitySet(
entitySetMetadata: DataServiceEntitySetMetadata
): any {
return function (target: any): any {
return class extends target {
getEntitySet() {
return entitySetMetadata.setName;
}
getEntityName() {
return entitySetMetadata.entityName;
}
getNamespace() {
return entitySetMetadata.namespace;
}
dataServiceState = DataServiceEntityState.Detached;
dataServiceId = currentDataServiceId++;
};
};
}
/**
* Registrates the data service key class decorator.
* @param entitySetMetadata the {@link EntitySetMetadata} object containing this class set information
*/
export function DataServiceKey(
dataServiceKeyMetadata: DataServiceKeyMetadata
): any {
return function (target: any): any {
return class extends target {
getEntityKeys() {
return dataServiceKeyMetadata.keys;
}
/**
* Gets the key value as a single composite string
*/
getKey() {
return dataServiceKeyMetadata.keys
.map((element) => this[element])
.join('@__PK__@');
}
};
};
}
/**
* Extends ObservableCollection to add addtional helper methods;
*/
export class DataServiceCollection<T> extends ObservableCollection<T> {
/**
*
* @param items the initial items to add to this collection
* @param trackingMode the { TrackingMode } that indicates how to track items of this
* collection.
*/
constructor(items?: Iterable<T>, private trackingMode?: TrackingMode) {
super(items);
}
/**
* Adds a new element at the end of this collection
* @param item the item to add at the end of the collection
*/
public add(item: T): void {
this.insert(this.count, item);
}
}
/**
* Defines an object that contains information about a {EntityObject} including
* the {EntityObject} itself.
*
* @export
* @class EntityDescriptor
* @wType System.Data.Services.Client.EntityDescriptor
*/
export class EntityDescriptor {
/**
* Creates an instance of EntityDescriptor.
* @param {*} entity the object to create the descriptor for
* @memberof EntityDescriptor
*/
constructor(entity: any) {
this.Entity = entity;
}
/**
* The entity described by this {EntityDescriptor} object
*
* @type {*}
* @memberof EntityDescriptor
*/
Entity: any;
}
/**
* Defines an object that holds the context of all objects to be synchronized to/from the
* service layer.
*
* @export
* @class DataServiceContext
* @wType System.Data.Services.Client.DataServiceContext
*/
export class DataServiceContext {
#entities = new Map<string, DSEntitySet>();
#functions: Map<string, FunctionParamInfo[]> = new Map<
string,
FunctionParamInfo[]
>();
/**
* Gets all entities managed by this {DataServiceContext} object in the form
* of a {EntityDescriptor} object.
*
* @return {*} {Iterable<EntityDescriptor>}
* @memberof DataServiceContext
*/
Entities(): ReadonlyCollection<EntityDescriptor> {
let entitiesArr = [];
for (const entities of this.#entities) {
for (const entity of entities[1]) {
entitiesArr.push(new EntityDescriptor(entity));
}
}
return new ReadonlyCollection(entitiesArr);
}
__IgnoreResourceNotFoundException: any;
/**
* Gets IgnoreResourceNotFoundException property
*
* @type {*}
* @memberof DataServiceContext
* @wNoMap
*/
get IgnoreResourceNotFoundException(): any {
Debugger.RegisterUse(
'Stub_System_Data_Services_Client_DataServiceContext.IgnoreResourceNotFoundException'
);
return this.__IgnoreResourceNotFoundException;
}
/**
* Sets IgnoreResourceNotFoundException property
*
* @memberof DataServiceContext
* @wNoMap
*/
set IgnoreResourceNotFoundException(value: any) {
Debugger.RegisterUse(
'Stub_System_Data_Services_Client_DataServiceContext.IgnoreResourceNotFoundException'
);
this.__IgnoreResourceNotFoundException = value;
}
__ResolveName: any;
/**
* Gets the resolve name
*
* @type {*}
* @memberof DataServiceContext
* @wNoMap
*/
get ResolveName(): any {
Debugger.RegisterUse(
'Stub_System_Data_Services_Client_DataServiceContext.ResolveName'
);
return this.__ResolveName;
}
/**
* Sets the resolve name
*
* @memberof DataServiceContext
* @wNoMap
*/
set ResolveName(value: any) {
Debugger.RegisterUse(
'Stub_System_Data_Services_Client_DataServiceContext.ResolveName'
);
this.__ResolveName = value;
}
__ResolveType: any;
/**
* Gets the resolve type
*
* @type {*}
* @memberof DataServiceContext
* @wNoMap
*/
get ResolveType(): any {
Debugger.RegisterUse(
'Stub_System_Data_Services_Client_DataServiceContext.ResolveType'
);
return this.__ResolveType;
}
/**
* Sets the resolve type
*
* @memberof DataServiceContext
* @wNoMap
*/
set ResolveType(value: any) {
Debugger.RegisterUse(
'Stub_System_Data_Services_Client_DataServiceContext.ResolveType'
);
this.__ResolveType = value;
}
/**
* BeginExecute method
*
* @param {*} parameters
* @return {*} {*}
* @memberof DataServiceContext
* @wNoMap
*/
BeginExecute(...parameters): any {
Debugger.RegisterUse(
'Stub_System_Data_Services_Client_DataServiceContext.BeginExecute'
);
Debugger.Throw('Calling stub method');
return undefined;
}
/**
* BeginExecuteBatch method
*
* @param {*} parameters
* @return {*} {*}
* @memberof DataServiceContext
* @wNoMap
*/
BeginExecuteBatch(...parameters): any {
Debugger.RegisterUse(
'Stub_System_Data_Services_Client_DataServiceContext.BeginExecuteBatch'
);
Debugger.Throw('Calling stub method');
return undefined;
}
/**
* DeleteLink method
*
* @param {*} parameters
* @return {*} {*}
* @memberof DataServiceContext
* @wNoMap
*/
DeleteLink(...parameters): any {
Debugger.RegisterUse(
'Stub_System_Data_Services_Client_DataServiceContext.DeleteLink'
);
Debugger.Throw('Calling stub method');
return undefined;
}
/**
* DetachLink method
*
* @param {*} parameters
* @return {*} {*}
* @memberof DataServiceContext
* @wNoMap
*/
DetachLink(...parameters): any {
Debugger.RegisterUse(
'Stub_System_Data_Services_Client_DataServiceContext.DetachLink'
);
Debugger.Throw('Calling stub method');
return undefined;
}
/**
* EndExecute method
*
* @template T0
* @param {*} parameters
* @return {*} {*}
* @memberof DataServiceContext
* @wNoMap
*/
EndExecute<T0>(...parameters): any {
Debugger.RegisterUse(
'Stub_System_Data_Services_Client_DataServiceContext.EndExecute'
);
Debugger.Throw('Calling stub method');
return undefined;
}
/**
* Gets all {EntitySet} objects registered in this dataservice context
*
* @readonly
* @type {Iterable<EntitySet>}
* @memberof DataServiceContext
*/
get entitySets(): Iterable<DSEntitySet> {
return this.#entities.values();
}
/**
* Creates a new {DataServiceContext} object for a specific service connection.
* @param httpClient the {IHttpConnection} object used to get access to the service layer.
* @param baseUrl the base service url used to build the final url request
* @param proxyUrl an optional url of the proxy server used to get access to services.
*/
public constructor(
private httpClient: IHttpConnection,
private baseUrl: Uri,
private proxyUrl?: Uri
) {}
/**
* Registers a function to the data service context. Functions must be register because the generated url is slightly
* different than other kind of queries specifacally when setting up the parameters.
*
* @param {string} functionName name of the function to register
* @memberof DataServiceContext
*/
registerFunction(functionName: string, params: FunctionParamInfo[]) {
if (!this.#functions.has(functionName)) {
this.#functions.set(functionName, params);
}
}
/**
* Indicates whether the given name is registered as a function or not.
*
* @param {string} functionName the name to test for
* @memberof DataServiceContext
*/
isFunction(functionName: string) {
return this.#functions.has(functionName);
}
/**
* Gets the information of a specific parameter of a function
*
* @param {string} functionName name of the function to get parameter info for.
* @param {string} paramName name of the parameter to get info for.
* @return {*}
* @memberof DataServiceContext
*/
getFunctionParam(functionName: string, paramName: string) {
if (this.isFunction(functionName)) {
let funcParams: FunctionParamInfo[] = this.#functions.get(functionName);
return funcParams.find((e) => e.name === paramName);
}
return undefined;
}
/**
* Adds a link between source and target objects trhu the given property.
*
* @param {*} source Entity object owning the relationship
* @param {string} property the property to create the link thru
* @param {*} target the target entity object to be linked
* @memberof DataServiceContext
* @wMethod AddLink
*/
addLink(source: any, property: string, target: any): void {
if (!source) {
throw new ArgumentNullException('source');
}
if (!property) {
throw new ArgumentNullException('property');
}
if (!target) {
throw new ArgumentNullException('target');
}
let sourceEntity: EntityObject = source as EntityObject;
this.validateEntitytoLink(sourceEntity);
let targetEntity: EntityObject = target as EntityObject;
this.validateEntitytoLink(targetEntity);
let entitySet: DSEntitySet = this.getEntitySet(targetEntity.getEntitySet());
entitySet.addLink(sourceEntity, property, targetEntity);
}
/**
* Attaches an entity that resulted from a query result. This method is for internal usage purpose only.
*
* @param {EntityObject} entity the entity to attach.
* @memberof DataServiceContext
*/
attachFromQuery(entity: EntityObject): EntityObject {
let entitySetName = entity.getEntitySet();
let entitySet = this.getOrCreateEntitySet(entitySetName);
const key = entity.getKey();
if (entitySet.existsKey(key)) {
// supporting AppendOnly merge option only
entity = entitySet.get(key);
} else {
entity.dataServiceState = DataServiceEntityState.Unchanged;
this.attachTo(entitySetName, entity);
}
return entity;
}
/**
* Attaches the given element. Once attached the object will be tracked and changes to it
* will be reported from and to the storage.
* The just attached object state is set to unchanged.
* @param entitySetName the entity set to attach the given element to.
* @param element the element to attach
* @wMethod AttachTo
*/
attachTo(entitySetName: string, element: any): void {
let entityObj = <EntityObject>element;
if (!entitySetName) {
throw new ArgumentNullException('entitySetName');
}
if (!element || element === undefined) {
throw new ArgumentNullException('element');
}
if (!entityObj.getEntityKeys() || entityObj.getEntityKeys().length === 0) {
throw new ArgumentException();
}
let entitySet: DSEntitySet = this.getOrCreateEntitySet(entitySetName);
entitySet.attach(element);
entityObj.dataServiceState = DataServiceEntityState.Unchanged;
}
/**
* Begins a asynchroous operation to save all pending changes. Changes are generated when
* calling the addObject, deleteObject, updateObject, deleteLink or addLink methods.
*
* @param {SaveChangeOptions} options The {SaveChangeOptions} object indicating how to perform
* the save operation. Currently only Batch option is supported.
* @param {AsyncCallback} callback callback to be invoked when the operation is performed
* @param {*} state a custom defined state object that will be returned
* @return {IAsyncResult} {IAsyncResult} object containg data about the resulting operation
* @param {boolean} useRelativeUrl indicates whether url contained in the batch message must use full or relative url
* @memberof DataServiceContext
* @wMethod BeginSaveChanges
*/
beginSaveChanges(
options: SaveChangeOptions,
callback: AsyncCallback,
state: any,
useRelativeUrl: boolean = false
): IAsyncResult {
if (options === SaveChangeOptions.Batch) {
let batchRequest = new BatchRequest(
this,
this.httpClient,
this.baseUrl,
this.proxyUrl,
useRelativeUrl
);
return batchRequest.save(callback, state, this);
} else {
Debugger.Throw('Unsupported method option');
return undefined;
}
}
/**
* Finishes the save changes process by getting the
*
* @param {IAsyncResult} asyncResult the {IAsyncResult} object returned by calling the {beginSaveChanges} method
* @return {*} {DataServiceResponse} an object containing the responses returned by the service
* @memberof DataServiceContext
* @wMethod EndSaveChanges
*/
endSaveChanges(asyncResult: IAsyncResult): DataServiceResponse {
if (asyncResult instanceof BatchRequestAsyncResult) {
let batchResult: BatchRequestAsyncResult = <BatchRequestAsyncResult>(
asyncResult
);
return batchResult.Responses;
}
return new DataServiceResponse([]);
}
/**
* Detaches the given entity from this entity set, once detached the object won't be tracked anymore
* so no changes or updates will be updated to/from the storage
* @param entity the entity to detach from this entity set
* @returns true if properly detached, false otherwise
* @wMethod Detach
*/
detach(element: any): boolean {
let entityObj = <EntityObject>element;
if (!element || element === undefined || !entityObj) {
throw new ArgumentNullException('element');
}
let entitySetName = entityObj.getEntitySet();
let entitySet = this.getEntitySet(entitySetName);
/* istanbul ignore else */
if (entitySet) {
return entitySet.detach(element);
}
return false;
}
/**
* Marks the given object as deleted
* @param element the object to delete
* @wMethod DeleteObject
*/
deleteObject(element: any) {
let entityObj = <EntityObject>element;
if (element === null || entityObj === null) {
throw new ArgumentNullException(element);
}
if (entityObj.dataServiceState === DataServiceEntityState.Detached) {
throw new ArgumentException();
}
if (entityObj.dataServiceState === DataServiceEntityState.Added) {
let setName = entityObj.getEntitySet();
let entitySet = this.getEntitySet(setName);
entitySet.removeAdded(entityObj);
}
entityObj.dataServiceState = DataServiceEntityState.Deleted;
}
/**
* Adds a new object to the given entity set.
* The key of the new object is checked against already tracked objects, it it exists
* then an exception is thrown.
* @param entitySetName The name of the entity set to add the object to.
* @param newElement the new element to add
* @wMethod AddObject
*/
addObject(entitySetName: string, newElement: any) {
let entityObj = <EntityObject>newElement;
if (!entitySetName) {
throw new ArgumentNullException('entitySetName');
}
if (!newElement || newElement === undefined) {
throw new ArgumentNullException('newElement');
}
if (!entityObj.getEntityKeys() || entityObj.getEntityKeys().length === 0) {
throw new ArgumentException();
}
let entitySet: DSEntitySet = this.getOrCreateEntitySet(entitySetName);
entitySet.add(newElement);
}
/**
* Marks the given object as modified. A modified object is sent as a MERGE operation to the server.
* @param element the object to be mark as updated.
* @wMethod UpdateObject
*/
updateObject(element: any): void {
let entityObj = <EntityObject>element;
if (element === null || entityObj === null) {
throw new ArgumentNullException(element);
}
if (entityObj.dataServiceState === DataServiceEntityState.Detached) {
throw new ArgumentException();
}
if (entityObj.dataServiceState === DataServiceEntityState.Unchanged) {
entityObj.dataServiceState = DataServiceEntityState.Modified;
}
}
/**
* Creates an object used to query the data service layer.
* @param entityType the type of the entity to query for.
* @param entityName the name of the entity to query for
* @returns a new {DataServiceQuery} object used to query the entity objects.
* @wMethod CreateQuery
*/
public createQuery<T>(
entityType: ClassType,
entityName: string
): DataServiceQuery<T> {
return new DataServiceQuery<T>(
this,
entityType,
entityName,
this.baseUrl,
this.proxyUrl,
this.httpClient
);
}
/**
* Executes a set of data service requests in a single http call.
* @param callback The {@link AsyncCallback} object that will be invoked when the batch execution is
* done.
* @param state a custom object that will sent as parameter to the given {@link AsyncCallback} object.
* @param dataServiceQueries a set of {@link DataServiceRequest} objects that will be executed in just
* one http call.
* @returns a {@link IAsyncResult} object containing the information of the request response once finished.
*/
beginExecuteBatch<T>(
callback: AsyncCallback,
state: any,
useRelativeUrl: boolean = false,
...dataServiceQueries: DataServiceRequest[]
): IAsyncResult {
let batchRequest = new BatchRequest(
this,
this.httpClient,
this.baseUrl,
this.proxyUrl,
useRelativeUrl
);
return batchRequest.query(callback, state, dataServiceQueries);
}
/**
* EndExecuteBatch method
*
* @param asyncResult the {@link IAsyncResult} result object returned by the {@link DataServiceContext.beginExecuteBatch} method.
* @returns a {@link Iterable} object with all {@link QueryOperationResponse} objects matching everyone of the
* data service requests called.
* @wMethod EndExecuteBatch
*/
endExecuteBatch(asyncResult: IAsyncResult): DataServiceResponse {
/* istanbul ignore else */
if (asyncResult instanceof BatchRequestAsyncResult) {
let batchResult: BatchRequestAsyncResult = <BatchRequestAsyncResult>(
asyncResult
);
return batchResult.Responses;
}
return new QueryOperationResponse[0]();
}
saveEntity(entity: EntityObject) {
let entitySetName: string = entity.getEntitySet();
if (!entitySetName) {
throw new Exception('Invalid set name for entity');
}
let entitySet: DSEntitySet = this.getEntitySet(entitySetName);
if (!entitySet) {
throw new Exception(`Entity set ${entitySetName} is not registered`);
}
entitySet.save(entity);
}
/**
* Gets the links associated to the given target entity
*
* @param {EntityObject} entity the entity to get the links targeted to
* @return {*} {Link}
* @memberof DataServiceContext
*/
public getLinks(entity: EntityObject): Link {
let entitySet: DSEntitySet = this.getEntitySet(entity.getEntitySet());
return entitySet.getLinks(entity);
}
private validateEntitytoLink(sourceEntity: EntityObject) {
if (
!sourceEntity ||
sourceEntity.dataServiceState === DataServiceEntityState.Deleted ||
sourceEntity.dataServiceState === DataServiceEntityState.Detached
) {
throw new InvalidOperationException('source not valid');
}
}
private getEntitySet(entitySetName: string) {
/* istanbul ignore else */
if (this.#entities.has(entitySetName)) {
return this.#entities.get(entitySetName);
}
return null;
}
private getOrCreateEntitySet(entitySetName: string): DSEntitySet {
if (!this.#entities.has(entitySetName)) {
let entitySet: DSEntitySet = new DSEntitySet(entitySetName);
this.#entities.set(entitySetName, entitySet);
}
return this.#entities.get(entitySetName);
}
}
/**
* Defines a {@link IAsyncResult} object that will be returned by the {@link DataServiceContext.beginExecuteBatch} method.
* This object keeps track of all {@link QueryOperationResponse} objects that will contain the requests data
* once the requests is finished.
*/
export class BatchRequestAsyncResult implements IAsyncResult {
AsyncState: any;
IsCompleted: boolean;
classConstructor?: ClassType;
callback?: (x: any) => void;
Responses: DataServiceResponse;
/**
* Creates a new {@link BatchRequestAsyncResult} object that handles the responses of a
* batch request.
* @param callback the {@link AsyncCallback} object that will be called once the request
* is done.
*/
public constructor(callback: AsyncCallback, responses: OperationResponse[]) {
this.IsCompleted = false;
this.callback = callback;
this.Responses = new DataServiceResponse(responses);
}
/**
* Marks this {@see BatchRequestAsyncResult} object as failed
*/
markAsFailed(exception: Exception): void {
for (const response of this.Responses) {
response.markInError(exception);
}
}
}
/**
* Defines an object that handles the response from a query operation.
* This object takes the raw data returned by the service (an xml formatted) and exposes
* the elements defined in such raw data
* @wType System.Data.Services.Client.QueryOperationResponse
*/
export class QueryOperationResponse extends OperationResponse {
protected currentIndex: number;
protected data: any;
#elementType: ClassType;
/**
* Constructs a new QueryOperationResponse object that processes the given response data.
* @param rawData the raw data returned by the request (a xml document)
*/
public constructor(elementType: ClassType) {
super();
this.currentIndex = -1;
this.#elementType = elementType;
}
// Gets the type of the elements returned by this QueryOperation
get ElementType(): ClassType {
return this.#elementType;
}
/**
* Defines an iterator generator function that returns all the {T} elements returned by this
* query.
*/
*[Symbol.iterator](): Iterator<any, any, any> {
let currentIndex = 0;
if (this.data?.entry) {
if (this.data.entry.length) {
while (currentIndex < this.data.entry.length) {
yield this.createResult(this.data.entry[currentIndex]);
currentIndex++;
}
} else {
yield this.createResult(this.data.entry);
}
}
}
public processResponse(rawData: any) {
this.data = xml2json(rawData).feed;
}
/**
* Unsupported
* @returns
*/
public getContinuation(): DataServiceQueryContinuation {
return null;
}
/**
* Creates a new class from the given json representation of an entity.
* @param entry the data service entry element
* @returns
*/
protected createResult(entry: any): any {
let result = new (this.#elementType as any)();
for (var item in entry.content['m:properties']) {
result[this.removePrefix(item)] = this.extractValue(
entry.content['m:properties'][item]
);
}
return result;
}
private extractValue(value: any): any {
if (typeof value == 'string') return value;
else {
let dataType = value.$['m:type'];
let rawValue = value['_'];
switch (dataType) {
case 'Edm.Byte':
return parseInt(rawValue);
case 'Edm.Int32':
return parseInt(rawValue);
case 'Edm.Boolean':
return 'true' === rawValue;
default:
return rawValue;
}
}
}
private removePrefix(name: string) {
let pos: number = name.indexOf(':');
if (pos !== -1) {
name = name.substring(pos + 1);
}
return name;
}
}
/**
* Specialized {@link QueryOperationResponse} object that productes elements of type T.
*
* @export
* @class QueryOperationResponseOf
* @extends {QueryOperationResponse}
* @implements {Iterable<T>}
* @template T
* @wType System.Data.Services.Client.QueryOperationResponse`1
*/
export class QueryOperationResponseOf<T>
extends QueryOperationResponse
implements Iterable<T>
{
#dataServiceContext: DataServiceContext;
public constructor(
dataServiceContext: DataServiceContext,
elementType: ClassType
) {
super(elementType);
this.#dataServiceContext = dataServiceContext;
}
*[Symbol.iterator](): Iterator<T, any, any> {
let currentIndex = 0;
if (this.data?.entry) {
if (this.data.entry.length) {
while (currentIndex < this.data.entry.length) {
let entity = this.createResult(this.data.entry[currentIndex]);
entity = this.#dataServiceContext.attachFromQuery(entity);
yield entity;
currentIndex++;
}
} else {
let entity = this.createResult(this.data.entry);
yield this.#dataServiceContext.attachFromQuery(entity) as unknown as T;
}
}
}
}
/**
* A basic service request object that provides both the request url and the elment type
* that it produces.
* @wType System.Data.Services.Client.DataServiceRequest
*/
export abstract class DataServiceRequest {
/**
* Gets the url object to query for objects.
*/
abstract get RequestUri(): string;
/**
* Gets the actual URI to the data services server. While { RequestUri } can point to a proxy the
* { ServiceUri } must return the actual data service url
*
* @abstract
* @type {string}
* @param {boolean} useRelativeUrl indicates whether the url must be absolute or relative one
* @memberof DataServiceRequest
*/
abstract getServiceUri(useRelativeUrl: boolean): string;
/**
* Gets a {@link ClassType} object that indicates the kind of elements this DataServiceRequest
* object will produce.
*/
abstract get ElementType(): ClassType;
/**
* Creates the matching {@link QueryOperationResponse} object for this {@link DataServiceRequest} object.
* Implementors are responsible for creating a {@link QueryOperationResponseOf} instance if required
*/
abstract createResponse(): QueryOperationResponse;
}
/**
* A {DataServiceQuery} object provides features to query the data service layer.
* @wType System.Data.Services.Client.DataServiceQuery`1
*/
export class DataServiceQuery<T>
extends DataServiceRequest
implements Iterable<T>
{
#dataServiceContext: DataServiceContext;
#queryResponse: QueryOperationResponse;
#state: any;
#queryOptions: ISimpleDictionary<string, string> = new SimpleDictionary<
string,
string
>();
#expand: string[] = [];
constructor(
dataServiceContext: DataServiceContext,
private entityType: ClassType,
private entityName: string,
private baseUrl: Uri,
private proxyUrl: Uri,
private httpConnection: IHttpConnection
) {
super();
this.#dataServiceContext = dataServiceContext;
}
// @inheritdoc
get ElementType(): ClassType {
return this.entityType;
}
// @inheritdoc
get RequestUri(): string {
let isFunction = this.#dataServiceContext.isFunction(this.entityName);
return (
(this.proxyUrl ? this.proxyUrl.ToString() : this.baseUrl.ToString()) +
this.entityName +
this.buildUrlParameters(isFunction)
);
}
// @inheritdoc
getServiceUri(useRelativeUrl: boolean): string {
return (
(useRelativeUrl ? '/' : this.baseUrl.ToString()) +
this.entityName +
this.buildUrlParameters(false)
);
}
/**
* Defines an iterator generator function that returns all the {T} elements returned by this
* query.
*/
*[Symbol.iterator](): Iterator<T, T, T> {
for (const element of this.#queryResponse) {
yield <T>element;
}
return null;
}
/**
* Adds a filter option to the given query, it generates a new {DataServiceQuery} instead of
* modifying current one
*
* @param {string} key the column or element to filter
* @param {string} value the value to filter
* @return {*} {*}
* @memberof DataServiceQuery
* @wMethod AddQueryOption
*/
addQueryOption(key: string, value: string): DataServiceQuery<T> {
let newQuery: DataServiceQuery<T> = new DataServiceQuery(
this.#dataServiceContext,
this.entityType,
this.entityName,
this.baseUrl,
this.proxyUrl,
this.httpConnection
);
this.cloneQueryOptions(newQuery);
this.cloneExpand(newQuery);
newQuery.#queryOptions.addEntry(key, value);
return newQuery;
}
/**
* Adds the ODATA $expand keyword to this {DataServiceQuery} object.
*
* @param {string} value the element to expand
* @return {*} {DataServiceQuery<T>}
* @memberof DataServiceQuery
* @wMethod Expand
*/
expand(value: string): DataServiceQuery<T> {
let newQuery: DataServiceQuery<T> = new DataServiceQuery(
this.#dataServiceContext,
this.entityType,
this.entityName,
this.baseUrl,
this.proxyUrl,
this.httpConnection
);
this.cloneExpand(newQuery);
this.cloneQueryOptions(newQuery);
newQuery.#expand.push(value);
return newQuery;
}
/**
* Begin an asynchronous query operation.
* @param callback the {AsyncCallback} object to call after asynchronous query response is gotten
* @param state a placeholder to be used to pass data to the {DataServiceQuery.endExecute} method.
* @returns the {IAsyncResult} to track the operation status.
* @wMethod BeginExecute
*/
beginExecute(callback: AsyncCallback, state: object): IAsyncResult {
let result: IAsyncResult = {
AsyncState: null,
IsCompleted: false,
classConstructor: this.entityType,
callback: callback,
};
this.#state = state;
this.httpConnection.doGet(
this.RequestUri,
(result: IAsyncResult) => this.processResults(result),
result
);
return result;
}
/**
*
* @param asyncResult the {IAsyncResult} returned by the {DataServiceQuery.beginExecute} operation
* to finish.
* @returns an {Iterable} object with the query results.
* @wMethod EndExecute
*/
endExecute(asyncResult: IAsyncResult): Iterable<T> {
return this.#queryResponse;
}
// inherited
createResponse(): QueryOperationResponse {
return new QueryOperationResponseOf<T>(
this.#dataServiceContext,
this.ElementType
);
}
private processResults(result: IAsyncResult) {
this.#queryResponse = new QueryOperationResponseOf<T>(
this.#dataServiceContext,
this.entityType
);
this.#queryResponse.markInError(result.AsyncState.exception);
this.#queryResponse.processResponse(result.AsyncState.data);
result.AsyncState = this.#state;
result.callback(result);
}
/**
* Builds the url parameters section based on the given query options
*
* @param isFunction Indicates if a call to a function is in progress (functions are different than standard query)
* @private
* @return {*} a empty string if no parameters or a string with the key=value elements
* @memberof DataServiceQuery
*/
private buildUrlParameters(isFunction: boolean): string {
let queryOptionsUrl: string = this.buildQueryOptionsUrl(isFunction);
let expandUrl: string = this.buildExpandUrl();
let separator: string =
queryOptionsUrl !== '' && expandUrl !== '' ? '&' : '';
let questionMark: string =
queryOptionsUrl !== '' || expandUrl !== '' ? '?' : '';
return `${questionMark}${queryOptionsUrl}${separator}${expandUrl}`;
}
/**
* Builds the expand option url fragment as a 'expandValue1/expandValue2/....'
*
* @private
* @return {*} {string}
* @memberof DataServiceQuery
*/
private buildExpandUrl(): string {
let expandUrl: string = '';
for (const expand of this.#expand) {
if (expandUrl) {
expandUrl += '/';
}
expandUrl += expand;
}
return expandUrl ? `$expand=${expandUrl}` : '';
}
/**
* Builds the query options url fragment as
*
* @param isFunction Indicates if a call to a function is in progress (functions are different than standard query)
* @private
* @return {*} {string}
* @memberof DataServiceQuery
*/
private buildQueryOptionsUrl(isFunction: boolean): string {
let andSep = '%20and%20';
let eqOp = '%20eq%20';
let filter = '$filter=';
/* istanbul ignore else */
if (isFunction) {
andSep = '&';
eqOp = '=';
filter = '';
}
let parameters = '';
for (const option of this.#queryOptions) {
if (parameters) {
parameters += andSep;
}
parameters += `${option[0]}${eqOp}${this.escapeValue(
isFunction,
option[0],
option[1]
)}`;
}
return parameters ? `${filter}${parameters}` : '';
}
/**
* Escapes the value used in a query options section
*
* @private
* @param {boolean} isFunction indicates whether we are calling a function or not
* @param {*} value the value to escape
* @return {*} {string}
* @memberof DataServiceQuery
*/
private escapeValue(
isFunction: boolean,
paramName: string,
value: any
): string {
if (!isFunction) {
return value;
}
let param: FunctionParamInfo = this.#dataServiceContext.getFunctionParam(
this.entityName,
paramName
);
switch (param.dataType) {
case DataServiceDataType.String:
case DataServiceDataType.Number:
case DataServiceDataType.Boolean:
return `${value}`;
case DataServiceDataType.Datetime:
return `'${value.toISOString()}'`;
default:
return `'${value}'`;
}
}
/**
* Clones the expand element from this query to the given one
*
* @private
* @param {DataServiceQuery<T>} newQuery the new { DataServiceQuery } object to clone expand options to
* @memberof DataServiceQuery
*/
private cloneExpand(newQuery: DataServiceQuery<T>) {
for (const e of this.#expand) {
newQuery.#expand.push(e);
}
}
/**
* Clones the query options from this query to the given one
*
* @private
* @param {DataServiceQuery<T>} newQuery the new { DataServiceQuery } object to clone query options to
* @memberof DataServiceQuery
*/
private cloneQueryOptions(newQuery: DataServiceQuery<T>) {
for (const option of this.#queryOptions) {
newQuery.#queryOptions.addEntry(option[0], option[1]);
}
}
}
/**
* Unsupported class
*/
export class DataServiceQueryContinuation {}
/**
* Defines an object that handles an http connection protocol.
*/
export interface IHttpConnection {
/**
* Does a post html request
* @param url the url to call. This is the specific service url that will be post appended to
* {IHttpConnection.BaseUrl} property.
* @param requestHeaders Http headers to be sent
* @param body The http post request body to be sent
* @param callback the {@link AsyncCallback} callback that will be fired once the results are gotten.
* @param result the {@link IAsyncResult} object containing the results of the call
*/
doPost(
url: string,
requestHeaders: HttpHeaders,
body: string,
callback: AsyncCallback,
callerResult: IAsyncResult
);
/**
*
* @param url the url to call. This is the specific service url that will be post appended to
* {IHttpConnection.BaseUrl} property.
* @param callback the {@link AsyncCallback} callback that will be fired once the results are gotten.
* @param result the {@link IAsyncResult} object containing the results of the call
*/
doGet(url: string, callback: AsyncCallback, result: IAsyncResult);
}
/**
* Default {IHttpConnection} that uses an {HttpClient} object to execute the calls
*/
export class HttpConnection implements IHttpConnection {
public requestHeaders: HttpHeaders = null;
/**
* Angular HttpClient instance to use with this object
*
* @type {HttpClient}
* @memberof HttpChannel
*/
public HttpClient: HttpClient = null;
constructor(HttpClient: HttpClient) {
this.HttpClient = HttpClient;
}
// @inheritdoc
/* istanbul ignore next */
public doPost(
url: string,
requestHeaders: HttpHeaders,
body: string,
callback: AsyncCallback,
callerResult: IAsyncResult
) {
const request = this.HttpClient.post(url, body, {
headers: requestHeaders,
withCredentials: true,
responseType: 'text',
})
.pipe(
catchError((e, y) => {
console.error(e);
if (callback) {
callerResult.AsyncState = { data: undefined, exception: e };
callerResult.IsCompleted = true;
callback(callerResult);
}
return [];
})
)
.subscribe((result) => {
callerResult.AsyncState = { data: result };
callerResult.IsCompleted = true;
callback(callerResult);
});
}
// @inheritdoc
/* istanbul ignore next */
public doGet(
url: string,
callback: AsyncCallback,
callerResult: IAsyncResult
) {
this.requestHeaders = new HttpHeaders();
this.requestHeaders = this.requestHeaders.set(
'Accept',
'application/atom+xml, application/xml'
);
const request = this.HttpClient.get(url, {
headers: this.requestHeaders,
withCredentials: true,
responseType: 'text',
})
.pipe(
catchError((e, y) => {
console.error(e);
if (callback) {
callerResult.AsyncState = { data: undefined, exception: e };
callerResult.IsCompleted = true;
callback(callerResult);
}
return [];
})
)
.subscribe((result) => {
callerResult.AsyncState = { data: result };
callerResult.IsCompleted = true;
callback(callerResult);
});
}
}
/**
* Defines an object that is responsible for executing batch requests. This
* object is responsible for building the message and keeping track of the results from
* every matching request response.
* Currently this class supports query and save operations in batch mode.
*
*
* @class BatchRequest
*/
class BatchRequest {
private static breakLine = '\r\n';
private state: any;
private batchBoundary: string = 'batch_' + new Date().getTime();
private changeSetBoundary: string = `changeset_${new Date().getTime()}`;
/**
* Creates an instance of BatchRequest.
* @param {DataServiceContext} dataServiceContext the {DataServiceContext} object generating the request.
* @param {IHttpConnection} httpConnection the {IHttpConnection} object used to perform the actual request.
* @param {Uri} baseUrl the url to request to.
* @param {boolean} useRelativeUrl indicates whether url contained in the batch message must use full or relative url
* @memberof BatchRequest
*/
public constructor(
private dataServiceContext: DataServiceContext,
private httpConnection: IHttpConnection,
private baseUrl: Uri,
private proxyUrl: Uri,
private useRelativeUrl: boolean
) {}
/**
* Gets the url used to generate the request (it could be a proxy !)
*
* @readonly
* @type {Uri}
* @memberof BatchRequest
*/
get requestUrl(): Uri {
return this.proxyUrl ?? this.baseUrl;
}
/**
* Executes a query operation over a set of {DataServiceRequest} objects. All queries are executed in a
* single request operation instead of one by one.
*
* @param {AsyncCallback} callback the callback method to call when request's response is processed.
* @param {*} state a user defined object state that will be included in the {IAsyncResult} object.
* @param {DataServiceRequest[]} dataServiceQueries an arary with all queries to be executed in a single request
* operation.
* @return {*} {IAsyncResult}
* @memberof BatchRequest
*/
public query(
callback: AsyncCallback,
state: any,
dataServiceQueries: DataServiceRequest[]
): IAsyncResult {
let body: string = this.buildRequestBody(dataServiceQueries);
let responses = dataServiceQueries.map((query) => query.createResponse());
let result: BatchRequestAsyncResult = new BatchRequestAsyncResult(
callback,
responses
);
this.doPost(
body,
callback,
state,
result,
(postResult: IAsyncResult, batchResult: BatchRequestAsyncResult) => {
this.processResults(postResult, batchResult);
}
);
return result;
}
/**
* Saves all pending operations of the given {DataServiceContext} object. All pending operations are sent to
* the server as a single request instead of one by one.
*
* @param {AsyncCallback} callback the callback method to call when request's response is processed.
* @param {*} state a user defined object state that will be included in the {IAsyncResult} object.
* @param {DataServiceContext} dataServiceContext containing all operations to be sent in a single request.
* @return {*} {IAsyncResult}
* @memberof BatchRequest
*/
public save(
callback: AsyncCallback,
state: any,
dataServiceContext: DataServiceContext
): IAsyncResult {
let responses: OperationResponse[] = [];
let body = this.obtainFullSaveBody(dataServiceContext, responses);
let result: BatchRequestAsyncResult = new BatchRequestAsyncResult(
callback,
responses
);
this.doPost(
body,
callback,
state,
result,
(postResult: IAsyncResult, batchResult: BatchRequestAsyncResult) => {
this.processResults(postResult, batchResult);
}
);
return result;
}
private obtainFullSaveBody(
dataServiceContext: DataServiceContext,
responses: OperationResponse[]
) {
return `--${this.batchBoundary}
Content-Type: multipart/mixed; boundary=${this.changeSetBoundary}
${this.buildSaveBody(dataServiceContext.entitySets, responses)}
--${this.batchBoundary}--
`;
}
private doPost(
body: string,
callback: AsyncCallback,
state: any,
result: BatchRequestAsyncResult,
resultsProcessor: (
postResult: IAsyncResult,
batchResult: BatchRequestAsyncResult
) => void
): void {
this.state = state;
let postUrl: string = this.requestUrl.ToString() + '$batch';
let contentLength: number = body.length;
let headers: HttpHeaders = this.buildRequestHeader(contentLength);
this.httpConnection.doPost(
postUrl,
headers,
body,
(postResult: IAsyncResult) => resultsProcessor(postResult, result),
result
);
}
private buildRequestBody(dataServiceQueries: DataServiceRequest[]): string {
let result: string = '';
dataServiceQueries.forEach((dsQuery) => {
result +=
'--' +
this.batchBoundary +
BatchRequest.breakLine +
'Content-Type: application/http' +
BatchRequest.breakLine +
'Content-Transfer-Encoding: binary' +
BatchRequest.breakLine +
BatchRequest.breakLine +
'GET ' +
dsQuery.getServiceUri(this.useRelativeUrl) +
' ' +
'HTTP/1.1' +
BatchRequest.breakLine +
BatchRequest.breakLine;
});
result += '--' + this.batchBoundary + '--' + BatchRequest.breakLine;
return result;
}
private buildSaveBody(
entitySets: Iterable<DSEntitySet>,
responses: OperationResponse[]
): string {
let body: string = '';
let linkBody: string = '';
var linkCounter: number = 100000000;
for (const entitySet of entitySets) {
for (const entity of entitySet) {
switch (entity.dataServiceState) {
case DataServiceEntityState.Added:
responses.push(
new SaveOperationResponse(this.dataServiceContext, entity)
);
let url = `POST ${this.buildBaseUrlForBody()}${entitySet.name}`;
body += this.buildAddBody(responses, entity, url);
break;
case DataServiceEntityState.Modified:
responses.push(
new SaveOperationResponse(this.dataServiceContext, entity)
);
let urlMerge = `MERGE ${this.buildBaseUrlForBody()}${
entitySet.name
}(${this.getKeysAsPredicate(entity)})`;
body += this.buildAddBody(responses, entity, urlMerge);
break;
case DataServiceEntityState.Deleted:
responses.push(
new SaveOperationResponse(this.dataServiceContext, entity)
);
let urlDelete = `DELETE ${this.buildBaseUrlForBody()}${
entitySet.name
}(${this.getKeysAsPredicate(entity)})`;
body += this.buildDeleteBody(responses, entity, urlDelete);
break;
}
let result: string[] = this.buildLinksMessage(
entity,
linkCounter,
responses
);
linkCounter = parseInt(result[0]);
linkBody += result[1];
}
}
// adding links at the end to ensure both entities are created
body += linkBody;
/* istanbul ignore else */
if (body) {
body += `--${this.changeSetBoundary}--`;
}
return body;
}
private buildBaseUrlForBody(): string {
return this.useRelativeUrl ? '/' : this.baseUrl.ToString();
}
private buildLinksMessage(
entity: EntityObject,
linkCounter: number,
responses: OperationResponse[]
): string[] {
let links = this.dataServiceContext.getLinks(entity);
let body: string = '';
if (links) {
for (const link of links) {
responses.push(
new SaveOperationResponse(this.dataServiceContext, entity)
);
let sourceEntityRef = this.createLinkSourceRef(
link.sourceEntity as EntityObject
);
var urlVerb = link.deleted ? 'DELETE' : 'POST';
var linkId: number = linkCounter++;
var message: string = `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<uri xmlns="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">$${entity.dataServiceId}</uri>`;
body += `--${this.changeSetBoundary}
Content-Type: application/http
Content-Transfer-Encoding: binary
${urlVerb} ${sourceEntityRef}/$links/${link.property} HTTP/1.1
Content-ID: ${linkId}
Content-Type: application/xml
Content-Length: ${message.length}
${message}
`;
}
}
return [linkCounter.toString(), body];
}
private createLinkSourceRef(sourceEntity: EntityObject) {
let sourceEntityRef = `$${sourceEntity.dataServiceId}`;
if (sourceEntity.dataServiceState !== DataServiceEntityState.Added) {
sourceEntityRef = `${this.baseUrl.ToString()}${sourceEntity.getEntitySet()}(${this.getKeysAsPredicate(
sourceEntity
)})`;
}
return sourceEntityRef;
}
/**
* Generates a predicate from the key of the given {EntityObject}.
* The predicate is a string with the form 'key1, key2'
*
* @private
* @param {EntityObject} entity
* @return {*}
* @memberof BatchRequest
*/
private getKeysAsPredicate(entity: EntityObject) {
let complexKey: boolean = entity.getEntityKeys().length > 1;
return entity
.getEntityKeys()
.map((element) => {
let complexKeyPredicate = complexKey ? `${element}=` : '';
let value = entity[element];
switch (typeof value) {
case 'string':
return `${complexKeyPredicate}'${value}'`;
case 'number':
case 'boolean':
return `${complexKeyPredicate}${value}`;
default:
if (value === undefined) {
return `${complexKeyPredicate}'null'`;
} else if (value instanceof Date) {
return `${complexKeyPredicate}'${value.toISOString()}'`;
} else {
return `${complexKeyPredicate}'null'`;
}
}
})
.join(',');
}
private buildDeleteBody(
responses: OperationResponse[],
entity: EntityObject,
urlAction: string
): string {
return `--${this.changeSetBoundary}
Content-Type: application/http
Content-Transfer-Encoding: binary
${urlAction} HTTP/1.1
Content-ID: ${entity.dataServiceId}
`;
}
private buildAddBody(
responses: OperationResponse[],
entity: EntityObject,
urlAction: string
): string {
let serializer: EntityXmlSerializer = new EntityXmlSerializer();
let entry: string = serializer.toXml(
entity,
`${entity.getNamespace()}.${entity.getEntityName()}`
);
return `--${this.changeSetBoundary}
Content-Type: application/http
Content-Transfer-Encoding: binary
${urlAction} HTTP/1.1
Content-ID: ${entity.dataServiceId}
Content-Type: application/atom+xml;type=entry
Content-Length: ${entry.length}
${entry}
`;
}
private buildRequestHeader(contentLength: number) {
let headers: HttpHeaders = new HttpHeaders();
headers = headers.set('Accept', 'application/atom+xml, application/xml');
headers = headers.set('Content-Length', contentLength.toString());
headers = headers.set(
'Content-Type',
'multipart/mixed; boundary=' + this.batchBoundary
);
return headers;
}
private processResults<T>(
postResult: IAsyncResult,
batchResult: BatchRequestAsyncResult
) {
let rawData: string = postResult.AsyncState.data;
let EOL: string = '\r\n';
if (rawData) {
let boundaryExpr = rawData.substring(0, rawData.indexOf(EOL, 0));
let searchPos: number = 0;
let boundarySize: number = boundaryExpr.length;
let index: number = 0;
while (rawData.indexOf(boundaryExpr, searchPos) != -1) {
searchPos = boundarySize + 2; // plus EOL
rawData = rawData.substring(searchPos);
searchPos = 0; // resetting position
const contentTypeLine = rawData.substring(
searchPos,
rawData.indexOf(EOL)
);
let responseInfo = this.extractResponseInfo(contentTypeLine);
let endPos = rawData.indexOf(boundaryExpr, searchPos);
/* istanbul ignore else */
if (endPos === -1) {
break;
}
searchPos += contentTypeLine.length;
let result: any;
switch (responseInfo.kind) {
case 0:
result = this.processQueryResult(
rawData,
searchPos,
endPos,
index,
batchResult
);
break;
case 1:
result = this.processUpdateResult(
rawData,
searchPos,
endPos,
index,
responseInfo.boundary,
batchResult
);
}
rawData = result.rawData;
searchPos = result.searchPos;
index = result.index;
/* istanbul ignore else */
if (result.ended) {
break;
}
}
} else {
batchResult.markAsFailed(postResult.AsyncState.exception);
}
batchResult.AsyncState = this.state;
batchResult.callback(batchResult);
}
private processUpdateResult(
rawData: string,
searchPos: number,
endPos: number,
index: number,
boundary: string,
batchResult: BatchRequestAsyncResult
): any {
while (rawData.indexOf(boundary, searchPos) != -1) {
searchPos += boundary.length;
let endChangeSetPos = rawData.indexOf(boundary, searchPos);
/* istanbul ignore else */
if (endChangeSetPos === -1) {
return {
searchPos: 0,
rawData: rawData.substring(boundary.length + 4),
index: index,
ended: true,
};
}
/* istanbul ignore else */
if (this.wasSuccessfulUpdate(rawData, searchPos, endChangeSetPos)) {
(
iuElementAt(batchResult.Responses, index++) as SaveOperationResponse
).markAsSuccess();
} else {
let e = new Exception('Response not success');
iuElementAt(batchResult.Responses, index++).markInError(e);
}
searchPos = 0;
rawData = rawData.substring(endChangeSetPos);
}
return {
searchPos: 0,
rawData: rawData,
index: index,
ended: false,
};
}
private wasSuccessfulUpdate(
rawData: string,
searchPos: number,
endChangeSetPos: number
) {
let httpIndex = rawData.indexOf('HTTP/1.1 ', searchPos);
/* istanbul ignore else */
if (httpIndex !== -1) {
let responseCode = rawData.substring(httpIndex + 9, httpIndex + 12);
let responseCodeInt = Number.parseInt(responseCode);
return responseCodeInt >= 200 && responseCodeInt <= 205;
}
return false;
}
private processQueryResult(
rawData: string,
searchPos: number,
endPos: number,
index: number,
batchResult: BatchRequestAsyncResult
): any {
searchPos = rawData.indexOf('<?xml version', searchPos);
/* istanbul ignore else */
if (searchPos === -1 || searchPos > endPos) {
return {
searchPos: searchPos,
rawData: rawData,
index: index,
ended: true,
};
}
let xmlData = rawData.substring(searchPos, endPos);
(
iuElementAt(batchResult.Responses, index) as QueryOperationResponse
).processResponse(xmlData);
return {
searchPos: 0,
rawData: rawData.substring(endPos),
index: index + 1,
ended: false,
};
}
private extractResponseInfo(response: string): any {
if (response.startsWith('Content-Type: application/http')) {
return {
kind: 0,
};
} else if (response.startsWith('Content-Type: multipart/mixed;')) {
return {
kind: 1,
boundary: '--' + response.substring(response.indexOf('boundary=') + 9),
};
}
return null;
}
}