File

projects/wms-framework/src/lib/dataService/dataServices.ts

Description

Default {IHttpConnection} that uses an {HttpClient} object to execute the calls

Implements

IHttpConnection

Index

Properties
Methods

Constructor

constructor(HttpClient: HttpClient)
Parameters :
Name Type Optional
HttpClient HttpClient No

Properties

Public HttpClient
Type : HttpClient
Default value : null

Angular HttpClient instance to use with this object

Public requestHeaders
Type : HttpHeaders
Default value : null

Methods

Public doGet
doGet(url: string, callback: AsyncCallback, callerResult: IAsyncResult)
Parameters :
Name Type Optional
url string No
callback AsyncCallback No
callerResult IAsyncResult No
Returns : void
Public doPost
doPost(url: string, requestHeaders: HttpHeaders, body: string, callback: AsyncCallback, callerResult: IAsyncResult)
Parameters :
Name Type Optional
url string No
requestHeaders HttpHeaders No
body string No
callback AsyncCallback No
callerResult IAsyncResult No
Returns : void
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;
  }
}

result-matching ""

    No results matching ""