File

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

Description

Defines a IAsyncResult object that will be returned by the DataServiceContext.beginExecuteBatch method. This object keeps track of all QueryOperationResponse objects that will contain the requests data once the requests is finished.

Implements

IAsyncResult

Index

Properties
Methods

Constructor

Public constructor(callback: AsyncCallback, responses: OperationResponse[])

Creates a new BatchRequestAsyncResult object that handles the responses of a batch request. is done.

Parameters :
Name Type Optional Description
callback AsyncCallback No

the {

responses OperationResponse[] No

Properties

AsyncState
Type : any
Optional callback
Type : function
Optional classConstructor
Type : ClassType
IsCompleted
Type : boolean
Responses
Type : DataServiceResponse

Methods

markAsFailed
markAsFailed(exception: Exception)

Marks this {@see BatchRequestAsyncResult} object as failed

Parameters :
Name Type Optional
exception Exception 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 ""