Răsfoiți Sursa

* Data Abstract Read/Write

michael 5 ani în urmă
părinte
comite
af47c521d7

+ 63 - 0
demo/dataabstract/sampledarw.html

@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+    <meta charset="utf-8" />
+    <title>
+      Data Abstract dataset demo
+    </title>
+
+    <!-- Bootstrap -->
+    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
+    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
+    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
+    
+    <style type="text/css">
+      body {
+        padding-top: 60px;
+      }
+    </style>
+    <script src="RemObjectsSDK.js" type="text/javascript"></script>
+    <script src="DataAbstract.js" type="text/javascript"></script>
+    <script src="DataAbstract4_intf.js" type="text/javascript"></script>
+    <script src="sampledarw.js" type="text/javascript"></script>
+  </head>
+
+  <body role="document">
+    <div class="navbar navbar-fixed-top" role="navigation">
+      <div class="container">
+        <div class="navbar-header">
+          <div class="navbar-brand">Data Abstract for Pas2JS</div>
+        </div>
+        <button id="btn-fetch" class="btn btn-default">Load Data</button>
+        <button id="btn-add" class="btn btn-default">Add record</button>
+        <button id="btn-edit" class="btn btn-default">Edit record</button>
+        <button id="btn-delete" class="btn btn-default">Delete record</button>
+        <div><a href="sampledarw.lpr"> View pascal sources</a></div>
+      </div>
+    </div>
+    <div id="wrapper" class="container" style="display:none;">
+      <div class="content">
+        <div class="row">
+          <table class="table table-bordered table-striped table-hover table-sm">
+            <thead>
+              <th>ISO</th>
+              <th>ISO 3</th>
+              <th>Name</th>
+              <th>Nice name</th>
+              <th>Num code</th>
+              <th>Phone code</th>
+            </thead>
+            <tbody id="tableRows"></tbody>
+          </table>
+        </div>
+      </div>
+    </div>
+    <hr />
+    </footer>
+
+    <script>
+      window.addEventListener("load",rtl.run);
+    </script>
+  </body>
+</html>

+ 80 - 0
demo/dataabstract/sampledarw.lpi

@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<CONFIG>
+  <ProjectOptions>
+    <Version Value="12"/>
+    <General>
+      <Flags>
+        <MainUnitHasCreateFormStatements Value="False"/>
+        <MainUnitHasTitleStatement Value="False"/>
+        <MainUnitHasScaledStatement Value="False"/>
+        <Runnable Value="False"/>
+        <CompatibilityMode Value="True"/>
+      </Flags>
+      <SessionStorage Value="InProjectDir"/>
+      <Title Value="sampledarw"/>
+      <UseAppBundle Value="False"/>
+      <ResourceType Value="res"/>
+    </General>
+    <CustomData Count="1">
+      <Item0 Name="PasJSWebBrowserProject" Value="1"/>
+    </CustomData>
+    <BuildModes Count="1">
+      <Item1 Name="Default" Default="True"/>
+    </BuildModes>
+    <PublishOptions>
+      <Version Value="2"/>
+      <UseFileFilters Value="True"/>
+    </PublishOptions>
+    <RunParams>
+      <FormatVersion Value="2"/>
+    </RunParams>
+    <Units Count="1">
+      <Unit0>
+        <Filename Value="sampledarw.lpr"/>
+        <IsPartOfProject Value="True"/>
+      </Unit0>
+    </Units>
+  </ProjectOptions>
+  <CompilerOptions>
+    <Version Value="11"/>
+    <Target FileExt=".js">
+      <Filename Value="sampledarw"/>
+    </Target>
+    <SearchPaths>
+      <IncludeFiles Value="$(ProjOutDir)"/>
+      <UnitOutputDirectory Value="js"/>
+    </SearchPaths>
+    <Parsing>
+      <SyntaxOptions>
+        <AllowLabel Value="False"/>
+        <UseAnsiStrings Value="False"/>
+      </SyntaxOptions>
+    </Parsing>
+    <CodeGeneration>
+      <TargetOS Value="browser"/>
+    </CodeGeneration>
+    <Linking>
+      <Debugging>
+        <GenerateDebugInfo Value="False"/>
+        <UseLineInfoUnit Value="False"/>
+      </Debugging>
+    </Linking>
+    <Other>
+      <CustomOptions Value="-Jeutf-8 -Jirtl.js -Jc"/>
+      <CompilerPath Value="$(pas2js)"/>
+    </Other>
+  </CompilerOptions>
+  <Debugging>
+    <Exceptions Count="3">
+      <Item1>
+        <Name Value="EAbort"/>
+      </Item1>
+      <Item2>
+        <Name Value="ECodetoolError"/>
+      </Item2>
+      <Item3>
+        <Name Value="EFOpenError"/>
+      </Item3>
+    </Exceptions>
+  </Debugging>
+</CONFIG>

+ 207 - 0
demo/dataabstract/sampledarw.lpr

@@ -0,0 +1,207 @@
+program sampledarw;
+
+{$mode objfpc}
+
+uses
+  Classes, SysUtils, Web, DB, dadataset;
+
+Type
+
+  { TSampleForm }
+
+  TSampleForm = Class(TComponent)
+  Private
+    divWrapper : TJSHTMLElement;
+    btnDeleteData,
+    btnAddData,
+    btnEditData,
+    btnGetData : TJSHTMLButtonElement;
+    tblBody : TJSHTMLElement;
+    FConn : TDAConnection;
+    FDataset : TDADataset;
+    procedure AfterLoad(DataSet: TDataSet);
+    procedure BindElements;
+    procedure CreateDataset;
+    function DoAddDataClick(aEvent: TJSMouseEvent): boolean;
+    procedure DoAfterApplyUpdates(Sender: TDataset; info: TResolveResults);
+    procedure DoCountriesOpen(DataSet: TDataSet);
+    function DoDeleteDataClick(aEvent: TJSMouseEvent): boolean;
+    function DoEditDataClick(aEvent: TJSMouseEvent): boolean;
+    function DoGetDataClick(aEvent: TJSMouseEvent): boolean;
+    procedure ShowCountries;
+  Public
+    Procedure Show;
+  end;
+
+{ TSampleForm }
+
+procedure TSampleForm.CreateDataset;
+
+begin
+  FConn:=TDaConnection.Create(Self);
+  FConn.URL:='/proxy/Server/bin';
+  FConn.StreamerType:=stBin;
+  FDataset:=TDaDataset.Create(Self);
+  FDataset.DAConnection:=FConn;
+  FDataset.TableName:='Country';
+  FDataset.AfterOpen:=@DoCountriesOpen;
+  FDataset.AfterApplyUpdates:=@DoAfterApplyUpdates;
+  FDataset.AfterLoad:=@AfterLoad;
+  FDataset.DAOptions:=[doRefreshAllFields];
+end;
+
+
+procedure TSampleForm.DoAfterApplyUpdates(Sender: TDataset; info: TResolveResults);
+
+Var
+  I : Integer;
+  ACount,aSucceeded : Integer;
+
+begin
+  aCount:=Length(Info.Records);
+  aSucceeded:=0;
+  For I:=0 to aCount-1 do
+    if Info.Records[i].ResolveStatus=rsResolved then
+      Inc(aSucceeded);
+  window.alert(Format('Sent %d records for update to the server, %d succeeded.',[aCount,aSucceeded]));
+  if aSucceeded>0 then
+    begin
+    FDataset.First;
+    ShowCountries;
+    end;
+end;
+
+procedure TSampleForm.BindElements;
+
+begin
+  btnGetData:=TJSHTMLButtonElement(Document.getElementById('btn-fetch'));
+  btnGetData.onClick:=@DoGetDataClick;
+  btnAddData:=TJSHTMLButtonElement(Document.getElementById('btn-add'));
+  btnAddData.onClick:=@DoAddDataClick;
+  btnAddData.Disabled:=True;
+  btnEditData:=TJSHTMLButtonElement(Document.getElementById('btn-edit'));
+  btnEditData.onClick:=@DoEditDataClick;
+  btnEditData.Disabled:=True;
+  btnDeleteData:=TJSHTMLButtonElement(Document.getElementById('btn-delete'));
+  btnDeleteData.onClick:=@DoDeleteDataClick;
+  btnDeleteData.Disabled:=True;
+  tblBody:=TJSHTMLElement(Document.getElementById('tableRows'));
+  divWrapper:=TJSHTMLElement(Document.getElementById('wrapper'));
+end;
+
+procedure TSampleForm.AfterLoad(DataSet: TDataSet);
+begin
+  Writeln('Loaded');
+end;
+
+procedure TSampleForm.DoCountriesOpen(DataSet: TDataSet);
+
+
+begin
+  Writeln('Countries open :',Dataset.RecordCount);
+  btnEditData.Disabled:=False;
+  btnAddData.Disabled:=False;
+  btnDeleteData.Disabled:=False;
+  ShowCountries;
+end;
+
+function TSampleForm.DoDeleteDataClick(aEvent: TJSMouseEvent): boolean;
+begin
+  Result:=False;
+  FDataset.last;
+  FDataset.Delete;
+  FDataset.ApplyUpdates;
+end;
+
+procedure TSampleForm.ShowCountries;
+
+  Function escape(S : String) : String;
+
+  begin
+    Result:=StringReplace(S,'&','&amp;',[rfReplaceAll]);
+    Result:=StringReplace(S,'<','&lt;',[rfReplaceAll]);
+    Result:=StringReplace(S,'>','&gt;',[rfReplaceAll]);
+    Result:=StringReplace(S,'"','&quot;',[rfReplaceAll]);
+    Result:=StringReplace(S,'''','&#39;',[rfReplaceAll]);
+  end;
+
+Var
+  FISo,FName,FNiceName,fISO3,Fnum,FPhone : TField;
+  HTML : String;
+
+begin
+  FISO:=FDataset.FieldByname('ISO');
+  FISO3:=FDataset.FieldByname('ISO3');
+  FName:=FDataset.FieldByname('Name');
+  FNiceName:=FDataset.FieldByname('NiceName');
+  FNum:=FDataset.FieldByname('Numcode');
+  FPhone:=FDataset.FieldByname('phonecode');
+  While not FDataset.EOF do
+    begin
+    html:=Html+'<TR><TD>'+Escape(FISO.AsString)+'</TD>'
+              +'<TD>'+Escape(FIso3.AsString)+'</TD>'
+              +'<TD>'+Escape(FName.AsString)+'</TD>'
+              +'<TD>'+Escape(FNiceName.AsString)+'</TD>'
+              +'<TD>'+Escape(FNum.AsString)+'</TD>'
+              +'<TD>'+Escape(FPHone.AsString)+'</TD></TR>';
+    FDataset.Next;
+    end;
+  tblBody.InnerHTMl:=HTML;
+  divWrapper['style']:='';
+end;
+
+function TSampleForm.DoAddDataClick(aEvent: TJSMouseEvent): boolean;
+begin
+  Result:=False;
+   With FDataset do
+     begin
+     Append;
+     FieldByname('ISO').AsString:='zz';
+     FieldByname('ISO3').AsString:='zzz';
+     FieldByname('Name').AsString:='CountryZ';
+     FieldByname('NiceName').AsString:='Country with name Z';
+     FieldByname('PhoneCode').AsInteger:=999;
+     FieldByname('NumCode').AsInteger:=99;
+     Post;
+     ApplyUpdates;
+     end;
+end;
+
+
+function TSampleForm.DoEditDataClick(aEvent: TJSMouseEvent): boolean;
+
+begin
+  Result:=False;
+  With FDataset do
+    begin
+    Last;
+    Edit;
+    FieldByname('ISO3').AsString:='zz3';
+    FieldByname('Name').AsString:='CountryZZ';
+    FieldByname('NiceName').AsString:='Country without name Z';
+    FieldByname('PhoneCode').AsInteger:=666;
+    FieldByname('NumCode').AsInteger:=66;
+    Post;
+    ApplyUpdates;
+    end;
+end;
+
+function TSampleForm.DoGetDataClick(aEvent: TJSMouseEvent): boolean;
+begin
+  Result:=False;
+  FDataset.Close;
+  FDataset.load([],Nil);
+end;
+
+
+procedure TSampleForm.Show;
+
+begin
+  CreateDataset;
+  BindElements;
+end;
+
+begin
+  With TSampleForm.Create(Nil) do
+    Show;
+end.

+ 15 - 7
packages/dataabstract/da.pas

@@ -65,8 +65,8 @@ Type
     function getStream : TDAStream;
     procedure setStream(aStream : TDAStream);
     procedure readDataset(aDataset : TDADataTable);
-    function readDelta : TDADelta;
-    procedure writeDelta(aDelta : TDADelta);
+    function readDelta : TDADeltas;
+    procedure writeDelta(aDelta : TDADeltas);
     Property Stream : TDAStream Read getStream write setStream;
   end;
 
@@ -96,13 +96,19 @@ Type
   end;
 
   TDAChange = class external name 'RemObjects.DataAbstract.Change' (TJSObject)
+    recid : Nativeint;
+    changetype : string;
+    status : string;
+    message : string;
+    old : TJSValueDynArray;
+    new_ : TJSValueDynArray; external name 'new';
   end;
 
   TDAChangeArray = array of TDAChange;
   
   TLogField = record
     name : string;
-    datatype : string;
+    datatype : string; external name 'type';
   end;
   TLogFieldArray = array of TLogfield;
   
@@ -115,13 +121,15 @@ Type
   Public
     Function intFindId(anId : Integer) : TDAChange;
     Property data : TDAChangeArray Read FData;
-    Property keyFields : TStringDynArray Read FKeyFields;
-    Property LoggedFields : TLogFieldArray Read FLoggedFields; 
-    Property Name : String Read FName;
+    Property keyFields : TStringDynArray Read FKeyFields Write FKeyFields;
+    Property LoggedFields : TLogFieldArray Read FLoggedFields Write FLoggedFields;
+    Property Name : String Read FName Write FName;
   end;
+  TDADeltaArray = Array of TDADelta;
 
   TDADeltas = class external name 'RemObjects.DataAbstract.Deltas' (TJSObject)
   Public
+    deltas : TDADeltaArray;
     Function FindByName (Const aName : String) : TDADelta;
   end;
 
@@ -193,7 +201,7 @@ Type
     dictionaryEntry : String;
     displayLabel : String;
     displayWidth : integer;
-    inPrimaryKey : Boolean;
+    inPrimaryKey : string;
     visible : boolean;
     required : boolean;
     size : integer;

+ 354 - 14
packages/dataabstract/dadataset.pas

@@ -15,6 +15,10 @@
  **********************************************************************}
 unit dadataset;
 
+{$mode objfpc}
+{$modeswitch externalclass}
+
+
 interface
 
 uses Types, Classes, DB, jsonDataset, JS, rosdk, da, dasdk;
@@ -54,11 +58,36 @@ Type
     class function GetWhereClause (aExpression : TDAExpression) : String;
   end;
 
+Type
+  TDADataRow = Class external name 'Object' (TJSObject)
+    _new,
+    _old : TJSValueDynArray;
+  end;
+  TDaDataRowArray = Array of TDADataRow;
+
+  TResolvedRow = Class external name 'Object' (TJSObject)
+    changes : TDAChange;
+    fields : TLogFieldArray;
+  end;
+
+  { TDAArrayFieldMapper }
+
+  TDAArrayFieldMapper = Class(TJSONArrayFieldMapper)
+  Public
+    Procedure RemoveField(Const FieldName : String; FieldIndex : Integer; Row : JSValue); override;
+    procedure SetJSONDataForField(Const FieldName{%H-} : String; FieldIndex : Integer; Row,Data : JSValue); override;
+    Function GetJSONDataForField(Const FieldName{%H-} : String; FieldIndex : Integer; Row : JSValue) : JSValue; override;
+    Function CreateRow : JSValue; override;
+  end;
+
 
   { TDADataset }
+  TDADatasetOption = (doRefreshAllFields);
+  TDADatasetOptions = Set of TDADatasetOption;
 
   TDADataset = class(TBaseJSONDataset)
   private
+    FDAOptions: TDADatasetOptions;
     FParams: TParams;
     FTableName: String;
     FDAConnection: TDAConnection;
@@ -67,7 +96,15 @@ Type
     function DataTypeToFieldType(s: String): TFieldType;
     procedure SetParams(AValue: TParams);
   Protected
+    function DoResolveRecordUpdate(anUpdate : TRecordUpdateDescriptor): Boolean; override;
     Procedure MetaDataToFieldDefs; override;
+    Procedure InternalEdit; override;
+    Procedure InternalDelete; override;
+    // These operate on metadata received from DA.
+    function GetDAFields: TDAFieldArray;
+    function GetExcludedFields : TNativeIntDynArray;
+    Function GetLoggedFields : TLogFieldArray;
+    function GetKeyFields: TStringDynArray;
   Public
     constructor create(aOwner : TComponent); override;
     Destructor Destroy; override;
@@ -84,6 +121,7 @@ Type
     Property DAConnection : TDAConnection Read FDAConnection Write FDAConnection;
     Property Params : TParams Read FParams Write SetParams;
     Property WhereClause : String Read FWhereClause Write FWhereClause;
+    Property DAOptions : TDADatasetOptions Read FDAOptions Write FDAOptions;
   end;
 
 
@@ -100,6 +138,7 @@ Type
   private
     FConnection: TDAConnection;
     function ConvertParams(DADS: TDADataset): TDADataParameterDataArray;
+    procedure ProcessUpdateResult(Res: JSValue; aBatch: TRecordUpdateBatch);
   Protected
     Function GetDataRequestClass : TDataRequestClass; override;
   Public
@@ -147,6 +186,8 @@ Type
     function DetectMessageType(Const aURL: String): TDAMessageType; virtual;
     Function CreateDataService : TDADataAbstractService; virtual;
     Function CreateLoginService : TDASimpleLoginService; virtual;
+    Function CreateStreamer : TDADataStreamer;
+    Function InterpretMessage(aRes : JSValue) : String;
   Public
     Constructor create(aOwner : TComponent); override;
     Destructor Destroy; override;
@@ -198,6 +239,35 @@ uses strutils, sysutils;
 resourcestring
   SErrInvalidDate = '%s is not a valid date value for %s';
 
+{ TDAArrayFieldMapper }
+
+procedure TDAArrayFieldMapper.RemoveField(const FieldName: String; FieldIndex: Integer; Row: JSValue);
+
+begin
+  Inherited RemoveField(FieldName,FieldIndex,TDADataRow(Row)._new);
+end;
+
+procedure TDAArrayFieldMapper.SetJSONDataForField(const FieldName: String; FieldIndex: Integer; Row, Data: JSValue);
+begin
+  Inherited SetJSONDataForField(FieldName,FieldIndex,TDADataRow(Row)._new,Data);
+end;
+
+function TDAArrayFieldMapper.GetJSONDataForField(const FieldName: String; FieldIndex: Integer; Row: JSValue): JSValue;
+begin
+  Result:=Inherited GetJSONDataForField(FieldName,FieldIndex,TDADataRow(Row)._new);
+end;
+
+function TDAArrayFieldMapper.CreateRow: JSValue;
+
+begin
+  Result:=TDADataRow.New;
+  With TDADataRow(Result) do
+    begin
+    _new:=[];
+    _old:=[];
+    end;
+end;
+
 { TDAWhereClauseBuilder }
 
 class function TDAWhereClauseBuilder.NewBinaryExpression(aLeft, aRight: TDAExpression; anOp: TDABinaryOperator): TDAExpression;
@@ -486,6 +556,21 @@ begin
   Result:=TDASimpleLoginService.New(FChannel,FMessage,LoginServiceName);
 end;
 
+function TDAConnection.CreateStreamer: TDADataStreamer;
+begin
+  Case StreamerType of
+    stJSON : Result:=TDAJSONDataStreamer.new;
+    stBIN: Result:=TDABIN2DataStreamer.new;
+  end;
+end;
+
+function TDAConnection.InterpretMessage(aRes: JSValue): String;
+begin
+  Result:=String(aRes);
+  if (EnsureMessageType=mtJSON) then
+    Result:=TROUtil.Frombase64(Result);
+end;
+
 constructor TDAConnection.create(aOwner: TComponent);
 begin
   inherited create(aOwner);
@@ -596,6 +681,35 @@ begin
   FParams.Assign(AValue);
 end;
 
+function TDADataset.DoResolveRecordUpdate(anUpdate: TRecordUpdateDescriptor): Boolean;
+
+Var
+  rIdx,I : Integer;
+  Fld : TField;
+  ResRow : TResolvedRow;
+  aRow : JSValue;
+
+begin
+  Result:=True;
+  if Assigned(anupDate.ServerData) and (anUpdate.Status<>usDeleted) then
+     begin
+     rIdx:=NativeInt(anUpdate.Bookmark.Data);
+     if Not (rIdx>=0) and (rIdx<Rows.Length) then
+        exit;
+     // Apply new values
+     aRow:=Rows[rIdx];
+     ResRow:=TResolvedRow(anUpdate.ServerData);
+     With ResRow do
+       for I:=0 to Length(Fields)-1 do
+         if (doRefreshAllFields in DAOptions) or (Changes.old[I]<>Changes.new_[I]) then
+            begin
+            Fld:=FieldByName(Fields[i].Name);
+            FieldMapper.SetJSONDataForField(Fld,aRow,Changes.New_[i]);
+            end;
+     TDADataRow(aRow)._old:=[];
+     end;
+end;
+
 function TDADataset.ConvertToDateTime(aField: TField; aValue: JSValue; ARaiseException: Boolean): TDateTime;
 begin
   Result:=0;
@@ -621,6 +735,100 @@ begin
   CreateFieldDefs(TJSArray(Metadata['fields']));
 end;
 
+procedure TDADataset.InternalEdit;
+
+Var
+  D : TDADataRow;
+
+begin
+  Inherited;
+  D:=TDADataRow(ActiveBuffer.Data);
+  if Length(D._old)=0 then
+    begin
+    asm
+    this.FEditRow._old=this.FEditRow._new.slice();
+    end;
+    end;
+  if Not isDefined(D._new) then
+    ActiveBuffer.Data:=FieldMapper.CreateRow
+end;
+
+procedure TDADataset.InternalDelete;
+begin
+  inherited InternalDelete;
+  asm
+    var len=this.FDeletedRows.length-1;
+    this.FDeletedRows[len]._old=this.FDeletedRows[len]._new.slice();
+  end;
+end;
+
+function TDADataset.GetDAFields: TDAFieldArray;
+
+begin
+  if Assigned(metadata) and Metadata.HasOwnProperty('fields') and isArray(MetaData['fields']) then
+    Result:=TDAFieldArray(Metadata['fields'])
+  else
+    Result:=nil;
+end;
+
+function TDADataset.GetExcludedFields: TNativeIntDynArray;
+
+Var
+  Flds : TDAFieldArray;
+  I : Integer;
+
+begin
+  Result:=[];
+  Flds:=GetDaFields;
+  For I:=0 to Length(Flds)-1 do
+    if not Flds[i].logChanges then
+      TJSArray(Result).Push(i);
+end;
+
+function TDADataset.GetKeyFields: TStringDynArray;
+
+Var
+  Flds : TDAFieldArray;
+  I,aLen : Integer;
+begin
+  Result:=[];
+  Flds:=GetDaFields;
+  aLen:=0;
+  SetLength(Result,Length(Flds));
+  For I:=0 to Length(Flds)-1 do
+    begin
+    if Flds[i].HasOwnProperty('inPrimaryKey') and  SameText(Flds[i].inPrimaryKey,'True') then
+      begin
+      Result[aLen]:=Flds[i].Name;
+      Inc(aLen);
+      end;
+    end;
+  SetLength(Result,aLen);
+end;
+
+
+function TDADataset.GetLoggedFields: TLogFieldArray;
+
+Var
+  Flds : TDAFieldArray;
+  I,aLen : Integer;
+begin
+  Result:=[];
+  Flds:=GetDaFields;
+  aLen:=0;
+  SetLength(Result,Length(Flds));
+  For I:=0 to Length(Flds)-1 do
+    begin
+    if Flds[i].logChanges then
+      begin
+      Result[aLen].name:=Flds[i].Name;
+      Result[aLen].datatype:=Flds[i].type_;
+      Inc(aLen);
+      end;
+    end;
+  SetLength(Result,aLen);
+end;
+
 function TDADataset.DoGetDataProxy: TDataProxy;
 begin
   Result:=TDADataProxy.Create(Self);
@@ -700,7 +908,7 @@ end;
 
 function TDADataset.CreateFieldMapper: TJSONFieldMapper;
 begin
-  Result := TJSONArrayFieldMapper.Create;
+  Result := TDAArrayFieldMapper.Create;
 end;
 
 { TDADataProxy }
@@ -734,6 +942,7 @@ Var
   DADS : TDADataset;
   PA : TDADataParameterDataArray;
   DS : TDADataAbstractService;
+
 begin
   // DA does not support this option...
   if loAtEOF in aRequest.LoadOptions then
@@ -772,14 +981,142 @@ begin
   Result:=TDADataRequest;
 end;
 
+
+procedure TDADataProxy.ProcessUpdateResult(Res: JSValue;aBatch: TRecordUpdateBatch);
+
+Var
+  I : Integer;
+  aDelta : TDADelta;
+  aDeltas : TDADeltas;
+  aStreamer : TDADataStreamer;
+  C : TDaChange;
+  ResolvedRow : TResolvedRow;
+
+begin
+  aStreamer:=Connection.CreateStreamer;
+  aStreamer.Stream:=Connection.InterpretMessage(Res);
+  aStreamer.initializeRead;
+  aDeltas:=aStreamer.ReadDelta;
+  if Length(aDeltas.deltas)>1 then
+    begin
+    For I:=0 to aBatch.List.Count-1 do
+      aBatch.List[i].ResolveFailed('More than 1 delta in result');
+    Exit;
+    end;
+  aDelta:=aDeltas.Deltas[0];
+  For C in aDelta.data do
+    begin
+    Case C.Status of
+     'failed' :
+        aBatch.List[C.recid].ResolveFailed(C.Message);
+     'resolved':
+        begin
+        ResolvedRow:=TResolvedRow.new;
+        ResolvedRow.changes:=C;
+        ResolvedRow.fields:=aDelta.LoggedFields;
+        aBatch.List[C.recid].Resolve(ResolvedRow);
+        end;
+    end;
+    end;
+  For I:=0 to aBatch.List.Count-1 do
+    if aBatch.List[i].ResolveStatus=rsResolving then
+      aBatch.List[i].Resolve(Null);
+  If Assigned(aBatch.OnResolve) then
+    ABatch.OnResolve(Self,aBatch);
+end;
+
+
 function TDADataProxy.ProcessUpdateBatch(aBatch: TRecordUpdateBatch): Boolean;
+
+  Procedure UpdateSuccess(res : JSValue);
+
+  begin
+    ProcessUpdateResult(Res,aBatch)
+  end;
+
+  Procedure UpdateFailure(response : TROMessage; Err : TJSError);
+
+  var
+     I : Integer;
+     aDesc : TRecordUpdateDescriptor;
+
+  begin
+    For I:=0 to aBatch.List.Count-1 do
+      begin
+      aDesc:=aBatch.List[i];
+      aDesc.ResolveFailed(extractErrorMsg(Err));
+      end;
+    If Assigned(aBatch.OnResolve) then
+      ABatch.OnResolve(Self,aBatch);
+  end;
+
+  Procedure ExcludeItems(aList : TNativeIntDynArray; aValue : TJSValueDynArray);
+
+  Var
+     I : Integer;
+
+  begin
+    // Backwards or index will shift with each delete!
+    for I:=Length(aList)-1 downto 1 do
+       TJSArray(aValue).Splice(aList[I],1);
+  end;
+
+
+
+Const
+  ChangeTypes : Array[TUpdateStatus] of String = ('update','insert','delete');
+
+Var
+  lDataset : TDaDataset;
+  excludedFields : TNativeIntDynArray;
+  I : Integer;
+  aDesc : TRecordUpdateDescriptor;
+  aDelta : TDADelta;
+  aDeltas : TDADeltas;
+  aChange : TDAChange;
+  DS : TDADataAbstractService;
+  DStr : TDADataStreamer;
+  S : String;
+
+
 begin
-  Result:=False;
+  lDataset:=TDADataset(aBatch.Dataset);
+  aDeltas:=TDADeltas.New;
+  aDelta:=TDADelta.New;
+  aDelta.Name:=lDataset.TableName;
+  aDelta.keyFields:=lDataset.GetKeyFields;
+  aDelta.loggedFields:=lDataset.GetLoggedFields;
+  excludedFields:=lDataset.GetExcludedFields;
+  TJSArray(aDeltas.deltas).Push(aDelta);
+  For I:=0 to aBatch.List.Count-1 do
+    begin
+    aDesc:=aBatch.List[i];
+    aChange:=TDaChange.New;
+    aChange.Status:='pending';
+    if aDesc.Status=usInserted then
+      aChange.old:=TJSValueDynArray(TJSArray(TDADataRow(aDesc.Data)._new).Slice())
+    else
+      aChange.old:=TJSValueDynArray(TJSArray(TDADataRow(aDesc.Data)._old).Slice());
+    aChange.new_:=TJSValueDynArray(TJSArray(TDADataRow(aDesc.Data)._new).Slice());
+    excludeItems(ExcludedFields,aChange.new_);
+    excludeItems(ExcludedFields,aChange.old);
+    aChange.changeType:=ChangeTypes[aDesc.Status];
+    aChange.recid:=I;
+    TJSArray(aDelta.data).push(aChange);
+    end;
+  DStr:=Connection.CreateStreamer;
+  DStr.initializeWrite;
+  DStr.writeDelta(aDeltas);
+  DStr.finalizeWrite;
+  S:=DStr.Stream;
+  DS:=Connection.EnsureDataservice;
+  DS.UpdateData(S,@UpdateSuccess,@UpdateFailure);
+  Result:=True;
 end;
 
 { TDADataRequest }
 
-procedure TDADataRequest.DoFail(response: TROMessage; fail: TjsError);
+procedure TDADataRequest.DoFail(response: TROMessage; fail: TJSError);
 
 Var
   O : TJSOBject;
@@ -805,11 +1142,14 @@ begin
   DoAfterRequest;
 end;
 
+
+
 procedure TDADataRequest.doSuccess(res: JSValue);
 
 Var
   S : String;
-  Rows : TJSArray;
+  Rows : TDADataRowArray;
+  aRow : TDADataRow;
   DADS : TDADataset;
   DStr : TDADataStreamer;
   DT : TDADatatable;
@@ -820,25 +1160,25 @@ begin
   DADS:=Dataset as TDADataset;
   if not Assigned(DADS.DAConnection) then
     Raise EDADataset.Create(DADS.Name+': Cannot process response, connection not available');
-  S:=String(Res);
-  if (DADS.DAConnection.EnsureMessageType=mtJSON) then
-    S:=TROUtil.Frombase64(S);
-  Case DADS.DAConnection.StreamerType of
-    stJSON : DStr:=TDAJSONDataStreamer.new;
-    stBIN: DStr:=TDABIN2DataStreamer.new;
-  end;
+  DStr:=DADS.DAConnection.CreateStreamer;
+  S:=DADS.DAConnection.InterpretMessage(Res);
   DStr.Stream:=S;
   DStr.initializeRead;
   DT:=TDADataTable.New;
   DT.name:=DADS.TableName;
   DStr.ReadDataset(DT);
   // Writeln('Row count : ',Length(DT.rows));
-  Rows:=TJSArray.New(Length(DT.rows));
+  SetLength(Rows,Length(DT.rows));
   for I:=0 to length(DT.rows)-1 do
-     Rows[i]:=DT.Rows[i].__newValues;
+      begin
+      aRow:=TDADataRow.New;
+      aRow._new:=TJSValueDynArray(DT.Rows[i].__newValues);
+      aRow._old:=[];
+      Rows[i]:=aRow;
+      end;
   (Dataset as TDADataset).Metadata:=New(['fields',TJSArray(DT.Fields)]);
   // Data:=aJSON['data'];
-  (Dataset as TDADataset).Rows:=Rows;
+  (Dataset as TDADataset).Rows:=TJSArray(Rows);
   Success:=rrOK;
   DoAfterRequest;
 end;