mORMot and Open Source friends
Check-in [6064a072aa]
Not logged in

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:{2804} BREAKING CHANGE: renamed all HTML_* constants as HTTP_* - since those return codes are HTTP-related, not HTML-related
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1: 6064a072aad7e71b10ba72967c057f1b87faeca4
User & Date: ab 2016-07-27 14:48:22
Context
2016-07-28
15:28
{2805} new SynLZDecompress() function with no memory allocation is data was stored and not SynLZ-compressed check-in: 2b5e049784 user: ab tags: trunk
2016-07-27
14:48
{2804} BREAKING CHANGE: renamed all HTML_* constants as HTTP_* - since those return codes are HTTP-related, not HTML-related check-in: 6064a072aa user: ab tags: trunk
14:29
{2803} supports redirection with 302 and 303 codes for MVC apps check-in: 047a6bf08b user: ab tags: trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Changes to CrossPlatform/SynCrossPlatformREST.pas.

840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
....
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
....
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
....
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
....
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
....
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
....
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
....
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
....
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
....
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
2901
2902
2903
2904
....
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
2919
2920
2921
2922
....
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
....
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
....
2983
2984
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
....
3003
3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
3016
3017
....
3035
3036
3037
3038
3039
3040
3041
3042
3043
3044
3045
3046
3047
3048
3049
....
3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
....
3190
3191
3192
3193
3194
3195
3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
3214
3215
    // - returns the corresponding index in the current BATCH sequence, -1 on error
    function BatchDelete(Value: TSQLRecord): integer; overload;
    /// retrieve the current number of pending transactions in the BATCH sequence
    // - every call to BatchAdd/Update/Delete methods increases this count
    function BatchCount: integer;
    /// execute a BATCH sequence started by BatchStart() method
    // - send all pending BatchAdd/Update/Delete statements to the remote server
    // - will return the URI Status value, i.e. 200/HTML_SUCCESS OK on success
    // - a dynamic array of 64 bit integers will be created in Results,
    // containing all ROWDID created for each BatchAdd call, or 200
    // (=HTML_SUCCESS) for all successfull BatchUpdate/BatchDelete, or 0 on error
    // - any error during server-side process MUST be checked against Results[]
    // (the main URI Status is 200 if about communication success, and won't
    // imply that all statements in the BATCH sequence were successfull
    function BatchSend(var Results: TIDDynArray): integer;
    /// abort a BATCH sequence started by BatchStart() method
    // - in short, nothing is sent to the remote server, and sequence is voided
    procedure BatchAbort;
................................................................................
    result := 0 else
    result := fBatchCount;
end;

function TSQLRest.BatchSend(var Results: TIDDynArray): integer;
begin
  if (self=nil) or (fBatch='') then
    result := HTML_BADREQUEST else
  try
    if BatchCount>0 then begin
      fBatch[length(fBatch)] := ']';
      if fBatchTable<>nil then
        fBatch := fBatch+'}';
      result := ExecuteBatchSend(fBatchTable,fBatch,Results);
    end else
      result := HTML_SUCCESS; // nothing to send
  finally
    BatchAbort;
  end;
end;

procedure TSQLRest.BatchAbort;
begin
................................................................................
  const aServer: string; aPort: integer; aRoot: string);
var Call: TSQLRestURIParams;
    userAgent: string;
begin
  LogClose;
  fLogClient := TSQLRestClientHTTP.Create(aServer,aPort,TSQLModel.Create([],aRoot),true);
  fLogClient.CallBackGet('TimeStamp',[],Call,nil); // synchronous connection
  if Call.OutStatus=HTML_SUCCESS then begin
    fLogLevel := LogLevel;
    OnLog := LogToRemoteServerText;
    asm @userAgent = navigator.userAgent; end;
    Log(sllClient,'Remote Cross-Platform Client Connected from AJAX app '+userAgent);
  end else
    LogClose;
end;
................................................................................
  if self=nil then
    exit;
  Log(sllSQL,SQL);
  // strict HTTP does not allow any body content -> encode SQL at URL
  // so we expect reUrlEncodedSQL to be defined in AllowRemoteExecute
  Call.Init(Model.Root+UrlEncode(['sql',sql]),'GET','');
  URI(Call);
  if Call.OutStatus=HTML_SUCCESS then begin
    json := Call.OutBodyUtf8;
    result := TSQLTableJSON.Create(json);
    result.fInternalState := fInternalState;
  end else
    Log(sllError,'ExecuteList failed');
end;

................................................................................
begin
  tableIndex := Model.GetTableIndexExisting(Value.RecordClass);
  Call.Url := getURIID(tableIndex,aID);
  if ForUpdate then
     Call.Verb := 'LOCK' else
     Call.Verb := 'GET';
  URI(Call);
  result := Call.OutStatus=HTML_SUCCESS;
  if result then begin
    json := Call.OutBodyUtf8;
    Value.FromJSON(json);
    Value.fInternalState := fInternalState;
  end;
  Log(LOGLEVELDB[result],'%s.Retrieve(ID=%d) %s',[Model.Info[tableIndex].Name,aID,json]);
end;
................................................................................
      [Call.Verb,Call.UrlWithoutSignature,Call.OutStatus,fInternalState,
       length(Call.InBody),length(Call.OutBody)]);
end;

procedure TSQLRestClientURI.URI(var Call: TSQLRestURIParams);
var sign: string;
begin
  Call.OutStatus := HTML_UNAVAILABLE;
  if self=nil then
    exit;
  Call.UrlWithoutSignature := Call.Url;
  if (fAuthentication<>nil) and (fAuthentication.SessionID<>0) then begin
    if Pos('?',Call.Url)=0 then
      sign := '?session_signature=' else
      sign := '&session_signature=';
................................................................................
procedure TSQLRestClientURI.InternalServiceCheck(const aMethodName: string;
  const Call: TSQLRestURIParams);
begin
  {$ifdef ISSMS}
  if Assigned(Call.OnSuccess) then
    exit; // asynchronous call do not have a result yet
  {$endif}
  if Call.OutStatus<>HTML_SUCCESS then
    Log(sllError,'Service %s returned %s',[aMethodName,Call.OutBodyUtf8]) else
    Log(sllServiceReturn,'%s success',[aMethodName]);
end;

procedure TSQLRestClientURI.CallBackGet(const aMethodName: string;
  const aNameValueParameters: array of const; var Call: TSQLRestURIParams;
  aTable: TSQLRecordClass; aID: TID);
................................................................................
    i: integer;
begin
  start := Now;
  Log(sllServiceCall,'BATCH with %d rows',[fBatchCount]);
  Call.Init(getURICallBack('Batch',Table,0),'POST',Data);
  URI(Call);
  result := Call.OutStatus;
  if result<>HTML_SUCCESS then begin
    Log(sllError,'BATCH error');
    exit; // transmission or internal server error
  end;
  Log(sllServiceReturn,'BATCH success in %s',[FormatDateTime('nn:ss:zzz',Now-start)]);
  {$ifdef ISSMS}
  Results.Clear;
  if Call.OutBody='["OK"]' then begin
    for i := 0 to fBatchCount-1 do
      Results.Add(HTML_SUCCESS);
  end else begin
    doc := JSON.Parse(Call.OutBody);
    if (VariantType(doc)=jvArray) and (doc.length=fBatchCount) then
      for i := 0 to fBatchCount-1 do
        Results.Add(integer(doc[i]));
  end;
  {$else}
  SetLength(Results,fBatchCount);
  HttpBodyToText(Call.OutBody,jsonres);
  if jsonres='["OK"]' then begin
    for i := 0 to fBatchCount-1 do
      Results[i] := HTML_SUCCESS;
  end else begin
    doc.Init(jsonres);
    if (doc.Kind=jvArray) and (doc.Count=fBatchCount) then
      for i := 0 to fBatchCount-1 do
        Results[i] := {$ifdef FPC}Int64{$endif}(doc.Values[i]);
  end;
  {$endif}
................................................................................
{$ifndef ISSMS}
var doc: TJSONVariantData;
    jsonres: string;
{$endif}
begin
  VarClear(result);
  outID := 0;
  if aCall.OutStatus<>HTML_SUCCESS then
    exit;
  {$ifdef ISSMS}
  var doc := JSON.Parse(aCall.OutBody);
  if VarIsValidRef(doc.result) then
    result := doc.result;
  if VarIsValidRef(doc.id) then
    outID := doc.id;
................................................................................
end;

procedure TSQLRestClientURI.Connect(onSuccess, onError: TSQLRestEvent);
var Call: TSQLRestURIParams;
begin
  SetAsynch(Call,onSuccess,onError,
  lambda
    result := (Call.OutStatus=HTML_SUCCESS) and SetServerTimeStamp(Call.OutBody);
  end);
  CallBackGet('TimeStamp',[],Call,nil); // asynchronous call
end;

procedure TSQLRestClientURI.CallRemoteServiceASynch(aCaller: TServiceClientAbstract;
  const aMethodName: string; aExpectedOutputParamsCount: integer;
  const aInputParams: array of variant;
................................................................................
begin
  // ForceServiceResultAsJSONObject not implemented yet
  SetAsynch(Call,
    lambda
      if not assigned(onSuccess) then
        exit; // no result to handle
      if aReturnsCustomAnswer then begin
        if Call.OutStatus=HTML_SUCCESS then begin
          var result: TVariantDynArray;
          result.Add(Call.OutBody);
          onSuccess(result);
        end else
          if Assigned(onError) then
            onError(self);
        exit;
................................................................................
        end;
      end else
        if Assigned(onError) then
          onError(self);
    end,
    onError,
    lambda
      result := (Call.OutStatus=HTML_SUCCESS) and (Call.OutBody<>'');
    end);
  CallRemoteServiceInternal(Call,aCaller,aMethodName,JSON.Stringify(variant(aInputParams)));
end;

function TSQLRestClientURI.CallRemoteServiceSynch(aCaller: TServiceClientAbstract;
  const aMethodName: string; aExpectedOutputParamsCount: integer;
  const aInputParams: array of variant; aReturnsCustomAnswer: boolean): TVariantDynArray;
................................................................................
  raise EServiceException.CreateFmt('Error calling %s.%s - returned status %d',
    [aCaller.fServiceName,aMethodName,Call.OutStatus]);
end;
begin
  // ForceServiceResultAsJSONObject not implemented yet
  CallRemoteServiceInternal(Call,aCaller,aMethodName,JSON.Stringify(variant(aInputParams)));
  if aReturnsCustomAnswer then begin
    if Call.OutStatus<>HTML_SUCCESS then
      RaiseError;
    result.Add(Call.OutBody);
    exit;
  end;
  outResult := CallGetResult(Call,outID); // from {result:...,id:...}
  if not VarIsValidRef(outResult) then
    RaiseError;
................................................................................
{$else}

function TSQLRestClientURI.Connect: boolean;
var Call: TSQLRestURIParams;
begin
  Log(sllInfo,'Connect',self);
  CallBackGet('TimeStamp',[],Call,nil);
  result := Call.OutStatus=HTML_SUCCESS;
  if not result then
    exit;
  result := SetServerTimeStamp(Call.OutBodyUtf8);
end;

procedure TSQLRestClientURI.CallRemoteService(aCaller: TServiceClientAbstract;
   const aMethodName: string; aExpectedOutputParamsCount: integer;
................................................................................
    arr: PJSONVariantData;
    i,outID: integer;
begin
  params.Init;
  for i := 0 to high(aInputParams) do
    params.AddValue(aInputParams[i]);
  CallRemoteServiceInternal(Call,aCaller,aMethodName,params.ToJSON);
  if Call.OutStatus<>HTML_SUCCESS then
    raise EServiceException.CreateFmt('Error calling %s.%s - returned status %d',
      [aCaller.fServiceName,aMethodName,Call.OutStatus]);
  if aReturnsCustomAnswer then begin
    SetLength(res,1);
    res[0] := HttpBodyToVariant(Call.OutBody);
    exit;
  end;
................................................................................
var Call: TSQLRestURIParams;
    location: string;
    i: integer;
begin
  result := 0;
  Call.Init(getURIID(tableIndex,0),'POST',json);
  URI(Call);
  if Call.OutStatus<>HTML_CREATED then begin
    Log(sllError,'Error creating %s with %s',[Model.Info[tableIndex].Name,json]);
    exit;
  end;
  location := GetOutHeader(Call,'location');
  for i := length(location) downto 1 do
    if not (ord(location[i]) in [ord('0')..ord('9')]) then begin
      result := StrToInt64Def(Copy(location,i+1,length(location)),0);
................................................................................
begin
  result := false;
  if ID<=0 then
    exit;
  tableIndex := Model.GetTableIndexExisting(Table);
  Call.Init(getURIID(tableIndex,ID),'DELETE','');
  URI(Call);
  if Call.OutStatus=HTML_SUCCESS then
    result := true;
  Log(LOGLEVELDB[result],'Delete %s.ID=%d',[Model.Info[tableIndex].Name,ID]);
end;

function TSQLRestClientURI.ExecuteUpdate(tableIndex: integer; ID: TID;
  const json: string): boolean; 
var Call: TSQLRestURIParams;
begin
  Call.Init(getURIID(tableIndex,ID),'PUT',json);
  URI(Call);
  result := Call.OutStatus=HTML_SUCCESS;
  Log(LOGLEVELDB[result],'Update %s.ID=%d with %s',[Model.Info[tableIndex].Name,ID,json]);
end;

function TSQLRestClientURI.SetUser(aAuthenticationClass: TSQLRestServerAuthenticationClass;
  const aUserName, aPassword: string; aHashedPassword: Boolean): boolean;
var aKey, aSessionID: string;
    i: integer;
................................................................................
        on E: Exception do begin
          Log(E);
          fConnection.Free;
          fConnection := nil;
        end;
      end;
    if fConnection=nil then begin
      Call.OutStatus := HTML_NOTIMPLEMENTED;
      break;
    end;
    try
      fConnection.URI(Call,inType,fKeepAlive);
      break; // do not retry on transmission success, or asynchronous request
    except
      on E: Exception do begin
        Log(E);
        fConnection.Free;
        fConnection := nil;
        Call.OutStatus := HTML_NOTIMPLEMENTED;
        if fForceTerminate then
          break;
      end; // will retry once (e.g. if connection broken)
    end;
  end;
end;







|


|







 







|







|







 







|







 







|







 







|







 







|







 







|







 







|








|











|







 







|







 







|







 







|







 







|







 







|







 







|







 







|







 







|







 







|










|







 







|










|







840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
....
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
....
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
....
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
....
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
....
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
....
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
....
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
....
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
....
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
2901
2902
2903
2904
....
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
2919
2920
2921
2922
....
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
....
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
....
2983
2984
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
....
3003
3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
3016
3017
....
3035
3036
3037
3038
3039
3040
3041
3042
3043
3044
3045
3046
3047
3048
3049
....
3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
....
3190
3191
3192
3193
3194
3195
3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
3214
3215
    // - returns the corresponding index in the current BATCH sequence, -1 on error
    function BatchDelete(Value: TSQLRecord): integer; overload;
    /// retrieve the current number of pending transactions in the BATCH sequence
    // - every call to BatchAdd/Update/Delete methods increases this count
    function BatchCount: integer;
    /// execute a BATCH sequence started by BatchStart() method
    // - send all pending BatchAdd/Update/Delete statements to the remote server
    // - will return the URI Status value, i.e. 200/HTTP_SUCCESS OK on success
    // - a dynamic array of 64 bit integers will be created in Results,
    // containing all ROWDID created for each BatchAdd call, or 200
    // (=HTTP_SUCCESS) for all successfull BatchUpdate/BatchDelete, or 0 on error
    // - any error during server-side process MUST be checked against Results[]
    // (the main URI Status is 200 if about communication success, and won't
    // imply that all statements in the BATCH sequence were successfull
    function BatchSend(var Results: TIDDynArray): integer;
    /// abort a BATCH sequence started by BatchStart() method
    // - in short, nothing is sent to the remote server, and sequence is voided
    procedure BatchAbort;
................................................................................
    result := 0 else
    result := fBatchCount;
end;

function TSQLRest.BatchSend(var Results: TIDDynArray): integer;
begin
  if (self=nil) or (fBatch='') then
    result := HTTP_BADREQUEST else
  try
    if BatchCount>0 then begin
      fBatch[length(fBatch)] := ']';
      if fBatchTable<>nil then
        fBatch := fBatch+'}';
      result := ExecuteBatchSend(fBatchTable,fBatch,Results);
    end else
      result := HTTP_SUCCESS; // nothing to send
  finally
    BatchAbort;
  end;
end;

procedure TSQLRest.BatchAbort;
begin
................................................................................
  const aServer: string; aPort: integer; aRoot: string);
var Call: TSQLRestURIParams;
    userAgent: string;
begin
  LogClose;
  fLogClient := TSQLRestClientHTTP.Create(aServer,aPort,TSQLModel.Create([],aRoot),true);
  fLogClient.CallBackGet('TimeStamp',[],Call,nil); // synchronous connection
  if Call.OutStatus=HTTP_SUCCESS then begin
    fLogLevel := LogLevel;
    OnLog := LogToRemoteServerText;
    asm @userAgent = navigator.userAgent; end;
    Log(sllClient,'Remote Cross-Platform Client Connected from AJAX app '+userAgent);
  end else
    LogClose;
end;
................................................................................
  if self=nil then
    exit;
  Log(sllSQL,SQL);
  // strict HTTP does not allow any body content -> encode SQL at URL
  // so we expect reUrlEncodedSQL to be defined in AllowRemoteExecute
  Call.Init(Model.Root+UrlEncode(['sql',sql]),'GET','');
  URI(Call);
  if Call.OutStatus=HTTP_SUCCESS then begin
    json := Call.OutBodyUtf8;
    result := TSQLTableJSON.Create(json);
    result.fInternalState := fInternalState;
  end else
    Log(sllError,'ExecuteList failed');
end;

................................................................................
begin
  tableIndex := Model.GetTableIndexExisting(Value.RecordClass);
  Call.Url := getURIID(tableIndex,aID);
  if ForUpdate then
     Call.Verb := 'LOCK' else
     Call.Verb := 'GET';
  URI(Call);
  result := Call.OutStatus=HTTP_SUCCESS;
  if result then begin
    json := Call.OutBodyUtf8;
    Value.FromJSON(json);
    Value.fInternalState := fInternalState;
  end;
  Log(LOGLEVELDB[result],'%s.Retrieve(ID=%d) %s',[Model.Info[tableIndex].Name,aID,json]);
end;
................................................................................
      [Call.Verb,Call.UrlWithoutSignature,Call.OutStatus,fInternalState,
       length(Call.InBody),length(Call.OutBody)]);
end;

procedure TSQLRestClientURI.URI(var Call: TSQLRestURIParams);
var sign: string;
begin
  Call.OutStatus := HTTP_UNAVAILABLE;
  if self=nil then
    exit;
  Call.UrlWithoutSignature := Call.Url;
  if (fAuthentication<>nil) and (fAuthentication.SessionID<>0) then begin
    if Pos('?',Call.Url)=0 then
      sign := '?session_signature=' else
      sign := '&session_signature=';
................................................................................
procedure TSQLRestClientURI.InternalServiceCheck(const aMethodName: string;
  const Call: TSQLRestURIParams);
begin
  {$ifdef ISSMS}
  if Assigned(Call.OnSuccess) then
    exit; // asynchronous call do not have a result yet
  {$endif}
  if Call.OutStatus<>HTTP_SUCCESS then
    Log(sllError,'Service %s returned %s',[aMethodName,Call.OutBodyUtf8]) else
    Log(sllServiceReturn,'%s success',[aMethodName]);
end;

procedure TSQLRestClientURI.CallBackGet(const aMethodName: string;
  const aNameValueParameters: array of const; var Call: TSQLRestURIParams;
  aTable: TSQLRecordClass; aID: TID);
................................................................................
    i: integer;
begin
  start := Now;
  Log(sllServiceCall,'BATCH with %d rows',[fBatchCount]);
  Call.Init(getURICallBack('Batch',Table,0),'POST',Data);
  URI(Call);
  result := Call.OutStatus;
  if result<>HTTP_SUCCESS then begin
    Log(sllError,'BATCH error');
    exit; // transmission or internal server error
  end;
  Log(sllServiceReturn,'BATCH success in %s',[FormatDateTime('nn:ss:zzz',Now-start)]);
  {$ifdef ISSMS}
  Results.Clear;
  if Call.OutBody='["OK"]' then begin
    for i := 0 to fBatchCount-1 do
      Results.Add(HTTP_SUCCESS);
  end else begin
    doc := JSON.Parse(Call.OutBody);
    if (VariantType(doc)=jvArray) and (doc.length=fBatchCount) then
      for i := 0 to fBatchCount-1 do
        Results.Add(integer(doc[i]));
  end;
  {$else}
  SetLength(Results,fBatchCount);
  HttpBodyToText(Call.OutBody,jsonres);
  if jsonres='["OK"]' then begin
    for i := 0 to fBatchCount-1 do
      Results[i] := HTTP_SUCCESS;
  end else begin
    doc.Init(jsonres);
    if (doc.Kind=jvArray) and (doc.Count=fBatchCount) then
      for i := 0 to fBatchCount-1 do
        Results[i] := {$ifdef FPC}Int64{$endif}(doc.Values[i]);
  end;
  {$endif}
................................................................................
{$ifndef ISSMS}
var doc: TJSONVariantData;
    jsonres: string;
{$endif}
begin
  VarClear(result);
  outID := 0;
  if aCall.OutStatus<>HTTP_SUCCESS then
    exit;
  {$ifdef ISSMS}
  var doc := JSON.Parse(aCall.OutBody);
  if VarIsValidRef(doc.result) then
    result := doc.result;
  if VarIsValidRef(doc.id) then
    outID := doc.id;
................................................................................
end;

procedure TSQLRestClientURI.Connect(onSuccess, onError: TSQLRestEvent);
var Call: TSQLRestURIParams;
begin
  SetAsynch(Call,onSuccess,onError,
  lambda
    result := (Call.OutStatus=HTTP_SUCCESS) and SetServerTimeStamp(Call.OutBody);
  end);
  CallBackGet('TimeStamp',[],Call,nil); // asynchronous call
end;

procedure TSQLRestClientURI.CallRemoteServiceASynch(aCaller: TServiceClientAbstract;
  const aMethodName: string; aExpectedOutputParamsCount: integer;
  const aInputParams: array of variant;
................................................................................
begin
  // ForceServiceResultAsJSONObject not implemented yet
  SetAsynch(Call,
    lambda
      if not assigned(onSuccess) then
        exit; // no result to handle
      if aReturnsCustomAnswer then begin
        if Call.OutStatus=HTTP_SUCCESS then begin
          var result: TVariantDynArray;
          result.Add(Call.OutBody);
          onSuccess(result);
        end else
          if Assigned(onError) then
            onError(self);
        exit;
................................................................................
        end;
      end else
        if Assigned(onError) then
          onError(self);
    end,
    onError,
    lambda
      result := (Call.OutStatus=HTTP_SUCCESS) and (Call.OutBody<>'');
    end);
  CallRemoteServiceInternal(Call,aCaller,aMethodName,JSON.Stringify(variant(aInputParams)));
end;

function TSQLRestClientURI.CallRemoteServiceSynch(aCaller: TServiceClientAbstract;
  const aMethodName: string; aExpectedOutputParamsCount: integer;
  const aInputParams: array of variant; aReturnsCustomAnswer: boolean): TVariantDynArray;
................................................................................
  raise EServiceException.CreateFmt('Error calling %s.%s - returned status %d',
    [aCaller.fServiceName,aMethodName,Call.OutStatus]);
end;
begin
  // ForceServiceResultAsJSONObject not implemented yet
  CallRemoteServiceInternal(Call,aCaller,aMethodName,JSON.Stringify(variant(aInputParams)));
  if aReturnsCustomAnswer then begin
    if Call.OutStatus<>HTTP_SUCCESS then
      RaiseError;
    result.Add(Call.OutBody);
    exit;
  end;
  outResult := CallGetResult(Call,outID); // from {result:...,id:...}
  if not VarIsValidRef(outResult) then
    RaiseError;
................................................................................
{$else}

function TSQLRestClientURI.Connect: boolean;
var Call: TSQLRestURIParams;
begin
  Log(sllInfo,'Connect',self);
  CallBackGet('TimeStamp',[],Call,nil);
  result := Call.OutStatus=HTTP_SUCCESS;
  if not result then
    exit;
  result := SetServerTimeStamp(Call.OutBodyUtf8);
end;

procedure TSQLRestClientURI.CallRemoteService(aCaller: TServiceClientAbstract;
   const aMethodName: string; aExpectedOutputParamsCount: integer;
................................................................................
    arr: PJSONVariantData;
    i,outID: integer;
begin
  params.Init;
  for i := 0 to high(aInputParams) do
    params.AddValue(aInputParams[i]);
  CallRemoteServiceInternal(Call,aCaller,aMethodName,params.ToJSON);
  if Call.OutStatus<>HTTP_SUCCESS then
    raise EServiceException.CreateFmt('Error calling %s.%s - returned status %d',
      [aCaller.fServiceName,aMethodName,Call.OutStatus]);
  if aReturnsCustomAnswer then begin
    SetLength(res,1);
    res[0] := HttpBodyToVariant(Call.OutBody);
    exit;
  end;
................................................................................
var Call: TSQLRestURIParams;
    location: string;
    i: integer;
begin
  result := 0;
  Call.Init(getURIID(tableIndex,0),'POST',json);
  URI(Call);
  if Call.OutStatus<>HTTP_CREATED then begin
    Log(sllError,'Error creating %s with %s',[Model.Info[tableIndex].Name,json]);
    exit;
  end;
  location := GetOutHeader(Call,'location');
  for i := length(location) downto 1 do
    if not (ord(location[i]) in [ord('0')..ord('9')]) then begin
      result := StrToInt64Def(Copy(location,i+1,length(location)),0);
................................................................................
begin
  result := false;
  if ID<=0 then
    exit;
  tableIndex := Model.GetTableIndexExisting(Table);
  Call.Init(getURIID(tableIndex,ID),'DELETE','');
  URI(Call);
  if Call.OutStatus=HTTP_SUCCESS then
    result := true;
  Log(LOGLEVELDB[result],'Delete %s.ID=%d',[Model.Info[tableIndex].Name,ID]);
end;

function TSQLRestClientURI.ExecuteUpdate(tableIndex: integer; ID: TID;
  const json: string): boolean; 
var Call: TSQLRestURIParams;
begin
  Call.Init(getURIID(tableIndex,ID),'PUT',json);
  URI(Call);
  result := Call.OutStatus=HTTP_SUCCESS;
  Log(LOGLEVELDB[result],'Update %s.ID=%d with %s',[Model.Info[tableIndex].Name,ID,json]);
end;

function TSQLRestClientURI.SetUser(aAuthenticationClass: TSQLRestServerAuthenticationClass;
  const aUserName, aPassword: string; aHashedPassword: Boolean): boolean;
var aKey, aSessionID: string;
    i: integer;
................................................................................
        on E: Exception do begin
          Log(E);
          fConnection.Free;
          fConnection := nil;
        end;
      end;
    if fConnection=nil then begin
      Call.OutStatus := HTTP_NOTIMPLEMENTED;
      break;
    end;
    try
      fConnection.URI(Call,inType,fKeepAlive);
      break; // do not retry on transmission success, or asynchronous request
    except
      on E: Exception do begin
        Log(E);
        fConnection.Free;
        fConnection := nil;
        Call.OutStatus := HTTP_NOTIMPLEMENTED;
        if fForceTerminate then
          break;
      end; // will retry once (e.g. if connection broken)
    end;
  end;
end;

Changes to CrossPlatform/SynCrossPlatformSpecific.pas.

260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
...
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663

const
  /// MIME content type used for JSON communication
  JSON_CONTENT_TYPE = 'application/json; charset=UTF-8';

  /// HTML Status Code for "Continue"
  HTML_CONTINUE = 100;
  /// HTML Status Code for "Switching Protocols"
  HTML_SWITCHINGPROTOCOLS = 101;
  /// HTML Status Code for "Success"
  HTML_SUCCESS = 200;
  /// HTML Status Code for "Created"
  HTML_CREATED = 201;
  /// HTML Status Code for "Accepted"
  HTML_ACCEPTED = 202;
  /// HTML Status Code for "Non-Authoritative Information"
  HTML_NONAUTHORIZEDINFO = 203;
  /// HTML Status Code for "No Content"
  HTML_NOCONTENT = 204;
  /// HTML Status Code for "Multiple Choices"
  HTML_MULTIPLECHOICES = 300;
  /// HTML Status Code for "Moved Permanently"
  HTML_MOVEDPERMANENTLY = 301;
  /// HTML Status Code for "Found"
  HTML_FOUND = 302;
  /// HTML Status Code for "See Other"
  HTML_SEEOTHER = 303;
  /// HTML Status Code for "Not Modified"
  HTML_NOTMODIFIED = 304;
  /// HTML Status Code for "Use Proxy"
  HTML_USEPROXY = 305;
  /// HTML Status Code for "Temporary Redirect"
  HTML_TEMPORARYREDIRECT = 307;
  /// HTML Status Code for "Bad Request"
  HTML_BADREQUEST = 400;
  /// HTML Status Code for "Unauthorized"
  HTML_UNAUTHORIZED = 401;
  /// HTML Status Code for "Forbidden"
  HTML_FORBIDDEN = 403;
  /// HTML Status Code for "Not Found"
  HTML_NOTFOUND = 404;
  // HTML Status Code for "Method Not Allowed"
  HTML_NOTALLOWED = 405;
  // HTML Status Code for "Not Acceptable"
  HTML_NOTACCEPTABLE = 406;
  // HTML Status Code for "Proxy Authentication Required"
  HTML_PROXYAUTHREQUIRED = 407;
  /// HTML Status Code for "Request Time-out"
  HTML_TIMEOUT = 408;
  /// HTML Status Code for "Internal Server Error"
  HTML_SERVERERROR = 500;
  /// HTML Status Code for "Not Implemented"
  HTML_NOTIMPLEMENTED = 501;
  /// HTML Status Code for "Bad Gateway"
  HTML_BADGATEWAY = 502;
  /// HTML Status Code for "Service Unavailable"
  HTML_UNAVAILABLE = 503;
  /// HTML Status Code for "Gateway Timeout"
  HTML_GATEWAYTIMEOUT = 504;
  /// HTML Status Code for "HTTP Version Not Supported"
  HTML_HTTPVERSIONNONSUPPORTED = 505;


/// gives access to the class type to implement a HTTP connection
// - will use WinHTTP API (from our SynCrtSock) under Windows
// - will use Indy for Delphi on other platforms
// - will use fcl-web (fphttpclient) with FreePascal
function HttpConnectionClass: TAbstractHttpConnectionClass;
................................................................................
    end;
    if Call.InBody<>nil then begin
      InStr.Write(Call.InBody[0],length(Call.InBody));
      InStr.Seek(0,soBeginning);
      fConnection.Request.Source := InStr;
    end;
    if Call.Verb='GET' then // allow 404 as valid Call.OutStatus
      fConnection.Get(fURL+Call.Url,OutStr,[HTML_SUCCESS,HTML_NOTFOUND]) else
    if Call.Verb='POST' then
      fConnection.Post(fURL+Call.Url,InStr,OutStr) else
    if Call.Verb='PUT' then
      fConnection.Put(fURL+Call.Url,InStr) else
    if Call.Verb='DELETE' then
      fConnection.Delete(fURL+Call.Url) else
      raise Exception.CreateFmt('Indy does not know method %s',[Call.Verb]);






|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|







 







|







260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
...
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663

const
  /// MIME content type used for JSON communication
  JSON_CONTENT_TYPE = 'application/json; charset=UTF-8';

  /// HTML Status Code for "Continue"
  HTTP_CONTINUE = 100;
  /// HTML Status Code for "Switching Protocols"
  HTTP_SWITCHINGPROTOCOLS = 101;
  /// HTML Status Code for "Success"
  HTTP_SUCCESS = 200;
  /// HTML Status Code for "Created"
  HTTP_CREATED = 201;
  /// HTML Status Code for "Accepted"
  HTTP_ACCEPTED = 202;
  /// HTML Status Code for "Non-Authoritative Information"
  HTTP_NONAUTHORIZEDINFO = 203;
  /// HTML Status Code for "No Content"
  HTTP_NOCONTENT = 204;
  /// HTML Status Code for "Multiple Choices"
  HTTP_MULTIPLECHOICES = 300;
  /// HTML Status Code for "Moved Permanently"
  HTTP_MOVEDPERMANENTLY = 301;
  /// HTML Status Code for "Found"
  HTTP_FOUND = 302;
  /// HTML Status Code for "See Other"
  HTTP_SEEOTHER = 303;
  /// HTML Status Code for "Not Modified"
  HTTP_NOTMODIFIED = 304;
  /// HTML Status Code for "Use Proxy"
  HTTP_USEPROXY = 305;
  /// HTML Status Code for "Temporary Redirect"
  HTTP_TEMPORARYREDIRECT = 307;
  /// HTML Status Code for "Bad Request"
  HTTP_BADREQUEST = 400;
  /// HTML Status Code for "Unauthorized"
  HTTP_UNAUTHORIZED = 401;
  /// HTML Status Code for "Forbidden"
  HTTP_FORBIDDEN = 403;
  /// HTML Status Code for "Not Found"
  HTTP_NOTFOUND = 404;
  // HTML Status Code for "Method Not Allowed"
  HTTP_NOTALLOWED = 405;
  // HTML Status Code for "Not Acceptable"
  HTTP_NOTACCEPTABLE = 406;
  // HTML Status Code for "Proxy Authentication Required"
  HTTP_PROXYAUTHREQUIRED = 407;
  /// HTML Status Code for "Request Time-out"
  HTTP_TIMEOUT = 408;
  /// HTML Status Code for "Internal Server Error"
  HTTP_SERVERERROR = 500;
  /// HTML Status Code for "Not Implemented"
  HTTP_NOTIMPLEMENTED = 501;
  /// HTML Status Code for "Bad Gateway"
  HTTP_BADGATEWAY = 502;
  /// HTML Status Code for "Service Unavailable"
  HTTP_UNAVAILABLE = 503;
  /// HTML Status Code for "Gateway Timeout"
  HTTP_GATEWAYTIMEOUT = 504;
  /// HTML Status Code for "HTTP Version Not Supported"
  HTTP_HTTPVERSIONNONSUPPORTED = 505;


/// gives access to the class type to implement a HTTP connection
// - will use WinHTTP API (from our SynCrtSock) under Windows
// - will use Indy for Delphi on other platforms
// - will use fcl-web (fphttpclient) with FreePascal
function HttpConnectionClass: TAbstractHttpConnectionClass;
................................................................................
    end;
    if Call.InBody<>nil then begin
      InStr.Write(Call.InBody[0],length(Call.InBody));
      InStr.Seek(0,soBeginning);
      fConnection.Request.Source := InStr;
    end;
    if Call.Verb='GET' then // allow 404 as valid Call.OutStatus
      fConnection.Get(fURL+Call.Url,OutStr,[HTTP_SUCCESS,HTTP_NOTFOUND]) else
    if Call.Verb='POST' then
      fConnection.Post(fURL+Call.Url,InStr,OutStr) else
    if Call.Verb='PUT' then
      fConnection.Put(fURL+Call.Url,InStr) else
    if Call.Verb='DELETE' then
      fConnection.Delete(fURL+Call.Url) else
      raise Exception.CreateFmt('Indy does not know method %s',[Call.Verb]);

Changes to CrossPlatform/SynCrossPlatformTests.pas.

540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
...
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
...
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
...
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
procedure TSynCrossPlatformClient.ORM;
var people: TSQLRecordPeople;
    Call: TSQLRestURIParams;
    i,id: integer;
begin
  fClient.CallBackGet('DropTable',[],Call,TSQLRecordPeople);
  Check(fClient.InternalState>0);
  Check(Call.OutStatus=HTML_SUCCESS);
  people := TSQLRecordPeople.Create;
  try
    Check(people.InternalState=0);
    for i := 1 to 200 do begin
      people.FirstName := 'First'+IntToStr(i);
      people.LastName := 'Last'+IntToStr(i);
      people.YearOfBirth := i+1800;
................................................................................
var people: TSQLRecordPeople;
    Call: TSQLRestURIParams;
    res: TIDDynArray;
    i,id: integer;
begin
  fClient.CallBackGet('DropTable',[],Call,TSQLRecordPeople);
  Check(fClient.InternalState>0);
  Check(Call.OutStatus=HTML_SUCCESS);
  fClient.BatchStart(TSQLRecordPeople);
  people := TSQLRecordPeople.Create;
  try
    for i := 1 to 200 do begin
      Check(people.InternalState=0);
      people.FirstName := 'First'+IntToStr(i);
      people.LastName := 'Last'+IntToStr(i);
................................................................................
      people.YearOfDeath := i+1825;
      people.Sexe := TPeopleSexe(i and 1);
      fClient.BatchAdd(people,true);
    end;
  finally
    people.Free;
  end;
  Check(fClient.BatchSend(res)=HTML_SUCCESS);
  Check(length(res)=200);
  for i := 1 to length(res) do
    Check(res[i-1]=i);
  people := TSQLRecordPeople.CreateAndFillPrepare(fClient,'','',[]);
  try
    Check(people.InternalState=0);
    id := 0;
................................................................................
        people.YearOfDeath := id+1825;
        Check(fClient.BatchUpdate(people,'YEarOFBIRTH,YEarOfDeath')>=0);
        Check(people.InternalState=0);
      finally
        people.Free;
      end;
    end;
  Check(fClient.BatchSend(res)=HTML_SUCCESS);
  Check(length(res)=14);
  for i := 1 to 14 do
    Check(res[i-1]=HTML_SUCCESS);
  for i := 1 to 200 do begin
    people := TSQLRecordPeople.Create(fClient,i);
    try
      if i and 15=0 then
        Check(people.ID=0) else begin
        Check(people.InternalState=fClient.InternalState);
        if i mod 82=0 then






|







 







|







 







|







 







|


|







540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
...
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
...
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
...
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
procedure TSynCrossPlatformClient.ORM;
var people: TSQLRecordPeople;
    Call: TSQLRestURIParams;
    i,id: integer;
begin
  fClient.CallBackGet('DropTable',[],Call,TSQLRecordPeople);
  Check(fClient.InternalState>0);
  Check(Call.OutStatus=HTTP_SUCCESS);
  people := TSQLRecordPeople.Create;
  try
    Check(people.InternalState=0);
    for i := 1 to 200 do begin
      people.FirstName := 'First'+IntToStr(i);
      people.LastName := 'Last'+IntToStr(i);
      people.YearOfBirth := i+1800;
................................................................................
var people: TSQLRecordPeople;
    Call: TSQLRestURIParams;
    res: TIDDynArray;
    i,id: integer;
begin
  fClient.CallBackGet('DropTable',[],Call,TSQLRecordPeople);
  Check(fClient.InternalState>0);
  Check(Call.OutStatus=HTTP_SUCCESS);
  fClient.BatchStart(TSQLRecordPeople);
  people := TSQLRecordPeople.Create;
  try
    for i := 1 to 200 do begin
      Check(people.InternalState=0);
      people.FirstName := 'First'+IntToStr(i);
      people.LastName := 'Last'+IntToStr(i);
................................................................................
      people.YearOfDeath := i+1825;
      people.Sexe := TPeopleSexe(i and 1);
      fClient.BatchAdd(people,true);
    end;
  finally
    people.Free;
  end;
  Check(fClient.BatchSend(res)=HTTP_SUCCESS);
  Check(length(res)=200);
  for i := 1 to length(res) do
    Check(res[i-1]=i);
  people := TSQLRecordPeople.CreateAndFillPrepare(fClient,'','',[]);
  try
    Check(people.InternalState=0);
    id := 0;
................................................................................
        people.YearOfDeath := id+1825;
        Check(fClient.BatchUpdate(people,'YEarOFBIRTH,YEarOfDeath')>=0);
        Check(people.InternalState=0);
      finally
        people.Free;
      end;
    end;
  Check(fClient.BatchSend(res)=HTTP_SUCCESS);
  Check(length(res)=14);
  for i := 1 to 14 do
    Check(res[i-1]=HTTP_SUCCESS);
  for i := 1 to 200 do begin
    people := TSQLRecordPeople.Create(fClient,i);
    try
      if i and 15=0 then
        Check(people.ID=0) else begin
        Check(people.InternalState=fClient.InternalState);
        if i mod 82=0 then

Changes to SQLite3/DDD/infra/dddInfraEmailer.pas.

742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
      Check(not service.IsEmailValidated('toto','toto@toto.com'),'no click yet');
      call.Url := service.ComputeURIForReply('titi','toto@toto.com');
      Check(IdemPChar(pointer(call.Url),'HTTP://VALIDATIONSERVER/ROOT/VALID/'));
      delete(call.Url,1,24);
      Check(IdemPChar(pointer(call.Url),'ROOT/VALID/'),'deleted host in URI');
      call.Method := 'GET';
      Rest.URI(call);
      Check(call.OutStatus=HTML_BADREQUEST,'wrong link');
      call.Url := service.ComputeURIForReply('toto','toto@toto.com');
      delete(call.Url,1,24);
      call.Method := 'GET';
      Rest.URI(call);
      Check(call.OutStatus=HTML_TEMPORARYREDIRECT,'emulated click on link');
      Check(call.OutHead='Location: http://officialwebsite/success&logon=toto');
      Check(service.IsEmailValidated('toto','toto@toto.com'),'after click');
      Check(daemon.Stop(info)=cqrsSuccess);
      Check(service.StartEmailValidation(template,'toto','toto@toto.com')=cqrsSuccess);
      info := service.LastErrorInfo;
      Check(VariantToUTF8(info)='{"Msg":"Already validated"}');
      Check(service.StartEmailValidation(template,'toto','toto2@toto.com')=cqrsSuccess);






|




|







742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
      Check(not service.IsEmailValidated('toto','toto@toto.com'),'no click yet');
      call.Url := service.ComputeURIForReply('titi','toto@toto.com');
      Check(IdemPChar(pointer(call.Url),'HTTP://VALIDATIONSERVER/ROOT/VALID/'));
      delete(call.Url,1,24);
      Check(IdemPChar(pointer(call.Url),'ROOT/VALID/'),'deleted host in URI');
      call.Method := 'GET';
      Rest.URI(call);
      Check(call.OutStatus=HTTP_BADREQUEST,'wrong link');
      call.Url := service.ComputeURIForReply('toto','toto@toto.com');
      delete(call.Url,1,24);
      call.Method := 'GET';
      Rest.URI(call);
      Check(call.OutStatus=HTTP_TEMPORARYREDIRECT,'emulated click on link');
      Check(call.OutHead='Location: http://officialwebsite/success&logon=toto');
      Check(service.IsEmailValidated('toto','toto@toto.com'),'after click');
      Check(daemon.Stop(info)=cqrsSuccess);
      Check(service.StartEmailValidation(template,'toto','toto@toto.com')=cqrsSuccess);
      info := service.LastErrorInfo;
      Check(VariantToUTF8(info)='{"Msg":"Already validated"}');
      Check(service.StartEmailValidation(template,'toto','toto2@toto.com')=cqrsSuccess);

Changes to SQLite3/Documentation/Synopse SQLite3 Framework.pro.

5922
5923
5924
5925
5926
5927
5928
5929
5930
5931
5932
5933
5934
5935
5936
....
6608
6609
6610
6611
6612
6613
6614
6615
6616
6617
6618
6619
6620
6621
6622
6623
6624
6625
6626
6627
6628
....
6853
6854
6855
6856
6857
6858
6859
6860
6861
6862
6863
6864
6865
6866
6867
....
8082
8083
8084
8085
8086
8087
8088
8089
8090
8091
8092
8093
8094
8095
8096
....
8616
8617
8618
8619
8620
8621
8622
8623
8624
8625
8626
8627
8628
8629
8630
8631
8632
8633
8634
8635
8636
8637
8638
8639
8640
8641
8642
8643
8644
8645
8646
8647
8648
....
8672
8673
8674
8675
8676
8677
8678
8679
8680
8681
8682
8683
8684
8685
8686
8687
8688
8689
8690
8691
8692
8693
8694
8695
8696
8697
8698
8699
8700
8701
8702
8703
8704
8705
8706
8707
.....
11352
11353
11354
11355
11356
11357
11358
11359
11360
11361
11362
11363
11364
11365
11366
.....
11763
11764
11765
11766
11767
11768
11769
11770
11771
11772
11773
11774
11775
11776
11777
.....
11845
11846
11847
11848
11849
11850
11851
11852
11853
11854
11855
11856
11857
11858
11859
.....
12672
12673
12674
12675
12676
12677
12678
12679
12680
12681
12682
12683
12684
12685
12686
.....
12786
12787
12788
12789
12790
12791
12792
12793
12794
12795
12796
12797
12798
12799
12800
.....
12810
12811
12812
12813
12814
12815
12816
12817
12818
12819
12820
12821
12822
12823
12824
.....
12949
12950
12951
12952
12953
12954
12955
12956
12957
12958
12959
12960
12961
12962
12963
.....
13546
13547
13548
13549
13550
13551
13552
13553
13554
13555
13556
13557
13558
13559
13560
!  aID := aExternalClient.Add(RExt,true);
!  (...)
!  aExternalClient.Retrieve(aID,RExt);
!  (...)
!  aExternalClient.BatchStart(TSQLRecordPeopleExt);
!  aExternalClient.BatchAdd(RExt,true);
!  (...)
!  Check(aExternalClient.BatchSend(BatchID)=HTML_SUCCESS);
!  Check(aExternalClient.TableHasRows(TSQLRecordPeopleExt));
!  Check(aExternalClient.TableRowCount(TSQLRecordPeopleExt)=n);
!  (...)
!  RExt.FillPrepare(aExternalClient,'FirstName=? and LastName=?',
!    [RInt.FirstName,RInt.LastName]); // query will use index -> fast :)
!  while RExt.FillOne do ...
!  (...)
................................................................................
!      R.Ints := nil;
!      R.DynArray(1).Add(i);
!      assert(Client.BatchAdd(R,True)>=0);
!    end;
!  finally
!    R.Free;
!  end;
!  assert(Client.BatchSend(IDs)=HTML_SUCCESS);
Or for deletion:
!  Client.BatchStart(TSQLORM);
!  for i := 5 to COLL_COUNT do
!    if i mod 5=0 then
!      assert(fClient.BatchDelete(i)>=0);
!  assert(Client.BatchSend(IDs)=HTML_SUCCESS);
Speed benefit may be huge in regard to individual Add/Delete operations, even on a local {\i MongoDB} server. We will see some benchmark numbers now.
:  ORM/ODM performance
You can take a look at @59@ to compare {\i MongoDB} as back-end for our ORM classes.
In respect to external @*SQL@ engines, it features very high speed, low CPU use, and almost no difference in use. We interfaced the {\f1\fs20 BatchAdd()} and {\f1\fs20 BatchDelete()} methods to benefit of {\i MongoDB} BULK process, and avoided most memory allocation during the process.
Here are some numbers, extracted from the {\f1\fs20 MongoDBTests.dpr} sample, which reflects the performance of our ORM/ODM, depending on the {\i Write Concern} mode used:
$2. ORM
$
................................................................................
!var git: TTestCustomJSONGitHubs;
! ...
!  U := zendframeworkJson;
!!  Check(DynArrayLoadJSON(git,@U[1],TypeInfo(TTestCustomJSONGitHubs))<>nil);
!!  U := DynArraySaveJSON(git,TypeInfo(TTestCustomJSONGitHubs));
You can see that the {\f1\fs20 record} serialization is auto-magically available at dynamic array level, which is pretty convenient in our case, since the {\f1\fs20 api.github.com} RESTful service returns a JSON array.
It will convert 160 KB of very verbose JSON information:
$[{"id":8079771,"name":"Component_ZendAuthentication","full_name":"zendframework/Component_ZendAuthentication","owner":{"login":"zendframework","id":296074,"avatar_url":"https://1.gravatar.com/avatar/460576a0866d93fdacb597da4b90f233?d=https%3A%2F%2Fidenticons.github.com%2F292b7433472e2946c926bdca195cec8c.png&r=x","gravatar_id":"460576a0866d93fdacb597da4b90f233","url":"https://api.github.com/users/zendframework","html_url":"https://github.com/zendframework","followers_url":"https://api.github.com/users/zendframework/followers","following_url":"https://api.github.com/users/zendframework/following{/other_user}","gists_url":"https://api.github.com/users/zendframework/gists{/gist_id}","starred_url":"https://api.github.com/users/zendframework/starred{/owner}{/repo}",...
Into the much smaller (6 KB) and readable JSON content, containing only the information we need:
$[
$ {
$  "name": "Component_ZendAuthentication",
$  "id": 8079771,
$  "description": "Authentication component from Zend Framework 2",
$  "fork": true,
................................................................................
!  Batch := TSQLRestBatch.Create(Server,TSQLRecordTest,30);
!  try
!    for i := 10000 to 10099 do begin
!      R.Int := i;
!      R.Test := Int32ToUTF8(i);
!      Check(Batch.Add(R,true)=i-10000);
!    end;
!    Check(Server.BatchSend(Batch,IDs)=HTML_SUCCESS);
!  finally
!    Batch.Free;
!  end;
The ability to handle several {\f1\fs20 TSQLRestBatch} classes in the same time will allow to implement the {\i @**Unit Of Work@} pattern. It can be used to maintain a list of objects affected by a business transaction and coordinates the writing out of changes and the resolution of concurrency problems, especially in a complex @*SOA@ application with a huge number of connected clients.
In a way, you can think of the {\i Unit of Work} as a place to dump all transaction-handling code.\line The responsibilities of the {\i Unit of Work} are to:
- Manage transactions;
- Order the database inserts, deletes, and updates;
................................................................................
$ {"Result":["One","two"]}
\page
: Returns non-JSON content
Using {\f1\fs20 Ctxt.Returns()} will let the method return the content in any format, e.g. as a JSON object (via the overloaded {\f1\fs20 Ctxt.Returns([])} method expecting field name/value pairs), or any content, since the returned @**MIME@-type can be defined as a parameter to {\f1\fs20 Ctxt.Returns()} - it may be useful to specify another mime-type than the default constant {\f1\fs20 JSON_CONTENT_TYPE}, i.e. {\f1\fs20 'application/json; charset=UTF-8'}, and returns plain text, HTML or binary.
For instance, you can return directly a value as plain text:
!procedure TSQLRestServer.TimeStamp(Ctxt: TSQLRestServerURIContext);
!begin
!  Ctxt.Returns(Int64ToUtf8(ServerTimeStamp),HTML_SUCCESS,TEXT_CONTENT_TYPE_HEADER);
!end;
Or you can return some binary file, retrieving the corresponding MIME type from its binary content:
!procedure TSQLRestServer.GetFile(Ctxt: TSQLRestServerURIContext);
!var fileName: TFileName;
!    content: RawByteString;
!    contentType: RawUTF8;
!begin
!  fileName :=  'c:\data\'+ExtractFileName(Ctxt['filename']); // or Ctxt.Input['filename']
!  content := StringFromFile(fileName);
!  if content='' then
!    Ctxt.Error('',HTML_NOTFOUND) else
!    Ctxt.Returns(content,HTML_SUCCESS,HEADER_CONTENT_TYPE+
!         GetMimeContentType(pointer(content),Length(content),fileName));
!end;
The corresponding client method may be defined as such:
!function TMyClient.GetFile(const aFileName: RawUTF8): RawByteString;
!begin
!  if CallBackGet('GetFile',['filename',aFileName],RawUTF8(result))<>HTML_SUCCESS then
!    raise Exception.CreateFmt('Impossible to get file: %s',[result]);
!end;
Note that the {\f1\fs20 Ctxt.ReturnFile()} method - see @95@ - is preferred than manual file retrieval as implemented in this {\f1\fs20 TSQLRestServer.GetFile()} method. It is shown here for demonstration purposes only.
If you use HTTP as communication protocol, you can consume these services, implemented Server-Side in fast {\i Delphi} code, with any @*AJAX@ application on the client side.
Using {\f1\fs20 GetMimeContentType()} when sending non JSON content (e.g. picture, pdf file, binary...) will be interpreted as expected by any standard Internet browser: it could be used to serve some good old HTML content within a page, not necessary consume the service via {\i JavaScript} .
\page
: Advanced process on server side
................................................................................
When used over a slow network (e.g. over the Internet), you can set the optional {\f1\fs20 Handle304NotModified} parameter of both {\f1\fs20 Ctxt.Returns()} and {\f1\fs20 Ctxt.Results()} methods to return the response body only if it has changed since last time.
In practice, result content will be hashed (using {\f1\fs20 crc32c} algorithm, and fast SSE 4.2 hardware instruction, if available) and in case of no modification will return "{\i 304 Not Modified}" status to the browser, without the actual result content. Therefore, the response will be transmitted and received much faster, and will save a lot of bandwidth, especially in case of periodic server pooling (e.g. for client screen refresh).
Note that in case of hash collision of the {\f1\fs20 crc32c} algorithm (we never did see it happen, but such a mathematical possibility exists), a false positive "not modified" status may be returned; this option is therefore unset by default, and should be enabled only if your client does not handle any sensitive accounting process, for instance.
Be aware that you should {\i disable authentication} for the methods using this {\f1\fs20 Handle304NotModified} parameter, via a {\f1\fs20 TSQLRestServer.ServiceMethodByPassAuthentication()} call. In fact, our @*REST@ful authentication - see @18@ - uses a per-URI signature, which change very often (to avoid men-in-the-middle attacks). Therefore, any browser-side caching benefit will be voided if authentication is used: browser internal cache will tend to grow for nothing since the previous URIs are deprecated, and it will be a cache-miss most of the time. But when serving some static content (e.g. HTML content, fixed JSON values or even UI binaries), this browser-side caching can be very useful.
This @*stateless@ @9@ model will enable several levels of caching, even using an external {\i Content Delivery Network} (@*CDN@) service. See @97@ for some potential hosting architectures, which may let your {\i mORMot} server scale to thousands of concurrent users, served around the world with the best responsiveness.
:95 Returning file content
Framework's HTTP server is able to handle returning a file as response to a method-based service.\line The @88@ is even able to serve the file content asynchronously from kernel mode, with outstanding performance.
You can use the {\f1\fs20 Ctxt.ReturnFile()} method to return a file directly.\line This method is also able to guess the MIME type from the file extension, and handle {\f1\fs20 HTML_NOTMODIFIED = 304} process, if {\f1\fs20 Handle304NotModified} parameter is {\f1\fs20 true}, using the file time stamp.
Another possibility may be to use the {\f1\fs20 Ctxt.ReturnFileFromFolder()} method, which is able to efficiently return any file specified by its URI, from a local folder. It may be very handy to
return some static web content from a {\i mORMot} HTTP server.
\page
: Handling errors
When using {\f1\fs20 Ctxt.Input*[]} properties, any missing parameter will raise an {\f1\fs20 EParsingException}. It will therefore be intercepted by the server process (as any other exception), and returned to the client with an error message containing the {\f1\fs20 Exception} class name and its associated message.
But you can have full access to the error workflow, if needed. In fact, calling either {\f1\fs20 Ctxt.Results()}, {\f1\fs20 Ctxt.Returns()}, {\f1\fs20 Ctxt.Success()} or {\f1\fs20 Ctxt.Error()} will specify the HTTP status code (e.g. 200 / "OK" for {\f1\fs20 Results()} and {\f1\fs20 Success()} methods by default, or 400 / "Bad Request" for {\f1\fs20 Error()}) as an {\f1\fs20 integer} value. For instance, here is how a service not returning any content can handle those status/error codes:
!procedure TSQLRestServer.Batch(Ctxt: TSQLRestServerURIContext);
!begin
!  if (Ctxt.Method=mPUT) and RunBatch(nil,nil,Ctxt) then
!    Ctxt.Success else
!    Ctxt.Error;
!end;
In case of an error on the server side, you may call {\f1\fs20 Ctxt.Error()} method (only the two valid status codes are {\f1\fs20 200} and {\f1\fs20 201}).
The {\f1\fs20 Ctxt.Error()} method has an optional parameter to specify a custom error message in plain English, which will be returned to the client in case of an invalid status code. If no custom text is specified, the framework will return the corresponding generic HTTP status text (e.g. {\f1\fs20 "Bad Request"} for default status code {\f1\fs20 HTML_BADREQUEST} = 400).
In this case, the client will receive a corresponding serialized JSON error object, e.g. for {\f1\fs20 Ctxt.Error('Missing Parameter',HTML_NOTFOUND)}:
${
$ "ErrorCode":404,
$ "ErrorText":"Missing Parameter"
$}
If called from an AJAX client, or a browser, this content should be easy to interpret.
Note that the framework core will catch any exception during the method execution, and will return a {\f1\fs20 "Internal Server Error" / HTML_SERVERERROR} = 500 error code with the associated textual exception details.
\page
: Benefits and limitations of this implementation
Method-based services allow fast and direct access to all {\f1\fs20 mORMot} Client-Server {\f1\fs20 RESTful} features, over all usual protocols of our framework: @*HTTP@/1.1, Named Pipe, Windows Messages, direct in-memory/in-process access.
The {\i mORMot} implementation of method-based services gives full access to the lowest-level of the framework core, so it has some advantages:
- It can be tuned to fit any purpose (such as retrieving or returning some HTML or binary data, or modifying the HTTP headers on the fly);
- It is integrated into the @*REST@ful URI model, so it can be related to any table/class of our @*ORM@ framework (like {\f1\fs20 DataAsHex} service above), or it can handle any remote query (e.g. any @*AJAX@ or @*SOAP@ requests);
- It has a very low performance overhead, so can be used to reduce server workload for some common tasks.
................................................................................
It may be used by @*AJAX@ or HTML applications to return any kind of data, i.e. not only JSON results, but pure text, HTML or even binary content. Our {\f1\fs20 TServiceFactoryClient} instance is also able to handle such requests, and will save client-server bandwidth when transmitting some BLOB data (since it won't serialized the content with {\f1\fs20 Base64} encoding).
In order to specify a custom format, you can use the following {\f1\fs20 @*TServiceCustomAnswer@ record} type as the {\f1\fs20 result} of an {\f1\fs20 interface function}:
!  TServiceCustomAnswer = record
!    Header: RawUTF8;
!    Content: RawByteString;
!    Status: cardinal;
!  end;
The {\f1\fs20 Header} field shall be not null (i.e. not equal to ''), and contains the expected content type header (e.g. {\f1\fs20 TEXT_CONTENT_TYPE_HEADER} or {\f1\fs20 HTML_CONTENT_TYPE_HEADER}).\line Then the {\f1\fs20 Content} value will be transmitted back directly to the client, with no JSON @*serialization@. Of course, no {\f1\fs20 var} nor {\f1\fs20 out} parameter will be transmitted (since there is no JSON result array any more).\line Finally, the {\f1\fs20 Status} field could be overridden with a property HTML code, if the default {\f1\fs20 HTML_SUCCESS} is not enough for your purpose. Note that when consumed from {\i Delphi} clients, {\f1\fs20 HTML_SUCCESS} is expected to be returned by the server: you should customize {\f1\fs20 Status} field only for plain AJAX / web clients.
In order to implement such method, you may define such an interface:
!  IComplexCalculator = interface(ICalculator)
!    ['{8D0F3839-056B-4488-A616-986CF8D4DEB7}']
!    function TestBlob(n: TComplexNumber): TServiceCustomAnswer;
!  end;
This may be implemented for instance as such:
!function TServiceComplexCalculator.TestBlob(n: TComplexNumber): TServiceCustomAnswer;
................................................................................
!var Client: TSQLRestClientHTTP;
!...
!  Client := GetClient('localhost','User','synopse')
The data model and the expected authentication scheme were included in the {\f1\fs20 GetClient()} function, which will raise the expected {\f1\fs20 ERestException} in case of any connection or authentication issue.
:   CRUD/ORM remote access
Thanks to {\f1\fs20 SynCrossPlatform*} units, you could easily perform any remote ORM operation on your {\i mORMot} server, with the usual {\f1\fs20 TSQLRest} CRUD methods.\line For instance, the {\f1\fs20 RegressionTests.dpr} sample performs the following operations
!!  fClient.CallBackGet('DropTable',[],Call,TSQLRecordPeople); // call of method-based service
!  check(Call.OutStatus=HTML_SUCCESS);
!  people := TSQLRecordPeople.Create; // create a record ORM
!  try
!    for i := 1 to 200 do begin
!      people.FirstName := 'First'+IntToStr(i);
!      people.LastName := 'Last'+IntToStr(i);
!      people.YearOfBirth := i+1800;
!      people.YearOfDeath := i+1825;
................................................................................
!      people.YearOfBirth := i+1800;
!      people.YearOfDeath := i+1825;
!!      fClient.BatchAdd(people,true);
!    end;
!  finally
!    people.Free;
!  end;
!!  fClient.fBatchSend(res)=HTML_SUCCESS);
!  check(length(res)=200);
!  for i := 1 to 200 do
!    check(res[i-1]=i); // server returned the IDs of the newly created records
Those {\f1\fs20 BatchAdd} / {\f1\fs20 BatchDelete} / {\f1\fs20 BatchUpdate} methods of {\f1\fs20 TSQLRest} have the benefit to introduce at client level:
- Much higher performance, especially on multi-insertion or multi-update of data;
- Transactional support: {\f1\fs20 TSQLRest.BatchStart()} has an optional {\f1\fs20 @*AutomaticTransactionPerRow@} parameter, set to {\f1\fs20 10000} by default, which will create a server-side transaction during the write process, enable @78@ or @99@ on the server side if available, and an ACID rollback in case of any failure.
You can note that all above code has exactly the same structure and methods than standard {\i mORMot} clients.
................................................................................
All ORM/SOA activity should be accessed remotely via {\f1\fs20 rest.project.com}, then will be handled as expected by the ORM/SOA methods of the {\f1\fs20 TSQLRestServer} instance.\line For proper AJAX / JavaScript process, you may have to write:
! aHttpServer.AccessControlAllowOrigin := '*'; // allow cross-site AJAX queries
Any attempt to access to the {\f1\fs20 project.com} or {\f1\fs20 www.project.com} URI will be redirected to the following method-based service:
!procedure TMyServer.Html(Ctxt: TSQLRestServerURIContext);
!begin
!  if fMyFileCache='' then
!    fMyFileCache := StringFromFile(ChangeFileExt(paramstr(0),'.html'));
!  Ctxt.Returns(fMyFileCache,HTML_SUCCESS,HTML_CONTENT_TYPE_HEADER,true);
!end;
This method will serve some static HTML content as the main front end page of this server connected to the Internet. For best performance, this UTF-8 content is cached in memory, and the HTTP 304 command will be handled, if the browser supports it. Of course, your application may return some more complex content, even serving a set of files hosted in a local folder, e.g. by calling {\f1\fs20 Ctxt.ReturnFile()} or {\f1\fs20 Ctxt.ReturnFileFromFolder()} methods in this {\f1\fs20 Html()} service:
!procedure TMyServer.Html(Ctxt: TSQLRestServerURIContext);
!begin
!  Ctxt.ReturnFileFromFolder('c:\www');
!end;
This single method will search for any matching file in the local {\f1\fs20 c:\\www} folder and its sub-directories, returning the default {\f1\fs20 index.html} content if no file is specified at URI level. See the optional parameters to the {\f1\fs20 Ctxt.ReturnFileFromFolder()} method for proper tuning, e.g. to change the default file name or disable the HTTP 304 answers. In all cases, the file content will be served by the @88@ directly from the kernel mode, so will be very fast.
................................................................................
!procedure TBlogApplication.AuthorView(var ID: integer; out Author: TSQLAuthor;
!  out Articles: variant);
!begin
!  RestModel.Retrieve(ID,Author);
!  if Author.ID<>0 then
!    Articles := RestModel.RetrieveListJSON(
!      TSQLArticle,'Author=? order by id desc limit 50',[ID],ARTICLE_FIELDS) else
!    raise EMVCApplication.CreateGotoError(HTML_NOTFOUND);
!end;
By convention, all parameters are allocated when {\f1\fs20 TMVCApplication} will execute a method. So you do not need to allocate or handle the {\f1\fs20 Author: TSQLAuthor} instance lifetime.\line You have direct access to the underlying {\f1\fs20 TSQLRest} instance via {\f1\fs20 TMVCApplication.RestModel}: so all CRUD operations are available. You can let the ORM do the low level SQL work for you: to retrieve all information about one {\f1\fs20 TSQLAuthor} and get the list of its associated articles, we just use a {\f1\fs20 TSQLRest} method with the appropriate WHERE clause. Here we returned the list of articles as a {\f1\fs20 TDocVariant}, so that they will be transmitted as a JSON array, without any intermediate marshalling to {\f1\fs20 TSQLArticle} instances, but with the {\f1\fs20 Tags} dynamic array published property returned as an array of integers (you may have used {\f1\fs20 TObjectList} or {\f1\fs20 RawJSON} instead, as will be detailed below).\line In case of any error, an {\f1\fs20 EMVCApplication} will be raised: when such an exception happens, the {\f1\fs20 TMVCApplication} will handle and convert it into a page change, and a redirection to the {\f1\fs20 IBlogApplication.Error()} method, which will return an error page, using the {\f1\fs20 Error.html} view template.
Let's take a look at a bit more complex method, which we talked about in @%%mORMotMVCSequence@:
!procedure TBlogApplication.ArticleView(
!  ID: integer; var WithComments: boolean; Direction: integer;
!  out Article: TSQLArticle; out Author: variant; out Comments: TObjectList);
!var newID: TID;
................................................................................
!    Author := RestModel.RetrieveDocVariant(
!      TSQLAuthor,'ID=?',[Article.Author.ID],'FirstName,FamilyName');
!    if WithComments then begin
!      Comments.Free; // we will override the TObjectList created at input
!      Comments := RestModel.RetrieveList(TSQLComment,'Article=?',[Article.ID]);
!    end;
!  end else
!    raise EMVCApplication.CreateGotoError(HTML_NOTFOUND);
!end;
This method has to manage several use cases:
- Display an {\f1\fs20 Article} from the database;
- Retrieve the {\f1\fs20 Author} first name and family name;
- Optionally display the associated {\f1\fs20 Comment}s;
- Optionally get the previous or next {\f1\fs20 Article};
- Trigger an error in case of an invalid request.
................................................................................
In order to have proper JSON serialization of the {\f1\fs20 record}, you will need to specify its structure, if you use a version of Delphi without the new RTII (i.e. before Delphi 2010) - see @51@.
Then we can use the {\f1\fs20 TMVCApplication.CurrentSession} property to perform the authentication:
!function TBlogApplication.Login(const LogonName, PlainPassword: RawUTF8): TMVCAction;
!var Author: TSQLAuthor;
!!    SessionInfo: TCookieData;
!begin
!!  if CurrentSession.CheckAndRetrieve<>0 then begin
!    GotoError(result,HTML_BADREQUEST);
!    exit;
!  end;
!!  Author := TSQLAuthor.Create(RestModel,'LogonName=?',[LogonName]);
!  try
!!    if (Author.ID<>0) and Author.CheckPlainPassword(PlainPassword) then begin
!      SessionInfo.AuthorName := Author.LogonName;
!      SessionInfo.AuthorID := Author.ID;
................................................................................
!    case URI.Method of
!    mPOST: begin       // POST=ADD=INSERT
!      if URI.Table=nil then begin
!      (...)
!    end else
!      // here, Table<>nil and TableIndex in [0..MAX_SQLTABLES-1]
!!      if not (URI.TableIndex in Call.RestAccessRights^.POST) then // check User
!!        Call.OutStatus := HTML_FORBIDDEN else
!      (...)
Making access rights a parameter allows this method to be handled as pure stateless, @*thread-safe@ and @*session@-free, from the bottom-most level of the framework.
On the other hand, the security policy defined by this global parameter does not allow tuned per-user authorization. In the current implementation, the {\f1\fs20 SUPERVISOR_ACCESS_RIGHTS} constant is transmitted for all handled communication protocols (direct access, Windows Messages, named pipe or HTTP). Only direct access via {\f1\fs20 @*TSQLRestClientDB@} will use {\f1\fs20 FULL_ACCESS_RIGHTS}, i.e. will have {\f1\fs20 AllowRemoteExecute} parameter set to all possible flags.
The light session process, as implemented by @18@, is used to override the access rights with the one defined in the {\f1\fs20 TSQLAuthGroup.AccessRights} field.
Be aware that this per-table access rights depend on the table order as defined in the associated {\f1\fs20 TSQLModel}. So if you add some tables to your database model, please take care to add the new tables {\i after} the existing. If you insert the new tables within current tables, you will need to update the access rights values.
:19  Additional safety
A {\f1\fs20 AllowRemoteExecute: TSQLAllowRemoteExecute} field has been made available in the {\f1\fs20 TSQLAccessRights} record to tune remote execution, depending on the authenticated user, and the group he/she is part of.






|







 







|





|







 







|







 







|







 







|










|
|





|







 







|













|
|





|







 







|







 







|







 







|







 







|







 







|







 







|







 







|







 







|







5922
5923
5924
5925
5926
5927
5928
5929
5930
5931
5932
5933
5934
5935
5936
....
6608
6609
6610
6611
6612
6613
6614
6615
6616
6617
6618
6619
6620
6621
6622
6623
6624
6625
6626
6627
6628
....
6853
6854
6855
6856
6857
6858
6859
6860
6861
6862
6863
6864
6865
6866
6867
....
8082
8083
8084
8085
8086
8087
8088
8089
8090
8091
8092
8093
8094
8095
8096
....
8616
8617
8618
8619
8620
8621
8622
8623
8624
8625
8626
8627
8628
8629
8630
8631
8632
8633
8634
8635
8636
8637
8638
8639
8640
8641
8642
8643
8644
8645
8646
8647
8648
....
8672
8673
8674
8675
8676
8677
8678
8679
8680
8681
8682
8683
8684
8685
8686
8687
8688
8689
8690
8691
8692
8693
8694
8695
8696
8697
8698
8699
8700
8701
8702
8703
8704
8705
8706
8707
.....
11352
11353
11354
11355
11356
11357
11358
11359
11360
11361
11362
11363
11364
11365
11366
.....
11763
11764
11765
11766
11767
11768
11769
11770
11771
11772
11773
11774
11775
11776
11777
.....
11845
11846
11847
11848
11849
11850
11851
11852
11853
11854
11855
11856
11857
11858
11859
.....
12672
12673
12674
12675
12676
12677
12678
12679
12680
12681
12682
12683
12684
12685
12686
.....
12786
12787
12788
12789
12790
12791
12792
12793
12794
12795
12796
12797
12798
12799
12800
.....
12810
12811
12812
12813
12814
12815
12816
12817
12818
12819
12820
12821
12822
12823
12824
.....
12949
12950
12951
12952
12953
12954
12955
12956
12957
12958
12959
12960
12961
12962
12963
.....
13546
13547
13548
13549
13550
13551
13552
13553
13554
13555
13556
13557
13558
13559
13560
!  aID := aExternalClient.Add(RExt,true);
!  (...)
!  aExternalClient.Retrieve(aID,RExt);
!  (...)
!  aExternalClient.BatchStart(TSQLRecordPeopleExt);
!  aExternalClient.BatchAdd(RExt,true);
!  (...)
!  Check(aExternalClient.BatchSend(BatchID)=HTTP_SUCCESS);
!  Check(aExternalClient.TableHasRows(TSQLRecordPeopleExt));
!  Check(aExternalClient.TableRowCount(TSQLRecordPeopleExt)=n);
!  (...)
!  RExt.FillPrepare(aExternalClient,'FirstName=? and LastName=?',
!    [RInt.FirstName,RInt.LastName]); // query will use index -> fast :)
!  while RExt.FillOne do ...
!  (...)
................................................................................
!      R.Ints := nil;
!      R.DynArray(1).Add(i);
!      assert(Client.BatchAdd(R,True)>=0);
!    end;
!  finally
!    R.Free;
!  end;
!  assert(Client.BatchSend(IDs)=HTTP_SUCCESS);
Or for deletion:
!  Client.BatchStart(TSQLORM);
!  for i := 5 to COLL_COUNT do
!    if i mod 5=0 then
!      assert(fClient.BatchDelete(i)>=0);
!  assert(Client.BatchSend(IDs)=HTTP_SUCCESS);
Speed benefit may be huge in regard to individual Add/Delete operations, even on a local {\i MongoDB} server. We will see some benchmark numbers now.
:  ORM/ODM performance
You can take a look at @59@ to compare {\i MongoDB} as back-end for our ORM classes.
In respect to external @*SQL@ engines, it features very high speed, low CPU use, and almost no difference in use. We interfaced the {\f1\fs20 BatchAdd()} and {\f1\fs20 BatchDelete()} methods to benefit of {\i MongoDB} BULK process, and avoided most memory allocation during the process.
Here are some numbers, extracted from the {\f1\fs20 MongoDBTests.dpr} sample, which reflects the performance of our ORM/ODM, depending on the {\i Write Concern} mode used:
$2. ORM
$
................................................................................
!var git: TTestCustomJSONGitHubs;
! ...
!  U := zendframeworkJson;
!!  Check(DynArrayLoadJSON(git,@U[1],TypeInfo(TTestCustomJSONGitHubs))<>nil);
!!  U := DynArraySaveJSON(git,TypeInfo(TTestCustomJSONGitHubs));
You can see that the {\f1\fs20 record} serialization is auto-magically available at dynamic array level, which is pretty convenient in our case, since the {\f1\fs20 api.github.com} RESTful service returns a JSON array.
It will convert 160 KB of very verbose JSON information:
$[{"id":8079771,"name":"Component_ZendAuthentication","full_name":"zendframework/Component_ZendAuthentication","owner":{"login":"zendframework","id":296074,"avatar_url":"https://1.gravatar.com/avatar/460576a0866d93fdacb597da4b90f233?d=https%3A%2F%2Fidenticons.github.com%2F292b7433472e2946c926bdca195cec8c.png&r=x","gravatar_id":"460576a0866d93fdacb597da4b90f233","url":"https://api.github.com/users/zendframework","HTTP_url":"https://github.com/zendframework","followers_url":"https://api.github.com/users/zendframework/followers","following_url":"https://api.github.com/users/zendframework/following{/other_user}","gists_url":"https://api.github.com/users/zendframework/gists{/gist_id}","starred_url":"https://api.github.com/users/zendframework/starred{/owner}{/repo}",...
Into the much smaller (6 KB) and readable JSON content, containing only the information we need:
$[
$ {
$  "name": "Component_ZendAuthentication",
$  "id": 8079771,
$  "description": "Authentication component from Zend Framework 2",
$  "fork": true,
................................................................................
!  Batch := TSQLRestBatch.Create(Server,TSQLRecordTest,30);
!  try
!    for i := 10000 to 10099 do begin
!      R.Int := i;
!      R.Test := Int32ToUTF8(i);
!      Check(Batch.Add(R,true)=i-10000);
!    end;
!    Check(Server.BatchSend(Batch,IDs)=HTTP_SUCCESS);
!  finally
!    Batch.Free;
!  end;
The ability to handle several {\f1\fs20 TSQLRestBatch} classes in the same time will allow to implement the {\i @**Unit Of Work@} pattern. It can be used to maintain a list of objects affected by a business transaction and coordinates the writing out of changes and the resolution of concurrency problems, especially in a complex @*SOA@ application with a huge number of connected clients.
In a way, you can think of the {\i Unit of Work} as a place to dump all transaction-handling code.\line The responsibilities of the {\i Unit of Work} are to:
- Manage transactions;
- Order the database inserts, deletes, and updates;
................................................................................
$ {"Result":["One","two"]}
\page
: Returns non-JSON content
Using {\f1\fs20 Ctxt.Returns()} will let the method return the content in any format, e.g. as a JSON object (via the overloaded {\f1\fs20 Ctxt.Returns([])} method expecting field name/value pairs), or any content, since the returned @**MIME@-type can be defined as a parameter to {\f1\fs20 Ctxt.Returns()} - it may be useful to specify another mime-type than the default constant {\f1\fs20 JSON_CONTENT_TYPE}, i.e. {\f1\fs20 'application/json; charset=UTF-8'}, and returns plain text, HTML or binary.
For instance, you can return directly a value as plain text:
!procedure TSQLRestServer.TimeStamp(Ctxt: TSQLRestServerURIContext);
!begin
!  Ctxt.Returns(Int64ToUtf8(ServerTimeStamp),HTTP_SUCCESS,TEXT_CONTENT_TYPE_HEADER);
!end;
Or you can return some binary file, retrieving the corresponding MIME type from its binary content:
!procedure TSQLRestServer.GetFile(Ctxt: TSQLRestServerURIContext);
!var fileName: TFileName;
!    content: RawByteString;
!    contentType: RawUTF8;
!begin
!  fileName :=  'c:\data\'+ExtractFileName(Ctxt['filename']); // or Ctxt.Input['filename']
!  content := StringFromFile(fileName);
!  if content='' then
!    Ctxt.Error('',HTTP_NOTFOUND) else
!    Ctxt.Returns(content,HTTP_SUCCESS,HEADER_CONTENT_TYPE+
!         GetMimeContentType(pointer(content),Length(content),fileName));
!end;
The corresponding client method may be defined as such:
!function TMyClient.GetFile(const aFileName: RawUTF8): RawByteString;
!begin
!  if CallBackGet('GetFile',['filename',aFileName],RawUTF8(result))<>HTTP_SUCCESS then
!    raise Exception.CreateFmt('Impossible to get file: %s',[result]);
!end;
Note that the {\f1\fs20 Ctxt.ReturnFile()} method - see @95@ - is preferred than manual file retrieval as implemented in this {\f1\fs20 TSQLRestServer.GetFile()} method. It is shown here for demonstration purposes only.
If you use HTTP as communication protocol, you can consume these services, implemented Server-Side in fast {\i Delphi} code, with any @*AJAX@ application on the client side.
Using {\f1\fs20 GetMimeContentType()} when sending non JSON content (e.g. picture, pdf file, binary...) will be interpreted as expected by any standard Internet browser: it could be used to serve some good old HTML content within a page, not necessary consume the service via {\i JavaScript} .
\page
: Advanced process on server side
................................................................................
When used over a slow network (e.g. over the Internet), you can set the optional {\f1\fs20 Handle304NotModified} parameter of both {\f1\fs20 Ctxt.Returns()} and {\f1\fs20 Ctxt.Results()} methods to return the response body only if it has changed since last time.
In practice, result content will be hashed (using {\f1\fs20 crc32c} algorithm, and fast SSE 4.2 hardware instruction, if available) and in case of no modification will return "{\i 304 Not Modified}" status to the browser, without the actual result content. Therefore, the response will be transmitted and received much faster, and will save a lot of bandwidth, especially in case of periodic server pooling (e.g. for client screen refresh).
Note that in case of hash collision of the {\f1\fs20 crc32c} algorithm (we never did see it happen, but such a mathematical possibility exists), a false positive "not modified" status may be returned; this option is therefore unset by default, and should be enabled only if your client does not handle any sensitive accounting process, for instance.
Be aware that you should {\i disable authentication} for the methods using this {\f1\fs20 Handle304NotModified} parameter, via a {\f1\fs20 TSQLRestServer.ServiceMethodByPassAuthentication()} call. In fact, our @*REST@ful authentication - see @18@ - uses a per-URI signature, which change very often (to avoid men-in-the-middle attacks). Therefore, any browser-side caching benefit will be voided if authentication is used: browser internal cache will tend to grow for nothing since the previous URIs are deprecated, and it will be a cache-miss most of the time. But when serving some static content (e.g. HTML content, fixed JSON values or even UI binaries), this browser-side caching can be very useful.
This @*stateless@ @9@ model will enable several levels of caching, even using an external {\i Content Delivery Network} (@*CDN@) service. See @97@ for some potential hosting architectures, which may let your {\i mORMot} server scale to thousands of concurrent users, served around the world with the best responsiveness.
:95 Returning file content
Framework's HTTP server is able to handle returning a file as response to a method-based service.\line The @88@ is even able to serve the file content asynchronously from kernel mode, with outstanding performance.
You can use the {\f1\fs20 Ctxt.ReturnFile()} method to return a file directly.\line This method is also able to guess the MIME type from the file extension, and handle {\f1\fs20 HTTP_NOTMODIFIED = 304} process, if {\f1\fs20 Handle304NotModified} parameter is {\f1\fs20 true}, using the file time stamp.
Another possibility may be to use the {\f1\fs20 Ctxt.ReturnFileFromFolder()} method, which is able to efficiently return any file specified by its URI, from a local folder. It may be very handy to
return some static web content from a {\i mORMot} HTTP server.
\page
: Handling errors
When using {\f1\fs20 Ctxt.Input*[]} properties, any missing parameter will raise an {\f1\fs20 EParsingException}. It will therefore be intercepted by the server process (as any other exception), and returned to the client with an error message containing the {\f1\fs20 Exception} class name and its associated message.
But you can have full access to the error workflow, if needed. In fact, calling either {\f1\fs20 Ctxt.Results()}, {\f1\fs20 Ctxt.Returns()}, {\f1\fs20 Ctxt.Success()} or {\f1\fs20 Ctxt.Error()} will specify the HTTP status code (e.g. 200 / "OK" for {\f1\fs20 Results()} and {\f1\fs20 Success()} methods by default, or 400 / "Bad Request" for {\f1\fs20 Error()}) as an {\f1\fs20 integer} value. For instance, here is how a service not returning any content can handle those status/error codes:
!procedure TSQLRestServer.Batch(Ctxt: TSQLRestServerURIContext);
!begin
!  if (Ctxt.Method=mPUT) and RunBatch(nil,nil,Ctxt) then
!    Ctxt.Success else
!    Ctxt.Error;
!end;
In case of an error on the server side, you may call {\f1\fs20 Ctxt.Error()} method (only the two valid status codes are {\f1\fs20 200} and {\f1\fs20 201}).
The {\f1\fs20 Ctxt.Error()} method has an optional parameter to specify a custom error message in plain English, which will be returned to the client in case of an invalid status code. If no custom text is specified, the framework will return the corresponding generic HTTP status text (e.g. {\f1\fs20 "Bad Request"} for default status code {\f1\fs20 HTTP_BADREQUEST} = 400).
In this case, the client will receive a corresponding serialized JSON error object, e.g. for {\f1\fs20 Ctxt.Error('Missing Parameter',HTTP_NOTFOUND)}:
${
$ "ErrorCode":404,
$ "ErrorText":"Missing Parameter"
$}
If called from an AJAX client, or a browser, this content should be easy to interpret.
Note that the framework core will catch any exception during the method execution, and will return a {\f1\fs20 "Internal Server Error" / HTTP_SERVERERROR} = 500 error code with the associated textual exception details.
\page
: Benefits and limitations of this implementation
Method-based services allow fast and direct access to all {\f1\fs20 mORMot} Client-Server {\f1\fs20 RESTful} features, over all usual protocols of our framework: @*HTTP@/1.1, Named Pipe, Windows Messages, direct in-memory/in-process access.
The {\i mORMot} implementation of method-based services gives full access to the lowest-level of the framework core, so it has some advantages:
- It can be tuned to fit any purpose (such as retrieving or returning some HTML or binary data, or modifying the HTTP headers on the fly);
- It is integrated into the @*REST@ful URI model, so it can be related to any table/class of our @*ORM@ framework (like {\f1\fs20 DataAsHex} service above), or it can handle any remote query (e.g. any @*AJAX@ or @*SOAP@ requests);
- It has a very low performance overhead, so can be used to reduce server workload for some common tasks.
................................................................................
It may be used by @*AJAX@ or HTML applications to return any kind of data, i.e. not only JSON results, but pure text, HTML or even binary content. Our {\f1\fs20 TServiceFactoryClient} instance is also able to handle such requests, and will save client-server bandwidth when transmitting some BLOB data (since it won't serialized the content with {\f1\fs20 Base64} encoding).
In order to specify a custom format, you can use the following {\f1\fs20 @*TServiceCustomAnswer@ record} type as the {\f1\fs20 result} of an {\f1\fs20 interface function}:
!  TServiceCustomAnswer = record
!    Header: RawUTF8;
!    Content: RawByteString;
!    Status: cardinal;
!  end;
The {\f1\fs20 Header} field shall be not null (i.e. not equal to ''), and contains the expected content type header (e.g. {\f1\fs20 TEXT_CONTENT_TYPE_HEADER} or {\f1\fs20 HTML_CONTENT_TYPE_HEADER}).\line Then the {\f1\fs20 Content} value will be transmitted back directly to the client, with no JSON @*serialization@. Of course, no {\f1\fs20 var} nor {\f1\fs20 out} parameter will be transmitted (since there is no JSON result array any more).\line Finally, the {\f1\fs20 Status} field could be overridden with a property HTML code, if the default {\f1\fs20 HTTP_SUCCESS} is not enough for your purpose. Note that when consumed from {\i Delphi} clients, {\f1\fs20 HTTP_SUCCESS} is expected to be returned by the server: you should customize {\f1\fs20 Status} field only for plain AJAX / web clients.
In order to implement such method, you may define such an interface:
!  IComplexCalculator = interface(ICalculator)
!    ['{8D0F3839-056B-4488-A616-986CF8D4DEB7}']
!    function TestBlob(n: TComplexNumber): TServiceCustomAnswer;
!  end;
This may be implemented for instance as such:
!function TServiceComplexCalculator.TestBlob(n: TComplexNumber): TServiceCustomAnswer;
................................................................................
!var Client: TSQLRestClientHTTP;
!...
!  Client := GetClient('localhost','User','synopse')
The data model and the expected authentication scheme were included in the {\f1\fs20 GetClient()} function, which will raise the expected {\f1\fs20 ERestException} in case of any connection or authentication issue.
:   CRUD/ORM remote access
Thanks to {\f1\fs20 SynCrossPlatform*} units, you could easily perform any remote ORM operation on your {\i mORMot} server, with the usual {\f1\fs20 TSQLRest} CRUD methods.\line For instance, the {\f1\fs20 RegressionTests.dpr} sample performs the following operations
!!  fClient.CallBackGet('DropTable',[],Call,TSQLRecordPeople); // call of method-based service
!  check(Call.OutStatus=HTTP_SUCCESS);
!  people := TSQLRecordPeople.Create; // create a record ORM
!  try
!    for i := 1 to 200 do begin
!      people.FirstName := 'First'+IntToStr(i);
!      people.LastName := 'Last'+IntToStr(i);
!      people.YearOfBirth := i+1800;
!      people.YearOfDeath := i+1825;
................................................................................
!      people.YearOfBirth := i+1800;
!      people.YearOfDeath := i+1825;
!!      fClient.BatchAdd(people,true);
!    end;
!  finally
!    people.Free;
!  end;
!!  fClient.fBatchSend(res)=HTTP_SUCCESS);
!  check(length(res)=200);
!  for i := 1 to 200 do
!    check(res[i-1]=i); // server returned the IDs of the newly created records
Those {\f1\fs20 BatchAdd} / {\f1\fs20 BatchDelete} / {\f1\fs20 BatchUpdate} methods of {\f1\fs20 TSQLRest} have the benefit to introduce at client level:
- Much higher performance, especially on multi-insertion or multi-update of data;
- Transactional support: {\f1\fs20 TSQLRest.BatchStart()} has an optional {\f1\fs20 @*AutomaticTransactionPerRow@} parameter, set to {\f1\fs20 10000} by default, which will create a server-side transaction during the write process, enable @78@ or @99@ on the server side if available, and an ACID rollback in case of any failure.
You can note that all above code has exactly the same structure and methods than standard {\i mORMot} clients.
................................................................................
All ORM/SOA activity should be accessed remotely via {\f1\fs20 rest.project.com}, then will be handled as expected by the ORM/SOA methods of the {\f1\fs20 TSQLRestServer} instance.\line For proper AJAX / JavaScript process, you may have to write:
! aHttpServer.AccessControlAllowOrigin := '*'; // allow cross-site AJAX queries
Any attempt to access to the {\f1\fs20 project.com} or {\f1\fs20 www.project.com} URI will be redirected to the following method-based service:
!procedure TMyServer.Html(Ctxt: TSQLRestServerURIContext);
!begin
!  if fMyFileCache='' then
!    fMyFileCache := StringFromFile(ChangeFileExt(paramstr(0),'.html'));
!  Ctxt.Returns(fMyFileCache,HTTP_SUCCESS,HTML_CONTENT_TYPE_HEADER,true);
!end;
This method will serve some static HTML content as the main front end page of this server connected to the Internet. For best performance, this UTF-8 content is cached in memory, and the HTTP 304 command will be handled, if the browser supports it. Of course, your application may return some more complex content, even serving a set of files hosted in a local folder, e.g. by calling {\f1\fs20 Ctxt.ReturnFile()} or {\f1\fs20 Ctxt.ReturnFileFromFolder()} methods in this {\f1\fs20 Html()} service:
!procedure TMyServer.Html(Ctxt: TSQLRestServerURIContext);
!begin
!  Ctxt.ReturnFileFromFolder('c:\www');
!end;
This single method will search for any matching file in the local {\f1\fs20 c:\\www} folder and its sub-directories, returning the default {\f1\fs20 index.html} content if no file is specified at URI level. See the optional parameters to the {\f1\fs20 Ctxt.ReturnFileFromFolder()} method for proper tuning, e.g. to change the default file name or disable the HTTP 304 answers. In all cases, the file content will be served by the @88@ directly from the kernel mode, so will be very fast.
................................................................................
!procedure TBlogApplication.AuthorView(var ID: integer; out Author: TSQLAuthor;
!  out Articles: variant);
!begin
!  RestModel.Retrieve(ID,Author);
!  if Author.ID<>0 then
!    Articles := RestModel.RetrieveListJSON(
!      TSQLArticle,'Author=? order by id desc limit 50',[ID],ARTICLE_FIELDS) else
!    raise EMVCApplication.CreateGotoError(HTTP_NOTFOUND);
!end;
By convention, all parameters are allocated when {\f1\fs20 TMVCApplication} will execute a method. So you do not need to allocate or handle the {\f1\fs20 Author: TSQLAuthor} instance lifetime.\line You have direct access to the underlying {\f1\fs20 TSQLRest} instance via {\f1\fs20 TMVCApplication.RestModel}: so all CRUD operations are available. You can let the ORM do the low level SQL work for you: to retrieve all information about one {\f1\fs20 TSQLAuthor} and get the list of its associated articles, we just use a {\f1\fs20 TSQLRest} method with the appropriate WHERE clause. Here we returned the list of articles as a {\f1\fs20 TDocVariant}, so that they will be transmitted as a JSON array, without any intermediate marshalling to {\f1\fs20 TSQLArticle} instances, but with the {\f1\fs20 Tags} dynamic array published property returned as an array of integers (you may have used {\f1\fs20 TObjectList} or {\f1\fs20 RawJSON} instead, as will be detailed below).\line In case of any error, an {\f1\fs20 EMVCApplication} will be raised: when such an exception happens, the {\f1\fs20 TMVCApplication} will handle and convert it into a page change, and a redirection to the {\f1\fs20 IBlogApplication.Error()} method, which will return an error page, using the {\f1\fs20 Error.html} view template.
Let's take a look at a bit more complex method, which we talked about in @%%mORMotMVCSequence@:
!procedure TBlogApplication.ArticleView(
!  ID: integer; var WithComments: boolean; Direction: integer;
!  out Article: TSQLArticle; out Author: variant; out Comments: TObjectList);
!var newID: TID;
................................................................................
!    Author := RestModel.RetrieveDocVariant(
!      TSQLAuthor,'ID=?',[Article.Author.ID],'FirstName,FamilyName');
!    if WithComments then begin
!      Comments.Free; // we will override the TObjectList created at input
!      Comments := RestModel.RetrieveList(TSQLComment,'Article=?',[Article.ID]);
!    end;
!  end else
!    raise EMVCApplication.CreateGotoError(HTTP_NOTFOUND);
!end;
This method has to manage several use cases:
- Display an {\f1\fs20 Article} from the database;
- Retrieve the {\f1\fs20 Author} first name and family name;
- Optionally display the associated {\f1\fs20 Comment}s;
- Optionally get the previous or next {\f1\fs20 Article};
- Trigger an error in case of an invalid request.
................................................................................
In order to have proper JSON serialization of the {\f1\fs20 record}, you will need to specify its structure, if you use a version of Delphi without the new RTII (i.e. before Delphi 2010) - see @51@.
Then we can use the {\f1\fs20 TMVCApplication.CurrentSession} property to perform the authentication:
!function TBlogApplication.Login(const LogonName, PlainPassword: RawUTF8): TMVCAction;
!var Author: TSQLAuthor;
!!    SessionInfo: TCookieData;
!begin
!!  if CurrentSession.CheckAndRetrieve<>0 then begin
!    GotoError(result,HTTP_BADREQUEST);
!    exit;
!  end;
!!  Author := TSQLAuthor.Create(RestModel,'LogonName=?',[LogonName]);
!  try
!!    if (Author.ID<>0) and Author.CheckPlainPassword(PlainPassword) then begin
!      SessionInfo.AuthorName := Author.LogonName;
!      SessionInfo.AuthorID := Author.ID;
................................................................................
!    case URI.Method of
!    mPOST: begin       // POST=ADD=INSERT
!      if URI.Table=nil then begin
!      (...)
!    end else
!      // here, Table<>nil and TableIndex in [0..MAX_SQLTABLES-1]
!!      if not (URI.TableIndex in Call.RestAccessRights^.POST) then // check User
!!        Call.OutStatus := HTTP_FORBIDDEN else
!      (...)
Making access rights a parameter allows this method to be handled as pure stateless, @*thread-safe@ and @*session@-free, from the bottom-most level of the framework.
On the other hand, the security policy defined by this global parameter does not allow tuned per-user authorization. In the current implementation, the {\f1\fs20 SUPERVISOR_ACCESS_RIGHTS} constant is transmitted for all handled communication protocols (direct access, Windows Messages, named pipe or HTTP). Only direct access via {\f1\fs20 @*TSQLRestClientDB@} will use {\f1\fs20 FULL_ACCESS_RIGHTS}, i.e. will have {\f1\fs20 AllowRemoteExecute} parameter set to all possible flags.
The light session process, as implemented by @18@, is used to override the access rights with the one defined in the {\f1\fs20 TSQLAuthGroup.AccessRights} field.
Be aware that this per-table access rights depend on the table order as defined in the associated {\f1\fs20 TSQLModel}. So if you add some tables to your database model, please take care to add the new tables {\i after} the existing. If you insert the new tables within current tables, you will need to update the access rights values.
:19  Additional safety
A {\f1\fs20 AllowRemoteExecute: TSQLAllowRemoteExecute} field has been made available in the {\f1\fs20 TSQLAccessRights} record to tune remote execution, depending on the authenticated user, and the group he/she is part of.

Changes to SQLite3/Samples/24 - MongoDB/MongoDBTestCases.pas.

435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
...
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
      R.Ints := nil;
      R.DynArray(1).Add(i);
      Check(fClient.BatchAdd(R,True)>=0);
    end;
  finally
    R.Free;
  end;
  Check(fClient.BatchSend(IDs)=HTML_SUCCESS);
  Check(length(IDs)=COLL_COUNT);
  NotifyTestSpeed('rows inserted',COLL_COUNT,fMongoClient.BytesTransmitted-bytes);
  Check(fClient.TableRowCount(TSQLORM)=COLL_COUNT);
end;

procedure TTestORM.TestOne(R: TSQLORM; aID: integer);
begin
................................................................................
  ExpectedCount := COLL_COUNT;
  fClient.BatchStart(TSQLORM);
  for i := 5 to COLL_COUNT do
    if i mod 5=0 then begin
      Check(fClient.BatchDelete(i)>=0);
      dec(ExpectedCount);
    end;
  Check(fClient.BatchSend(IDs)=HTML_SUCCESS);
  Check(length(IDs)=COLL_COUNT-ExpectedCount);
  NotifyTestSpeed('rows deleted',length(IDs),fMongoClient.BytesTransmitted-bytes);
  R := TSQLORM.CreateAndFillPrepare(fClient,'');
  try
    n := 0;
    i := 0;
    while R.FillOne do begin






|







 







|







435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
...
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
      R.Ints := nil;
      R.DynArray(1).Add(i);
      Check(fClient.BatchAdd(R,True)>=0);
    end;
  finally
    R.Free;
  end;
  Check(fClient.BatchSend(IDs)=HTTP_SUCCESS);
  Check(length(IDs)=COLL_COUNT);
  NotifyTestSpeed('rows inserted',COLL_COUNT,fMongoClient.BytesTransmitted-bytes);
  Check(fClient.TableRowCount(TSQLORM)=COLL_COUNT);
end;

procedure TTestORM.TestOne(R: TSQLORM; aID: integer);
begin
................................................................................
  ExpectedCount := COLL_COUNT;
  fClient.BatchStart(TSQLORM);
  for i := 5 to COLL_COUNT do
    if i mod 5=0 then begin
      Check(fClient.BatchDelete(i)>=0);
      dec(ExpectedCount);
    end;
  Check(fClient.BatchSend(IDs)=HTTP_SUCCESS);
  Check(length(IDs)=COLL_COUNT-ExpectedCount);
  NotifyTestSpeed('rows deleted',length(IDs),fMongoClient.BytesTransmitted-bytes);
  R := TSQLORM.CreateAndFillPrepare(fClient,'');
  try
    n := 0;
    i := 0;
    while R.FillOne do begin

Changes to SQLite3/Samples/26 - RESTful ORM/RESTServerClass.pas.

67
68
69
70
71
72
73
74
75
76
77
78
79
    mPOST,mPUT: begin
      FileFromString(Ctxt.Call.InBody,FileName);
      Ctxt.Success;
    end;
    mDELETE:
      if DeleteFile(FileName) then
        Ctxt.Success else
        Ctxt.Error('',HTML_NOTFOUND);
    end;
  end;
end;

end.






|





67
68
69
70
71
72
73
74
75
76
77
78
79
    mPOST,mPUT: begin
      FileFromString(Ctxt.Call.InBody,FileName);
      Ctxt.Success;
    end;
    mDELETE:
      if DeleteFile(FileName) then
        Ctxt.Success else
        Ctxt.Error('',HTTP_NOTFOUND);
    end;
  end;
end;

end.

Changes to SQLite3/Samples/26 - RESTful ORM/RestClientMain.pas.

72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
...
100
101
102
103
104
105
106
107
108
109
110
111
var ID: TID;
    resp: RawUTF8;
begin
  if fClient=nil then
    exit;
  if not TryStrToInt64(edtGetID.Text,Int64(ID)) then
    exit;
  if fClient.CallBackGet('blob',[],resp,TSQLNoteFile,ID)=HTML_SUCCESS then
    mmoGet.Text := UTF8ToString(resp) else
    mmoGet.Text := '? not found';
end;

procedure TMainForm.btnNewClick(Sender: TObject);
var Note: TSQLNoteFile;
begin
................................................................................
end;

procedure TMainForm.btnSetClick(Sender: TObject);
var resp: RawUTF8;
begin
  if fClient=nil then
    exit;
  if fClient.CallBackPut('blob',StringToUTF8(mmoSet.Text),resp,TSQLNoteFile,Tag)=HTML_CREATED then
    mmoSet.Text := '.. saved ..';
end;

end.






|







 







|




72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
...
100
101
102
103
104
105
106
107
108
109
110
111
var ID: TID;
    resp: RawUTF8;
begin
  if fClient=nil then
    exit;
  if not TryStrToInt64(edtGetID.Text,Int64(ID)) then
    exit;
  if fClient.CallBackGet('blob',[],resp,TSQLNoteFile,ID)=HTTP_SUCCESS then
    mmoGet.Text := UTF8ToString(resp) else
    mmoGet.Text := '? not found';
end;

procedure TMainForm.btnNewClick(Sender: TObject);
var Note: TSQLNoteFile;
begin
................................................................................
end;

procedure TMainForm.btnSetClick(Sender: TObject);
var resp: RawUTF8;
begin
  if fClient=nil then
    exit;
  if fClient.CallBackPut('blob',StringToUTF8(mmoSet.Text),resp,TSQLNoteFile,Tag)=HTTP_CREATED then
    mmoSet.Text := '.. saved ..';
end;

end.

Changes to SQLite3/Samples/27 - CrossPlatform Clients/PeopleServer.pas.

31
32
33
34
35
36
37










38
39
40
41
42
43
44
..
50
51
52
53
54
55
56

57
58
59
60
61
62
63
64
65
66
67
68

69
70
71
72
73
74
75
..
79
80
81
82
83
84
85


86
87
88
89
90
91
92
...
109
110
111
112
113
114
115


116
117
118
119
120
121
122
...
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
...
217
218
219
220
221
222
223


224
225
226
227
228
229
230







231
232
233
234
235
236

237
238
239
240
241
    J: array of packed record
      J1: byte;
      J2: TGUID;
      J3: TRecordEnum;
    end;
  end;
  {$endif TESTRECORD}











  TPeopleSexe = (sFemale, sMale);

  TPeopleSexeDynArray = array of TPeopleSexe;

  TSimpleRecord = packed record
    A,B: integer;
................................................................................
  TSQLRecordPeople = class(TSQLRecord)
  protected
    fData: TSQLRawBlob;
    fFirstName: RawUTF8;
    fLastName: RawUTF8;
    fYearOfBirth: integer;
    fYearOfDeath: word;

    {$ifdef TESTRECORD}
    fSexe: TPeopleSexe;
    fSimple: TTestCustomJSONArraySimpleArray;
  public
    class procedure InternalRegisterCustomProperties(Props: TSQLRecordProperties); override;
    {$endif}
  published
    property FirstName: RawUTF8 read fFirstName write fFirstName;
    property LastName: RawUTF8 read fLastName write fLastName;
    property Data: TSQLRawBlob read fData write fData;
    property YearOfBirth: integer read fYearOfBirth write fYearOfBirth;
    property YearOfDeath: word read fYearOfDeath write fYearOfDeath;

  {$ifdef TESTRECORD}
    property Sexe: TPeopleSexe read fSexe write fSexe;
  public
    property Simple: TTestCustomJSONArraySimpleArray read fSimple;
  {$endif}
  end;

................................................................................
    procedure ToText(Value: Currency; const Curr: RawUTF8;
      var Sexe: TPeopleSexe; var Name: RawUTF8);
    {$ifdef TESTRECORD}
    function RecordToText(var Rec: TTestCustomJSONArraySimpleArray): string;
    {$endif}
    function GetPeople(id: TID; out People: TSQLRecordPeople;
      out Sexes: TPeopleSexeDynArray; var arr: TSimpleRecordDynArray): boolean;


  end;

  TCustomServer = class(TSQLRestServerFullMemory)
  published
    procedure DropTable(Ctxt: TSQLRestServerURIContext);
  end;

................................................................................
    procedure ToText(Value: Currency; const Curr: RawUTF8;
      var Sexe: TPeopleSexe; var Name: RawUTF8);
    {$ifdef TESTRECORD}
    function RecordToText(var Rec: TTestCustomJSONArraySimpleArray): string;
    {$endif}
    function GetPeople(id: TID; out People: TSQLRecordPeople;
      out Sexes: TPeopleSexeDynArray; var arr: TSimpleRecordDynArray): boolean;


  end;

function TServiceCalculator.Add(n1, n2: integer): integer;
begin
  result := n1+n2;
end;

................................................................................
begin
  Name := FormatUTF8('% % for % %',[Curr,Value,SEX_TEXT[Sexe],Name]);
  Sexe := sFemale;
end;

function TServiceCalculator.GetPeople(id: TID;
  out People: TSQLRecordPeople; out Sexes: TPeopleSexeDynArray;
  var arr: TSimpleRecordDynArray): boolean;
var n: integer;
begin
  result := ServiceContext.Request.Server.Retrieve(id,People);
  n := length(arr);
  SetLength(arr,n+1);
  arr[n].A := id;
  arr[n].C := People.FirstName;
................................................................................

const
  __TTestCustomJSONArraySimpleArray =
  'F RawUTF8 G array of RawUTF8 '+
  'H {H1 integer H2 WideString H3{H3a boolean H3b RawByteString}} I TDateTime '+
  'J [J1 byte J2 TGUID J3 TRecordEnum]';
  __TSimpleRecord = 'A,B:integer C: RawUTF8';



class procedure TSQLRecordPeople.InternalRegisterCustomProperties(
  Props: TSQLRecordProperties);
begin
  Props.RegisterCustomPropertyFromRTTI(Self,TypeInfo(TTestCustomJSONArraySimpleArray),
    'Simple',@TSQLRecordPeople(nil).fSimple);
end;








initialization
  TTextWriter.RegisterCustomJSONSerializerFromTextSimpleType(TypeInfo(TRecordEnum));
  TTextWriter.RegisterCustomJSONSerializerFromText(
    TypeInfo(TTestCustomJSONArraySimpleArray),__TTestCustomJSONArraySimpleArray);
  TTextWriter.RegisterCustomJSONSerializerFromText(TypeInfo(TSimpleRecord),__TSimpleRecord);



{$endif TESTRECORD}

end.






>
>
>
>
>
>
>
>
>
>







 







>












>







 







>
>







 







>
>







 







|







 







>
>







>
>
>
>
>
>
>






>





31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
..
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
..
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
...
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
...
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
...
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
    J: array of packed record
      J1: byte;
      J2: TGUID;
      J3: TRecordEnum;
    end;
  end;
  {$endif TESTRECORD}

  {
  TRecB=packed record
    a1:array of packed record
      v1,v2:integer;
    end;
    a2:array of integer;
    v1:integer;
  end;
  }

  TPeopleSexe = (sFemale, sMale);

  TPeopleSexeDynArray = array of TPeopleSexe;

  TSimpleRecord = packed record
    A,B: integer;
................................................................................
  TSQLRecordPeople = class(TSQLRecord)
  protected
    fData: TSQLRawBlob;
    fFirstName: RawUTF8;
    fLastName: RawUTF8;
    fYearOfBirth: integer;
    fYearOfDeath: word;
    fAnother: TSQLRecordPeople;
    {$ifdef TESTRECORD}
    fSexe: TPeopleSexe;
    fSimple: TTestCustomJSONArraySimpleArray;
  public
    class procedure InternalRegisterCustomProperties(Props: TSQLRecordProperties); override;
    {$endif}
  published
    property FirstName: RawUTF8 read fFirstName write fFirstName;
    property LastName: RawUTF8 read fLastName write fLastName;
    property Data: TSQLRawBlob read fData write fData;
    property YearOfBirth: integer read fYearOfBirth write fYearOfBirth;
    property YearOfDeath: word read fYearOfDeath write fYearOfDeath;
    property Another: TSQLRecordPeople read fAnother write fAnother;
  {$ifdef TESTRECORD}
    property Sexe: TPeopleSexe read fSexe write fSexe;
  public
    property Simple: TTestCustomJSONArraySimpleArray read fSimple;
  {$endif}
  end;

................................................................................
    procedure ToText(Value: Currency; const Curr: RawUTF8;
      var Sexe: TPeopleSexe; var Name: RawUTF8);
    {$ifdef TESTRECORD}
    function RecordToText(var Rec: TTestCustomJSONArraySimpleArray): string;
    {$endif}
    function GetPeople(id: TID; out People: TSQLRecordPeople;
      out Sexes: TPeopleSexeDynArray; var arr: TSimpleRecordDynArray): boolean;
//      var rec: TRecB): boolean;
    //function Test(toto: integer): TServiceCustomAnswer;
  end;

  TCustomServer = class(TSQLRestServerFullMemory)
  published
    procedure DropTable(Ctxt: TSQLRestServerURIContext);
  end;

................................................................................
    procedure ToText(Value: Currency; const Curr: RawUTF8;
      var Sexe: TPeopleSexe; var Name: RawUTF8);
    {$ifdef TESTRECORD}
    function RecordToText(var Rec: TTestCustomJSONArraySimpleArray): string;
    {$endif}
    function GetPeople(id: TID; out People: TSQLRecordPeople;
      out Sexes: TPeopleSexeDynArray; var arr: TSimpleRecordDynArray): boolean;
//      var rec: TRecB): boolean;
    function Test(toto: integer): TServiceCustomAnswer;
  end;

function TServiceCalculator.Add(n1, n2: integer): integer;
begin
  result := n1+n2;
end;

................................................................................
begin
  Name := FormatUTF8('% % for % %',[Curr,Value,SEX_TEXT[Sexe],Name]);
  Sexe := sFemale;
end;

function TServiceCalculator.GetPeople(id: TID;
  out People: TSQLRecordPeople; out Sexes: TPeopleSexeDynArray;
  var arr: TSimpleRecordDynArray{; var rec: TRecB}): boolean;
var n: integer;
begin
  result := ServiceContext.Request.Server.Retrieve(id,People);
  n := length(arr);
  SetLength(arr,n+1);
  arr[n].A := id;
  arr[n].C := People.FirstName;
................................................................................

const
  __TTestCustomJSONArraySimpleArray =
  'F RawUTF8 G array of RawUTF8 '+
  'H {H1 integer H2 WideString H3{H3a boolean H3b RawByteString}} I TDateTime '+
  'J [J1 byte J2 TGUID J3 TRecordEnum]';
  __TSimpleRecord = 'A,B:integer C: RawUTF8';
   __TRecB = 'a2:array of integer v1:integer';
  //  __TRecB = 'a1 [v1,v2:integer] a2:array of integer v1:integer';

class procedure TSQLRecordPeople.InternalRegisterCustomProperties(
  Props: TSQLRecordProperties);
begin
  Props.RegisterCustomPropertyFromRTTI(Self,TypeInfo(TTestCustomJSONArraySimpleArray),
    'Simple',@TSQLRecordPeople(nil).fSimple);
end;

function TServiceCalculator.Test(toto: integer): TServiceCustomAnswer;
begin
  result.Header := TEXT_CONTENT_TYPE_HEADER;
  result.Content := Int32ToUtf8(toto);
  result.Status := HTTP_SUCCESS;
end;

initialization
  TTextWriter.RegisterCustomJSONSerializerFromTextSimpleType(TypeInfo(TRecordEnum));
  TTextWriter.RegisterCustomJSONSerializerFromText(
    TypeInfo(TTestCustomJSONArraySimpleArray),__TTestCustomJSONArraySimpleArray);
  TTextWriter.RegisterCustomJSONSerializerFromText(TypeInfo(TSimpleRecord),__TSimpleRecord);
//  TTextWriter.RegisterCustomJSONSerializerFromText(TypeInfo(TRecB),__TRecB);


{$endif TESTRECORD}

end.

Changes to SQLite3/Samples/29 - SmartMobileStudio Client/LoginForm.sfm.

3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    <Created>2014-06-17T10:47:09.864</Created>
    <Modified>2014-07-18T15:32:06.707</Modified>
    <object type="TW3Form">
      <Caption>mORMot Web Test</Caption>
      <Name>LoginForm</Name>
      <object type="TW3Label">
        <Caption>&lt;b&gt;&lt;i&gt;mORMot&lt;/i&gt; Web Test&lt;/b&gt;</Caption>
        <AlignText>1</AlignText>
        <Width>184</Width>
        <Top>8</Top>
        <StyleClass>h1</StyleClass>
        <Left>32</Left>
        <Height>32</Height>
        <Transparent>True</Transparent>
        <Name>W3Label1</Name>






<







3
4
5
6
7
8
9

10
11
12
13
14
15
16
    <Created>2014-06-17T10:47:09.864</Created>
    <Modified>2014-07-18T15:32:06.707</Modified>
    <object type="TW3Form">
      <Caption>mORMot Web Test</Caption>
      <Name>LoginForm</Name>
      <object type="TW3Label">
        <Caption>&lt;b&gt;&lt;i&gt;mORMot&lt;/i&gt; Web Test&lt;/b&gt;</Caption>

        <Width>184</Width>
        <Top>8</Top>
        <StyleClass>h1</StyleClass>
        <Left>32</Left>
        <Height>32</Height>
        <Transparent>True</Transparent>
        <Name>W3Label1</Name>

Changes to SQLite3/Samples/29 - SmartMobileStudio Client/SmartTests.pas.

1
2
3
4
5
6
7
8

9
10
11
12
13
14
15
..
56
57
58
59
60
61
62
63
64

65
66
67
68
69
70
71
...
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
unit SmartTests;

interface

uses 
  SmartCL.System,
  System.Types,
  w3c.date,

  SynCrossPlatformSpecific,
  SynCrossPlatformREST,
  SynCrossPlatformCrypto;

procedure TestSMS;

procedure ORMTest(client: TSQLRestClientURI);
................................................................................
var D: TDateTime;
    i: integer;
    s,x: string;
    T: TTimeLog;
begin
  s := '2014-06-28T11:50:22';
  D := Iso8601ToDateTime(s);
  assert(Abs(D-41818.49331)<OneSecDateTime);
  assert(DateTimeToIso8601(D)=s);

  x := TTimeLogToIso8601(135181810838);
  assert(x=s);
  T := DateTimeToTTimeLog(D);
  assert(T=135181810838);
  D := Now/20+Random*20; // some starting random date/time
  for i := 1 to 2000 do begin
    Test(D);
................................................................................
var people: TSQLRecordPeople;
    Call: TSQLRestURIParams;
    res: TIntegerDynArray;
    i,id: integer;
begin // all this is run in synchronous mode -> only 200 records in the set
  client.CallBackGet('DropTable',[],Call,TSQLRecordPeople);
  assert(client.InternalState>0);
  assert(Call.OutStatus=HTML_SUCCESS);
  client.BatchStart(TSQLRecordPeople);
  people := TSQLRecordPeople.Create;
  assert(people.InternalState=0);
  for i := 1 to 200 do begin
    people.FirstName := 'First'+IntToStr(i);
    people.LastName := 'Last'+IntToStr(i);
    people.YearOfBirth := i+1800;
    people.YearOfDeath := i+1825;
    assert(client.BatchAdd(people,true)=i-1);
    assert(people.InternalState=0);
  end;
  assert(client.BatchSend(res)=HTML_SUCCESS);
  assert(length(res)=200);
  for i := 1 to 200 do
    assert(res[i-1]=i);
  people := TSQLRecordPeople.CreateAndFillPrepare(client,'','',[]);
  assert(people.InternalState=0);
  id := 0;
  while people.FillOne do begin






|
>







 







<

>







 







|











|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
..
57
58
59
60
61
62
63

64
65
66
67
68
69
70
71
72
...
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
unit SmartTests;

interface

uses 
  SmartCL.System,
  System.Types,
  ECMA.Date,
  System.Date,
  SynCrossPlatformSpecific,
  SynCrossPlatformREST,
  SynCrossPlatformCrypto;

procedure TestSMS;

procedure ORMTest(client: TSQLRestClientURI);
................................................................................
var D: TDateTime;
    i: integer;
    s,x: string;
    T: TTimeLog;
begin
  s := '2014-06-28T11:50:22';
  D := Iso8601ToDateTime(s);

  assert(DateTimeToIso8601(D)=s);
  assert(Abs(D-41818.40997685185)<OneSecDateTime);
  x := TTimeLogToIso8601(135181810838);
  assert(x=s);
  T := DateTimeToTTimeLog(D);
  assert(T=135181810838);
  D := Now/20+Random*20; // some starting random date/time
  for i := 1 to 2000 do begin
    Test(D);
................................................................................
var people: TSQLRecordPeople;
    Call: TSQLRestURIParams;
    res: TIntegerDynArray;
    i,id: integer;
begin // all this is run in synchronous mode -> only 200 records in the set
  client.CallBackGet('DropTable',[],Call,TSQLRecordPeople);
  assert(client.InternalState>0);
  assert(Call.OutStatus=HTTP_SUCCESS);
  client.BatchStart(TSQLRecordPeople);
  people := TSQLRecordPeople.Create;
  assert(people.InternalState=0);
  for i := 1 to 200 do begin
    people.FirstName := 'First'+IntToStr(i);
    people.LastName := 'Last'+IntToStr(i);
    people.YearOfBirth := i+1800;
    people.YearOfDeath := i+1825;
    assert(client.BatchAdd(people,true)=i-1);
    assert(people.InternalState=0);
  end;
  assert(client.BatchSend(res)=HTTP_SUCCESS);
  assert(length(res)=200);
  for i := 1 to 200 do
    assert(res[i-1]=i);
  people := TSQLRecordPeople.CreateAndFillPrepare(client,'','',[]);
  assert(people.InternalState=0);
  id := 0;
  while people.FillOne do begin

Changes to SQLite3/Samples/29 - SmartMobileStudio Client/WebForm.sproj.

1
2
3
4
5
6
7
8
9
10
11
12

13
14
15
16
17










18
19
20
21
22
23
24
..
28
29
30
31
32
33
34

35
36
37
38

39
40
41
42
43
44

45
46
47
48
49
50
51

52

53
54
55
56
57
58
59
60
61
62
63
64
65


66
67
68
69
70
71
72
73
74
..
91
92
93
94
95
96
97








98
99
<SMART>
  <Project version="2" subversion="1">
    <Name>WebForm</Name>
    <Created>T00:00:00.000</Created>
    <Modified>2014-09-08T18:54:45.673</Modified>
    <Author>A. Bouchez</Author>
    <Company>Synopse</Company>
    <Version>
      <Major>1</Major>
      <Minor>18</Minor>
      <Revision>0</Revision>
    </Version>

    <Apple>
      <FormatDetection>0</FormatDetection>
      <StatusBarStyle>default</StatusBarStyle>
      <WebAppCapable>1</WebAppCapable>
    </Apple>










    <Options>
      <PostBuild>
        <Enabled>0</Enabled>
        <Script>//CopyFile(&apos;.\www\index.html&apos;,&apos;.\www\debug\index.html&apos;,true);</Script>
      </PostBuild>
      <Compiler>
        <Assertions>1</Assertions>
................................................................................
      <Codegen>
        <Obfuscation>0</Obfuscation>
        <RangeChecking>0</RangeChecking>
        <InstanceChecking>0</InstanceChecking>
        <ConditionChecking>1</ConditionChecking>
        <LoopChecking>1</LoopChecking>
        <InlineMagics>1</InlineMagics>

        <EmitSourceLocation>1</EmitSourceLocation>
        <EmitRTTI>0</EmitRTTI>
        <Devirtualize>1</Devirtualize>
        <MainBody>1</MainBody>

        <SmartLinking>1</SmartLinking>
        <Verbosity>1</Verbosity>
      </Codegen>
      <ConditionalDefines>
        <HandleExceptions>1</HandleExceptions>
        <AutoRefresh>0</AutoRefresh>

      </ConditionalDefines>
      <Linker>
        <SourceMap>0</SourceMap>
        <CompressCSS>1</CompressCSS>
        <GenerateMobileManifest>1</GenerateMobileManifest>
        <GenerateChromeManifest>0</GenerateChromeManifest>
        <GenerateFireFoxManifest>0</GenerateFireFoxManifest>

        <GenerateWidgetPackageConfigXML>0</GenerateWidgetPackageConfigXML>

        <ExternalCSS>0</ExternalCSS>
        <Theme>default.css</Theme>
        <CustomTheme>0</CustomTheme>
        <EmbedJavaScript>1</EmbedJavaScript>
      </Linker>
      <Output>
        <HtmlFileName>index.html</HtmlFileName>
        <OutputFilePath>www\</OutputFilePath>
      </Output>
      <Import />
      <Execute>
        <ServeManifest>0</ServeManifest>
        <Server>1</Server>


        <PauseAfterExecution>0</PauseAfterExecution>
        <ExecuteType>0</ExecuteType>
        <ExecuteableParams>%output%</ExecuteableParams>
      </Execute>
    </Options>
    <Files>
      <File type="main">
        <Name>WebForm</Name>
        <Created>2014-07-11T23:12:17.628Z</Created>
................................................................................
          <IsMainForm>1</IsMainForm>
          <Order>1</Order>
        </AutoCreate>
      </File>
    </Files>
    <Target>Browser</Target>
    <Generator>Visual Component Project</Generator>








  </Project>
</SMART>
|


|







>
|
|
|
|
|
>
>
>
>
>
>
>
>
>
>







 







>




>






>




|
|

>

>













>
>

|







 







>
>
>
>
>
>
>
>


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
..
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
...
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
<SMART>
  <Project version="2" subversion="2">
    <Name>WebForm</Name>
    <Created>T00:00:00.000</Created>
    <Modified>2016-07-27T16:44:14.890</Modified>
    <Author>A. Bouchez</Author>
    <Company>Synopse</Company>
    <Version>
      <Major>1</Major>
      <Minor>18</Minor>
      <Revision>0</Revision>
    </Version>
    <VendorSpecific>
      <Apple>
        <FormatDetection>0</FormatDetection>
        <StatusBarStyle>default</StatusBarStyle>
        <WebAppCapable>1</WebAppCapable>
      </Apple>
      <ChromeApp>
        <Kiosk>0</Kiosk>
        <KioskOnly>1</KioskOnly>
        <OfflineEnabled>1</OfflineEnabled>
      </ChromeApp>
      <Cordova>
        <WidgetID>com.smartmobilestudio.app</WidgetID>
        <AllowIntent>http://*/*&#13;&#10;https://*/*&#13;&#10;tel:*&#13;&#10;sms:*&#13;&#10;mailto:*&#13;&#10;geo:*</AllowIntent>
      </Cordova>
    </VendorSpecific>
    <Options>
      <PostBuild>
        <Enabled>0</Enabled>
        <Script>//CopyFile(&apos;.\www\index.html&apos;,&apos;.\www\debug\index.html&apos;,true);</Script>
      </PostBuild>
      <Compiler>
        <Assertions>1</Assertions>
................................................................................
      <Codegen>
        <Obfuscation>0</Obfuscation>
        <RangeChecking>0</RangeChecking>
        <InstanceChecking>0</InstanceChecking>
        <ConditionChecking>1</ConditionChecking>
        <LoopChecking>1</LoopChecking>
        <InlineMagics>1</InlineMagics>
        <IgnorePublishedInImplementation>0</IgnorePublishedInImplementation>
        <EmitSourceLocation>1</EmitSourceLocation>
        <EmitRTTI>0</EmitRTTI>
        <Devirtualize>1</Devirtualize>
        <MainBody>1</MainBody>
        <CodePacking>0</CodePacking>
        <SmartLinking>1</SmartLinking>
        <Verbosity>1</Verbosity>
      </Codegen>
      <ConditionalDefines>
        <HandleExceptions>1</HandleExceptions>
        <AutoRefresh>0</AutoRefresh>
        <LegacySupportForIE>0</LegacySupportForIE>
      </ConditionalDefines>
      <Linker>
        <SourceMap>0</SourceMap>
        <CompressCSS>1</CompressCSS>
        <GenerateAppCacheManifest>1</GenerateAppCacheManifest>
        <GenerateChromeAppManifest>0</GenerateChromeAppManifest>
        <GenerateFireFoxManifest>0</GenerateFireFoxManifest>
        <GenerateWebAppManifest>1</GenerateWebAppManifest>
        <GenerateWidgetPackageConfigXML>0</GenerateWidgetPackageConfigXML>
        <GenerateCordovaConfigXML>1</GenerateCordovaConfigXML>
        <ExternalCSS>0</ExternalCSS>
        <Theme>default.css</Theme>
        <CustomTheme>0</CustomTheme>
        <EmbedJavaScript>1</EmbedJavaScript>
      </Linker>
      <Output>
        <HtmlFileName>index.html</HtmlFileName>
        <OutputFilePath>www\</OutputFilePath>
      </Output>
      <Import />
      <Execute>
        <ServeManifest>0</ServeManifest>
        <Server>1</Server>
        <CustomFile></CustomFile>
        <LoadCustomFile>0</LoadCustomFile>
        <PauseAfterExecution>0</PauseAfterExecution>
        <ExecuteType>1</ExecuteType>
        <ExecuteableParams>%output%</ExecuteableParams>
      </Execute>
    </Options>
    <Files>
      <File type="main">
        <Name>WebForm</Name>
        <Created>2014-07-11T23:12:17.628Z</Created>
................................................................................
          <IsMainForm>1</IsMainForm>
          <Order>1</Order>
        </AutoCreate>
      </File>
    </Files>
    <Target>Browser</Target>
    <Generator>Visual Component Project</Generator>
    <Statistics>
      <BackgroundCompilations>36</BackgroundCompilations>
      <EditTime>00:03:35.206</EditTime>
      <CompileTime>00:00:34.311</CompileTime>
      <TotalTime>01:24:55.433</TotalTime>
      <DesigningTime>00:04:05.595</DesigningTime>
      <RunningTime>00:44:02.193</RunningTime>
    </Statistics>
  </Project>
</SMART>

Changes to SQLite3/Samples/30 - MVC Server/MVCViewModel.pas.

200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
...
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
...
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
...
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
      article.Abstract := TSynTestCase.RandomTextParagraph(30,'!');
      article.Content := TSynTestCase.RandomTextParagraph(200,'.','http://synopse.info');
      article.Tags := nil;
      for t := 1 to Random(6) do
        article.TagsAddOrdered(tags[random(length(tags))],fTagsLookup);
      batch.Add(article,true);
    end;
    if RestModel.BatchSend(batch,articles)=HTML_SUCCESS then begin
      fTagsLookup.SaveOccurence(RestModel);
      comment.Author := article.Author;
      comment.AuthorName := article.AuthorName;
      batch.Reset(TSQLComment,20000);
      for n := 1 to FAKEDATA_ARTICLESCOUNT*2 do begin
        comment.Article := Pointer(articles[random(length(articles))]);
        comment.Title := TSynTestCase.RandomTextParagraph(5,' ');
................................................................................
    Author := RestModel.RetrieveDocVariant(
      TSQLAuthor,'RowID=?',[Article.Author.ID],'FirstName,FamilyName');
    if WithComments then begin
      Comments.Free; // we will override the TObjectList created at input
      Comments := RestModel.RetrieveList(TSQLComment,'Article=?',[Article.ID]);
    end;
  end else
    raise EMVCApplication.CreateGotoError(HTML_NOTFOUND);
end;

procedure TBlogApplication.AuthorView(var ID: TID; out Author: TSQLAuthor;
  out Articles: variant);
begin
  RestModel.Retrieve(ID,Author);
  Author.HashedPassword := ''; // no need to publish it
  if Author.ID<>0 then
    Articles := RestModel.RetrieveDocVariantArray(
      TSQLArticle,'','Author=? order by RowId desc limit 50',[ID],ARTICLE_FIELDS) else
    raise EMVCApplication.CreateGotoError(HTML_NOTFOUND);
end;

function TBlogApplication.Login(const LogonName, PlainPassword: RawUTF8): TMVCAction;
var Author: TSQLAuthor;
    SessionInfo: TCookieData;
begin
  if CurrentSession.CheckAndRetrieve<>0 then begin
    GotoError(result,HTML_BADREQUEST);
    exit;
  end;
  Author := TSQLAuthor.Create(RestModel,'LogonName=?',[LogonName]);
  try
    if (Author.ID<>0) and Author.CheckPlainPassword(PlainPassword) then begin
      SessionInfo.AuthorName := Author.LogonName;
      SessionInfo.AuthorID := Author.ID;
................................................................................
  auto := TSQLComment.AutoFree(comm);
  AuthorID := GetLoggedAuthorID(canComment,comm);
  if AuthorID=0 then begin
    GotoError(result,sErrorNeedValidAuthorSession);
    exit;
  end;
  if not RestModel.MemberExists(TSQLArticle,ID) then begin
    GotoError(result,HTML_UNAVAILABLE);
    exit;
  end;
  comm.Title := Title;
  comm.Content := Comment;
  comm.Article := TSQLArticle(ID);
  if comm.FilterAndValidate(RestModel,error) and
     (RestModel.Add(comm,true)<>0) then
    GotoView(result,'ArticleView',['ID',ID,'withComments',true]) else
    GotoView(result,'ArticleView',['ID',ID,'withComments',true,'Scope',_ObjFast([
      'CommentError',error,'CommentTitle',comm.Title,'CommentContent',comm.Content])],
      HTML_BADREQUEST);
end;

function TBlogApplication.ArticleMatch(const Match: RawUTF8): TMVCAction;
begin
  if Match='' then
    GotoError(result,HTML_NOTMODIFIED) else
    GotoView(result,'Default',['scope',_ObjFast(['match',Match])]);
end;

procedure TBlogApplication.ArticleEdit(var ID: TID;
  const Title,Content: RawUTF8; const ValidationError: variant;
  out Article: TSQLArticle);
var AuthorID: PtrUInt;
begin
  AuthorID := GetLoggedAuthorID(canPost,Article);
  if AuthorID=0 then
    raise EMVCApplication.CreateGotoError(sErrorNeedValidAuthorSession);
  if ID<>0 then
    if not RestModel.Retrieve(ID,Article) then
      raise EMVCApplication.CreateGotoError(HTML_UNAVAILABLE) else
    if Article.Author<>pointer(AuthorID) then
      raise EMVCApplication.CreateGotoError(sErrorNeedValidAuthorSession);
  if Title<>'' then
    Article.Title := Title;
  if Content<>'' then
    Article.Content := Content;
end;
................................................................................
  end;
  FlushAnyCache;
  Article.Title := Title;
  Article.Content := Content;
  if not Article.FilterAndValidate(RestModel,error) then
    GotoView(result,'ArticleEdit',
      ['ValidationError',error,'ID',ID,
       'Title',Article.Title,'Content',Article.Content],HTML_BADREQUEST) else
    if Article.ID=0 then begin
      Article.PublishedMonth := TSQLArticle.CurrentPublishedMonth;
      if RestModel.Add(Article,true)<>0 then
        GotoView(result,'ArticleView',['ID',Article.ID],HTML_SUCCESS) else
        GotoError(result,sErrorWriting);
    end else
      RestModel.Update(Article);
end;

{$ifndef ISDELPHI2010}







|







 







|










|







|







 







|










|





|













|







 







|



|







200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
...
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
...
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
...
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
      article.Abstract := TSynTestCase.RandomTextParagraph(30,'!');
      article.Content := TSynTestCase.RandomTextParagraph(200,'.','http://synopse.info');
      article.Tags := nil;
      for t := 1 to Random(6) do
        article.TagsAddOrdered(tags[random(length(tags))],fTagsLookup);
      batch.Add(article,true);
    end;
    if RestModel.BatchSend(batch,articles)=HTTP_SUCCESS then begin
      fTagsLookup.SaveOccurence(RestModel);
      comment.Author := article.Author;
      comment.AuthorName := article.AuthorName;
      batch.Reset(TSQLComment,20000);
      for n := 1 to FAKEDATA_ARTICLESCOUNT*2 do begin
        comment.Article := Pointer(articles[random(length(articles))]);
        comment.Title := TSynTestCase.RandomTextParagraph(5,' ');
................................................................................
    Author := RestModel.RetrieveDocVariant(
      TSQLAuthor,'RowID=?',[Article.Author.ID],'FirstName,FamilyName');
    if WithComments then begin
      Comments.Free; // we will override the TObjectList created at input
      Comments := RestModel.RetrieveList(TSQLComment,'Article=?',[Article.ID]);
    end;
  end else
    raise EMVCApplication.CreateGotoError(HTTP_NOTFOUND);
end;

procedure TBlogApplication.AuthorView(var ID: TID; out Author: TSQLAuthor;
  out Articles: variant);
begin
  RestModel.Retrieve(ID,Author);
  Author.HashedPassword := ''; // no need to publish it
  if Author.ID<>0 then
    Articles := RestModel.RetrieveDocVariantArray(
      TSQLArticle,'','Author=? order by RowId desc limit 50',[ID],ARTICLE_FIELDS) else
    raise EMVCApplication.CreateGotoError(HTTP_NOTFOUND);
end;

function TBlogApplication.Login(const LogonName, PlainPassword: RawUTF8): TMVCAction;
var Author: TSQLAuthor;
    SessionInfo: TCookieData;
begin
  if CurrentSession.CheckAndRetrieve<>0 then begin
    GotoError(result,HTTP_BADREQUEST);
    exit;
  end;
  Author := TSQLAuthor.Create(RestModel,'LogonName=?',[LogonName]);
  try
    if (Author.ID<>0) and Author.CheckPlainPassword(PlainPassword) then begin
      SessionInfo.AuthorName := Author.LogonName;
      SessionInfo.AuthorID := Author.ID;
................................................................................
  auto := TSQLComment.AutoFree(comm);
  AuthorID := GetLoggedAuthorID(canComment,comm);
  if AuthorID=0 then begin
    GotoError(result,sErrorNeedValidAuthorSession);
    exit;
  end;
  if not RestModel.MemberExists(TSQLArticle,ID) then begin
    GotoError(result,HTTP_UNAVAILABLE);
    exit;
  end;
  comm.Title := Title;
  comm.Content := Comment;
  comm.Article := TSQLArticle(ID);
  if comm.FilterAndValidate(RestModel,error) and
     (RestModel.Add(comm,true)<>0) then
    GotoView(result,'ArticleView',['ID',ID,'withComments',true]) else
    GotoView(result,'ArticleView',['ID',ID,'withComments',true,'Scope',_ObjFast([
      'CommentError',error,'CommentTitle',comm.Title,'CommentContent',comm.Content])],
      HTTP_BADREQUEST);
end;

function TBlogApplication.ArticleMatch(const Match: RawUTF8): TMVCAction;
begin
  if Match='' then
    GotoError(result,HTTP_NOTMODIFIED) else
    GotoView(result,'Default',['scope',_ObjFast(['match',Match])]);
end;

procedure TBlogApplication.ArticleEdit(var ID: TID;
  const Title,Content: RawUTF8; const ValidationError: variant;
  out Article: TSQLArticle);
var AuthorID: PtrUInt;
begin
  AuthorID := GetLoggedAuthorID(canPost,Article);
  if AuthorID=0 then
    raise EMVCApplication.CreateGotoError(sErrorNeedValidAuthorSession);
  if ID<>0 then
    if not RestModel.Retrieve(ID,Article) then
      raise EMVCApplication.CreateGotoError(HTTP_UNAVAILABLE) else
    if Article.Author<>pointer(AuthorID) then
      raise EMVCApplication.CreateGotoError(sErrorNeedValidAuthorSession);
  if Title<>'' then
    Article.Title := Title;
  if Content<>'' then
    Article.Content := Content;
end;
................................................................................
  end;
  FlushAnyCache;
  Article.Title := Title;
  Article.Content := Content;
  if not Article.FilterAndValidate(RestModel,error) then
    GotoView(result,'ArticleEdit',
      ['ValidationError',error,'ID',ID,
       'Title',Article.Title,'Content',Article.Content],HTTP_BADREQUEST) else
    if Article.ID=0 then begin
      Article.PublishedMonth := TSQLArticle.CurrentPublishedMonth;
      if RestModel.Add(Article,true)<>0 then
        GotoView(result,'ArticleView',['ID',Article.ID],HTTP_SUCCESS) else
        GotoError(result,sErrorWriting);
    end else
      RestModel.Update(Article);
end;

{$ifndef ISDELPHI2010}

Changes to SQLite3/mORMot.pas.

795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
...
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
....
4537
4538
4539
4540
4541
4542
4543
4544
4545
4546
4547
4548
4549
4550
4551
4552
4553
4554
4555
4556
4557
4558
4559
4560
4561
4562
4563
4564
4565
4566
4567
4568
4569
4570
4571
4572
4573
4574
4575
4576
4577
4578
4579
4580
4581
4582
4583
4584
4585
4586
4587
4588
4589
4590
4591
4592
4593
4594
4595
4596
4597
4598
4599
4600
4601
4602
4603
4604
4605
....
4617
4618
4619
4620
4621
4622
4623
4624
4625
4626
4627
4628
4629
4630
4631
4632
4633
4634
4635
....
5548
5549
5550
5551
5552
5553
5554
5555
5556
5557
5558
5559
5560
5561
5562
5563
5564
5565
5566
5567
5568
5569
5570
....
5894
5895
5896
5897
5898
5899
5900
5901
5902
5903
5904
5905
5906
5907
5908
....
6178
6179
6180
6181
6182
6183
6184
6185
6186
6187
6188
6189
6190
6191
6192
6193
6194
6195
6196
6197
6198
6199
6200
6201
6202
6203
6204
6205
6206
6207
6208
6209
6210
6211
6212
6213
6214
6215
6216
6217
6218
6219
6220
6221
6222
6223
6224
6225
6226
6227
6228
6229
6230
6231
6232
6233
6234
....
6247
6248
6249
6250
6251
6252
6253
6254
6255
6256
6257
6258
6259
6260
6261
6262
6263
6264
6265
6266
6267
6268
6269
6270
6271
6272
6273
6274
6275
6276
6277
6278
6279
6280
6281
6282
6283
6284
6285
6286
6287
6288
6289
6290
6291
6292
6293
6294
6295
6296
....
6430
6431
6432
6433
6434
6435
6436
6437
6438
6439
6440
6441
6442
6443
6444
.....
10455
10456
10457
10458
10459
10460
10461
10462
10463
10464
10465
10466
10467
10468
10469
10470
10471
10472
10473
10474
10475
10476
10477
10478
10479
.....
11851
11852
11853
11854
11855
11856
11857
11858
11859
11860
11861
11862
11863
11864
11865
.....
11910
11911
11912
11913
11914
11915
11916
11917
11918
11919
11920
11921
11922
11923
11924
.....
12208
12209
12210
12211
12212
12213
12214
12215
12216
12217
12218
12219
12220
12221
12222
12223
12224
12225
12226
.....
13950
13951
13952
13953
13954
13955
13956
13957
13958
13959
13960
13961
13962
13963
13964
13965
13966
.....
14936
14937
14938
14939
14940
14941
14942
14943
14944
14945
14946
14947
14948
14949
14950
.....
15399
15400
15401
15402
15403
15404
15405
15406
15407
15408
15409
15410
15411
15412
15413
.....
15618
15619
15620
15621
15622
15623
15624
15625
15626
15627
15628
15629
15630
15631
15632
15633
15634
15635
.....
15883
15884
15885
15886
15887
15888
15889
15890
15891
15892
15893
15894
15895
15896
15897
.....
16525
16526
16527
16528
16529
16530
16531
16532
16533
16534
16535
16536
16537
16538
16539
.....
16638
16639
16640
16641
16642
16643
16644
16645
16646
16647
16648
16649
16650
16651
16652
.....
17506
17507
17508
17509
17510
17511
17512
17513
17514
17515
17516
17517
17518
17519
17520
17521
17522
17523
17524
17525
17526
17527
17528
17529
17530
17531
17532
17533
17534
17535
17536
17537
17538
17539
17540
17541
17542
17543
17544
17545
17546
17547
17548
17549
.....
17770
17771
17772
17773
17774
17775
17776
17777
17778
17779
17780
17781
17782
17783
17784
.....
17864
17865
17866
17867
17868
17869
17870
17871
17872
17873
17874
17875
17876
17877
17878
.....
18032
18033
18034
18035
18036
18037
18038
18039
18040
18041
18042
18043
18044
18045
18046
18047
18048
18049
18050
18051
18052
18053
18054
18055
18056
18057
18058
18059
18060
18061
18062
.....
18066
18067
18068
18069
18070
18071
18072
18073
18074
18075
18076
18077
18078
18079
18080
18081
18082
18083
18084
18085
18086
18087
.....
18180
18181
18182
18183
18184
18185
18186
18187
18188
18189
18190
18191
18192
18193
18194
18195
18196
18197
18198
18199
18200
18201
.....
18241
18242
18243
18244
18245
18246
18247
18248
18249
18250
18251
18252
18253
18254
18255
.....
18309
18310
18311
18312
18313
18314
18315
18316
18317
18318
18319
18320
18321
18322
18323
.....
18400
18401
18402
18403
18404
18405
18406
18407
18408
18409
18410
18411
18412
18413
18414
.....
19219
19220
19221
19222
19223
19224
19225
19226
19227
19228
19229
19230
19231
19232
19233
.....
22926
22927
22928
22929
22930
22931
22932
22933
22934
22935
22936
22937
22938
22939
22940
22941
22942
22943
22944
22945
22946
22947
22948
22949
22950
22951
22952
22953
22954
22955
22956
22957
22958
22959
22960
22961
22962
22963
22964
22965
22966
22967
22968
22969
22970
22971
22972
22973
22974
22975
22976
22977
22978
22979
22980
22981
.....
33582
33583
33584
33585
33586
33587
33588
33589
33590
33591
33592
33593
33594
33595
33596
33597
33598
33599
33600
33601
33602
33603
33604
33605
33606
.....
34994
34995
34996
34997
34998
34999
35000
35001
35002
35003
35004
35005
35006
35007
35008
35009
35010
35011
35012
35013
35014
35015
35016
35017
35018
35019
35020
35021
35022
35023
35024
35025
35026
35027
35028
35029
35030
35031
35032
.....
35039
35040
35041
35042
35043
35044
35045
35046
35047
35048
35049
35050
35051
35052
35053
35054
35055
35056
35057
35058
35059
35060
35061
35062
35063
35064
35065
35066
35067
35068
35069
35070
35071
35072
35073
35074
35075
35076
35077
.....
35149
35150
35151
35152
35153
35154
35155
35156
35157
35158
35159
35160
35161
35162
35163
35164
35165
35166
35167
35168
35169
35170
35171
35172
35173
35174
35175
35176
35177
.....
35187
35188
35189
35190
35191
35192
35193
35194
35195
35196
35197
35198
35199
35200
35201
35202
35203
35204
35205
35206
35207
35208
35209
35210
35211
35212
35213
35214
35215
.....
35372
35373
35374
35375
35376
35377
35378
35379
35380
35381
35382
35383
35384
35385
35386
.....
35418
35419
35420
35421
35422
35423
35424
35425
35426
35427
35428
35429
35430
35431
35432
35433
35434
35435
35436
35437
35438
35439
.....
35592
35593
35594
35595
35596
35597
35598
35599
35600
35601
35602
35603
35604
35605
35606
35607
.....
35624
35625
35626
35627
35628
35629
35630
35631
35632
35633
35634
35635
35636
35637
35638
.....
35688
35689
35690
35691
35692
35693
35694
35695
35696
35697
35698
35699
35700
35701
35702
.....
35732
35733
35734
35735
35736
35737
35738
35739
35740
35741
35742
35743
35744
35745
35746
35747
35748
35749
35750
35751
35752
35753
35754
35755
.....
35764
35765
35766
35767
35768
35769
35770
35771
35772
35773
35774
35775
35776
35777
35778
35779
35780
35781
35782
35783
35784
35785
35786
35787
35788
35789
35790
35791
35792
35793
35794
35795
.....
35796
35797
35798
35799
35800
35801
35802
35803
35804
35805
35806
35807
35808
35809
35810
35811
35812
35813
35814
35815
35816
35817
35818
35819
35820
35821
35822
35823
35824
35825
35826
35827
35828
35829
.....
35840
35841
35842
35843
35844
35845
35846
35847
35848
35849
35850
35851
35852
35853
35854
.....
35926
35927
35928
35929
35930
35931
35932
35933
35934
35935
35936
35937
35938
35939
35940
.....
35947
35948
35949
35950
35951
35952
35953
35954
35955
35956
35957
35958
35959
35960
35961
.....
35972
35973
35974
35975
35976
35977
35978
35979
35980
35981
35982
35983
35984
35985
35986
35987
35988
35989
35990
35991
35992
35993
35994
35995
35996
35997
35998
35999
36000
36001
36002
36003
36004
36005
36006
36007
36008
36009
36010
.....
36012
36013
36014
36015
36016
36017
36018
36019
36020
36021
36022
36023
36024
36025
36026
36027
36028
36029
36030
36031
36032
36033
36034
36035
36036
36037
36038
36039
36040
36041
36042
36043
36044
36045
36046
36047
36048
36049
36050
36051
36052
36053
36054
36055
36056
36057
36058
36059
36060
36061
36062
36063
36064
36065
36066
36067
36068
36069
36070
36071
36072
36073
36074
36075
36076
36077
36078
36079
36080
36081
36082
36083
36084
36085
36086
36087
.....
36088
36089
36090
36091
36092
36093
36094
36095
36096
36097
36098
36099
36100
36101
36102
.....
36163
36164
36165
36166
36167
36168
36169
36170
36171
36172
36173
36174
36175
36176
36177
.....
36194
36195
36196
36197
36198
36199
36200
36201
36202
36203
36204
36205
36206
36207
36208
.....
36326
36327
36328
36329
36330
36331
36332
36333
36334
36335
36336
36337
36338
36339
36340
36341
36342
36343
36344
36345
36346
36347
36348
36349
36350
36351
.....
37007
37008
37009
37010
37011
37012
37013
37014
37015
37016
37017
37018
37019
37020
37021
.....
37838
37839
37840
37841
37842
37843
37844
37845
37846
37847
37848
37849
37850
37851
37852
.....
37858
37859
37860
37861
37862
37863
37864
37865
37866
37867
37868
37869
37870
37871
37872
.....
37963
37964
37965
37966
37967
37968
37969
37970
37971
37972
37973
37974
37975
37976
37977
.....
38121
38122
38123
38124
38125
38126
38127
38128
38129
38130
38131
38132
38133
38134
38135
.....
38231
38232
38233
38234
38235
38236
38237
38238
38239
38240
38241
38242
38243
38244
38245
38246
38247
38248
38249
38250
38251
38252
.....
38258
38259
38260
38261
38262
38263
38264
38265
38266
38267
38268
38269
38270
38271
38272
38273
38274
38275
38276
38277
38278
38279
38280
38281
38282
38283
38284
38285
38286
38287
38288
38289
38290
38291
38292
38293
38294
38295
38296
38297
38298
38299
38300
38301
38302
.....
38313
38314
38315
38316
38317
38318
38319
38320
38321
38322
38323
38324
38325
38326
38327
38328
38329
.....
38378
38379
38380
38381
38382
38383
38384
38385
38386
38387
38388
38389
38390
38391
38392
.....
38405
38406
38407
38408
38409
38410
38411
38412
38413
38414
38415
38416
38417
38418
38419
38420
38421
38422
38423
38424
38425
38426
38427
38428
38429
38430
38431
38432
38433
38434
38435
38436
38437
.....
38464
38465
38466
38467
38468
38469
38470
38471
38472
38473
38474
38475
38476
38477
38478
38479
38480
38481
38482
38483
38484
38485
38486
38487
38488
38489
38490
38491
38492
38493
38494
38495
38496
38497
38498
38499
38500
.....
38518
38519
38520
38521
38522
38523
38524
38525
38526
38527
38528
38529
38530
38531
38532
38533
38534
38535
38536
38537
38538
38539
38540
38541
38542
38543
38544
38545
38546
38547
38548
38549
38550
38551
38552
38553
38554
38555
38556
38557
38558
38559
38560
38561
38562
38563
38564
38565
38566
38567
38568
38569
.....
38573
38574
38575
38576
38577
38578
38579
38580
38581
38582
38583
38584
38585
38586
38587
38588
38589
38590
38591
38592
38593
38594
38595
38596
38597
38598
38599
38600
38601
38602
38603
38604
38605
38606
38607
38608
38609
38610
38611
38612
38613
38614
.....
38972
38973
38974
38975
38976
38977
38978
38979
38980
38981
38982
38983
38984
38985
38986
38987
38988
38989
38990
38991
38992
38993
.....
39026
39027
39028
39029
39030
39031
39032
39033
39034
39035
39036
39037
39038
39039
39040
39041
39042
39043
39044
39045
39046
39047
39048
39049
39050
39051
39052
.....
39070
39071
39072
39073
39074
39075
39076
39077
39078
39079
39080
39081
39082
39083
39084
39085
.....
39414
39415
39416
39417
39418
39419
39420
39421
39422
39423
39424
39425
39426
39427
39428
39429
39430
39431
39432
39433
39434
39435
39436
39437
39438
39439
39440
.....
39460
39461
39462
39463
39464
39465
39466
39467
39468
39469
39470
39471
39472
39473
39474
39475
39476
39477
39478
39479
39480
39481
39482
39483
39484
39485
39486
39487
.....
39796
39797
39798
39799
39800
39801
39802
39803
39804
39805
39806
39807
39808
39809
39810
.....
39828
39829
39830
39831
39832
39833
39834
39835
39836
39837
39838
39839
39840
39841
39842
39843
39844
39845
39846
39847
39848
39849
.....
41001
41002
41003
41004
41005
41006
41007
41008
41009
41010
41011
41012
41013
41014
41015
41016
41017
41018
41019
41020
41021
41022
41023
41024
41025
41026
41027
41028
41029
41030
41031
41032
41033
41034
41035
41036
.....
41064
41065
41066
41067
41068
41069
41070
41071
41072
41073
41074
41075
41076
41077
41078
.....
41171
41172
41173
41174
41175
41176
41177
41178
41179
41180
41181
41182
41183
41184
41185
.....
41198
41199
41200
41201
41202
41203
41204
41205
41206
41207
41208
41209
41210
41211
41212
.....
41297
41298
41299
41300
41301
41302
41303
41304
41305
41306
41307
41308
41309
41310
41311
.....
41723
41724
41725
41726
41727
41728
41729
41730
41731
41732
41733
41734
41735
41736
41737
.....
41767
41768
41769
41770
41771
41772
41773
41774
41775
41776
41777
41778
41779
41780
41781
41782
41783
41784
41785
41786
41787
41788
41789
41790
41791
41792
41793
41794
.....
42114
42115
42116
42117
42118
42119
42120
42121
42122
42123
42124
42125
42126
42127
42128
.....
44258
44259
44260
44261
44262
44263
44264
44265
44266
44267
44268
44269
44270
44271
44272
.....
46719
46720
46721
46722
46723
46724
46725
46726
46727
46728
46729
46730
46731
46732
46733
.....
46755
46756
46757
46758
46759
46760
46761
46762
46763
46764
46765
46766
46767
46768
46769
46770
46771
46772
46773
46774
46775
46776
46777
46778
46779
46780
46781
46782
46783
46784
46785
.....
46789
46790
46791
46792
46793
46794
46795
46796
46797
46798
46799
46800
46801
46802
46803
.....
49648
49649
49650
49651
49652
49653
49654
49655
49656
49657
49658
49659
49660
49661
49662
.....
50050
50051
50052
50053
50054
50055
50056
50057
50058
50059
50060
50061
50062
50063
50064
.....
50089
50090
50091
50092
50093
50094
50095
50096
50097
50098
50099
50100
50101
50102
50103
.....
50122
50123
50124
50125
50126
50127
50128
50129
50130
50131
50132
50133
50134
50135
50136
.....
50152
50153
50154
50155
50156
50157
50158
50159
50160
50161
50162
50163
50164
50165
50166
.....
54994
54995
54996
54997
54998
54999
55000
55001
55002
55003
55004
55005
55006
55007
55008
.....
55059
55060
55061
55062
55063
55064
55065
55066
55067
55068
55069
55070
55071
55072
55073
55074
55075
55076
55077
55078
55079
55080
55081
55082
55083
55084
55085
55086
55087
55088
55089
55090
55091
.....
55133
55134
55135
55136
55137
55138
55139
55140
55141
55142
55143
55144
55145
55146
55147
55148
55149
55150
55151
55152
55153
55154
55155
55156
55157
.....
56777
56778
56779
56780
56781
56782
56783
56784
56785
56786
56787
56788
56789
56790
56791
.....
57251
57252
57253
57254
57255
57256
57257
57258
57259
57260
57261
57262
57263
57264
57265
57266
57267
57268
57269
      (can be useful e.g. when consuming services from JavaScript)
    - interface-based services can now return the result value as XML object
      instead of JSON array or object if TServiceFactoryServer.ResultAsJSONObject
      is set (can be useful e.g. when consuming services from XML only clients) -
      as an alternative, ResultAsXMLObjectIfAcceptOnlyXML option will recognize
      'Accept: application/xml' or 'Accept: text/xml' HTTP header and return
      XML content instead of JSON - with optional ResultAsXMLObjectNameSpace
    - added TServiceCustomAnswer.Status member to override default HTML_SUCCESS
    - new TSQLRest.Service<T: IInterface> method to retrieve a service instance
    - added TServiceMethodArgument.AddJSON/AddValueJSON/AddDefaultJSON methods
    - method-based services are now able to handle "304 Not Modified" optimized
      response to save bandwidth, in TSQLRestServerURIContext.Returns/Results
    - added TSQLRestServerURIContext.ReturnFile() and ReturnFileFromFolder()
      methods, for direct fast transmission to a HTTP client, handling
      "304 Not Modified" and proper mime type recognition
................................................................................
      callbacks, and TSQLRestServerURIContext.AuthenticationFailed virtual method
    - added TSQLRestServer.SessionClass property to specify the class type
      to handle in-memory sessions, and override e.g. IsValidURI() method
    - CreateMissingTables() method is now declared as virtual in TSQLRestServer
    - TSQLRestServer.URI() and TSQLRestClientURI.InternalURI() methods now uses
      one TSQLRestURIParams parameter for all request input and output values
    - TSQLRestServer.URI() method will return "405 Method Not Allowed" error
      (HTML_NOTALLOWED) if the supplied URI does not match RestAccessRights
    - TSQLRestServer.URI() will now handle POST/PUT/DELETE ModelRoot/MethodName
      as method-based services
    - added TSQLRestServerFullMemory.Flush method-based service
    - added TSQLRestServerFullMemory.DropDatabase method
    - TSQLRestServerFullMemory now generates its expected InternalState value
    - completed HTML_* constant list and messages - feature request [d8de3eb76a]
    - handle HTML_NOTMODIFIED and HTML_TEMPORARYREDIRECT as successful status -
      as expected by feature request [5d2634e8a3]
    - enhanced sllAuth session creation/deletion logged information
    - introducing TSQLRest.LogClass property, allowing to set a custom log class
    - added TAuthSession.SentHeaders, RemoteIP and ConnectionID properties
    - added process of Variant and WideString types in TSQLRecord properties,
      including any custom type, like TDocVariant or TBSONVariant (for MongoDB
      objects), or even a dynamic array of variants (see [d9d091baab])
................................................................................
      Level: TSynLogInfo = sllNone);
    /// optional service locator for by-key Dependency Injection
    property OnKeyResolve: TOnKeyResolve read fOnKeyResolve write fOnKeyResolve;
  end;

const
  /// HTML Status Code for "Continue"
  HTML_CONTINUE = 100;
  /// HTML Status Code for "Switching Protocols"
  HTML_SWITCHINGPROTOCOLS = 101;
  /// HTML Status Code for "Success"
  HTML_SUCCESS = 200;
  /// HTML Status Code for "Created"
  HTML_CREATED = 201;
  /// HTML Status Code for "Accepted"
  HTML_ACCEPTED = 202;
  /// HTML Status Code for "Non-Authoritative Information"
  HTML_NONAUTHORIZEDINFO = 203;
  /// HTML Status Code for "No Content"
  HTML_NOCONTENT = 204;
  /// HTML Status Code for "Multiple Choices"
  HTML_MULTIPLECHOICES = 300;
  /// HTML Status Code for "Moved Permanently"
  HTML_MOVEDPERMANENTLY = 301;
  /// HTML Status Code for "Found"
  HTML_FOUND = 302;
  /// HTML Status Code for "See Other"
  HTML_SEEOTHER = 303;
  /// HTML Status Code for "Not Modified"
  HTML_NOTMODIFIED = 304;
  /// HTML Status Code for "Use Proxy"
  HTML_USEPROXY = 305;
  /// HTML Status Code for "Temporary Redirect"
  HTML_TEMPORARYREDIRECT = 307;
  /// HTML Status Code for "Bad Request"
  HTML_BADREQUEST = 400;
  /// HTML Status Code for "Unauthorized"
  HTML_UNAUTHORIZED = 401;
  /// HTML Status Code for "Forbidden"
  HTML_FORBIDDEN = 403;
  /// HTML Status Code for "Not Found"
  HTML_NOTFOUND = 404;
  // HTML Status Code for "Method Not Allowed"
  HTML_NOTALLOWED = 405;
  // HTML Status Code for "Not Acceptable"
  HTML_NOTACCEPTABLE = 406;
  // HTML Status Code for "Proxy Authentication Required"
  HTML_PROXYAUTHREQUIRED = 407;
  /// HTML Status Code for "Request Time-out"
  HTML_TIMEOUT = 408;
  /// HTML Status Code for "Internal Server Error"
  HTML_SERVERERROR = 500;
  /// HTML Status Code for "Not Implemented"
  HTML_NOTIMPLEMENTED = 501;
  /// HTML Status Code for "Bad Gateway"
  HTML_BADGATEWAY = 502;
  /// HTML Status Code for "Service Unavailable"
  HTML_UNAVAILABLE = 503;
  /// HTML Status Code for "Gateway Timeout"
  HTML_GATEWAYTIMEOUT = 504;
  /// HTML Status Code for "HTTP Version Not Supported"
  HTML_HTTPVERSIONNONSUPPORTED = 505;

  /// you can use this cookie value to delete a cookie on the browser side
  COOKIE_EXPIRED = '; Expires=Sat, 01 Jan 2010 00:00:01 GMT';

  /// used e.g. by THttpApiServer.Request for http.sys to send a static file
  // - the OutCustomHeader should contain the proper 'Content-type: ....'
  // corresponding to the file (e.g. by calling GetMimeContentType() function
................................................................................

  /// HTTP header used e.g. by THttpApiServer.Request for http.sys to send
  // a static file in kernel mode
  STATICFILE_CONTENT_TYPE_HEADER = HEADER_CONTENT_TYPE+STATICFILE_CONTENT_TYPE;
  /// uppercase version of HTTP header for static file content serving
  STATICFILE_CONTENT_TYPE_HEADER_UPPPER = HEADER_CONTENT_TYPE_UPPER+STATICFILE_CONTENT_TYPE;

/// convert any HTML_* constant to a short English text
// - see @http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
procedure StatusCodeToErrorMsg(Code: integer; var result: RawUTF8); overload;

/// convert any HTML_* constant to an integer error code and its English text
// - see @http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
function StatusCodeToErrorMsg(Code: integer): RawUTF8; overload;

/// returns true for SUCCESS (200), CREATED (201), NOCONTENT (204),
// NOTMODIFIED (304) or TEMPORARYREDIRECT (307) codes
function StatusCodeIsSuccess(Code: integer): boolean;
  {$ifdef HASINLINE}inline;{$endif}
................................................................................
    // - but consider also using TSQLRestServerURIContext.InHeader['remoteip']
    InHead: RawUTF8;
    /// input parameter containing the caller message body
    // - e.g. some GET/POST/PUT JSON data can be specified here
    InBody: RawUTF8;
    /// output parameter to be set to the response message header
    // - it is the right place to set the returned message body content type,
    // e.g. TEXT_CONTENT_TYPE_HEADER or HTML_CONTENT_TYPE_HEADER: if not set,
    // the default JSON_CONTENT_TYPE_HEADER will be returned to the client,
    // meaning that the message is JSON
    // - you can use OutBodyType() function to retrieve the stored content-type
    OutHead: RawUTF8;
    /// output parameter to be set to the response message body
    OutBody: RawUTF8;
    /// output parameter to be set to the HTTP status integer code
    // - HTML_NOTFOUND=404 e.g. if the url doesn't start with Model.Root (caller
    // can try another TSQLRestServer)
    OutStatus: cardinal;
    /// output parameter to be set to the database internal state
    OutInternalState: cardinal;
    /// associated RESTful access rights
    // - AccessRights must be handled by the TSQLRestServer child, according
    // to the Application Security Policy (user logging, authentification and
................................................................................
    // - abstract implementation which is to be overridden
    procedure URIDecodeSOAByInterface; virtual; abstract;
    /// process authentication
    // - return FALSE in case of invalid signature, TRUE if authenticated
    function Authenticate: boolean; virtual;
    /// method called in case of authentication failure
    // - the failure origin is stated by the Reason parameter
    // - this default implementation will just set OutStatus := HTML_FORBIDDEN
    // and call TSQLRestServer.OnAuthenticationFailed event (if any)
    procedure AuthenticationFailed(Reason: TNotifyAuthenticationFailedReason); virtual;
    /// direct launch of a method-based service
    // - URI() will ensure that MethodIndex>=0 before calling it
    procedure ExecuteSOAByMethod; virtual;
    /// direct launch of an interface-based service
    // - URI() will ensure that Service<>nil before calling it
................................................................................
    function ClientSQLRecordOptions: TJSONSerializerSQLRecordOptions;
    /// true if called from TSQLRestServer.AdministrationExecute
    function IsRemoteAdministrationExecute: boolean;
    /// compute the file name corresponding to the URI
    // - e.g. '/root/methodname/toto/index.html' will return 'toto\index.html'
    property ResourceFileName: TFileName read GetResourceFileName;
    /// use this method to send back directly a result value to the caller
    // - expects Status to be either HTML_SUCCESS, HTML_NOTMODIFIED,
    // HTML_CREATED, or HTML_TEMPORARYREDIRECT, and will return as answer the
    // supplied Result content with no transformation
    // - if Status is an error code, it will call Error() method
    // - CustomHeader optional parameter can be set e.g. to
    // TEXT_CONTENT_TYPE_HEADER if the default JSON_CONTENT_TYPE is not OK,
    // or calling GetMimeContentTypeHeader() on the returned binary buffer
    // - if Handle304NotModified is TRUE and Status is HTML_SUCCESS, the Result
    // content will be hashed (using crc32c) and in case of no modification
    // will return HTML_NOTMODIFIED to the browser, without the actual result
    // content (to save bandwidth)
    procedure Returns(const Result: RawUTF8; Status: integer=HTML_SUCCESS;
      const CustomHeader: RawUTF8=''; Handle304NotModified: boolean=false;
      HandleErrorAsRegularResult: boolean=false); overload;
    /// use this method to send back a JSON object to the caller
    // - this method will encode the supplied values e.g. as
    // ! JSONEncode(['name','John','year',1972]) = '{"name":"John","year":1972}'
    // - implementation is just a wrapper around Returns(JSONEncode([]))
    // - note that cardinal values should be type-casted to Int64() (otherwise
    // the integer mapped value will be transmitted, therefore wrongly)
    // - expects Status to be either HTML_SUCCESS or HTML_CREATED
    // - caller can set Handle304NotModified=TRUE for Status=HTML_SUCCESS
    procedure Returns(const NameValuePairs: array of const; Status: integer=HTML_SUCCESS;
      Handle304NotModified: boolean=false; HandleErrorAsRegularResult: boolean=false); overload;
    /// use this method to send back any object as JSON document to the caller
    // - this method will call ObjectToJson() to compute the returned content
    // - you can customize SQLRecordOptions, to force the returned JSON
    // object to have its TSQLRecord nested fields serialized as true JSON
    // arrays or objects, or add an "ID_str" string field for JavaScript
    procedure Returns(Value: TObject; Status: integer=HTML_SUCCESS;
      Handle304NotModified: boolean=false;
      SQLRecordOptions: TJSONSerializerSQLRecordOptions=[]); overload;
    /// use this method to send back any variant as JSON to the caller
    // - this method will call VariantSaveJSON() to compute the returned content
    procedure ReturnsJson(const Value: variant; Status: integer=HTML_SUCCESS;
      Handle304NotModified: boolean=false; Escape: TTextWriterKind=twJSONEscape;
      MakeHumanReadable: boolean=false);
    /// uses this method to send back directly any binary content to the caller
    // - the exact MIME type will be retrieved using GetMimeContentTypeHeader(),
    // from the supplied Blob binary buffer, and optional a file name
    // - by default, the HTML_NOTMODIFIED process will take place, to minimize
    // bandwidth between the server and the client
    procedure ReturnBlob(const Blob: RawByteString; Status: integer=HTML_SUCCESS;
      Handle304NotModified: boolean=true; const FileName: TFileName='');
    /// use this method to send back a file to the caller
    // - this method will let the HTTP server return the file content
    // - if Handle304NotModified is TRUE, will check the file age to ensure
    // that the file content will be sent back to the server only if it changed
    // - if ContentType is left to default '', method will guess the expected
    // mime-type from the file name extension
................................................................................
    // - this method will let the HTTP server return the file content
    // - if Handle304NotModified is TRUE, will check the file age to ensure
    // that the file content will be sent back to the server only if it changed
    procedure ReturnFileFromFolder(const FolderName: TFileName;
      Handle304NotModified: boolean=true; const DefaultFileName: TFileName='index.html';
      const Error404Redirect: RawUTF8='');
    /// use this method notify the caller that the resource URI has changed
    // - returns a HTML_TEMPORARYREDIRECT status with the specified location,
    // or HTML_MOVEDPERMANENTLY if PermanentChange is TRUE
    procedure Redirect(const NewLocation: RawUTF8; PermanentChange: boolean=false);
    /// use this method to send back a JSON object with a "result" field
    // - this method will encode the supplied values as a {"result":"...}
    // JSON object, as such for one value:
    // $ {"result":"OneValue"}
    // (with one value, you can just call TSQLRestClientURI.CallBackGetResult
    // method to call and decode this value)
    // or as a JSON object containing an array of values:
    // $ {"result":["One","two"]}
    // - expects Status to be either HTML_SUCCESS or HTML_CREATED
    // - caller can set Handle304NotModified=TRUE for Status=HTML_SUCCESS
    procedure Results(const Values: array of const; Status: integer=HTML_SUCCESS;
      Handle304NotModified: boolean=false);
    /// use this method if the caller expect no data, just a status
    // - just wrap the overloaded Returns() method with no result value
    // - if Status is an error code, it will call Error() method
    // - by default, calling this method will mark process as successfull
    procedure Success(Status: integer=HTML_SUCCESS); virtual;
    /// use this method to send back an error to the caller
    // - expects Status to not be HTML_SUCCESS neither HTML_CREATED,
    // and will send back a JSON error message to the caller, with the
    // supplied error text
    // - if no ErrorMessage is specified, will return a default text
    // corresponding to the Status code
    procedure Error(const ErrorMessage: RawUTF8='';
      Status: integer=HTML_BADREQUEST); overload; virtual;
    /// use this method to send back an error to the caller
    // - implementation is just a wrapper over Error(FormatUTF8(Format,Args))
    procedure Error(const Format: RawUTF8; const Args: array of const;
      Status: integer=HTML_BADREQUEST); overload;
    /// use this method to send back an error to the caller
    // - will serialize the supplied exception, with an optional error message
    procedure Error(E: Exception; const Format: RawUTF8; const Args: array of const;
      Status: integer=HTML_BADREQUEST); overload;
    /// implements a method-based service for live update of some settings
    // - should be called from a method-based service, e.g. Configuration()
    // - the settings are expected to be stored e.g. in a TSynAutoCreateFields
    // instance, potentially with nested objects
    // - accept the following REST methods to read and write the settings:
    // ! GET http://server:888/root/configuration
    // ! GET http://server:888/root/configuration/propname
................................................................................
  // - if process succeeded, implementation shall call Ctxt.Results([]) method to
  // set a JSON response object with one "result" field name or Ctxt.Returns([])
  // with a JSON object described in Name/Value pairs; if the returned value is
  // not JSON_CONTENT_TYPE, use Ctxt.Returns() and its optional CustomHeader
  // parameter can specify a custom header like TEXT_CONTENT_TYPE_HEADER
  // - if process succeeded, and no data is expected to be returned to the caller,
  // implementation shall call overloaded Ctxt.Success() method with the
  // expected status (i.e. just Ctxt.Success will return HTML_SUCCESS)
  // - if process failed, implementation shall call Ctxt.Error() method to
  // set the corresponding error message and error code number
  // - a typical implementation may be:
  // ! procedure TSQLRestServerTest.Sum(Ctxt: TSQLRestServerURIContext);
  // ! var a,b: TSynExtended;
  // ! begin
  // !   if UrlDecodeNeedParameters(Ctxt.Parameters,'A,B') then begin
................................................................................
  // as result will allow a response of any type (e.g. binary, HTML or text)
  // - this kind of answer will be understood by our TServiceContainerClient
  // implementation, and it may be used with plain AJAX or HTML requests
  // (via POST), to retrieve some custom content
  TServiceCustomAnswer = record
    /// mandatory response type, as encoded in the HTTP header
    // - useful to set the response mime-type - see e.g. the
    // TEXT_CONTENT_TYPE_HEADER or HTML_CONTENT_TYPE_HEADER constants or
    // GetMimeContentType() function
    // - in order to be handled as expected, this field SHALL be set to NOT ''
    // (otherwise TServiceCustomAnswer will be transmitted as raw JSON)
    Header: RawUTF8;
    /// the response body
    // - corresponding to the response type, as defined in Header
    Content: RawByteString;
    /// the HTML response code
    // - if not overriden, will default to HTML_SUCCESS = 200 on server side
    // - on client side, would always contain HTML_SUCCESS = 200 on success,
    // or any error should be handled as expected by the caller (e.g. using
    // TServiceFactoryClient.GetErrorMessage for decoding REST/SOA errors)
    Status: cardinal;
  end;

  PServiceCustomAnswer = ^TServiceCustomAnswer;

................................................................................
  // - see Ctxt.Service, Ctxt.ServiceMethodIndex and Ctxt.ServiceParameters
  // are used to identify the executed method context
  // - Method parameter would help identify easily the corresponding method, and
  // would contain in fact Service.InterfaceFactory.Methods[ServiceMethodIndex]
  // - should return TRUE if the method can be executed
  // - should return FALSE if the method should not be executed, and set the
  // corresponding error to the supplied context e.g.
  // ! Ctxt.Error('Unauthorized method',HTML_NOTALLOWED);
  // - i.e. called by TSQLRestServerURIContext.InternalExecuteSOAByInterface
  TOnServiceCanExecute = function(Ctxt: TSQLRestServerURIContext;
    const Method: TServiceMethod): boolean of object;

  /// a service provider implemented on the server side
  // - each registered interface has its own TServiceFactoryServer instance,
  // available as one TSQLServiceContainerServer item from TSQLRest.Services property
................................................................................
    // - Ctxt.ServiceMethodIndex=-1, then it will free/release corresponding aInstanceID
    // (is called  e.g. from {"method":"_free_", "params":[], "id":1234} )
    // - Ctxt.ServiceParameters is e.g. '[1,2]' i.e. a true JSON array, which
    // will contain the incoming parameters in the same exact order than the
    // corresponding implemented interface method
    // - Ctxt.ID is an optional number, to be used in case of sicClientDriven
    // kind of Instance creation to identify the corresponding client session
    // - returns 200/HTML_SUCCESS on success, or an HTTP error status, with an
    // optional error message in aErrorMsg
    // - on success, Ctxt.Call.OutBody shall contain a serialized JSON object
    // with one nested result property, which may be a JSON array, containing
    // all "var" or "out" parameters values, and then the method main result -
    // for instance, ExecuteMethod(..,'[1,2]') over ICalculator.Add will return:
    // $ {"result":[3],"id":0}
    // the returned "id" number is the Instance identifier to be used for any later
................................................................................
    /// retrieve the published signature of this interface
    // - TServiceFactoryClient will be able to retrieve it only if
    // TServiceContainerServer.PublishSignature is set to TRUE (which is not the
    // default setting, for security reasons) - this function is always available
    // on TServiceFactoryServer side
    function RetrieveSignature: RawUTF8; override;
    /// convert a HTTP error from mORMot's REST/SOA into an English text message
    // - would recognize the HTML_UNAVAILABLE, HTML_NOTIMPLEMENTED,
    // HTML_NOTALLOWED, HTML_UNAUTHORIZED or HTML_NOTACCEPTABLE errors, as
    // generated by the TSQLRestServer side
    // - is used by TServiceFactoryClient.InternalInvoke, but may be called
    // on client side for TServiceCustomAnswer.Status <> HTML_SUCCESS 
    class function GetErrorMessage(status: integer): RawUTF8;
    /// define execution options for a given set of methods
    // - methods names should be specified as an array (e.g. ['Add','Multiply'])
    // - if no method name is given (i.e. []), option will be set for all methods
    // - only supports optNoLogInput and optNoLogOutput on the client side
    procedure SetOptions(const aMethod: array of RawUTF8; aOptions: TServiceMethodOptions);
    /// persist all service calls into a database instead of calling the client 
................................................................................
    // ! AcquireExecutionMode[execORMWrite] := amBackgroundThread;
    // ! AcquireWriteMode := amBackgroundThread; // same as previous
    procedure RollBack(SessionID: cardinal); virtual;
    /// execute a BATCH sequence prepared in a TSQLRestBatch instance
    // - implements the "Unit Of Work" pattern, i.e. safe transactional process
    // even on multi-thread environments
    // - send all pending Add/Update/Delete statements to the DB or remote server
    // - will return the URI Status value, i.e. 200/HTML_SUCCESS OK on success
    // - a dynamic array of integers will be created in Results,
    // containing all ROWDID created for each BatchAdd call, 200 (=HTML_SUCCESS)
    // for all successfull BatchUpdate/BatchDelete, or 0 on error
    // - any error during server-side process MUST be checked against Results[]
    // (the main URI Status is 200 if about communication success, and won't
    // imply that all statements in the BATCH sequence were successfull
    // - note that the caller shall still free the supplied Batch instance
    function BatchSend(Batch: TSQLRestBatch; var Results: TIDDynArray): integer; overload; virtual;
    /// execute a BATCH sequence prepared in a TSQLRestBatch instance
................................................................................
      User: TSQLAuthUser; const aPassWord: RawUTF8): boolean; virtual;
  public
    /// will check URI-level signature
    // - retrieve the session ID from 'session_signature=...' parameter
    // - will also check incoming "Authorization: Basic ...." HTTP header
    function RetrieveSession(Ctxt: TSQLRestServerURIContext): TAuthSession; override;
    /// handle the Auth RESTful method with HTTP Basic
    // - will first return HTML_UNAUTHORIZED (401), then expect user and password
    // to be supplied as incoming "Authorization: Basic ...." headers
    function Auth(Ctxt: TSQLRestServerURIContext): boolean; override;
  end;

  {$ifdef SSPIAUTH}

  /// authentication of the current logged user using Windows Security Support
................................................................................
    // - this method is thread-safe
    procedure NotifyORMTable(TableIndex, DataSize: integer; Write: boolean;
       const MicroSecondsElapsed: QWord);
  published
    /// when this monitoring instance (therefore the server) was created
    property StartDate: RawUTF8 read fStartDate;
    /// number of valid responses
    // - i.e. which returned status code 200/HTML_SUCCESS or 201/HTML_CREATED
    // - any invalid request will increase the TSynMonitor.Errors property
    property Success: TSynMonitorCount64 read fSuccess;
    /// count of the remote method-based service calls
    property ServiceMethod: TSynMonitorCount64 read fServiceMethod;
    /// count of the remote interface-based service calls
    property ServiceInterface: TSynMonitorCount64 read fServiceInterface;
    /// count of files transmitted directly (not part of Output size property)
................................................................................
  // limitation of 53-bit for integers - only for AJAX (non Delphi) clients
  // - unauthenticated requests from browsers (i.e. not Delphi clients) may
  // be redirected to the TSQLRestServer.Auth() method via rsoRedirectForbiddenToAuth
  // (e.g. for TSQLRestServerAuthenticationHttpBasic popup)
  // - some REST/AJAX clients may expect to return status code 204 as
  // instead of 200 in case of a successful operation, but with no returned
  // body (e.g. a DELETE with SAPUI5 / OpenUI5 framework): include
  // rsoHtml200WithNoBodyReturns204 so that any HTML_SUCCESS (200) with no
  // returned body would return a HTML_NOCONTENT (204)
  // - by default, Add() or Update() would return HTML_CREATED (201) or
  // HTML_SUCCESS (200) with no body, unless rsoAddUpdateReturnsContent is set
  // to return as JSON the last inserted/updated record
  // - TModTime / TCreateTime fields are expected to be filled on client side,
  // unless you set rsoComputeFieldsBeforeWriteOnServerSide so that AJAX requests
  // would set the fields on the server side by calling the TSQLRecord
  // ComputeFieldsBeforeWrite virtual method, before writing to the database
  TSQLRestServerOption = (
    rsoNoAJAXJSON,
................................................................................
    // - the supplied Ctxt parameter would give access to the command about to
    // be executed, e.g. Ctxt.Command=execSOAByInterface would identify a SOA
    // service execution, with the corresponding Service and ServiceMethodIndex
    // parameters as set by TSQLRestServerURIContext.URIDecodeSOAByInterface
    // - should return TRUE if the method can be executed
    // - should return FALSE if the method should not be executed, and the
    // callback should set the corresponding error to the supplied context e.g.
    // ! Ctxt.Error('Unauthorized method',HTML_NOTALLOWED);
    // - since this event would be executed by every TSQLRestServer.URI call,
    // it should better not make any slow process (like writing to a remote DB)
    OnBeforeURI: TNotifyBeforeURI;
    /// event trigerred when URI() finished to process a request
    // - the supplied Ctxt parameter would give access to the command which has
    // been executed, e.g. via Ctxt.Call.OutStatus or Ctxt.MicroSecondsElapsed
    // - since this event would be executed by every TSQLRestServer.URI call,
................................................................................
    // property name is supplied for every value
    // - the "not expanded" layout, NoAJAXJSON property is set to TRUE,
    // reflects exactly the layout of the SQL request - first line contains the
    // field names, then all next lines are the field content
    // - is in fact stored in rsoNoAJAXJSON item in Options property
    property NoAJAXJSON: boolean read GetNoAJAXJSON write SetNoAJAXJSON;
    /// allow to customize how TSQLRestServer.URI process the requests
    // - e.g. if HTML_SUCCESS with no body should be translated into HTML_NOCONTENT
    property Options: TSQLRestServerOptions read fOptions write fOptions;
    /// set to true if the server will handle per-user authentication and
    // access right management
    // - i.e. if the associated TSQLModel contains TSQLAuthUser and
    // TSQLAuthGroup tables (set by constructor)
    property HandleAuthentication: boolean read fHandleAuthentication;
    /// allow to by-pass Authentication for a given set of HTTP verbs
................................................................................
    // - this method would require an authenticated client, for safety
    // - expect input as JSON commands:
    // & '{"Table":["cmd":values,...]}'
    // or for multiple tables:
    // & '["cmd@Table":values,...]'
    // with cmd in POST/PUT with {object} as value or DELETE with ID
    // - returns an array of integers: '[200,200,...]' or '["OK"]' if all
    // returned status codes are 200 (HTML_SUCCESS)
    // - URI are either 'ModelRoot/TableName/Batch' or 'ModelRoot/Batch'
    procedure Batch(Ctxt: TSQLRestServerURIContext);
  end;

  /// REST class with direct access to an external database engine
  // - you can set an alternate per-table database engine by using this class
  // - this abstract class is to be overridden with a proper implementation
................................................................................
    /// overridden method which will call ClientRetrieve()
    function EngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8; override;
    /// create a new member
    // - implements REST POST collection
    // - URI is 'ModelRoot/TableName' with POST method
    // - if SendData is true, content of Value is sent to the server as JSON
    // - if ForceID is true, client sends the Value.ID field to use this ID
    // - server must return Status 201/HTML_CREATED on success
    // - server must send on success an header entry with
    // $ Location: ModelRoot/TableName/TableID
    // - on success, returns the new ROWID value; on error, returns 0
    // - on success, Value.ID is updated with the new ROWID
    // - if aValue is TSQLRecordFTS3, Value.ID is stored to the virtual table
    // - this overridden method will send BLOB fields, if ForceBlobTransfert is set
    function InternalAdd(Value: TSQLRecord; SendData: boolean; CustomFields: PSQLFieldBits;
      ForceID, DoNotAutoComputeFields: boolean): TID; override; 
  public
    /// update a member
    // - implements REST PUT collection
    // - URI is 'ModelRoot/TableName/TableID' with PUT method
    // - server must return Status 200/HTML_SUCCESS OK on success
    // - this overridden method will call BeforeUpdateEvent and also update BLOB
    // fields, if any ForceBlobTransfert is set and CustomFields=[]
    function Update(Value: TSQLRecord; const CustomFields: TSQLFieldBits=[];
      DoNotAutoComputeFields: boolean=false): boolean; override;
    /// get a member from its ID
    // - implements REST GET collection
    // - URI is 'ModelRoot/TableName/TableID' with GET method
    // - server must return Status 200/HTML_SUCCESS OK on success
    // - if ForUpdate is true, the REST method is LOCK and not GET: it tries to lock
    // the corresponding record, then retrieve its content; caller has to call
    // UnLock() method after Value usage, to release the record
    function Retrieve(aID: TID; Value: TSQLRecord; ForUpdate: boolean=false): boolean; override;
    /// get a member from its ID
    // - implements REST GET collection
    // - URI is 'ModelRoot/TableName/TableID' with GET method
    // - returns true on server returned 200/HTML_SUCCESS OK success, false on error
    // - set Refreshed to true if the content changed
    function Refresh(aID: TID; Value: TSQLRecord; var Refreshed: boolean): boolean;

    /// retrieve a list of members as a TSQLTable
    // - implements REST GET collection
    // - default SQL statement is 'SELECT ID FROM TableName;' (i.e. retrieve
    // the list of all ID of this collection members)
................................................................................
    function GetOnIdleBackgroundThreadActive: boolean;
{$endif}
    constructor RegisteredClassCreateFrom(aModel: TSQLModel;
      aDefinition: TSynConnectionDefinition); override;
    function GetCurrentSessionUserID: TID; override;
    function InternalRemoteLogSend(const aText: RawUTF8): boolean;
    procedure InternalNotificationMethodExecute(var Ctxt: TSQLRestURIParams); virtual;
    procedure SetLastException(E: Exception=nil; ErrorCode: integer=HTML_BADREQUEST;
      Call: PSQLRestURIParams=nil);
    // register the user session to the TSQLRestClientURI instance
    function SessionCreate(aAuth: TSQLRestServerAuthenticationClass;
      var aUser: TSQLAuthUser; const aSessionKey: RawUTF8): boolean;
    /// abstract method to be implemented with a local, piped or HTTP/1.1 provider
    // - you can specify some POST/PUT data in Call.OutBody (leave '' otherwise)
    // - return the execution result in Call.OutStatus
................................................................................
    function URI(const url, method: RawUTF8; Resp: PRawUTF8=nil;
      Head: PRawUTF8=nil; SendData: PRawUTF8=nil): Int64Rec;
    /// retrieve a list of members as a TSQLTable
    // - implements REST GET collection
    // - URI is 'ModelRoot/TableName' with GET method
    // - SQLSelect and SQLWhere are encoded as 'select=' and 'where=' URL parameters
    // (using inlined parameters via :(...): in SQLWhere is always a good idea)
    // - server must return Status 200/HTML_SUCCESS OK on success
    function List(const Tables: array of TSQLRecordClass; const SQLSelect: RawUTF8 = 'RowID';
      const SQLWhere: RawUTF8 = ''): TSQLTableJSON; override;
    /// unlock the corresponding record
    // - URI is 'ModelRoot/TableName/TableID' with UNLOCK method
    // - returns true on success
    function UnLock(Table: TSQLRecordClass; aID: TID): boolean; override;
    /// Execute directly a SQL statement, expecting a list of resutls
................................................................................
    function BatchDelete(Table: TSQLRecordClass; ID: TID): integer; overload;
    /// retrieve the current number of pending transactions in the BATCH sequence
    // - every call to BatchAdd/Update/Delete methods increases this count
    function BatchCount: integer;
    /// execute a BATCH sequence started by BatchStart method
    // - send all pending BatchAdd/Update/Delete statements to the remote server
    // - URI is 'ModelRoot/TableName/0' with POST (or PUT) method
    // - will return the URI Status value, i.e. 200/HTML_SUCCESS OK on success
    // - a dynamic array of integers will be created in Results,
    // containing all ROWDID created for each BatchAdd call, 200 (=HTML_SUCCESS)
    // for all successfull BatchUpdate/BatchDelete, or 0 on error
    // - any error during server-side process MUST be checked against Results[]
    // (the main URI Status is 200 if about communication success, and won't
    // imply that all statements in the BATCH sequence were successfull
    function BatchSend(var Results: TIDDynArray): integer; overload;
    /// abort a BATCH sequence started by BatchStart method
    // - in short, nothing is sent to the remote server, and current BATCH
    // sequence is closed
    // - will Free the TSQLRestBatch stored in this TSQLRestClientURI instance
    procedure BatchAbort;

    /// wrapper to the protected URI method to call a method on the server, using
    // a ModelRoot/[TableName/[ID/]]MethodName RESTful GET request
    // - returns the HTTP error code (e.g. 200/HTML_SUCCESS on success)
    // - this version will use a GET with supplied parameters (which will be encoded
    // with the URL)
    function CallBackGet(const aMethodName: RawUTF8;
      const aNameValueParameters: array of const;
      out aResponse: RawUTF8; aTable: TSQLRecordClass=nil; aID: TID=0;
      aResponseHead: PRawUTF8=nil): integer;
    /// wrapper to the protected URI method to call a method on the server, using
................................................................................
    // - this version will use a GET with supplied parameters (which will be encoded
    // with the URL)
    function CallBackGetResult(const aMethodName: RawUTF8;
      const aNameValueParameters: array of const;
      aTable: TSQLRecordClass=nil; aID: TID=0): RawUTF8;
    /// wrapper to the protected URI method to call a method on the server, using
    //  a ModelRoot/[TableName/[ID/]]MethodName RESTful PUT request
    // - returns the HTTP error code (e.g. 200/HTML_SUCCESS on success)
    // - this version will use a PUT with the supplied raw UTF-8 data
    function CallBackPut(const aMethodName, aSentData: RawUTF8;
      out aResponse: RawUTF8; aTable: TSQLRecordClass=nil; aID: TID=0;
      aResponseHead: PRawUTF8=nil): integer;
    /// wrapper to the protected URI method to call a method on the server, using
    //  a ModelRoot/[TableName/[ID/]]MethodName RESTful with any kind of request
    // - returns the HTTP error code (e.g. 200/HTML_SUCCESS on success)
    // - for GET/PUT methods, you should better use CallBackGet/CallBackPut
    function CallBack(method: TSQLURIMethod; const aMethodName,aSentData: RawUTF8;
      out aResponse: RawUTF8; aTable: TSQLRecordClass=nil; aID: TID=0;
      aResponseHead: PRawUTF8=nil): integer;
    /// register one or several Services on the client side via their interfaces
    // - this methods expects a list of interfaces to be registered to the client
    // (e.g. [TypeInfo(IMyInterface)])
................................................................................
    // - is defines as a class procedure, since the underlying TSQLRestClientURI
    // instance has no impact here: a single WM_* handler is enough for
    // several TSQLRestClientURI instances
    class procedure ServiceNotificationMethodExecute(var Msg : TMessage);
    {$endif MSWINDOWS}
  published
    /// low-level error code, as returned by server
    // - check this value about HTML_* constants
    // - HTML_SUCCESS or HTML_CREATED mean no error
    // - otherwise, check LastErrorMessage property for additional information
    // - this property value will record status codes returned by URI() method
    property LastErrorCode: integer read fLastErrorCode;
    /// low-level error message, as returned by server
    // - this property value will record content returned by URI() method in
    // case of an error, or '' if LastErrorCode is HTML_SUCCESS or HTML_CREATED
    property LastErrorMessage: RawUTF8 read fLastErrorMessage;
    /// low-level exception class, if any
    // - will record any Exception class raised within URI() method
    // - contains nil if URI() execution did not raise any exception (which
    // is the most expected behavior, since server-side errors are trapped
    // into LastErrorCode/LastErrorMessage properties
    property LastErrorException: ExceptClass read fLastErrorException;
................................................................................
    // during process
    // - to be used e.g. to ensure no re-entrance from User Interface messages
    property OnIdleBackgroundThreadActive: Boolean read GetOnIdleBackgroundThreadActive;
{$endif}
    /// this Event is called in case of remote authentication failure
    // - client software can ask the user to enter a password and user name
    // - if no event is specified, the URI() method will return directly
    // an HTML_FORBIDDEN "403 Forbidden" error code
    property OnAuthentificationFailed: TOnAuthentificationFailed
      read fOnAuthentificationFailed write fOnAuthentificationFailed;
    /// this Event is called if URI() was not successfull
    // - the callback would have all needed information
    property OnFailed: TOnClientFailed read fOnFailed write fOnFailed;
    /// this Event is called when a user is authenticated
    // - is called always, on each TSQLRestClientURI.SetUser call
................................................................................
    // - will make a copy of the aRedirected.Model, and own it
    constructor Create(aRedirected: TSQLRest); reintroduce; overload;
    /// would pass all client commands to the supplied TSQLRestServer instance
    // - aRedirected would be owned by this TSQLRestClientRedirect
    constructor CreateOwned(aRedirected: TSQLRestServer); reintroduce;
    /// allows to change redirection to a client on the fly
    // - if aRedirected is nil, redirection would be disabled and any URI() call
    // would return an HTML_GATEWAYTIMEOUT 504 error status
    procedure RedirectTo(aRedirected: TSQLRest);
  end;

  {$ifdef MSWINDOWS}

  /// Rest client with remote access to a server through Windows messages
  // - use only one TURIMapRequest function for the whole communication
................................................................................
  protected
    constructor RegisteredClassCreateFrom(aModel: TSQLModel;
      aDefinition: TSynConnectionDefinition); override;
    /// method calling the RESTful server through a DLL or executable, by using
    // a named pipe (faster than TCP/IP or HTTP connection)
    // - return status code in result.Lo
    // - return database internal state in result.Hi
    // - status code 501 HTML_NOTIMPLEMENTED if no server is available
    procedure InternalURI(var Call: TSQLRestURIParams); override;
    /// overridden protected method to handle named-pipe connection
    function InternalCheckOpen: boolean; override;
    /// overridden protected method to close named-pipe connection
    procedure InternalClose; override;
  public
    /// connect to a server contained in a running application
................................................................................
  /// if this variable is TRUE, the URIRequest() function won't use
  // Win32 API GlobalAlloc() function, but fastest native Getmem()
  // - can be also useful for debugg
  USEFASTMM4ALLOC: boolean = false;

/// this function can be exported from a DLL to remotely access to a TSQLRestServer
// - use TSQLRestServer.ExportServer to assign a server to this function
// - return 501 HTML_NOTIMPLEMENTED if no TSQLRestServer.ExportServer has been assigned
// - memory for Resp and Head are allocated with GlobalAlloc(): client must release
// this pointers with GlobalFree() after having retrieved their content
// - simply use TSQLRestClientURIDll to access to an exported URIRequest() function
function URIRequest(url, method, SendData: PUTF8Char; Resp, Head: PPUTF8Char): Int64Rec; cdecl;


threadvar
................................................................................
    '%.IndexByNameUnflattenedOrExcept(%): unkwnown field in %',[self,aName,fTable]);
end;


procedure StatusCodeToErrorMsg(Code: integer; var result: RawUTF8);
begin // see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
  case Code of
    HTML_CONTINUE:            result := 'Continue';
    HTML_SWITCHINGPROTOCOLS:  result := 'Switching Protocols';
    HTML_SUCCESS:             result := 'OK';
    HTML_CREATED:             result := 'Created';
    HTML_ACCEPTED:            result := 'Accepted';
    HTML_NONAUTHORIZEDINFO:   result := 'Non-Authoritative Information';
    HTML_NOCONTENT:           result := 'No Content';
    HTML_MULTIPLECHOICES:     result := 'Multiple Choices';
    HTML_MOVEDPERMANENTLY:    result := 'Moved Permanently';
    HTML_FOUND:               result := 'Found';
    HTML_SEEOTHER:            result := 'See Other';
    HTML_NOTMODIFIED:         result := 'Not Modified';
    HTML_USEPROXY:            result := 'Use Proxy';
    HTML_TEMPORARYREDIRECT:   result := 'Temporary Redirect';
    HTML_BADREQUEST:          result := 'Bad Request';
    HTML_UNAUTHORIZED:        result := 'Unauthorized';
    HTML_FORBIDDEN:           result := 'Forbidden';
    HTML_NOTFOUND:            result := 'Not Found';
    HTML_NOTALLOWED:          result := 'Method Not Allowed';
    HTML_NOTACCEPTABLE:       result := 'Not Acceptable';
    HTML_PROXYAUTHREQUIRED:   result := 'Proxy Authentication Required';
    HTML_TIMEOUT:             result := 'Request Timeout';
    HTML_SERVERERROR:         result := 'Internal Server Error';
    HTML_BADGATEWAY:          result := 'Bad Gateway';
    HTML_GATEWAYTIMEOUT:      result := 'Gateway Timeout';
    HTML_UNAVAILABLE:         result := 'Service Unavailable';
    HTML_HTTPVERSIONNONSUPPORTED: result := 'HTTP Version Not Supported';
    else                      result := 'Invalid Request';
  end;
end;

function StatusCodeToErrorMsg(Code: integer): RawUTF8;
begin
  StatusCodeToErrorMsg(Code,result);
  result := FormatUTF8('HTTP Error % - %',[Code,result]);
end;

function StatusCodeIsSuccess(Code: integer): boolean;
begin
  case Code of
  HTML_SUCCESS, HTML_NOCONTENT, HTML_CREATED,
  HTML_NOTMODIFIED, HTML_TEMPORARYREDIRECT:
    result := true;
  else
    result := false;
  end;
end;

function StringToMethod(const method: RawUTF8): TSQLURIMethod;
................................................................................
  end;
end;

function TSQLRest.BatchSend(Batch: TSQLRestBatch;
  var Results: TIDDynArray): integer;
var Data: RawUTF8;
begin
  result := HTML_BADREQUEST;
  if (self=nil) or (Batch=nil) then // no opened BATCH sequence
    exit;
  if Batch.PrepareForSending(Data) then
    if Data='' then // i.e. Batch.Count=0
      result := HTML_SUCCESS else
      try
        result := EngineBatchSend(Batch.Table,Data,Results,Batch.Count);
      except
        on Exception do // e.g. from TSQLRestServer.EngineBatchSend()
          result := HTML_SERVERERROR;
      end;
end;

function TSQLRest.BatchSend(Batch: TSQLRestBatch): integer;
var Res: TIDDynArray;
begin
  result := BatchSend(Batch,Res);
................................................................................



{ TSQLRestClientURI }

function TSQLRestClientURI.EngineExecute(const SQL: RawUTF8): boolean;
begin
  result := URI(Model.Root,'POST',nil,nil,@SQL).Lo in [HTML_SUCCESS,HTML_NOCONTENT];
end;

function TSQLRestClientURI.URIGet(Table: TSQLRecordClass; ID: TID;
  var Resp: RawUTF8; ForUpdate: boolean=false): Int64Rec;
const METHOD: array[boolean] of RawUTF8 = ('GET','LOCK');
begin
  result := URI(Model.getURIID(Table,ID),METHOD[ForUpdate],@Resp,nil,nil);
end;

function TSQLRestClientURI.UnLock(Table: TSQLRecordClass; aID: TID): boolean;
begin
  if (self=nil) or not Model.UnLock(Table,aID) then
    result := false else // was not locked by the client
    result := URI(Model.getURIID(Table,aID),'UNLOCK').Lo in [HTML_SUCCESS,HTML_NOCONTENT];
end;

function TSQLRestClientURI.ExecuteList(const Tables: array of TSQLRecordClass;
  const SQL: RawUTF8): TSQLTableJSON;
var Resp: RawUTF8;
begin
  if self=nil then
    result := nil else
  with URI(Model.Root,'GET',@Resp,nil,@SQL) do
    if Lo=HTML_SUCCESS then begin // GET with SQL sent
      if high(Tables)=0 then
        result := TSQLTableJSON.CreateFromTables([Tables[0]],SQL,Resp) else
        result := TSQLTableJSON.CreateFromTables(Tables,SQL,Resp);
      result.fInternalState := Hi;
    end else // get data
    result := nil;
end;
................................................................................
end;

function TSQLRestClientURI.ServerCacheFlush(aTable: TSQLRecordClass; aID: TID): boolean;
var aResp: RawUTF8;
begin
  if (Self=nil) or (Model=nil) then // avoid GPF
    result := false else
    result := CallBackGet('CacheFlush',[],aResp,aTable,aID) in [HTML_SUCCESS,HTML_NOCONTENT];
end;

function TSQLRestClientURI.ServerTimeStampSynchronize: boolean;
var status: integer;
    aResp: RawUTF8;
begin
  if self=nil then begin
    result := false;
    exit;
  end;
  fServerTimeStampOffset := 0.0001; // avoid endless recursive call
  status := CallBackGet('TimeStamp',[],aResp);
  result := (status=HTML_SUCCESS) and (aResp<>'');
  if result then
    SetServerTimeStamp(GetInt64(pointer(aResp))) else begin
    InternalLog('/TimeStamp call failed -> Server not available',sllWarning);
    fLastErrorMessage := 'Server not available  - '+Trim(fLastErrorMessage);
  end;
end;

function TSQLRestClientURI.InternalRemoteLogSend(const aText: RawUTF8): boolean;
begin
  result := URI(Model.getURICallBack('RemoteLog',nil,0),
    'PUT',nil,nil,@aText).Lo in [HTML_SUCCESS,HTML_NOCONTENT];
end;


{$ifdef MSWINDOWS}
type
  TSQLRestClientURIServiceNotification = class(TServiceMethodExecute)
  protected
................................................................................
      Ctxt.OutHead := exec.ServiceCustomAnswerHead;
      Ctxt.OutStatus := exec.ServiceCustomAnswerStatus;
    finally
      exec.Free;
    end;
  end;
begin
  Ctxt.OutStatus := HTML_BADREQUEST;
  url := Ctxt.Url;
  if url='' then
    exit;
  if url[1]='/' then
    system.delete(url,1,1);
  Split(Split(url,'/',root),'/',interfmethod,id); // 'root/BidirCallback.AsynchEvent/1'
  if not IdemPropNameU(root,Model.Root) then
    exit;
  callback.ID := GetInteger(pointer(id));
  if callback.ID<=0 then
    exit;
  if interfmethod=SERVICE_PSEUDO_METHOD[imFree] then begin
    if fFakeCallbacks.FindAndRelease(callback.ID) then
      Ctxt.OutStatus := HTML_SUCCESS;
    exit;
  end;
  if not fFakeCallbacks.FindEntry(callback) then
    exit;
  if (Ctxt.InHead<>'') and
     (callback.Factory.MethodIndexCurrentFrameCallback>=0) then begin
    frames := FindIniNameValue(pointer(Ctxt.InHead),'SEC-WEBSOCKET-FRAME: ');
................................................................................
      WR.AddShort('{"result":[');
      if frames='[0]' then // call before the first method of the jumbo frame
        Call(callback.Factory.MethodIndexCurrentFrameCallback,frames,nil);
      Call(methodIndex,Ctxt.InBody,WR);
      if ok then begin
        if Ctxt.OutHead='' then begin // <>'' if set via TServiceCustomAnswer
          WR.Add(']','}');
          Ctxt.OutStatus := HTML_SUCCESS;
        end;
        Ctxt.OutBody := WR.Text;
      end else
        Ctxt.OutStatus := HTML_SERVERERROR;
      if frames='[1]' then // call after the last method of the jumbo frame
        Call(callback.Factory.MethodIndexCurrentFrameCallback,frames,nil);
    finally
      WR.Free;
    end;
  except
    on E: Exception do begin
      Ctxt.OutHead := '';
      Ctxt.OutBody := ObjectToJSONDebug(E);
      Ctxt.OutStatus := HTML_SERVERERROR;
    end;
  end;
end;

{$ifdef LVCL} // SyncObjs.TEvent not available in LVCL yet

function TSQLRestClientURI.ServerRemoteLog(Sender: TTextWriter; Level: TSynLogInfo;
................................................................................
  State := ServerInternalState; // get revision state from server
  for i := 0 to high(Data) do
    if Data[i]<>nil then
    if TObject(Data[i]).InheritsFrom(TSQLTableJSON) then begin
      T := TSQLTableJSON((Data[i]));
      if (T.QuerySQL<>'') and (T.InternalState<>State) then begin // refresh needed?
        with URI(Model.Root,'GET',@Resp,nil,@T.QuerySQL) do
          if Lo=HTML_SUCCESS then begin // GET with SQL sent
            if Assigned(OnTableUpdate) then
              OnTableUpdate(T,tusPrepare);
            TRefreshed := false;
            if not T.UpdateFrom(Resp,TRefreshed,PCurrentRow) then
              result := false else // mark error retrieving new content
              T.fInternalState := Hi;
            if TRefreshed then
................................................................................
    if SQLWhere<>'' then begin
      if U<>'' then
        U := U+'&where=' else
        U := U+'?where=';
      U := U+UrlEncode(SQLWhere);
    end;
    with URI(Model.URI[TSQLRecordClass(Tables[0])]+U,'GET',@Resp) do
      if Lo<>HTML_SUCCESS then
        exit else
        InternalState := Hi;
    result := TSQLTableJSON.CreateFromTables([Tables[0]],SQL,Resp); // get data
  end else begin
    // multiple tables -> send SQL statement as HTTP body
    with URI(Model.Root,'GET',@Resp,nil,@SQL) do
      if Lo<>HTML_SUCCESS then
        exit else
        InternalState := Hi;
    result := TSQLTableJSON.CreateFromTables(Tables,SQL,Resp); // get data
  end;
  result.fInternalState := InternalState;
end;

................................................................................
function TSQLRestClientURI.TransactionBegin(aTable: TSQLRecordClass;
  SessionID: cardinal): boolean;
begin
  result := inherited TransactionBegin(aTable,CONST_AUTHENTICATION_NOT_USED);
  if result then
    // fTransactionActiveSession flag was not already set
    if aTable=nil then
      result := URI(Model.Root,'BEGIN').Lo in [HTML_SUCCESS,HTML_NOCONTENT] else
      result := URI(Model.URI[aTable],'BEGIN').Lo in [HTML_SUCCESS,HTML_NOCONTENT];
end;

function TSQLRestClientURI.TransactionBeginRetry(aTable: TSQLRecordClass;
  Retries: integer): boolean;
begin
  if Retries>50 then
    Retries := 50; // avoid loop for more than 10 seconds
................................................................................
  aTable: TSQLRecordClass; aID: TID; aResponseHead: PRawUTF8): integer;
var url, header: RawUTF8;
    {$ifdef WITHLOG}
    Log: ISynLog; // for Enter auto-leave to work with FPC
    {$endif}
begin
  if self=nil then
    result := HTML_UNAVAILABLE else begin
    url := Model.getURICallBack(aMethodName,aTable,aID)+
      UrlEncode(aNameValueParameters);
    {$ifdef WITHLOG}
    Log := fLogClass.Enter('CallBackGet %',[url],self);
    {$endif}
    result := URI(url,'GET',@aResponse,@header).Lo;
    if aResponseHead<>nil then
................................................................................
  ProcessOpaqueParam: pointer);
var Call: ^TSQLRestURIParams absolute ProcessOpaqueParam;
begin
  if Call=nil then
    exit;
  InternalURI(Call^);
  if OnIdleBackgroundThreadActive then
    if Call^.OutStatus=HTML_NOTIMPLEMENTED then begin
      // InternalCheckOpen failed -> force recreate connection
      InternalClose;
      if OnIdleBackgroundThreadActive then
        InternalURI(Call^); // try request again
    end;
end;

................................................................................
    aUserName, aPassword: string;
    StatusMsg: RawUTF8;
    Call: TSQLRestURIParams;
    aRetryOnceOnTimeout, aPasswordHashed: boolean;
label DoRetry;
begin
  if self=nil then begin
    Int64(result) := HTML_UNAVAILABLE;
    SetLastException(nil,HTML_UNAVAILABLE);
    exit;
  end;
  aRetryOnceOnTimeout := RetryOnceOnTimeout;
  fLastErrorMessage := '';
  fLastErrorException := nil;
  if fServerTimeStampOffset=0 then
    if not ServerTimeStampSynchronize then begin
      Int64(result) := HTML_UNAVAILABLE;
      exit; // if /TimeStamp is not available, server is down!
    end;
  Call.Init;
  if (Head<>nil) and (Head^<>'') then
    Call.InHead := Head^;
  if fSessionHttpHeader<>'' then
    Call.InHead := Trim(Call.InHead+#13#10+fSessionHttpHeader);
................................................................................
      Call.InBody := SendData^;
{$ifndef LVCL}
    if Assigned(fOnIdle) then begin
      if fBackgroundThread=nil then
        fBackgroundThread := TSynBackgroundThreadEvent.Create(OnBackgroundProcess,
          OnIdle,FormatUTF8('% "%" background',[self,Model.Root]));
      if not fBackgroundThread.RunAndWait(@Call) then
        Call.OutStatus := HTML_UNAVAILABLE;
    end else
{$endif}
    begin
      InternalURI(Call);
      if Call.OutStatus=HTML_NOTIMPLEMENTED then begin // InternalCheckOpen failed
        InternalClose;     // force recreate connection
        InternalURI(Call); // try request again
      end;
    end;
    result.Lo := Call.OutStatus;
    result.Hi := Call.OutInternalState;
    if Head<>nil then
      Head^ := Call.OutHead;
    if Resp<>nil then
      Resp^ := Call.OutBody;
    fLastErrorCode := Call.OutStatus;
    if (Call.OutStatus=HTML_TIMEOUT) and aRetryOnceOnTimeout then begin
      aRetryOnceOnTimeout := false;
      InternalLog('% % returned "408 Request Timeout" -> RETRY',[method,url],sllError);
      goto DoRetry;
    end;
    if not StatusCodeIsSuccess(Call.OutStatus) then begin
      StatusCodeToErrorMsg(Call.OutStatus,StatusMsg);
      if Call.OutBody='' then
................................................................................
        fLastErrorMessage := StatusMsg else
        fLastErrorMessage := Call.OutBody;
      InternalLog('% % returned % (%) with message  %',
        [method,url,Call.OutStatus,StatusMsg,fLastErrorMessage],sllError);
      if Assigned(fOnFailed) then
        fOnFailed(self,nil,@Call);
    end;
    if (Call.OutStatus<>HTML_FORBIDDEN) or not Assigned(OnAuthentificationFailed) then
      break;
    // "403 Forbidden" in case of authentication failure -> try relog
    if not OnAuthentificationFailed(Retry+2,aUserName,aPassword,aPasswordHashed) or
       not SetUser(StringToUTF8(aUserName),StringToUTF8(aPassword),aPasswordHashed) then
      break;
  except
    on E: Exception do begin
      Int64(result) := HTML_NOTIMPLEMENTED; // 501
      SetLastException(E,HTML_NOTIMPLEMENTED,@Call);
      exit;
    end;
  end;
end;

function TSQLRestClientURI.CallBackGetResult(const aMethodName: RawUTF8;
  const aNameValueParameters: array of const; aTable: TSQLRecordClass; aID: TID): RawUTF8;
var aResponse: RawUTF8;
begin
  if CallBackGet(aMethodName,aNameValueParameters,aResponse,aTable,aID)=HTML_SUCCESS then
    result := JSONDecode(aResponse) else
    result := '';
end;

function TSQLRestClientURI.CallBackPut(const aMethodName,
  aSentData: RawUTF8; out aResponse: RawUTF8; aTable: TSQLRecordClass;
  aID: TID; aResponseHead: PRawUTF8): integer;
................................................................................
  'MKACTIVITY','MKCALENDAR','CHECKOUT','MERGE','NOTIFY','PATCH','SEARCH','CONNECT');
var u: RawUTF8;
{$ifdef WITHLOG}
   Log: ISynLog; // for Enter auto-leave to work with FPC
{$endif}
begin
  if (self=nil) or (method<Low(NAME)) then
    result := HTML_UNAVAILABLE else begin
    u := Model.getURICallBack(aMethodName,aTable,aID);
    {$ifdef WITHLOG}
    Log := fLogClass.Enter('Callback %',[u],self);
    {$endif}
    result := URI(u,NAME[method],@aResponse,aResponseHead,@aSentData).Lo;
    InternalLog('% result=% resplen=%',[NAME[method],result,length(aResponse)],
      sllServiceReturn);
................................................................................
  fServicePublishOwnInterfaces := OwnServer.ServicesPublishedInterfaces;
end;

function TSQLRestClientURI.ServiceRetrieveAssociated(const aServiceName: RawUTF8;
  out URI: TSQLRestServerURIDynArray): boolean;
var json: RawUTF8;
begin
  result := (CallBackGet('stat',['findservice',aServiceName],json)=HTML_SUCCESS) and
    (DynArrayLoadJSON(URI,pointer(json),TypeInfo(TSQLRestServerURIDynArray))<>nil);
end;

function TSQLRestClientURI.ServiceRetrieveAssociated(const aInterface: TGUID;
  out URI: TSQLRestServerURIDynArray): boolean;
var fact: TInterfaceFactory;
begin
................................................................................
function TSQLRestClientURI.EngineAdd(TableModelIndex: integer;
  const SentData: RawUTF8): TID;
var P: PUTF8Char;
    url, Head: RawUTF8;
begin
  result := 0;
  url := Model.URI[Model.Tables[TableModelIndex]];
  if URI(url,'POST',nil,@Head,@SentData).Lo<>HTML_CREATED then
    exit; // response must be '201 Created'
  P := pointer(Head); // we need to check the headers
  if P<>nil then
  repeat
    // find ID from 'Location: Member Entry URI' header entry
    if IdemPChar(P,'LOCATION:') then begin // 'Location: root/People/11012' e.g.
      inc(P,9);
................................................................................
  until false;
end;

function TSQLRestClientURI.EngineDelete(TableModelIndex: integer; ID: TID): boolean;
var url: RawUTF8;
begin
  url := Model.getURIID(Model.Tables[TableModelIndex],ID);
  result := URI(url,'DELETE').Lo in [HTML_SUCCESS,HTML_NOCONTENT];
end;

function TSQLRestClientURI.EngineDeleteWhere(TableModelIndex: Integer;
  const SQLWhere: RawUTF8; const IDs: TIDDynArray): boolean;
var url: RawUTF8;
begin  // ModelRoot/TableName?where=WhereClause to delete members
  url := Model.getURI(Model.Tables[TableModelIndex])+'?where='+UrlEncode(SQLWhere);
  result := URI(url,'DELETE').Lo in [HTML_SUCCESS,HTML_NOCONTENT];
end;

function TSQLRestClientURI.EngineList(const SQL: RawUTF8;
  ForceAJAX: Boolean; ReturnedRowCount: PPtrInt): RawUTF8;
begin
  if (self=nil) or (SQL='') or (ReturnedRowCount<>nil) or
     (URI(Model.Root,'GET',@result,nil,@SQL).Lo<>HTML_SUCCESS) then
    result := '';
end;

function TSQLRestClientURI.ClientRetrieve(TableModelIndex: integer; ID: TID;
  ForUpdate: boolean; var InternalState: cardinal; var Resp: RawUTF8): boolean;
begin
  if cardinal(TableModelIndex)<=cardinal(Model.fTablesMax) then
  with URIGet(Model.Tables[TableModelIndex],ID,Resp,ForUpdate) do
    if Lo=HTML_SUCCESS then begin
      InternalState := Hi;
      result := true;
    end else
      result := false else
      result := false;
end;

................................................................................
  BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean;
var url: RawUTF8;
begin
  if (self=nil) or (aID<=0) or (BlobField=nil) then
    result := false else begin
    // URI is 'ModelRoot/TableName/TableID/BlobFieldName' with GET method
    url := Model.getURICallBack(BlobField^.Name,Model.Tables[TableModelIndex],aID);
    result := URI(url,'GET',@BlobData).Lo=HTML_SUCCESS;
  end;
end;

function TSQLRestClientURI.EngineUpdate(TableModelIndex: integer; ID: TID;
  const SentData: RawUTF8): boolean;
var url: RawUTF8;
begin
  url := Model.getURIID(Model.Tables[TableModelIndex],ID);
  result := URI(url,'PUT',nil,nil,@SentData).Lo in [HTML_SUCCESS,HTML_NOCONTENT];
end;

function TSQLRestClientURI.EngineUpdateBlob(TableModelIndex: integer; aID: TID;
  BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean;
var url, Head: RawUTF8;
begin
  Head := 'Content-Type: application/octet-stream';
  if (self=nil) or (aID<=0) or (BlobField=nil) then
    result := false else begin
    // PUT ModelRoot/TableName/TableID/BlobFieldName
    FormatUTF8('%/%/%',[Model.URI[Model.Tables[TableModelIndex]],aID,BlobField^.Name],url);
    result := URI(url,'PUT',nil,@Head,@BlobData).Lo in [HTML_SUCCESS,HTML_NOCONTENT];
  end;
end;

function TSQLRestClientURI.EngineUpdateField(TableModelIndex: integer;
  const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean;
var url: RawUTF8;
begin
  if TableModelIndex<0 then
    result := false else begin
    // PUT ModelRoot/TableName?setname=..&set=..&wherename=..&where=..
    FormatUTF8('%?setname=%&set=%&wherename=%&where=%',
      [Model.URI[Model.Tables[TableModelIndex]],
       SetFieldName,UrlEncode(SetValue),WhereFieldName,UrlEncode(WhereValue)],url);
    result := URI(url,'PUT').Lo in [HTML_SUCCESS,HTML_NOCONTENT];
  end;
end;

function TSQLRestClientURI.EngineBatchSend(Table: TSQLRecordClass; const Data: RawUTF8;
  var Results: TIDDynArray; ExpectedResultsCount: integer): integer;
var Resp: RawUTF8;
    R: PUTF8Char;
    i: integer;
begin // TSQLRest.BatchSend() ensured that Batch contains some data
  try
    // URI is 'ModelRoot/Batch' or 'ModelRoot/Batch/TableName' with PUT method
    result := URI(Model.getURICallBack('Batch',Table,0),'PUT',@Resp,nil,@Data).Lo;
    if result<>HTML_SUCCESS then
      exit;
    // returned Resp shall be an array of integers: '[200,200,...]'
    R := pointer(Resp);
    if R<>nil then
      while not (R^ in ['[',#0]) do inc(R);
    result := HTML_BADREQUEST;
    if (R=nil) or (R^<>'[') then
      // invalid response
      exit;
    SetLength(Results,ExpectedResultsCount);
    if IdemPChar(R,'["OK"]') then begin // to save bandwith if no adding
      for i := 0 to ExpectedResultsCount-1 do
        Results[i] := HTML_SUCCESS;
    end else begin
      inc(R); // jump first '['
      for i := 0 to ExpectedResultsCount-1 do begin
        Results[i] := GetJSONInt64Var(R);
        while R^ in [#1..' '] do inc(R);
        case R^ of
          ',': inc(R);
................................................................................
          ']': break;
          else exit;
        end;
      end;
      if R^<>']' then
        exit;
    end;
    result := HTML_SUCCESS; // returns OK
  finally
    BatchAbort;
  end;
end;

procedure TSQLRestClientURI.BatchAbort;
begin
................................................................................
begin
  if self<>nil then
  try
    result := BatchSend(fBatchCurrent,Results);
  finally
    FreeAndNil(fBatchCurrent);
  end else
    result := HTML_BADREQUEST;
end;


{ TSQLRestServer }

const
  ServerPipeNamePrefix: TFileName = '\\.\pipe\mORMot_';
................................................................................
      GetMem(result,L);
    MoveFast(pointer(s)^,result^,L);
  end;
end;
var call: TSQLRestURIParams;
begin
  if GlobalURIRequestServer=nil then begin
    Int64(result) := HTML_NOTIMPLEMENTED; // 501
    exit;
  end;
  call.Init;
  call.Url := url;
  call.Method := method;
  call.LowLevelConnectionID := PtrInt(GlobalURIRequestServer);
  call.InHead := 'RemoteIP: 127.0.0.1';
................................................................................
      Magic: cardinal;
      Status: cardinal;
      InternalState: cardinal;
    end;
    Data: TCopyDataStruct;
    Header, ResStr: RawUTF8;
begin
  Msg.Result := HTML_NOTFOUND;
  if (self=nil) or (Msg.From=0) then
    exit;
  input := PCopyDataStruct(Msg.CopyDataStruct);
  P := input^.lpData;
  if (P=nil) or (input^.cbData<=7) then
    exit;
  if PCardinal(P)^<>MAGIC_SYN then
    exit; // invalid layout: a broadcasted WM_COPYDATA message? :(
  inc(P,4);
  // #1 is a field delimiter below, since Get*Item() functions return nil for #0
  Msg.Result := HTML_SUCCESS; // Send something back
  call.Init;
  call.Url := GetNextItem(P,#1);
  call.Method := GetNextItem(P,#1);
  call.InHead := GetNextItem(P,#1);
  call.LowLevelConnectionID := Msg.From;
  Header := 'RemoteIP: 127.0.0.1';
  if call.InHead='' then
................................................................................
      result := fRecordVersionMax;
      Writer.Free;
      break;
    end else
    try
      fAcquireExecution[execORMWrite].Safe.Lock;
      fRecordVersionDeleteIgnore := true;
      if BatchSend(Writer,IDs)=HTML_SUCCESS then begin
        InternalLog('%.RecordVersionSynchronize Added=% Updated=% Deleted=% on %',
          [ClassType,Writer.AddCount,Writer.UpdateCount,Writer.DeleteCount,Master],sllDebug);
        if ChunkRowLimit=0 then begin
          result := fRecordVersionMax;
          break;
        end;
      end else begin
................................................................................
begin
  {$ifdef WITHLOG}
  Log.Log(sllUserAuth,'AuthenticationFailed(%) for % (session=%)',[GetEnumName(
    TypeInfo(TNotifyAuthenticationFailedReason),ord(Reason))^,Call^.Url,Session],self);
  {$endif}
  // 401 Unauthorized response MUST include a WWW-Authenticate header,
  // which is not what we used, so here we won't send 401 error code but 403
  Call.OutStatus := HTML_FORBIDDEN;
  // call the notification event
  if Assigned(Server.OnAuthenticationFailed) then
    Server.OnAuthenticationFailed(Server,Reason,nil,self);
end;

destructor TSQLRestAcquireExecution.Destroy;
begin
................................................................................
procedure TimeOut;
begin
  {$ifdef WITHLOG}
  Log.Log(sllServer,'TimeOut %.Execute(%) after % ms',[self,ToText(Command)^,
    Server.fAcquireExecution[Command].LockedTimeOut],self);
  {$endif}
  if Call<>nil then
    Call^.OutStatus := HTML_TIMEOUT; // 408 Request Time-out
end;
var Method: TThreadMethod;
    Start64: Int64;
begin
  with Server.fAcquireExecution[Command] do begin
    case Command of
      execSOAByMethod:
................................................................................
        ClassFieldNamesAllPropsAsText(SettingsStorage.ClassType,true)]);
      exit;
    end;
  end;
  ObjectToVariant(SettingsStorage,config,[woDontStoreDefault]);
  if URIBlobFieldName<>'' then
    config := TDocVariantData(config).GetValueByPath(URIBlobFieldName);
  ReturnsJson(config,HTML_SUCCESS,true,twJsonEscape,true);
end;

procedure StatsAddSizeForCall(Stats: TSynMonitorInputOutput; const Call: TSQLRestURIParams);
begin
  Stats.AddSize( // rough estimation
    length(Call.Url)+length(Call.Method)+length(Call.InHead)+length(Call.InBody)+12,
    length(Call.OutHead)+length(Call.OutBody)+16);
................................................................................
        exit;
      end;
      ServiceExecution := @Service.fExecution[ServiceMethodIndex];
    end;
    end;
    if (Session>CONST_AUTHENTICATION_NOT_USED) and (ServiceExecution<>nil) and
       (SessionGroup-1 in ServiceExecution.Denied) then begin
      Error('Unauthorized method',HTML_NOTALLOWED);
      exit;
    end;
    // if we reached here, we have to run the service method
    Service.ExecuteMethod(self);
  end;
var xml: RawUTF8;
    m: integer;
................................................................................
            Static := nil;
            if SQLisSelect then begin
              TableIndexes := Server.Model.GetTableIndexesFromSQLSelect(SQL);
              if TableIndexes=nil then begin
                // check for SELECT without any known table
                if not (reSQLSelectWithoutTable in
                   Call.RestAccessRights^.AllowRemoteExecute) then begin
                  Call.OutStatus := HTML_NOTALLOWED;
                  exit;
                end;
              end else begin
                // check for SELECT with one (or several JOINed) tables
                for i := 0 to high(TableIndexes) do
                  if not (TableIndexes[i] in Call.RestAccessRights^.GET) then begin
                    Call.OutStatus := HTML_NOTALLOWED;
                    exit;
                  end;
                // use the first static table (poorman's JOIN)
                Static := Server.InternalAdaptSQL(TableIndexes[0],SQL);
              end;
            end;
            if Static<>nil then  begin
................................................................................
            if Call.OutBody<>'' then begin // got JSON list '[{...}]' ?
              if (SQLSelect<>'') and (length(TableIndexes)=1) then begin
                InternalSetTableFromTableIndex(TableIndexes[0]);
                opt := ClientSQLRecordOptions;
                if opt<>[] then
                  ConvertOutBodyAsPlainJSON(SQLSelect,opt);
              end;
              Call.OutStatus := HTML_SUCCESS;  // 200 OK
              if not SQLisSelect then // accurate fStats.NotifyORM(Method) below
                Method := TSQLURIMethod(IdemPCharArray(SQLBegin(pointer(SQL)),
                  ['INSERT','UPDATE','DELETE'])+2); // -1+2 -> mGET=1
            end;
          end;
        end;
      end;
    end else
    // here, Table<>nil and TableIndex in [0..MAX_SQLTABLES-1]
    if not (TableIndex in Call.RestAccessRights^.GET) then // check User Access
      Call.OutStatus := HTML_NOTALLOWED else begin
      if TableID>0 then begin
        // GET ModelRoot/TableName/TableID[/BlobFieldName] to retrieve one member,
        // with or w/out locking, or a specified BLOB field content
        if Method=mLOCK then // Safe.Lock is to be followed by PUT -> check user
          if not (TableIndex in Call.RestAccessRights^.PUT) then
            Call.OutStatus := HTML_NOTALLOWED else
            if Server.Model.Lock(TableIndex,TableID) then
              Method := mGET; // mark successfully locked
        if Method<>mLOCK then
          if URIBlobFieldName<>'' then begin
            // GET ModelRoot/TableName/TableID/BlobFieldName: retrieve BLOB field content
            Blob := Table.RecordProps.BlobFieldPropFromRawUTF8(URIBlobFieldName);
            if Blob<>nil then begin
              if TableEngine.EngineRetrieveBlob(TableIndex,
                   TableID,Blob,TSQLRawBlob(Call.OutBody)) then begin
                Call.OutHead := GetMimeContentTypeHeader(Call.OutBody);
                Call.OutStatus := HTML_SUCCESS; // 200 OK
              end else
                Call.OutStatus := HTML_NOTFOUND;
            end;
          end else begin
            // GET ModelRoot/TableName/TableID: retrieve a member content, JSON encoded
            Call.OutBody := Server.fCache.Retrieve(TableIndex,TableID);
            if Call.OutBody='' then begin
              // get JSON object '{...}'
              if Static<>nil then
................................................................................
                rec := Table.CreateFrom(Call.OutBody); // cached? -> make private
                try
                  Call.OutBody := rec.GetJSONValues(true,true,soSelect,nil,opt);
                finally
                  rec.Free;
                end;
              end;
              Call.OutStatus := HTML_SUCCESS;
            end else // 200 OK
              Call.OutStatus := HTML_NOTFOUND;
          end;
      end else
      // ModelRoot/TableName with 'select=..&where=' or YUI paging
      if Method<>mLOCK then begin // Safe.Lock not available here
        SQLSelect := 'RowID'; // if no select is specified (i.e. ModelRoot/TableName)
        // all IDs of this table are returned to the client
        SQLTotalRowsCount := 0;
................................................................................
        SQL := Server.Model.TableProps[TableIndex].
          SQLFromSelectWhere(SQLSelect,trim(SQLWhere));
        Call.OutBody := Server.InternalListRawUTF8(TableIndex,SQL);
        if Call.OutBody<>'' then begin // got JSON list '[{...}]' ?
          opt := ClientSQLRecordOptions;
          if opt<>[] then
            ConvertOutBodyAsPlainJSON(SQLSelect,opt);
          Call.OutStatus := HTML_SUCCESS;  // 200 OK
          if Server.URIPagingParameters.SendTotalRowsCountFmt<>'' then
            // insert "totalRows":% optional value to the JSON output
            if Server.NoAJAXJSON or (ClientKind=ckFramework) then begin
              P := pointer(Call.OutBody);
              L := length(Call.OutBody);
              P := NotExpandedBufferRowCountPos(P,P+L);
              j := 0;
................................................................................
              if SQLTotalRowsCount=0 then // avoid sending fields array
                Call.OutBody := '[]' else
                Call.OutBody := trim(Call.OutBody);
              Call.OutBody := '{"values":'+Call.OutBody+
                FormatUTF8(Server.URIPagingParameters.SendTotalRowsCountFmt,[SQLTotalRowsCount])+'}';
            end;
        end else
          Call.OutStatus := HTML_NOTFOUND;
      end;
    end;
    if Call.OutStatus=HTML_SUCCESS then
      Server.fStats.NotifyORM(Method);
  end;
  mUNLOCK: begin
    // ModelRoot/TableName/TableID to unlock a member
    if not (TableIndex in Call.RestAccessRights^.PUT) then
      Call.OutStatus := HTML_NOTALLOWED else
    if (Table<>nil) and (TableID>0) and
       Server.Model.UnLock(Table,TableID) then
      Call.OutStatus := HTML_SUCCESS; // 200 OK
  end;
  mSTATE: begin
    // STATE method for TSQLRestClientServerInternalState
    // this method is called with Root (-> Table=nil -> Static=nil)
    // we need a specialized method in order to avoid fStats.Invalid increase
    Call.OutStatus := HTML_SUCCESS;
    for i := 0 to high(Server.fStaticData) do
      if (Server.fStaticData[i]<>nil) and
         Server.fStaticData[i].InheritsFrom(TSQLRestStorage) then
        if TSQLRestStorage(Server.fStaticData[i]).RefreshedAndModified then begin
          inc(Server.InternalState); // force refresh
          break;
        end;
................................................................................
begin
  if MethodIndex=Server.fPublishedMethodBatchIndex then begin
    ExecuteSOAByMethod; // run the BATCH process in execORMWrite context
    exit;
  end;
  if not Call.RestAccessRights^.CanExecuteORMWrite(
     Method,Table,TableIndex,TableID,self) then begin
    Call.OutStatus := HTML_FORBIDDEN;
    exit;
  end;
  case Method of
  mPOST: // POST=ADD=INSERT
    if Table=nil then begin
      // ModelRoot with free SQL statement sent as UTF-8 (only for Admin group)
      // see e.g. TSQLRestClientURI.EngineExecute
      if reSQL in Call.RestAccessRights^.AllowRemoteExecute then
        if (Call.InBody<>'') and
           (not (GotoNextNotSpace(Pointer(Call.InBody))^ in [#0,'[','{'])) and
           Server.EngineExecute(Call.InBody) then begin
          Call.OutStatus := HTML_SUCCESS; // 200 OK
        end else
        Call.OutStatus := HTML_FORBIDDEN;
    end else begin
      // ModelRoot/TableName with possible JSON SentData: create a new member
      // here, Table<>nil, TableID<0 and TableIndex in [0..MAX_SQLTABLES-1]
      if rsoComputeFieldsBeforeWriteOnServerSide in Server.Options then
        ComputeInBodyFields(seAdd);
      TableID := TableEngine.EngineAdd(TableIndex,Call.InBody);
      if TableID<>0 then begin
        Call.OutStatus := HTML_CREATED; // 201 Created
        Call.OutHead := 'Location: '+URI+'/'+Int64ToUtf8(TableID);
        if rsoAddUpdateReturnsContent in Server.Options then begin
          Server.fCache.NotifyDeletion(TableIndex,TableID);
          Call.OutBody := TableEngine.EngineRetrieve(TableIndex,TableID);
          Server.fCache.Notify(TableIndex,TableID,Call.OutBody,soInsert);
        end else
          Server.fCache.Notify(TableIndex,TableID,Call.InBody,soInsert);
................................................................................
          if OK then begin // flush (no CreateTime in JSON)
            Server.fCache.NotifyDeletion(TableIndex,TableID);
            if rsoAddUpdateReturnsContent in Server.Options then
              Call.OutBody := TableEngine.EngineRetrieve(TableIndex,TableID);
          end;
        end;
        if OK then
          Call.OutStatus := HTML_SUCCESS; // 200 OK
      end else
      Call.OutStatus := HTML_FORBIDDEN;
    end else
    if Parameters<>nil then begin // e.g. from TSQLRestClient.EngineUpdateField
      // PUT ModelRoot/TableName?setname=..&set=..&wherename=..&where=..
      repeat
        UrlDecodeValue(Parameters,'SETNAME=',SQLSelect);
        UrlDecodeValue(Parameters,'SET=',SQLDir);
        UrlDecodeValue(Parameters,'WHERENAME=',SQLSort);
        UrlDecodeValue(Parameters,'WHERE=',SQLWhere,@Parameters);
      until Parameters=nil;
      if (SQLSelect<>'') and (SQLDir<>'') and (SQLSort<>'') and (SQLWhere<>'') then
        if TableEngine.EngineUpdateField(TableIndex,SQLSelect,SQLDir,SQLSort,SQLWhere) then begin
          if rsoAddUpdateReturnsContent in Server.Options then
            Call.OutBody := TableEngine.EngineRetrieve(TableIndex,TableID);
          Call.OutStatus := HTML_SUCCESS; // 200 OK
        end;
    end;
  mDELETE:
    if TableID>0 then
      // ModelRoot/TableName/TableID to delete a member
      if not Server.RecordCanBeUpdated(Table,TableID,seDelete,@CustomErrorMsg) then
        Call.OutStatus := HTML_FORBIDDEN else begin
        if TableEngine.EngineDelete(TableIndex,TableID) and
           Server.AfterDeleteForceCoherency(TableIndex,TableID) then begin
          Call.OutStatus := HTML_SUCCESS; // 200 OK
          Server.fCache.NotifyDeletion(TableIndex,TableID);
        end;
      end else
    if Parameters<>nil then begin
      // ModelRoot/TableName?where=WhereClause to delete members
      repeat
        if UrlDecodeValue(Parameters,'WHERE=',SQLWhere,@Parameters) then begin
          SQLWhere := trim(SQLWhere);
          if SQLWhere<>'' then begin
            if Server.Delete(Table,SQLWhere) then
              Call.OutStatus := HTML_SUCCESS; // 200 OK
          end;
          break;
        end;
      until Parameters=nil;
    end;
  mBEGIN: begin      // BEGIN TRANSACTION
    // TSQLVirtualTableJSON/External will rely on SQLite3 module
................................................................................
      if (Static<>nil) and (StaticKind=sVirtualTable) then
        Static.TransactionBegin(Table,Session) else
      if (Static=nil) and (Server.fTransactionTable<>nil) then begin
        Static := Server.StaticVirtualTable[Server.fTransactionTable];
        if Static<>nil then
          Static.TransactionBegin(Table,Session);
      end;
      Call.OutStatus := HTML_SUCCESS; // 200 OK
    end;
  end;
  mEND: begin        // END=COMMIT
    // this method is called with Root (-> Table=nil -> Static=nil)
    // mEND logic is just the opposite of mBEGIN: release static, then main
    if (Static<>nil) and (StaticKind=sVirtualTable) then
      Static.Commit(Session,false) else
    if (Static=nil) and (Server.fTransactionTable<>nil) then begin
      Static := Server.StaticVirtualTable[Server.fTransactionTable];
      if Static<>nil then
        Static.Commit(Session,false);
    end;
    Server.Commit(Session,false);
    Call.OutStatus := HTML_SUCCESS; // 200 OK
  end;
  mABORT: begin      // ABORT=ROLLBACK
    // this method is called with Root (-> Table=nil -> Static=nil)
    // mABORT logic is just the opposite of mBEGIN: release static, then main
    if (Static<>nil) and (StaticKind=sVirtualTable) then
      Static.RollBack(Session) else
    if (Static=nil) and (Server.fTransactionTable<>nil) then begin
      Static := Server.StaticVirtualTable[Server.fTransactionTable];
      if Static<>nil then
        Static.RollBack(Session);
    end;
    Server.RollBack(Session);
    Call.OutStatus := HTML_SUCCESS; // 200 OK
  end;
  end;
  if StatusCodeIsSuccess(Call.OutStatus) then
    Server.fStats.NotifyORM(Method);
end;

procedure TSQLRestServerURIContext.FillInput(const LogInputIdent: RawUTF8);
................................................................................
  if HandleErrorAsRegularResult or StatusCodeIsSuccess(Status) then begin
    Call.OutStatus := Status;
    Call.OutBody := Result;
    if CustomHeader<>'' then
      Call.OutHead := CustomHeader else
      if Call.OutHead='' then
        Call.OutHead := JSON_CONTENT_TYPE_HEADER_VAR;
    if Handle304NotModified and (Status=HTML_SUCCESS) and
       (Length(Result)>64) then begin
      clientHash := FindIniNameValue(pointer(Call.InHead),'IF-NONE-MATCH: ');
      serverHash := '"'+crc32cUTF8ToHex(Result)+'"';
      if clientHash<>serverHash then
        Call.OutHead := Call.OutHead+#13#10'ETag: '+serverHash else begin
        Call.OutBody := ''; // save bandwidth for "304 Not Modified"
        Call.OutStatus := HTML_NOTMODIFIED;
      end;
    end;
  end else
    Error(Result,Status);
end;

procedure TSQLRestServerURIContext.Returns(Value: TObject; Status: integer;
................................................................................
begin
  if FileName='' then
    FileTime := 0 else
    FileTime := FileAgeToDateTime(FileName);
  if FileTime=0 then
    if Error404Redirect<>'' then
      Redirect(Error404Redirect) else
      Error('',HTML_NOTFOUND) else begin
    if Call.OutHead<>'' then
      Call.OutHead := Call.OutHead+#13#10;
    if ContentType<>'' then
      Call.OutHead := Call.OutHead+HEADER_CONTENT_TYPE+ContentType else
      Call.OutHead := Call.OutHead+GetMimeContentTypeHeader('',FileName);
    Call.OutStatus := HTML_SUCCESS;
    if Handle304NotModified then begin
      clientHash := FindIniNameValue(pointer(Call.InHead),'IF-NONE-MATCH: ');
      serverHash := '"'+DateTimeToIso8601(FileTime,false)+'"';
      Call.OutHead := Call.OutHead+#13#10'ETag: '+serverHash;
      if clientHash=serverHash then begin
        Call.OutStatus := HTML_NOTMODIFIED;
        exit;
      end;
    end;
    // Content-Type: appears twice: 1st to notify static file, 2nd for mime type
    Call.OutHead := STATICFILE_CONTENT_TYPE_HEADER+#13#10+Call.OutHead;
    StringToUTF8(FileName,Call.OutBody); // body=filename for STATICFILE_CONTENT
    if AttachmentFileName<>'' then
................................................................................
  ReturnFile(fileName,Handle304NotModified,'','',Error404Redirect);
end;

procedure TSQLRestServerURIContext.Redirect(const NewLocation: RawUTF8;
  PermanentChange: boolean);
begin
  if PermanentChange then
    Call.OutStatus := HTML_MOVEDPERMANENTLY else
    Call.OutStatus := HTML_TEMPORARYREDIRECT;
  Call.OutHead := 'Location: '+NewLocation;
end;

procedure TSQLRestServerURIContext.Returns(const NameValuePairs: array of const;
  Status: integer; Handle304NotModified,HandleErrorAsRegularResult: boolean);
begin
  Returns(JSONEncode(NameValuePairs),Status,'',Handle304NotModified,
................................................................................
  Log := fLogClass.Enter('URI(% % inlen=%)',[Call.Method,Call.Url,length(Call.InBody)],self);
{$else}
begin
{$endif}
  QueryPerformanceCounter(timeStart);
  fStats.AddCurrentRequestCount(1);
  Call.OutInternalState := InternalState; // other threads may change it
  Call.OutStatus := HTML_BADREQUEST; // default error code is 400 BAD REQUEST
  Ctxt := ServicesRouting.Create(self,Call);
  try
    {$ifdef WITHLOG}
    Ctxt.Log := Log.Instance;
    {$endif}
    if fShutdownRequested then
      Ctxt.Error('Server is shutting down',HTML_UNAVAILABLE) else
    if Ctxt.Method=mNone then
      Ctxt.Error('Unknown VERB') else
    // 1. decode URI
    if not Ctxt.URIDecodeREST then
      Ctxt.Error('Invalid Root',HTML_NOTFOUND) else
    if (RootRedirectGet<>'') and (Ctxt.Method=mGet) and
       (Call.Url=Model.Root) and (Call.InBody='') then
      Ctxt.Redirect(RootRedirectGet) else begin
      Ctxt.URIDecodeSOAByMethod;
      if (Ctxt.MethodIndex<0) and (Ctxt.URI<>'') then
        Ctxt.URIDecodeSOAByInterface;
      // 2. handle security
................................................................................
          Ctxt.Command := execORMWrite;
        if (not Assigned(OnBeforeURI)) or OnBeforeURI(Ctxt) then
          Ctxt.ExecuteCommand;
      except
        on E: Exception do
          if (not Assigned(OnErrorURI)) or OnErrorURI(Ctxt,E) then
            // return 500 internal server error
            Ctxt.Error(E,'',[],HTML_SERVERERROR);
      end;
    end;
    // 4. returns expected result to the client and update Server statistics
    if StatusCodeIsSuccess(Call.OutStatus) then begin
      outcomingfile := false;
      if Call.OutBody<>'' then begin
        len := length(Call.OutHead);
        outcomingfile := (len>=25) and (Call.OutHead[15]='!') and
          IdemPChar(pointer(Call.OutHead),STATICFILE_CONTENT_TYPE_HEADER_UPPPER);
      end else // Call.OutBody=''
        if (Call.OutStatus=HTML_SUCCESS) and
           (rsoHtml200WithNoBodyReturns204 in fOptions) then
          Call.OutStatus := HTML_NOCONTENT;
      fStats.ProcessSuccess(outcomingfile);
    end else begin
      fStats.ProcessErrorNumber(Call.OutStatus);
      if Call.OutBody='' then // if no custom error message, compute it now as JSON
        Ctxt.Error(Ctxt.CustomErrorMsg,Call.OutStatus);
    end;
    StatsAddSizeForCall(fStats,Call);
................................................................................
  info: TDocVariantData;
begin
  if IdemPropNameU(Ctxt.URIBlobFieldName,'info') then begin
    info.InitFast;
    InternalInfo(info);
    Ctxt.Returns(info.ToJSON('','',jsonHumanReadable));
  end else
    Ctxt.Returns(Int64ToUtf8(ServerTimeStamp),HTML_SUCCESS,TEXT_CONTENT_TYPE_HEADER);
end;

procedure TSQLRestServer.CacheFlush(Ctxt: TSQLRestServerURIContext);
begin
  case Ctxt.Method of
  mGET: begin
    if Ctxt.Table=nil then
................................................................................
    Ctxt.Error('PUT/POST only');
    exit;
  end;
  try
    EngineBatchSend(Ctxt.Table,Ctxt.Call.InBody,TIDDynArray(Results),0);
  except
    on E: Exception do begin
      Ctxt.Error(E,'did break % BATCH process',[Ctxt.Table],HTML_SERVERERROR);
      exit;
    end;
  end;
  // send back operation status array
  Ctxt.Call.OutStatus := HTML_SUCCESS;
  for i := 0 to length(Results)-1 do
    if Results[i]<>HTML_SUCCESS then begin
      Ctxt.Call.OutBody := Int64DynArrayToCSV(Results,length(Results),'[',']');
      exit;
    end;
  Ctxt.Call.OutBody := '["OK"]';  // to save bandwith if no adding
end;

function ServerNonce(Previous: boolean): RawUTF8;
................................................................................
          RunningBatchTable := RunTable;
          RunningBatchURIMethod := URIMethod;
        end;
        if Count>=length(Results) then
          SetLength(Results,Count+256+Count shr 3);
      end;
      // process CRUD method operation
      Results[Count] := HTML_NOTMODIFIED;
      case URIMethod of
      mDELETE: begin
        OK := EngineDelete(RunTableIndex,ID);
        if OK then begin
          if fCache<>nil then
            fCache.NotifyDeletion(RunTableIndex,ID);
          if (RunningBatchRest<>nil) or
             AfterDeleteForceCoherency(RunTableIndex,ID) then
            Results[Count] := HTML_SUCCESS; // 200 OK
        end;
      end;
      mPOST: begin
        ID := EngineAdd(RunTableIndex,Value);
        Results[Count] := ID;
        if (ID<>0) and (fCache<>nil) then
          fCache.Notify(RunTableIndex,ID,Value,soInsert);
      end;
      mPUT: begin
        OK := EngineUpdate(RunTableIndex,ID,Value);
        if OK then begin
          Results[Count] := HTML_SUCCESS; // 200 OK
          if fCache<>nil then // JSON Value may be uncomplete -> delete from cache
            fCache.NotifyDeletion(RunTableIndex,ID);
        end;
      end;
      else raise EORMBatchException.CreateUTF8('%.EngineBatchSend: Unknown "%" method',
        [self,Method]);
      end;
................................................................................
      raise EORMBatchException.CreateUTF8('%.EngineBatchSend: % Truncated',[self,Table]);
    while not (Sent^ in ['}',#0]) do inc(Sent);
    if Sent^<>'}' then
      raise EORMBatchException.CreateUTF8('%.EngineBatchSend(%): Missing }',[self,Table]);
  end;
  // if we reached here, process was OK
  SetLength(Results,Count);
  result := HTML_SUCCESS;
end;

function CurrentServiceContext: TServiceRunningContext;
begin
  result := ServiceContext;
end;

................................................................................
  {$else}
  aDLL := LoadLibrary(pointer(DllName));
  {$endif}
  {$endif}
  if aDLL=0 then
    raise ECommunicationException.CreateUTF8('%.Create: LoadLibrary(%)',[self,DllName]);
  aRequest := GetProcAddress(aDLL,'URIRequest');
  if (@aRequest=nil) or (aRequest(nil,nil,nil,nil,nil).Lo<>HTML_NOTFOUND) then begin
    FreeLibrary(aDLL);
    raise ECommunicationException.CreateUTF8(
      '%.Create: % doesn''t export a valid URIRequest() function',[self,DllName]);
  end;
  Create(aModel,aRequest);
  fLibraryHandle := aDLL;
end;
................................................................................
end;

procedure TSQLRestClientURIDll.InternalURI(var Call: TSQLRestURIParams);
var result: Int64Rec;
    pHead, pResp: PUTF8Char;
begin
  if @Func=nil then begin
    Call.OutStatus := HTML_NOTIMPLEMENTED; // 501 (no valid application or library)
    exit;
  end;
  pResp := nil;
  pHead := nil;
  try
    result := Func(pointer(Call.Url),pointer(Call.Method),pointer(Call.InBody),
      @pResp,@pHead);
................................................................................
  fSafe.Enter;
  try
    if Assigned(fRedirectedServer) then
      fRedirectedServer.URI(Call) else
    if Assigned(fRedirectedClient) then
      // hook to access InternalURI() protected method
      TSQLRestClientRedirect(fRedirectedClient).InternalURI(Call) else
      Call.OutStatus := HTML_GATEWAYTIMEOUT;
  finally
    fSafe.Leave;
  end;
end;


{$ifdef MSWINDOWS}
................................................................................
    {$ifdef WITHLOG}
    Log: ISynLog;
    {$endif}
begin
  {$ifdef WITHLOG}
  Log := fLogClass.Enter(self);
  {$endif}
  Call.OutStatus := HTML_NOTIMPLEMENTED; // 501 (no valid application or library)
  fSafe.Enter;
  try
    if InternalCheckOpen then
    try
      Card := MAGIC_SYN; // magic word
      if FileWrite(fServerPipe,Card,4)<>4 then begin
        SleepHiRes(0);
................................................................................
          FileRead(fServerPipe,Call.OutStatus,sizeof(cardinal));
          FileRead(fServerPipe,Call.OutInternalState,sizeof(cardinal));
          Call.OutHead := ReadString(fServerPipe);
          Call.OutBody := ReadString(fServerPipe);
          exit;
        end else
        SleepHiRes(i);
      Call.OutStatus := HTML_TIMEOUT; // 408 Request Timeout Error
{$else}
      if FileRead(fServerPipe,Call.OutStatus,sizeof(cardinal))=sizeof(cardinal) then begin
        // FileRead() waits till response arrived (or pipe is broken)
        FileRead(fServerPipe,Call.OutInternalState,sizeof(cardinal));
        Call.OutHead := ReadString(fServerPipe);
        Call.OutBody := ReadString(fServerPipe);
      end else
        Call.OutStatus := HTML_NOTFOUND;
{$endif}
     except
       on E: Exception do begin // error in ReadString()
         InternalLog('% for PipeName=%',[E,fPipeName],sllLastError);
         Call.OutStatus := HTML_NOTIMPLEMENTED; // 501 (no valid application or library)
         WriteString(fServerPipe,''); // try to notify the server of client logout
         FileClose(fServerPipe);
         fServerPipe := 0;
       end;
     end;
  finally
    fSafe.Leave;
................................................................................
    Safe.UnLock;
  end;
end;

function TServicesPublishedInterfacesList.RegisterFromServer(Client: TSQLRestClientURI): boolean;
var json: RawUTF8;
begin
  result := Client.CallBackGet('stat',['findservice','*'],json)=HTML_SUCCESS;
  if result and (json<>'') then
    RegisterFromServerJSON(json);
end;

procedure TServicesPublishedInterfacesList.RegisterFromServerJSON(
  var PublishedJson: RawUTF8);
var tix: Int64;
................................................................................

procedure TSQLRestStorageShard.InternalBatchStop;
var i: integer;
begin
  try
    for i := 0 to high(fShardBatch) do
      if fShardBatch[i]<>nil then
        if fShards[i].BatchSend(fShardBatch[i])<>HTML_SUCCESS then
          InternalLog('%.InternalBatchStop(%): %.BatchSend failed for shard #%',
            [ClassType,fStoredClass,fShards[i].ClassType,i],sllWarning);
  finally
    ObjArrayClear(fShardBatch);
    StorageUnLock;
  end;
end;
................................................................................
    Log: ISynLog;
    {$endif}
begin
  {$ifdef WITHLOG}
  Log := fLogClass.Enter(self);
  {$endif}
  if (fClientWindow=0) or not InternalCheckOpen then begin
    Call.OutStatus := HTML_NOTIMPLEMENTED; // 501
    InternalLog('InternalCheckOpen failure',sllClient);
    exit;
  end;
  // 1. send request
  // #1 is a field delimiter below, since Get*Item() functions return nil for #0
  SetString(Msg,PAnsiChar(@MAGIC_SYN),4);
  Msg := Msg+Call.Url+#1+Call.Method+#1+Call.InHead+#1+Call.InBody;
................................................................................
        if not DoNotProcessMessages then
          while PeekMessage(aMsg,0,0,0,PM_REMOVE) do begin
            TranslateMessage(aMsg);
            DispatchMessage(aMsg);
          end;
        SleepHiRes(0);
        if GetTickCount64>Finished64 then begin
          Call.OutStatus := HTML_TIMEOUT; // 408 Request Timeout Error
          exit;
        end;
      until fCurrentResponse<>#0;
    end;
    // 3. return answer to caller
    if length(fCurrentResponse)<=sizeof(Int64) then
      Call.OutStatus := HTML_NOTIMPLEMENTED else begin
      P := pointer(fCurrentResponse);
      if PCardinal(P)^<>MAGIC_SYN then // broadcasted WM_COPYDATA message? :(
        Call.OutStatus := 0 else begin
        Call.OutStatus := PIntegerArray(P)[1];
        Call.OutInternalState := PIntegerArray(P)[2];
        inc(P,sizeof(integer)*3);
      end;
      if Call.OutStatus=0 then
        Call.OutStatus := HTML_NOTFOUND else begin
        Call.OutHead := GetNextItem(P,#1);
        if P<>nil then
          SetString(Call.OutBody,P,length(fCurrentResponse)-(P-pointer(fCurrentResponse)));
      end;
    end;
  finally
    fSafe.Leave;
................................................................................
end;

procedure TSQLRestClientURIMessage.WMCopyData(var Msg: TWMCopyData);
begin
  if (self=nil) or (Msg.From<>fServerWindow) or
     (PCopyDataStruct(Msg.CopyDataStruct)^.dwData<>fServerWindow) then
    exit;
  Msg.Result := HTML_SUCCESS; // Send something back
  if fCurrentResponse=#0 then // expect some response?
    SetString(fCurrentResponse,PAnsiChar(PCopyDataStruct(Msg.CopyDataStruct)^.lpData),
      PCopyDataStruct(Msg.CopyDataStruct)^.cbData);
end;

function TSQLRestClientURIMessage.InternalCheckOpen: boolean;
begin
................................................................................
end;

class function TSQLRestServerAuthentication.ClientGetSessionKey(
  Sender: TSQLRestClientURI; User: TSQLAuthUser; const aNameValueParameters: array of const): RawUTF8;
var resp: RawUTF8;
    values: TPUtf8CharDynArray;
begin
  if (Sender.CallBackGet('Auth',aNameValueParameters,resp)<>HTML_SUCCESS) or
     (JSONDecode(pointer(resp),['result','data','server','version',
       'logonid','logonname','logondisplay','logongroup'],values)=nil) then begin
    Sender.fSessionData := '';
    result := '';
  end else begin
    SetString(result,values[0],StrLen(values[0]));
    Base64ToBin(PAnsiChar(values[1]),StrLen(values[1]),Sender.fSessionData);
................................................................................
        Ctxt.AuthenticationFailed(afInvalidPassword);
    finally
      U.Free;
    end else
      Ctxt.AuthenticationFailed(afUnknownUser);
  end else begin
    Ctxt.Call.OutHead := 'WWW-Authenticate: Basic realm="mORMot Server"';;
    Ctxt.Error('',HTML_UNAUTHORIZED); // will popup for credentials in browser
  end;
end;


{$ifdef SSPIAUTH}

{ TSQLRestServerAuthenticationSSPI }
................................................................................
  InDataEnc := Ctxt.InputUTF8['Data'];
  if InDataEnc='' then begin
    // client is browser and used HTTP headers to send auth data
    InDataEnc := FindIniNameValue(pointer(Ctxt.Call.InHead),SECPKGNAMEHTTPAUTHORIZATION);
    if InDataEnc = '' then begin
      // no auth data sent, reply with supported auth methods
      Ctxt.Call.OutHead := SECPKGNAMEHTTPWWWAUTHENTICATE;
      Ctxt.Call.OutStatus := HTML_UNAUTHORIZED;
      StatusCodeToErrorMsg(Ctxt.Call.OutStatus, Ctxt.Call.OutBody);
      exit;
    end;
    BrowserAuth := True;
  end else
    BrowserAuth := False;
  CtxArr.InitSpecific(TypeInfo(TSecContextDynArray),fSSPIAuthContexts,djInt64);
................................................................................
    SecCtxIdx := CtxArr.New; // add a new entry to fSSPIAuthContexts[]
    InvalidateSecContext(fSSPIAuthContexts[SecCtxIdx],ConnectionID);
  end;
  // call SSPI provider
  if ServerSSPIAuth(fSSPIAuthContexts[SecCtxIdx], Base64ToBin(InDataEnc), OutData) then begin
    if BrowserAuth then begin
      Ctxt.Call.OutHead := (SECPKGNAMEHTTPWWWAUTHENTICATE+' ')+BinToBase64(OutData);
      Ctxt.Call.OutStatus := HTML_UNAUTHORIZED;
      StatusCodeToErrorMsg(Ctxt.Call.OutStatus, Ctxt.Call.OutBody);
    end else
      Ctxt.Returns(['result','','data',BinToBase64(OutData)]);
    exit; // 1st call: send back OutData to the client
  end;
  // 2nd call: user was authenticated -> release used context
  ServerSSPIAuthUser(fSSPIAuthContexts[SecCtxIdx],UserName);
................................................................................
      if Session<>nil then
        with Session.User do
        if BrowserAuth then
          Ctxt.Returns(JSONEncode(['result',Session.fPrivateSalt,
            'logonid',IDValue,'logonname',LogonName,'logondisplay',DisplayName,
            'logongroup',GroupRights.IDValue,
            'server',ExeVersion.ProgramName,'version',ExeVersion.Version.Detailed]),
            HTML_SUCCESS,(SECPKGNAMEHTTPWWWAUTHENTICATE+' ')+BinToBase64(OutData)) else
          Ctxt.Returns([
            'result',BinToBase64(SecEncrypt(fSSPIAuthContexts[SecCtxIdx],Session.fPrivateSalt)),
            'logonid',IDValue,'logonname',LogonName,'logondisplay',DisplayName,
            'logongroup',GroupRights.ID,'server',ExeVersion.ProgramName,
            'version',ExeVersion.Version.Detailed,'data',BinToBase64(OutData)]);
    finally
      User.Free;
................................................................................

  function GetFullMethodName: RawUTF8;
  begin
    if cardinal(Ctxt.ServiceMethodIndex)<fInterface.fMethodsCount then
      result := fInterface.fMethods[Ctxt.ServiceMethodIndex].InterfaceDotMethodName else
      result := fInterface.fInterfaceName;
  end;
  procedure Error(const Msg: RawUTF8; Status: integer=HTML_BADREQUEST);
  begin
    Ctxt.Error('(%) % for %',[ToText(InstanceCreation)^,Msg,GetFullMethodName],Status);
  end;
  function StatsCreate: TSynMonitorInputOutput;
  begin
    result := TSynMonitorInputOutput.Create(GetFullMethodName);
  end;
................................................................................
      else
        if Ctxt.Session>CONST_AUTHENTICATION_NOT_USED then
          case InstanceCreation of // authenticated user -> handle context
          sicPerSession: Inst.InstanceID := Ctxt.Session;
          sicPerUser:    Inst.InstanceID := Ctxt.SessionUser;
          sicPerGroup:   Inst.InstanceID := Ctxt.SessionGroup;
          end else begin
            Error('mode expects an authenticated session',HTML_UNAUTHORIZED);
            exit;
          end;
      end;
      if InternalInstanceRetrieve(Inst,Ctxt.ServiceMethodIndex) then begin
        Ctxt.Success; // was SERVICE_METHODINDEX_FREEINSTANCE
        exit;         // {"method":"_free_", "params":[], "id":1234}
      end;
    end;
  end;
  if Inst.Instance=nil then begin
    Error('instance not found or deprecated',HTML_BADREQUEST);
    exit;
  end;
  Ctxt.ServiceInstanceID := Inst.InstanceID;
  // 2. call method implementation
  if (Ctxt.ServiceExecution=nil) or
     (cardinal(Ctxt.ServiceMethodIndex)>=fInterface.fMethodsCount) then begin
    Error('ServiceExecution=nil',HTML_SERVERERROR);
    exit;
  end;
  if mlInterfaces in TSQLRestServer(Rest).StatLevels then begin
    stats := fStats[Ctxt.ServiceMethodIndex];
    if stats=nil then begin
      stats := StatsCreate;
      fStats[Ctxt.ServiceMethodIndex] := stats;
................................................................................
          MultiEventMerge(exec.fOnExecute,fOnExecute);
        if Ctxt.ServiceExecution.LogRest<>nil then
          exec.AddInterceptor(OnLogRestExecuteMethod);
        if exec.ExecuteJson([instancePtr],Ctxt.ServiceParameters,WR,Ctxt.ForceServiceResultAsJSONObject) then begin
          Ctxt.Call.OutHead := exec.ServiceCustomAnswerHead;
          Ctxt.Call.OutStatus := exec.ServiceCustomAnswerStatus;
        end else begin
          Error('execution failed (probably due to bad input parameters)',HTML_NOTACCEPTABLE);
          exit; // wrong request
        end;
      finally
        if dolock then
          LeaveCriticalSection(fInstanceLock);
      end;
      if Ctxt.Call.OutHead='' then begin // <>'' for TServiceCustomAnswer
        Ctxt.ServiceResultEnd(WR,Inst.InstanceID);
        Ctxt.Call.OutHead := JSON_CONTENT_TYPE_HEADER_VAR;
        Ctxt.Call.OutStatus := HTML_SUCCESS;
      end;
      WR.SetText(Ctxt.Call.OutBody);
    finally
      Ctxt.fThreadServer^.Factory := nil;
      WR.Free;
    end;
  finally
................................................................................
      // handle custom content (not JSON array/object answer)
      if ArgsResultIsServiceCustomAnswer then
        with PServiceCustomAnswer(fValues[ArgsResultIndex])^ do
        if Header<>'' then begin
          fServiceCustomAnswerHead := Header;
          Res.ForceContent(Content);
          if Status=0 then // Values[]=@Records[] is filled with 0 by default
            fServiceCustomAnswerStatus := HTML_SUCCESS else
            fServiceCustomAnswerStatus := Status;
          Result := true;
          exit;
        end;
      // write the '{"result":[...' array or object
      for a := ArgsOutFirst to ArgsOutLast do
      with Args[a] do
................................................................................
    result := InternalInvoke(
      aMethod.URI,aParams,aResult,aErrorMsg,aClientDrivenID,aServiceCustomAnswer);
end;

class function TServiceFactoryClient.GetErrorMessage(status: integer): RawUTF8;
begin
  case status of
    HTML_UNAVAILABLE: result := 'Check the communication parameters';
    HTML_NOTIMPLEMENTED: result := 'Server not reachable';
    HTML_NOTALLOWED: result := 'Method forbidden for this User group';
    HTML_UNAUTHORIZED: result := 'No active session';
    HTML_NOTACCEPTABLE: result := 'Invalid input parameters';
    else result := '';
  end;
end;

function TServiceFactoryClient.InternalInvoke(const aMethod: RawUTF8;
  const aParams: RawUTF8; aResult: PRawUTF8; aErrorMsg: PRawUTF8;
  aClientDrivenID: PCardinal; aServiceCustomAnswer: PServiceCustomAnswer;






|







 







|





|
|







 







|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|







 







|



|







 







|







|







 







|







 







|
|





|

|

|








|
|
|






|




|





|

|







 







|
|









|
|
|





|

|





|



|



|







 







|







 







|








|
|







 







|







 







|







 







|
|


|







 







|

|







 







|







 







|







 







|
|
|
|







 







|







 







|







 







|







 







|












|







|







|







 







|







 







|







 







|

|













|







 







|






|







 







|
|





|







 







|







 







|







 







|







 







|







 







|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|













|
|







 







|




|




|







 







|













|









|







 







|












|










|







 







|













|







 







|



|









|







 







|







 







|






|







 







|
|







 







|







 







|







 







|
|







|







 







|




|











|







 







|







|
|









|







 







|







 







|







 







|







 







|







|






|








|







 







|








|











|













|












|





|






|







 







|







 







|







 







|







 







|










|







 







|







 







|







 







|







 







|







 







|







 







|






|







 







|










|





|










|

|







 







|

|







 







|







 







|


|





|


|





|







 







|











|

|







|







 







|

|













|






|


|










|







 







|













|












|







 







|






|







 







|





|





|







 







|
|







 







|






|




|







 







|










|

|







 







|







 







|




|

|







 







|








|











|







 







|







 







|







 







|







 







|







 







|







 







|







|




|







 







|







 







|







 







|







 







|






|








|







 







|







 







|







 







|







 







|







 







|







 







|







 







|







 







|










|






|







 







|









|







 







|







 







|
|
|
|
|







795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
...
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
....
4537
4538
4539
4540
4541
4542
4543
4544
4545
4546
4547
4548
4549
4550
4551
4552
4553
4554
4555
4556
4557
4558
4559
4560
4561
4562
4563
4564
4565
4566
4567
4568
4569
4570
4571
4572
4573
4574
4575
4576
4577
4578
4579
4580
4581
4582
4583
4584
4585
4586
4587
4588
4589
4590
4591
4592
4593
4594
4595
4596
4597
4598
4599
4600
4601
4602
4603
4604
4605
....
4617
4618
4619
4620
4621
4622
4623
4624
4625
4626
4627
4628
4629
4630
4631
4632
4633
4634
4635
....
5548
5549
5550
5551
5552
5553
5554
5555
5556
5557
5558
5559
5560
5561
5562
5563
5564
5565
5566
5567
5568
5569
5570
....
5894
5895
5896
5897
5898
5899
5900
5901
5902
5903
5904
5905
5906
5907
5908
....
6178
6179
6180
6181
6182
6183
6184
6185
6186
6187
6188
6189
6190
6191
6192
6193
6194
6195
6196
6197
6198
6199
6200
6201
6202
6203
6204
6205
6206
6207
6208
6209
6210
6211
6212
6213
6214
6215
6216
6217
6218
6219
6220
6221
6222
6223
6224
6225
6226
6227
6228
6229
6230
6231
6232
6233
6234
....
6247
6248
6249
6250
6251
6252
6253
6254
6255
6256
6257
6258
6259
6260
6261
6262
6263
6264
6265
6266
6267
6268
6269
6270
6271
6272
6273
6274
6275
6276
6277
6278
6279
6280
6281
6282
6283
6284
6285
6286
6287
6288
6289
6290
6291
6292
6293
6294
6295
6296
....
6430
6431
6432
6433
6434
6435
6436
6437
6438
6439
6440
6441
6442
6443
6444
.....
10455
10456
10457
10458
10459
10460
10461
10462
10463
10464
10465
10466
10467
10468
10469
10470
10471
10472
10473
10474
10475
10476
10477
10478
10479
.....
11851
11852
11853
11854
11855
11856
11857
11858
11859
11860
11861
11862
11863
11864
11865
.....
11910
11911
11912
11913
11914
11915
11916
11917
11918
11919
11920
11921
11922
11923
11924
.....
12208
12209
12210
12211
12212
12213
12214
12215
12216
12217
12218
12219
12220
12221
12222
12223
12224
12225
12226
.....
13950
13951
13952
13953
13954
13955
13956
13957
13958
13959
13960
13961
13962
13963
13964
13965
13966
.....
14936
14937
14938
14939
14940
14941
14942
14943
14944
14945
14946
14947
14948
14949
14950
.....
15399
15400
15401
15402
15403
15404
15405
15406
15407
15408
15409
15410
15411
15412
15413
.....
15618
15619
15620
15621
15622
15623
15624
15625
15626
15627
15628
15629
15630
15631
15632
15633
15634
15635
.....
15883
15884
15885
15886
15887
15888
15889
15890
15891
15892
15893
15894
15895
15896
15897
.....
16525
16526
16527
16528
16529
16530
16531
16532
16533
16534
16535
16536
16537
16538
16539
.....
16638
16639
16640
16641
16642
16643
16644
16645
16646
16647
16648
16649
16650
16651
16652
.....
17506
17507
17508
17509
17510
17511
17512
17513
17514
17515
17516
17517
17518
17519
17520
17521
17522
17523
17524
17525
17526
17527
17528
17529
17530
17531
17532
17533
17534
17535
17536
17537
17538
17539
17540
17541
17542
17543
17544
17545
17546
17547
17548
17549
.....
17770
17771
17772
17773
17774
17775
17776
17777
17778
17779
17780
17781
17782
17783
17784
.....
17864
17865
17866
17867
17868
17869
17870
17871
17872
17873
17874
17875
17876
17877
17878
.....
18032
18033
18034
18035
18036
18037
18038
18039
18040
18041
18042
18043
18044
18045
18046
18047
18048
18049
18050
18051
18052
18053
18054
18055
18056
18057
18058
18059
18060
18061
18062
.....
18066
18067
18068
18069
18070
18071
18072
18073
18074
18075
18076
18077
18078
18079
18080
18081
18082
18083
18084
18085
18086
18087
.....
18180
18181
18182
18183
18184
18185
18186
18187
18188
18189
18190
18191
18192
18193
18194
18195
18196
18197
18198
18199
18200
18201
.....
18241
18242
18243
18244
18245
18246
18247
18248
18249
18250
18251
18252
18253
18254
18255
.....
18309
18310
18311
18312
18313
18314
18315
18316
18317
18318
18319
18320
18321
18322
18323
.....
18400
18401
18402
18403
18404
18405
18406
18407
18408
18409
18410
18411
18412
18413
18414
.....
19219
19220
19221
19222
19223
19224
19225
19226
19227
19228
19229
19230
19231
19232
19233
.....
22926
22927
22928
22929
22930
22931
22932
22933
22934
22935
22936
22937
22938
22939
22940
22941
22942
22943
22944
22945
22946
22947
22948
22949
22950
22951
22952
22953
22954
22955
22956
22957
22958
22959
22960
22961
22962
22963
22964
22965
22966
22967
22968
22969
22970
22971
22972
22973
22974
22975
22976
22977
22978
22979
22980
22981
.....
33582
33583
33584
33585
33586
33587
33588
33589
33590
33591
33592
33593
33594
33595
33596
33597
33598
33599
33600
33601
33602
33603
33604
33605
33606
.....
34994
34995
34996
34997
34998
34999
35000
35001
35002
35003
35004
35005
35006
35007
35008
35009
35010
35011
35012
35013
35014
35015
35016
35017
35018
35019
35020
35021
35022
35023
35024
35025
35026
35027
35028
35029
35030
35031
35032
.....
35039
35040
35041
35042
35043
35044
35045
35046
35047
35048
35049
35050
35051
35052
35053
35054
35055
35056
35057
35058
35059
35060
35061
35062
35063
35064
35065
35066
35067
35068
35069
35070
35071
35072
35073
35074
35075
35076
35077
.....
35149
35150
35151
35152
35153
35154
35155
35156
35157
35158
35159
35160
35161
35162
35163
35164
35165
35166
35167
35168
35169
35170
35171
35172
35173
35174
35175
35176
35177
.....
35187
35188
35189
35190
35191
35192
35193
35194
35195
35196
35197
35198
35199
35200
35201
35202
35203
35204
35205
35206
35207
35208
35209
35210
35211
35212
35213
35214
35215
.....
35372
35373
35374
35375
35376
35377
35378
35379
35380
35381
35382
35383
35384
35385
35386
.....
35418
35419
35420
35421
35422
35423
35424
35425
35426
35427
35428
35429
35430
35431
35432
35433
35434
35435
35436
35437
35438
35439
.....
35592
35593
35594
35595
35596
35597
35598
35599
35600
35601
35602
35603
35604
35605
35606
35607
.....
35624
35625
35626
35627
35628
35629
35630
35631
35632
35633
35634
35635
35636
35637
35638
.....
35688
35689
35690
35691
35692
35693
35694
35695
35696
35697
35698
35699
35700
35701
35702
.....
35732
35733
35734
35735
35736
35737
35738
35739
35740
35741
35742
35743
35744
35745
35746
35747
35748
35749
35750
35751
35752
35753
35754
35755
.....
35764
35765
35766
35767
35768
35769
35770
35771
35772
35773
35774
35775
35776
35777
35778
35779
35780
35781
35782
35783
35784
35785
35786
35787
35788
35789
35790
35791
35792
35793
35794
35795
.....
35796
35797
35798
35799
35800
35801
35802
35803
35804
35805
35806
35807
35808
35809
35810
35811
35812
35813
35814
35815
35816
35817
35818
35819
35820
35821
35822
35823
35824
35825
35826
35827
35828
35829
.....
35840
35841
35842
35843
35844
35845
35846
35847
35848
35849
35850
35851
35852
35853
35854
.....
35926
35927
35928
35929
35930
35931
35932
35933
35934
35935
35936
35937
35938
35939
35940
.....
35947
35948
35949
35950
35951
35952
35953
35954
35955
35956
35957
35958
35959
35960
35961
.....
35972
35973
35974
35975
35976
35977
35978
35979
35980
35981
35982
35983
35984
35985
35986
35987
35988
35989
35990
35991
35992
35993
35994
35995
35996
35997
35998
35999
36000
36001
36002
36003
36004
36005
36006
36007
36008
36009
36010
.....
36012
36013
36014
36015
36016
36017
36018
36019
36020
36021
36022
36023
36024
36025
36026
36027
36028
36029
36030
36031
36032
36033
36034
36035
36036
36037
36038
36039
36040
36041
36042
36043
36044
36045
36046
36047
36048
36049
36050
36051
36052
36053
36054
36055
36056
36057
36058
36059
36060
36061
36062
36063
36064
36065
36066
36067
36068
36069
36070
36071
36072
36073
36074
36075
36076
36077
36078
36079
36080
36081
36082
36083
36084
36085
36086
36087
.....
36088
36089
36090
36091
36092
36093
36094
36095
36096
36097
36098
36099
36100
36101
36102
.....
36163
36164
36165
36166
36167
36168
36169
36170
36171
36172
36173
36174
36175
36176
36177
.....
36194
36195
36196
36197
36198
36199
36200
36201
36202
36203
36204
36205
36206
36207
36208
.....
36326
36327
36328
36329
36330
36331
36332
36333
36334
36335
36336
36337
36338
36339
36340
36341
36342
36343
36344
36345
36346
36347
36348
36349
36350
36351
.....
37007
37008
37009
37010
37011
37012
37013
37014
37015
37016
37017
37018
37019
37020
37021
.....
37838
37839
37840
37841
37842
37843
37844
37845
37846
37847
37848
37849
37850
37851
37852
.....
37858
37859
37860
37861
37862
37863
37864
37865
37866
37867
37868
37869
37870
37871
37872
.....
37963
37964
37965
37966
37967
37968
37969
37970
37971
37972
37973
37974
37975
37976
37977
.....
38121
38122
38123
38124
38125
38126
38127
38128
38129
38130
38131
38132
38133
38134
38135
.....
38231
38232
38233
38234
38235
38236
38237
38238
38239
38240
38241
38242
38243
38244
38245
38246
38247
38248
38249
38250
38251
38252
.....
38258
38259
38260
38261
38262
38263
38264
38265
38266
38267
38268
38269
38270
38271
38272
38273
38274
38275
38276
38277
38278
38279
38280
38281
38282
38283
38284
38285
38286
38287
38288
38289
38290
38291
38292
38293
38294
38295
38296
38297
38298
38299
38300
38301
38302
.....
38313
38314
38315
38316
38317
38318
38319
38320
38321
38322
38323
38324
38325
38326
38327
38328
38329
.....
38378
38379
38380
38381
38382
38383
38384
38385
38386
38387
38388
38389
38390
38391
38392
.....
38405
38406
38407
38408
38409
38410
38411
38412
38413
38414
38415
38416
38417
38418
38419
38420
38421
38422
38423
38424
38425
38426
38427
38428
38429
38430
38431
38432
38433
38434
38435
38436
38437
.....
38464
38465
38466
38467
38468
38469
38470
38471
38472
38473
38474
38475
38476
38477
38478
38479
38480
38481
38482
38483
38484
38485
38486
38487
38488
38489
38490
38491
38492
38493
38494
38495
38496
38497
38498
38499
38500
.....
38518
38519
38520
38521
38522
38523
38524
38525
38526
38527
38528
38529
38530
38531
38532
38533
38534
38535
38536
38537
38538
38539
38540
38541
38542
38543
38544
38545
38546
38547
38548
38549
38550
38551
38552
38553
38554
38555
38556
38557
38558
38559
38560
38561
38562
38563
38564
38565
38566
38567
38568
38569
.....
38573
38574
38575
38576
38577
38578
38579
38580
38581
38582
38583
38584
38585
38586
38587
38588
38589
38590
38591
38592
38593
38594
38595
38596
38597
38598
38599
38600
38601
38602
38603
38604
38605
38606
38607
38608
38609
38610
38611
38612
38613
38614
.....
38972
38973
38974
38975
38976
38977
38978
38979
38980
38981
38982
38983
38984
38985
38986
38987
38988
38989
38990
38991
38992
38993
.....
39026
39027
39028
39029
39030
39031
39032
39033
39034
39035
39036
39037
39038
39039
39040
39041
39042
39043
39044
39045
39046
39047
39048
39049
39050
39051
39052
.....
39070
39071
39072
39073
39074
39075
39076
39077
39078
39079
39080
39081
39082
39083
39084
39085
.....
39414
39415
39416
39417
39418
39419
39420
39421
39422
39423
39424
39425
39426
39427
39428
39429
39430
39431
39432
39433
39434
39435
39436
39437
39438
39439
39440
.....
39460
39461
39462
39463
39464
39465
39466
39467
39468
39469
39470
39471
39472
39473
39474
39475
39476
39477
39478
39479
39480
39481
39482
39483
39484
39485
39486
39487
.....
39796
39797
39798
39799
39800
39801
39802
39803
39804
39805
39806
39807
39808
39809
39810
.....
39828
39829
39830
39831
39832
39833
39834
39835
39836
39837
39838
39839
39840
39841
39842
39843
39844
39845
39846
39847
39848
39849
.....
41001
41002
41003
41004
41005
41006
41007
41008
41009
41010
41011
41012
41013
41014
41015
41016
41017
41018
41019
41020
41021
41022
41023
41024
41025
41026
41027
41028
41029
41030
41031
41032
41033
41034
41035
41036
.....
41064
41065
41066
41067
41068
41069
41070
41071
41072
41073
41074
41075
41076
41077
41078
.....
41171
41172
41173
41174
41175
41176
41177
41178
41179
41180
41181
41182
41183
41184
41185
.....
41198
41199
41200
41201
41202
41203
41204
41205
41206
41207
41208
41209
41210
41211
41212
.....
41297
41298
41299
41300
41301
41302
41303
41304
41305
41306
41307
41308
41309
41310
41311
.....
41723
41724
41725
41726
41727
41728
41729
41730
41731
41732
41733
41734
41735
41736
41737
.....
41767
41768
41769
41770
41771
41772
41773
41774
41775
41776
41777
41778
41779
41780
41781
41782
41783
41784
41785
41786
41787
41788
41789
41790
41791
41792
41793
41794
.....
42114
42115
42116
42117
42118
42119
42120
42121
42122
42123
42124
42125
42126
42127
42128
.....
44258
44259
44260
44261
44262
44263
44264
44265
44266
44267
44268
44269
44270
44271
44272
.....
46719
46720
46721
46722
46723
46724
46725
46726
46727
46728
46729
46730
46731
46732
46733
.....
46755
46756
46757
46758
46759
46760
46761
46762
46763
46764
46765
46766
46767
46768
46769
46770
46771
46772
46773
46774
46775
46776
46777
46778
46779
46780
46781
46782
46783
46784
46785
.....
46789
46790
46791
46792
46793
46794
46795
46796
46797
46798
46799
46800
46801
46802
46803
.....
49648
49649
49650
49651
49652
49653
49654
49655
49656
49657
49658
49659
49660
49661
49662
.....
50050
50051
50052
50053
50054
50055
50056
50057
50058
50059
50060
50061
50062
50063
50064
.....
50089
50090
50091
50092
50093
50094
50095
50096
50097
50098
50099
50100
50101
50102
50103
.....
50122
50123
50124
50125
50126
50127
50128
50129
50130
50131
50132
50133
50134
50135
50136
.....
50152
50153
50154
50155
50156
50157
50158
50159
50160
50161
50162
50163
50164
50165
50166
.....
54994
54995
54996
54997
54998
54999
55000
55001
55002
55003
55004
55005
55006
55007
55008
.....
55059
55060
55061
55062
55063
55064
55065
55066
55067
55068
55069
55070
55071
55072
55073
55074
55075
55076
55077
55078
55079
55080
55081
55082
55083
55084
55085
55086
55087
55088
55089
55090
55091
.....
55133
55134
55135
55136
55137
55138
55139
55140
55141
55142
55143
55144
55145
55146
55147
55148
55149
55150
55151
55152
55153
55154
55155
55156
55157
.....
56777
56778
56779
56780
56781
56782
56783
56784
56785
56786
56787
56788
56789
56790
56791
.....
57251
57252
57253
57254
57255
57256
57257
57258
57259
57260
57261
57262
57263
57264
57265
57266
57267
57268
57269
      (can be useful e.g. when consuming services from JavaScript)
    - interface-based services can now return the result value as XML object
      instead of JSON array or object if TServiceFactoryServer.ResultAsJSONObject
      is set (can be useful e.g. when consuming services from XML only clients) -
      as an alternative, ResultAsXMLObjectIfAcceptOnlyXML option will recognize
      'Accept: application/xml' or 'Accept: text/xml' HTTP header and return
      XML content instead of JSON - with optional ResultAsXMLObjectNameSpace
    - added TServiceCustomAnswer.Status member to override default HTTP_SUCCESS
    - new TSQLRest.Service<T: IInterface> method to retrieve a service instance
    - added TServiceMethodArgument.AddJSON/AddValueJSON/AddDefaultJSON methods
    - method-based services are now able to handle "304 Not Modified" optimized
      response to save bandwidth, in TSQLRestServerURIContext.Returns/Results
    - added TSQLRestServerURIContext.ReturnFile() and ReturnFileFromFolder()
      methods, for direct fast transmission to a HTTP client, handling
      "304 Not Modified" and proper mime type recognition
................................................................................
      callbacks, and TSQLRestServerURIContext.AuthenticationFailed virtual method
    - added TSQLRestServer.SessionClass property to specify the class type
      to handle in-memory sessions, and override e.g. IsValidURI() method
    - CreateMissingTables() method is now declared as virtual in TSQLRestServer
    - TSQLRestServer.URI() and TSQLRestClientURI.InternalURI() methods now uses
      one TSQLRestURIParams parameter for all request input and output values
    - TSQLRestServer.URI() method will return "405 Method Not Allowed" error
      (HTTP_NOTALLOWED) if the supplied URI does not match RestAccessRights
    - TSQLRestServer.URI() will now handle POST/PUT/DELETE ModelRoot/MethodName
      as method-based services
    - added TSQLRestServerFullMemory.Flush method-based service
    - added TSQLRestServerFullMemory.DropDatabase method
    - TSQLRestServerFullMemory now generates its expected InternalState value
    - completed HTTP_* constant list and messages - feature request [d8de3eb76a]
    - handle HTTP_NOTMODIFIED and HTTP_TEMPORARYREDIRECT as successful status -
      as expected by feature request [5d2634e8a3]
    - enhanced sllAuth session creation/deletion logged information
    - introducing TSQLRest.LogClass property, allowing to set a custom log class
    - added TAuthSession.SentHeaders, RemoteIP and ConnectionID properties
    - added process of Variant and WideString types in TSQLRecord properties,
      including any custom type, like TDocVariant or TBSONVariant (for MongoDB
      objects), or even a dynamic array of variants (see [d9d091baab])
................................................................................
      Level: TSynLogInfo = sllNone);
    /// optional service locator for by-key Dependency Injection
    property OnKeyResolve: TOnKeyResolve read fOnKeyResolve write fOnKeyResolve;
  end;

const
  /// HTML Status Code for "Continue"
  HTTP_CONTINUE = 100;
  /// HTML Status Code for "Switching Protocols"
  HTTP_SWITCHINGPROTOCOLS = 101;
  /// HTML Status Code for "Success"
  HTTP_SUCCESS = 200;
  /// HTML Status Code for "Created"
  HTTP_CREATED = 201;
  /// HTML Status Code for "Accepted"
  HTTP_ACCEPTED = 202;
  /// HTML Status Code for "Non-Authoritative Information"
  HTTP_NONAUTHORIZEDINFO = 203;
  /// HTML Status Code for "No Content"
  HTTP_NOCONTENT = 204;
  /// HTML Status Code for "Multiple Choices"
  HTTP_MULTIPLECHOICES = 300;
  /// HTML Status Code for "Moved Permanently"
  HTTP_MOVEDPERMANENTLY = 301;
  /// HTML Status Code for "Found"
  HTTP_FOUND = 302;
  /// HTML Status Code for "See Other"
  HTTP_SEEOTHER = 303;
  /// HTML Status Code for "Not Modified"
  HTTP_NOTMODIFIED = 304;
  /// HTML Status Code for "Use Proxy"
  HTTP_USEPROXY = 305;
  /// HTML Status Code for "Temporary Redirect"
  HTTP_TEMPORARYREDIRECT = 307;
  /// HTML Status Code for "Bad Request"
  HTTP_BADREQUEST = 400;
  /// HTML Status Code for "Unauthorized"
  HTTP_UNAUTHORIZED = 401;
  /// HTML Status Code for "Forbidden"
  HTTP_FORBIDDEN = 403;
  /// HTML Status Code for "Not Found"
  HTTP_NOTFOUND = 404;
  // HTML Status Code for "Method Not Allowed"
  HTTP_NOTALLOWED = 405;
  // HTML Status Code for "Not Acceptable"
  HTTP_NOTACCEPTABLE = 406;
  // HTML Status Code for "Proxy Authentication Required"
  HTTP_PROXYAUTHREQUIRED = 407;
  /// HTML Status Code for "Request Time-out"
  HTTP_TIMEOUT = 408;
  /// HTML Status Code for "Internal Server Error"
  HTTP_SERVERERROR = 500;
  /// HTML Status Code for "Not Implemented"
  HTTP_NOTIMPLEMENTED = 501;
  /// HTML Status Code for "Bad Gateway"
  HTTP_BADGATEWAY = 502;
  /// HTML Status Code for "Service Unavailable"
  HTTP_UNAVAILABLE = 503;
  /// HTML Status Code for "Gateway Timeout"
  HTTP_GATEWAYTIMEOUT = 504;
  /// HTML Status Code for "HTTP Version Not Supported"
  HTTP_HTTPVERSIONNONSUPPORTED = 505;

  /// you can use this cookie value to delete a cookie on the browser side
  COOKIE_EXPIRED = '; Expires=Sat, 01 Jan 2010 00:00:01 GMT';

  /// used e.g. by THttpApiServer.Request for http.sys to send a static file
  // - the OutCustomHeader should contain the proper 'Content-type: ....'
  // corresponding to the file (e.g. by calling GetMimeContentType() function
................................................................................

  /// HTTP header used e.g. by THttpApiServer.Request for http.sys to send
  // a static file in kernel mode
  STATICFILE_CONTENT_TYPE_HEADER = HEADER_CONTENT_TYPE+STATICFILE_CONTENT_TYPE;
  /// uppercase version of HTTP header for static file content serving
  STATICFILE_CONTENT_TYPE_HEADER_UPPPER = HEADER_CONTENT_TYPE_UPPER+STATICFILE_CONTENT_TYPE;

/// convert any HTTP_* constant to a short English text
// - see @http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
procedure StatusCodeToErrorMsg(Code: integer; var result: RawUTF8); overload;

/// convert any HTTP_* constant to an integer error code and its English text
// - see @http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
function StatusCodeToErrorMsg(Code: integer): RawUTF8; overload;

/// returns true for SUCCESS (200), CREATED (201), NOCONTENT (204),
// NOTMODIFIED (304) or TEMPORARYREDIRECT (307) codes
function StatusCodeIsSuccess(Code: integer): boolean;
  {$ifdef HASINLINE}inline;{$endif}
................................................................................
    // - but consider also using TSQLRestServerURIContext.InHeader['remoteip']
    InHead: RawUTF8;
    /// input parameter containing the caller message body
    // - e.g. some GET/POST/PUT JSON data can be specified here
    InBody: RawUTF8;
    /// output parameter to be set to the response message header
    // - it is the right place to set the returned message body content type,
    // e.g. TEXT_CONTENT_TYPE_HEADER or HTTP_CONTENT_TYPE_HEADER: if not set,
    // the default JSON_CONTENT_TYPE_HEADER will be returned to the client,
    // meaning that the message is JSON
    // - you can use OutBodyType() function to retrieve the stored content-type
    OutHead: RawUTF8;
    /// output parameter to be set to the response message body
    OutBody: RawUTF8;
    /// output parameter to be set to the HTTP status integer code
    // - HTTP_NOTFOUND=404 e.g. if the url doesn't start with Model.Root (caller
    // can try another TSQLRestServer)
    OutStatus: cardinal;
    /// output parameter to be set to the database internal state
    OutInternalState: cardinal;
    /// associated RESTful access rights
    // - AccessRights must be handled by the TSQLRestServer child, according
    // to the Application Security Policy (user logging, authentification and
................................................................................
    // - abstract implementation which is to be overridden
    procedure URIDecodeSOAByInterface; virtual; abstract;
    /// process authentication
    // - return FALSE in case of invalid signature, TRUE if authenticated
    function Authenticate: boolean; virtual;
    /// method called in case of authentication failure
    // - the failure origin is stated by the Reason parameter
    // - this default implementation will just set OutStatus := HTTP_FORBIDDEN
    // and call TSQLRestServer.OnAuthenticationFailed event (if any)
    procedure AuthenticationFailed(Reason: TNotifyAuthenticationFailedReason); virtual;
    /// direct launch of a method-based service
    // - URI() will ensure that MethodIndex>=0 before calling it
    procedure ExecuteSOAByMethod; virtual;
    /// direct launch of an interface-based service
    // - URI() will ensure that Service<>nil before calling it
................................................................................
    function ClientSQLRecordOptions: TJSONSerializerSQLRecordOptions;
    /// true if called from TSQLRestServer.AdministrationExecute
    function IsRemoteAdministrationExecute: boolean;
    /// compute the file name corresponding to the URI
    // - e.g. '/root/methodname/toto/index.html' will return 'toto\index.html'
    property ResourceFileName: TFileName read GetResourceFileName;
    /// use this method to send back directly a result value to the caller
    // - expects Status to be either HTTP_SUCCESS, HTTP_NOTMODIFIED,
    // HTTP_CREATED, or HTTP_TEMPORARYREDIRECT, and will return as answer the
    // supplied Result content with no transformation
    // - if Status is an error code, it will call Error() method
    // - CustomHeader optional parameter can be set e.g. to
    // TEXT_CONTENT_TYPE_HEADER if the default JSON_CONTENT_TYPE is not OK,
    // or calling GetMimeContentTypeHeader() on the returned binary buffer
    // - if Handle304NotModified is TRUE and Status is HTTP_SUCCESS, the Result
    // content will be hashed (using crc32c) and in case of no modification
    // will return HTTP_NOTMODIFIED to the browser, without the actual result
    // content (to save bandwidth)
    procedure Returns(const Result: RawUTF8; Status: integer=HTTP_SUCCESS;
      const CustomHeader: RawUTF8=''; Handle304NotModified: boolean=false;
      HandleErrorAsRegularResult: boolean=false); overload;
    /// use this method to send back a JSON object to the caller
    // - this method will encode the supplied values e.g. as
    // ! JSONEncode(['name','John','year',1972]) = '{"name":"John","year":1972}'
    // - implementation is just a wrapper around Returns(JSONEncode([]))
    // - note that cardinal values should be type-casted to Int64() (otherwise
    // the integer mapped value will be transmitted, therefore wrongly)
    // - expects Status to be either HTTP_SUCCESS or HTTP_CREATED
    // - caller can set Handle304NotModified=TRUE for Status=HTTP_SUCCESS
    procedure Returns(const NameValuePairs: array of const; Status: integer=HTTP_SUCCESS;
      Handle304NotModified: boolean=false; HandleErrorAsRegularResult: boolean=false); overload;
    /// use this method to send back any object as JSON document to the caller
    // - this method will call ObjectToJson() to compute the returned content
    // - you can customize SQLRecordOptions, to force the returned JSON
    // object to have its TSQLRecord nested fields serialized as true JSON
    // arrays or objects, or add an "ID_str" string field for JavaScript
    procedure Returns(Value: TObject; Status: integer=HTTP_SUCCESS;
      Handle304NotModified: boolean=false;
      SQLRecordOptions: TJSONSerializerSQLRecordOptions=[]); overload;
    /// use this method to send back any variant as JSON to the caller
    // - this method will call VariantSaveJSON() to compute the returned content
    procedure ReturnsJson(const Value: variant; Status: integer=HTTP_SUCCESS;
      Handle304NotModified: boolean=false; Escape: TTextWriterKind=twJSONEscape;
      MakeHumanReadable: boolean=false);
    /// uses this method to send back directly any binary content to the caller
    // - the exact MIME type will be retrieved using GetMimeContentTypeHeader(),
    // from the supplied Blob binary buffer, and optional a file name
    // - by default, the HTTP_NOTMODIFIED process will take place, to minimize
    // bandwidth between the server and the client
    procedure ReturnBlob(const Blob: RawByteString; Status: integer=HTTP_SUCCESS;
      Handle304NotModified: boolean=true; const FileName: TFileName='');
    /// use this method to send back a file to the caller
    // - this method will let the HTTP server return the file content
    // - if Handle304NotModified is TRUE, will check the file age to ensure
    // that the file content will be sent back to the server only if it changed
    // - if ContentType is left to default '', method will guess the expected
    // mime-type from the file name extension
................................................................................
    // - this method will let the HTTP server return the file content
    // - if Handle304NotModified is TRUE, will check the file age to ensure
    // that the file content will be sent back to the server only if it changed
    procedure ReturnFileFromFolder(const FolderName: TFileName;
      Handle304NotModified: boolean=true; const DefaultFileName: TFileName='index.html';
      const Error404Redirect: RawUTF8='');
    /// use this method notify the caller that the resource URI has changed
    // - returns a HTTP_TEMPORARYREDIRECT status with the specified location,
    // or HTTP_MOVEDPERMANENTLY if PermanentChange is TRUE
    procedure Redirect(const NewLocation: RawUTF8; PermanentChange: boolean=false);
    /// use this method to send back a JSON object with a "result" field
    // - this method will encode the supplied values as a {"result":"...}
    // JSON object, as such for one value:
    // $ {"result":"OneValue"}
    // (with one value, you can just call TSQLRestClientURI.CallBackGetResult
    // method to call and decode this value)
    // or as a JSON object containing an array of values:
    // $ {"result":["One","two"]}
    // - expects Status to be either HTTP_SUCCESS or HTTP_CREATED
    // - caller can set Handle304NotModified=TRUE for Status=HTTP_SUCCESS
    procedure Results(const Values: array of const; Status: integer=HTTP_SUCCESS;
      Handle304NotModified: boolean=false);
    /// use this method if the caller expect no data, just a status
    // - just wrap the overloaded Returns() method with no result value
    // - if Status is an error code, it will call Error() method
    // - by default, calling this method will mark process as successfull
    procedure Success(Status: integer=HTTP_SUCCESS); virtual;
    /// use this method to send back an error to the caller
    // - expects Status to not be HTTP_SUCCESS neither HTTP_CREATED,
    // and will send back a JSON error message to the caller, with the
    // supplied error text
    // - if no ErrorMessage is specified, will return a default text
    // corresponding to the Status code
    procedure Error(const ErrorMessage: RawUTF8='';
      Status: integer=HTTP_BADREQUEST); overload; virtual;
    /// use this method to send back an error to the caller
    // - implementation is just a wrapper over Error(FormatUTF8(Format,Args))
    procedure Error(const Format: RawUTF8; const Args: array of const;
      Status: integer=HTTP_BADREQUEST); overload;
    /// use this method to send back an error to the caller
    // - will serialize the supplied exception, with an optional error message
    procedure Error(E: Exception; const Format: RawUTF8; const Args: array of const;
      Status: integer=HTTP_BADREQUEST); overload;
    /// implements a method-based service for live update of some settings
    // - should be called from a method-based service, e.g. Configuration()
    // - the settings are expected to be stored e.g. in a TSynAutoCreateFields
    // instance, potentially with nested objects
    // - accept the following REST methods to read and write the settings:
    // ! GET http://server:888/root/configuration
    // ! GET http://server:888/root/configuration/propname
................................................................................
  // - if process succeeded, implementation shall call Ctxt.Results([]) method to
  // set a JSON response object with one "result" field name or Ctxt.Returns([])
  // with a JSON object described in Name/Value pairs; if the returned value is
  // not JSON_CONTENT_TYPE, use Ctxt.Returns() and its optional CustomHeader
  // parameter can specify a custom header like TEXT_CONTENT_TYPE_HEADER
  // - if process succeeded, and no data is expected to be returned to the caller,
  // implementation shall call overloaded Ctxt.Success() method with the
  // expected status (i.e. just Ctxt.Success will return HTTP_SUCCESS)
  // - if process failed, implementation shall call Ctxt.Error() method to
  // set the corresponding error message and error code number
  // - a typical implementation may be:
  // ! procedure TSQLRestServerTest.Sum(Ctxt: TSQLRestServerURIContext);
  // ! var a,b: TSynExtended;
  // ! begin
  // !   if UrlDecodeNeedParameters(Ctxt.Parameters,'A,B') then begin
................................................................................
  // as result will allow a response of any type (e.g. binary, HTML or text)
  // - this kind of answer will be understood by our TServiceContainerClient
  // implementation, and it may be used with plain AJAX or HTML requests
  // (via POST), to retrieve some custom content
  TServiceCustomAnswer = record
    /// mandatory response type, as encoded in the HTTP header
    // - useful to set the response mime-type - see e.g. the
    // TEXT_CONTENT_TYPE_HEADER or HTTP_CONTENT_TYPE_HEADER constants or
    // GetMimeContentType() function
    // - in order to be handled as expected, this field SHALL be set to NOT ''
    // (otherwise TServiceCustomAnswer will be transmitted as raw JSON)
    Header: RawUTF8;
    /// the response body
    // - corresponding to the response type, as defined in Header
    Content: RawByteString;
    /// the HTML response code
    // - if not overriden, will default to HTTP_SUCCESS = 200 on server side
    // - on client side, would always contain HTTP_SUCCESS = 200 on success,
    // or any error should be handled as expected by the caller (e.g. using
    // TServiceFactoryClient.GetErrorMessage for decoding REST/SOA errors)
    Status: cardinal;
  end;

  PServiceCustomAnswer = ^TServiceCustomAnswer;

................................................................................
  // - see Ctxt.Service, Ctxt.ServiceMethodIndex and Ctxt.ServiceParameters
  // are used to identify the executed method context
  // - Method parameter would help identify easily the corresponding method, and
  // would contain in fact Service.InterfaceFactory.Methods[ServiceMethodIndex]
  // - should return TRUE if the method can be executed
  // - should return FALSE if the method should not be executed, and set the
  // corresponding error to the supplied context e.g.
  // ! Ctxt.Error('Unauthorized method',HTTP_NOTALLOWED);
  // - i.e. called by TSQLRestServerURIContext.InternalExecuteSOAByInterface
  TOnServiceCanExecute = function(Ctxt: TSQLRestServerURIContext;
    const Method: TServiceMethod): boolean of object;

  /// a service provider implemented on the server side
  // - each registered interface has its own TServiceFactoryServer instance,
  // available as one TSQLServiceContainerServer item from TSQLRest.Services property
................................................................................
    // - Ctxt.ServiceMethodIndex=-1, then it will free/release corresponding aInstanceID
    // (is called  e.g. from {"method":"_free_", "params":[], "id":1234} )
    // - Ctxt.ServiceParameters is e.g. '[1,2]' i.e. a true JSON array, which
    // will contain the incoming parameters in the same exact order than the
    // corresponding implemented interface method
    // - Ctxt.ID is an optional number, to be used in case of sicClientDriven
    // kind of Instance creation to identify the corresponding client session
    // - returns 200/HTTP_SUCCESS on success, or an HTTP error status, with an
    // optional error message in aErrorMsg
    // - on success, Ctxt.Call.OutBody shall contain a serialized JSON object
    // with one nested result property, which may be a JSON array, containing
    // all "var" or "out" parameters values, and then the method main result -
    // for instance, ExecuteMethod(..,'[1,2]') over ICalculator.Add will return:
    // $ {"result":[3],"id":0}
    // the returned "id" number is the Instance identifier to be used for any later
................................................................................
    /// retrieve the published signature of this interface
    // - TServiceFactoryClient will be able to retrieve it only if
    // TServiceContainerServer.PublishSignature is set to TRUE (which is not the
    // default setting, for security reasons) - this function is always available
    // on TServiceFactoryServer side
    function RetrieveSignature: RawUTF8; override;
    /// convert a HTTP error from mORMot's REST/SOA into an English text message
    // - would recognize the HTTP_UNAVAILABLE, HTTP_NOTIMPLEMENTED,
    // HTTP_NOTALLOWED, HTTP_UNAUTHORIZED or HTTP_NOTACCEPTABLE errors, as
    // generated by the TSQLRestServer side
    // - is used by TServiceFactoryClient.InternalInvoke, but may be called
    // on client side for TServiceCustomAnswer.Status <> HTTP_SUCCESS 
    class function GetErrorMessage(status: integer): RawUTF8;
    /// define execution options for a given set of methods
    // - methods names should be specified as an array (e.g. ['Add','Multiply'])
    // - if no method name is given (i.e. []), option will be set for all methods
    // - only supports optNoLogInput and optNoLogOutput on the client side
    procedure SetOptions(const aMethod: array of RawUTF8; aOptions: TServiceMethodOptions);
    /// persist all service calls into a database instead of calling the client 
................................................................................
    // ! AcquireExecutionMode[execORMWrite] := amBackgroundThread;
    // ! AcquireWriteMode := amBackgroundThread; // same as previous
    procedure RollBack(SessionID: cardinal); virtual;
    /// execute a BATCH sequence prepared in a TSQLRestBatch instance
    // - implements the "Unit Of Work" pattern, i.e. safe transactional process
    // even on multi-thread environments
    // - send all pending Add/Update/Delete statements to the DB or remote server
    // - will return the URI Status value, i.e. 200/HTTP_SUCCESS OK on success
    // - a dynamic array of integers will be created in Results,
    // containing all ROWDID created for each BatchAdd call, 200 (=HTTP_SUCCESS)
    // for all successfull BatchUpdate/BatchDelete, or 0 on error
    // - any error during server-side process MUST be checked against Results[]
    // (the main URI Status is 200 if about communication success, and won't
    // imply that all statements in the BATCH sequence were successfull
    // - note that the caller shall still free the supplied Batch instance
    function BatchSend(Batch: TSQLRestBatch; var Results: TIDDynArray): integer; overload; virtual;
    /// execute a BATCH sequence prepared in a TSQLRestBatch instance
................................................................................
      User: TSQLAuthUser; const aPassWord: RawUTF8): boolean; virtual;
  public
    /// will check URI-level signature
    // - retrieve the session ID from 'session_signature=...' parameter
    // - will also check incoming "Authorization: Basic ...." HTTP header
    function RetrieveSession(Ctxt: TSQLRestServerURIContext): TAuthSession; override;
    /// handle the Auth RESTful method with HTTP Basic
    // - will first return HTTP_UNAUTHORIZED (401), then expect user and password
    // to be supplied as incoming "Authorization: Basic ...." headers
    function Auth(Ctxt: TSQLRestServerURIContext): boolean; override;
  end;

  {$ifdef SSPIAUTH}

  /// authentication of the current logged user using Windows Security Support
................................................................................
    // - this method is thread-safe
    procedure NotifyORMTable(TableIndex, DataSize: integer; Write: boolean;
       const MicroSecondsElapsed: QWord);
  published
    /// when this monitoring instance (therefore the server) was created
    property StartDate: RawUTF8 read fStartDate;
    /// number of valid responses
    // - i.e. which returned status code 200/HTTP_SUCCESS or 201/HTTP_CREATED
    // - any invalid request will increase the TSynMonitor.Errors property
    property Success: TSynMonitorCount64 read fSuccess;
    /// count of the remote method-based service calls
    property ServiceMethod: TSynMonitorCount64 read fServiceMethod;
    /// count of the remote interface-based service calls
    property ServiceInterface: TSynMonitorCount64 read fServiceInterface;
    /// count of files transmitted directly (not part of Output size property)
................................................................................
  // limitation of 53-bit for integers - only for AJAX (non Delphi) clients
  // - unauthenticated requests from browsers (i.e. not Delphi clients) may
  // be redirected to the TSQLRestServer.Auth() method via rsoRedirectForbiddenToAuth
  // (e.g. for TSQLRestServerAuthenticationHttpBasic popup)
  // - some REST/AJAX clients may expect to return status code 204 as
  // instead of 200 in case of a successful operation, but with no returned
  // body (e.g. a DELETE with SAPUI5 / OpenUI5 framework): include
  // rsoHtml200WithNoBodyReturns204 so that any HTTP_SUCCESS (200) with no
  // returned body would return a HTTP_NOCONTENT (204)
  // - by default, Add() or Update() would return HTTP_CREATED (201) or
  // HTTP_SUCCESS (200) with no body, unless rsoAddUpdateReturnsContent is set
  // to return as JSON the last inserted/updated record
  // - TModTime / TCreateTime fields are expected to be filled on client side,
  // unless you set rsoComputeFieldsBeforeWriteOnServerSide so that AJAX requests
  // would set the fields on the server side by calling the TSQLRecord
  // ComputeFieldsBeforeWrite virtual method, before writing to the database
  TSQLRestServerOption = (
    rsoNoAJAXJSON,
................................................................................
    // - the supplied Ctxt parameter would give access to the command about to
    // be executed, e.g. Ctxt.Command=execSOAByInterface would identify a SOA
    // service execution, with the corresponding Service and ServiceMethodIndex
    // parameters as set by TSQLRestServerURIContext.URIDecodeSOAByInterface
    // - should return TRUE if the method can be executed
    // - should return FALSE if the method should not be executed, and the
    // callback should set the corresponding error to the supplied context e.g.
    // ! Ctxt.Error('Unauthorized method',HTTP_NOTALLOWED);
    // - since this event would be executed by every TSQLRestServer.URI call,
    // it should better not make any slow process (like writing to a remote DB)
    OnBeforeURI: TNotifyBeforeURI;
    /// event trigerred when URI() finished to process a request
    // - the supplied Ctxt parameter would give access to the command which has
    // been executed, e.g. via Ctxt.Call.OutStatus or Ctxt.MicroSecondsElapsed
    // - since this event would be executed by every TSQLRestServer.URI call,
................................................................................
    // property name is supplied for every value
    // - the "not expanded" layout, NoAJAXJSON property is set to TRUE,
    // reflects exactly the layout of the SQL request - first line contains the
    // field names, then all next lines are the field content
    // - is in fact stored in rsoNoAJAXJSON item in Options property
    property NoAJAXJSON: boolean read GetNoAJAXJSON write SetNoAJAXJSON;
    /// allow to customize how TSQLRestServer.URI process the requests
    // - e.g. if HTTP_SUCCESS with no body should be translated into HTTP_NOCONTENT
    property Options: TSQLRestServerOptions read fOptions write fOptions;
    /// set to true if the server will handle per-user authentication and
    // access right management
    // - i.e. if the associated TSQLModel contains TSQLAuthUser and
    // TSQLAuthGroup tables (set by constructor)
    property HandleAuthentication: boolean read fHandleAuthentication;
    /// allow to by-pass Authentication for a given set of HTTP verbs
................................................................................
    // - this method would require an authenticated client, for safety
    // - expect input as JSON commands:
    // & '{"Table":["cmd":values,...]}'
    // or for multiple tables:
    // & '["cmd@Table":values,...]'
    // with cmd in POST/PUT with {object} as value or DELETE with ID
    // - returns an array of integers: '[200,200,...]' or '["OK"]' if all
    // returned status codes are 200 (HTTP_SUCCESS)
    // - URI are either 'ModelRoot/TableName/Batch' or 'ModelRoot/Batch'
    procedure Batch(Ctxt: TSQLRestServerURIContext);
  end;

  /// REST class with direct access to an external database engine
  // - you can set an alternate per-table database engine by using this class
  // - this abstract class is to be overridden with a proper implementation
................................................................................
    /// overridden method which will call ClientRetrieve()
    function EngineRetrieve(TableModelIndex: integer; ID: TID): RawUTF8; override;
    /// create a new member
    // - implements REST POST collection
    // - URI is 'ModelRoot/TableName' with POST method
    // - if SendData is true, content of Value is sent to the server as JSON
    // - if ForceID is true, client sends the Value.ID field to use this ID
    // - server must return Status 201/HTTP_CREATED on success
    // - server must send on success an header entry with
    // $ Location: ModelRoot/TableName/TableID
    // - on success, returns the new ROWID value; on error, returns 0
    // - on success, Value.ID is updated with the new ROWID
    // - if aValue is TSQLRecordFTS3, Value.ID is stored to the virtual table
    // - this overridden method will send BLOB fields, if ForceBlobTransfert is set
    function InternalAdd(Value: TSQLRecord; SendData: boolean; CustomFields: PSQLFieldBits;
      ForceID, DoNotAutoComputeFields: boolean): TID; override; 
  public
    /// update a member
    // - implements REST PUT collection
    // - URI is 'ModelRoot/TableName/TableID' with PUT method
    // - server must return Status 200/HTTP_SUCCESS OK on success
    // - this overridden method will call BeforeUpdateEvent and also update BLOB
    // fields, if any ForceBlobTransfert is set and CustomFields=[]
    function Update(Value: TSQLRecord; const CustomFields: TSQLFieldBits=[];
      DoNotAutoComputeFields: boolean=false): boolean; override;
    /// get a member from its ID
    // - implements REST GET collection
    // - URI is 'ModelRoot/TableName/TableID' with GET method
    // - server must return Status 200/HTTP_SUCCESS OK on success
    // - if ForUpdate is true, the REST method is LOCK and not GET: it tries to lock
    // the corresponding record, then retrieve its content; caller has to call
    // UnLock() method after Value usage, to release the record
    function Retrieve(aID: TID; Value: TSQLRecord; ForUpdate: boolean=false): boolean; override;
    /// get a member from its ID
    // - implements REST GET collection
    // - URI is 'ModelRoot/TableName/TableID' with GET method
    // - returns true on server returned 200/HTTP_SUCCESS OK success, false on error
    // - set Refreshed to true if the content changed
    function Refresh(aID: TID; Value: TSQLRecord; var Refreshed: boolean): boolean;

    /// retrieve a list of members as a TSQLTable
    // - implements REST GET collection
    // - default SQL statement is 'SELECT ID FROM TableName;' (i.e. retrieve
    // the list of all ID of this collection members)
................................................................................
    function GetOnIdleBackgroundThreadActive: boolean;
{$endif}
    constructor RegisteredClassCreateFrom(aModel: TSQLModel;
      aDefinition: TSynConnectionDefinition); override;
    function GetCurrentSessionUserID: TID; override;
    function InternalRemoteLogSend(const aText: RawUTF8): boolean;
    procedure InternalNotificationMethodExecute(var Ctxt: TSQLRestURIParams); virtual;
    procedure SetLastException(E: Exception=nil; ErrorCode: integer=HTTP_BADREQUEST;
      Call: PSQLRestURIParams=nil);
    // register the user session to the TSQLRestClientURI instance
    function SessionCreate(aAuth: TSQLRestServerAuthenticationClass;
      var aUser: TSQLAuthUser; const aSessionKey: RawUTF8): boolean;
    /// abstract method to be implemented with a local, piped or HTTP/1.1 provider
    // - you can specify some POST/PUT data in Call.OutBody (leave '' otherwise)
    // - return the execution result in Call.OutStatus
................................................................................
    function URI(const url, method: RawUTF8; Resp: PRawUTF8=nil;
      Head: PRawUTF8=nil; SendData: PRawUTF8=nil): Int64Rec;
    /// retrieve a list of members as a TSQLTable
    // - implements REST GET collection
    // - URI is 'ModelRoot/TableName' with GET method
    // - SQLSelect and SQLWhere are encoded as 'select=' and 'where=' URL parameters
    // (using inlined parameters via :(...): in SQLWhere is always a good idea)
    // - server must return Status 200/HTTP_SUCCESS OK on success
    function List(const Tables: array of TSQLRecordClass; const SQLSelect: RawUTF8 = 'RowID';
      const SQLWhere: RawUTF8 = ''): TSQLTableJSON; override;
    /// unlock the corresponding record
    // - URI is 'ModelRoot/TableName/TableID' with UNLOCK method
    // - returns true on success
    function UnLock(Table: TSQLRecordClass; aID: TID): boolean; override;
    /// Execute directly a SQL statement, expecting a list of resutls
................................................................................
    function BatchDelete(Table: TSQLRecordClass; ID: TID): integer; overload;
    /// retrieve the current number of pending transactions in the BATCH sequence
    // - every call to BatchAdd/Update/Delete methods increases this count
    function BatchCount: integer;
    /// execute a BATCH sequence started by BatchStart method
    // - send all pending BatchAdd/Update/Delete statements to the remote server
    // - URI is 'ModelRoot/TableName/0' with POST (or PUT) method
    // - will return the URI Status value, i.e. 200/HTTP_SUCCESS OK on success
    // - a dynamic array of integers will be created in Results,
    // containing all ROWDID created for each BatchAdd call, 200 (=HTTP_SUCCESS)
    // for all successfull BatchUpdate/BatchDelete, or 0 on error
    // - any error during server-side process MUST be checked against Results[]
    // (the main URI Status is 200 if about communication success, and won't
    // imply that all statements in the BATCH sequence were successfull
    function BatchSend(var Results: TIDDynArray): integer; overload;
    /// abort a BATCH sequence started by BatchStart method
    // - in short, nothing is sent to the remote server, and current BATCH
    // sequence is closed
    // - will Free the TSQLRestBatch stored in this TSQLRestClientURI instance
    procedure BatchAbort;

    /// wrapper to the protected URI method to call a method on the server, using
    // a ModelRoot/[TableName/[ID/]]MethodName RESTful GET request
    // - returns the HTTP error code (e.g. 200/HTTP_SUCCESS on success)
    // - this version will use a GET with supplied parameters (which will be encoded
    // with the URL)
    function CallBackGet(const aMethodName: RawUTF8;
      const aNameValueParameters: array of const;
      out aResponse: RawUTF8; aTable: TSQLRecordClass=nil; aID: TID=0;
      aResponseHead: PRawUTF8=nil): integer;
    /// wrapper to the protected URI method to call a method on the server, using
................................................................................
    // - this version will use a GET with supplied parameters (which will be encoded
    // with the URL)
    function CallBackGetResult(const aMethodName: RawUTF8;
      const aNameValueParameters: array of const;
      aTable: TSQLRecordClass=nil; aID: TID=0): RawUTF8;
    /// wrapper to the protected URI method to call a method on the server, using
    //  a ModelRoot/[TableName/[ID/]]MethodName RESTful PUT request
    // - returns the HTTP error code (e.g. 200/HTTP_SUCCESS on success)
    // - this version will use a PUT with the supplied raw UTF-8 data
    function CallBackPut(const aMethodName, aSentData: RawUTF8;
      out aResponse: RawUTF8; aTable: TSQLRecordClass=nil; aID: TID=0;
      aResponseHead: PRawUTF8=nil): integer;
    /// wrapper to the protected URI method to call a method on the server, using
    //  a ModelRoot/[TableName/[ID/]]MethodName RESTful with any kind of request
    // - returns the HTTP error code (e.g. 200/HTTP_SUCCESS on success)
    // - for GET/PUT methods, you should better use CallBackGet/CallBackPut
    function CallBack(method: TSQLURIMethod; const aMethodName,aSentData: RawUTF8;
      out aResponse: RawUTF8; aTable: TSQLRecordClass=nil; aID: TID=0;
      aResponseHead: PRawUTF8=nil): integer;
    /// register one or several Services on the client side via their interfaces
    // - this methods expects a list of interfaces to be registered to the client
    // (e.g. [TypeInfo(IMyInterface)])
................................................................................
    // - is defines as a class procedure, since the underlying TSQLRestClientURI
    // instance has no impact here: a single WM_* handler is enough for
    // several TSQLRestClientURI instances
    class procedure ServiceNotificationMethodExecute(var Msg : TMessage);
    {$endif MSWINDOWS}
  published
    /// low-level error code, as returned by server
    // - check this value about HTTP_* constants
    // - HTTP_SUCCESS or HTTP_CREATED mean no error
    // - otherwise, check LastErrorMessage property for additional information
    // - this property value will record status codes returned by URI() method
    property LastErrorCode: integer read fLastErrorCode;
    /// low-level error message, as returned by server
    // - this property value will record content returned by URI() method in
    // case of an error, or '' if LastErrorCode is HTTP_SUCCESS or HTTP_CREATED
    property LastErrorMessage: RawUTF8 read fLastErrorMessage;
    /// low-level exception class, if any
    // - will record any Exception class raised within URI() method
    // - contains nil if URI() execution did not raise any exception (which
    // is the most expected behavior, since server-side errors are trapped
    // into LastErrorCode/LastErrorMessage properties
    property LastErrorException: ExceptClass read fLastErrorException;
................................................................................
    // during process
    // - to be used e.g. to ensure no re-entrance from User Interface messages
    property OnIdleBackgroundThreadActive: Boolean read GetOnIdleBackgroundThreadActive;
{$endif}
    /// this Event is called in case of remote authentication failure
    // - client software can ask the user to enter a password and user name
    // - if no event is specified, the URI() method will return directly
    // an HTTP_FORBIDDEN "403 Forbidden" error code
    property OnAuthentificationFailed: TOnAuthentificationFailed
      read fOnAuthentificationFailed write fOnAuthentificationFailed;
    /// this Event is called if URI() was not successfull
    // - the callback would have all needed information
    property OnFailed: TOnClientFailed read fOnFailed write fOnFailed;
    /// this Event is called when a user is authenticated
    // - is called always, on each TSQLRestClientURI.SetUser call
................................................................................
    // - will make a copy of the aRedirected.Model, and own it
    constructor Create(aRedirected: TSQLRest); reintroduce; overload;
    /// would pass all client commands to the supplied TSQLRestServer instance
    // - aRedirected would be owned by this TSQLRestClientRedirect
    constructor CreateOwned(aRedirected: TSQLRestServer); reintroduce;
    /// allows to change redirection to a client on the fly
    // - if aRedirected is nil, redirection would be disabled and any URI() call
    // would return an HTTP_GATEWAYTIMEOUT 504 error status
    procedure RedirectTo(aRedirected: TSQLRest);
  end;

  {$ifdef MSWINDOWS}

  /// Rest client with remote access to a server through Windows messages
  // - use only one TURIMapRequest function for the whole communication
................................................................................
  protected
    constructor RegisteredClassCreateFrom(aModel: TSQLModel;
      aDefinition: TSynConnectionDefinition); override;
    /// method calling the RESTful server through a DLL or executable, by using
    // a named pipe (faster than TCP/IP or HTTP connection)
    // - return status code in result.Lo
    // - return database internal state in result.Hi
    // - status code 501 HTTP_NOTIMPLEMENTED if no server is available
    procedure InternalURI(var Call: TSQLRestURIParams); override;
    /// overridden protected method to handle named-pipe connection
    function InternalCheckOpen: boolean; override;
    /// overridden protected method to close named-pipe connection
    procedure InternalClose; override;
  public
    /// connect to a server contained in a running application
................................................................................
  /// if this variable is TRUE, the URIRequest() function won't use
  // Win32 API GlobalAlloc() function, but fastest native Getmem()
  // - can be also useful for debugg
  USEFASTMM4ALLOC: boolean = false;

/// this function can be exported from a DLL to remotely access to a TSQLRestServer
// - use TSQLRestServer.ExportServer to assign a server to this function
// - return 501 HTTP_NOTIMPLEMENTED if no TSQLRestServer.ExportServer has been assigned
// - memory for Resp and Head are allocated with GlobalAlloc(): client must release
// this pointers with GlobalFree() after having retrieved their content
// - simply use TSQLRestClientURIDll to access to an exported URIRequest() function
function URIRequest(url, method, SendData: PUTF8Char; Resp, Head: PPUTF8Char): Int64Rec; cdecl;


threadvar
................................................................................
    '%.IndexByNameUnflattenedOrExcept(%): unkwnown field in %',[self,aName,fTable]);
end;


procedure StatusCodeToErrorMsg(Code: integer; var result: RawUTF8);
begin // see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
  case Code of
    HTTP_CONTINUE:            result := 'Continue';
    HTTP_SWITCHINGPROTOCOLS:  result := 'Switching Protocols';
    HTTP_SUCCESS:             result := 'OK';
    HTTP_CREATED:             result := 'Created';
    HTTP_ACCEPTED:            result := 'Accepted';
    HTTP_NONAUTHORIZEDINFO:   result := 'Non-Authoritative Information';
    HTTP_NOCONTENT:           result := 'No Content';
    HTTP_MULTIPLECHOICES:     result := 'Multiple Choices';
    HTTP_MOVEDPERMANENTLY:    result := 'Moved Permanently';
    HTTP_FOUND:               result := 'Found';
    HTTP_SEEOTHER:            result := 'See Other';
    HTTP_NOTMODIFIED:         result := 'Not Modified';
    HTTP_USEPROXY:            result := 'Use Proxy';
    HTTP_TEMPORARYREDIRECT:   result := 'Temporary Redirect';
    HTTP_BADREQUEST:          result := 'Bad Request';
    HTTP_UNAUTHORIZED:        result := 'Unauthorized';
    HTTP_FORBIDDEN:           result := 'Forbidden';
    HTTP_NOTFOUND:            result := 'Not Found';
    HTTP_NOTALLOWED:          result := 'Method Not Allowed';
    HTTP_NOTACCEPTABLE:       result := 'Not Acceptable';
    HTTP_PROXYAUTHREQUIRED:   result := 'Proxy Authentication Required';
    HTTP_TIMEOUT:             result := 'Request Timeout';
    HTTP_SERVERERROR:         result := 'Internal Server Error';
    HTTP_BADGATEWAY:          result := 'Bad Gateway';
    HTTP_GATEWAYTIMEOUT:      result := 'Gateway Timeout';
    HTTP_UNAVAILABLE:         result := 'Service Unavailable';
    HTTP_HTTPVERSIONNONSUPPORTED: result := 'HTTP Version Not Supported';
    else                      result := 'Invalid Request';
  end;
end;

function StatusCodeToErrorMsg(Code: integer): RawUTF8;
begin
  StatusCodeToErrorMsg(Code,result);
  result := FormatUTF8('HTTP Error % - %',[Code,result]);
end;

function StatusCodeIsSuccess(Code: integer): boolean;
begin
  case Code of
  HTTP_SUCCESS, HTTP_NOCONTENT, HTTP_CREATED,
  HTTP_NOTMODIFIED, HTTP_TEMPORARYREDIRECT:
    result := true;
  else
    result := false;
  end;
end;

function StringToMethod(const method: RawUTF8): TSQLURIMethod;
................................................................................
  end;
end;

function TSQLRest.BatchSend(Batch: TSQLRestBatch;
  var Results: TIDDynArray): integer;
var Data: RawUTF8;
begin
  result := HTTP_BADREQUEST;
  if (self=nil) or (Batch=nil) then // no opened BATCH sequence
    exit;
  if Batch.PrepareForSending(Data) then
    if Data='' then // i.e. Batch.Count=0
      result := HTTP_SUCCESS else
      try
        result := EngineBatchSend(Batch.Table,Data,Results,Batch.Count);
      except
        on Exception do // e.g. from TSQLRestServer.EngineBatchSend()
          result := HTTP_SERVERERROR;
      end;
end;

function TSQLRest.BatchSend(Batch: TSQLRestBatch): integer;
var Res: TIDDynArray;
begin
  result := BatchSend(Batch,Res);
................................................................................



{ TSQLRestClientURI }

function TSQLRestClientURI.EngineExecute(const SQL: RawUTF8): boolean;
begin
  result := URI(Model.Root,'POST',nil,nil,@SQL).Lo in [HTTP_SUCCESS,HTTP_NOCONTENT];
end;

function TSQLRestClientURI.URIGet(Table: TSQLRecordClass; ID: TID;
  var Resp: RawUTF8; ForUpdate: boolean=false): Int64Rec;
const METHOD: array[boolean] of RawUTF8 = ('GET','LOCK');
begin
  result := URI(Model.getURIID(Table,ID),METHOD[ForUpdate],@Resp,nil,nil);
end;

function TSQLRestClientURI.UnLock(Table: TSQLRecordClass; aID: TID): boolean;
begin
  if (self=nil) or not Model.UnLock(Table,aID) then
    result := false else // was not locked by the client
    result := URI(Model.getURIID(Table,aID),'UNLOCK').Lo in [HTTP_SUCCESS,HTTP_NOCONTENT];
end;

function TSQLRestClientURI.ExecuteList(const Tables: array of TSQLRecordClass;
  const SQL: RawUTF8): TSQLTableJSON;
var Resp: RawUTF8;
begin
  if self=nil then
    result := nil else
  with URI(Model.Root,'GET',@Resp,nil,@SQL) do
    if Lo=HTTP_SUCCESS then begin // GET with SQL sent
      if high(Tables)=0 then
        result := TSQLTableJSON.CreateFromTables([Tables[0]],SQL,Resp) else
        result := TSQLTableJSON.CreateFromTables(Tables,SQL,Resp);
      result.fInternalState := Hi;
    end else // get data
    result := nil;
end;
................................................................................
end;

function TSQLRestClientURI.ServerCacheFlush(aTable: TSQLRecordClass; aID: TID): boolean;
var aResp: RawUTF8;
begin
  if (Self=nil) or (Model=nil) then // avoid GPF
    result := false else
    result := CallBackGet('CacheFlush',[],aResp,aTable,aID) in [HTTP_SUCCESS,HTTP_NOCONTENT];
end;

function TSQLRestClientURI.ServerTimeStampSynchronize: boolean;
var status: integer;
    aResp: RawUTF8;
begin
  if self=nil then begin
    result := false;
    exit;
  end;
  fServerTimeStampOffset := 0.0001; // avoid endless recursive call
  status := CallBackGet('TimeStamp',[],aResp);
  result := (status=HTTP_SUCCESS) and (aResp<>'');
  if result then
    SetServerTimeStamp(GetInt64(pointer(aResp))) else begin
    InternalLog('/TimeStamp call failed -> Server not available',sllWarning);
    fLastErrorMessage := 'Server not available  - '+Trim(fLastErrorMessage);
  end;
end;

function TSQLRestClientURI.InternalRemoteLogSend(const aText: RawUTF8): boolean;
begin
  result := URI(Model.getURICallBack('RemoteLog',nil,0),
    'PUT',nil,nil,@aText).Lo in [HTTP_SUCCESS,HTTP_NOCONTENT];
end;


{$ifdef MSWINDOWS}
type
  TSQLRestClientURIServiceNotification = class(TServiceMethodExecute)
  protected
................................................................................
      Ctxt.OutHead := exec.ServiceCustomAnswerHead;
      Ctxt.OutStatus := exec.ServiceCustomAnswerStatus;
    finally
      exec.Free;
    end;
  end;
begin
  Ctxt.OutStatus := HTTP_BADREQUEST;
  url := Ctxt.Url;
  if url='' then
    exit;
  if url[1]='/' then
    system.delete(url,1,1);
  Split(Split(url,'/',root),'/',interfmethod,id); // 'root/BidirCallback.AsynchEvent/1'
  if not IdemPropNameU(root,Model.Root) then
    exit;
  callback.ID := GetInteger(pointer(id));
  if callback.ID<=0 then
    exit;
  if interfmethod=SERVICE_PSEUDO_METHOD[imFree] then begin
    if fFakeCallbacks.FindAndRelease(callback.ID) then
      Ctxt.OutStatus := HTTP_SUCCESS;
    exit;
  end;
  if not fFakeCallbacks.FindEntry(callback) then
    exit;
  if (Ctxt.InHead<>'') and
     (callback.Factory.MethodIndexCurrentFrameCallback>=0) then begin
    frames := FindIniNameValue(pointer(Ctxt.InHead),'SEC-WEBSOCKET-FRAME: ');
................................................................................
      WR.AddShort('{"result":[');
      if frames='[0]' then // call before the first method of the jumbo frame
        Call(callback.Factory.MethodIndexCurrentFrameCallback,frames,nil);
      Call(methodIndex,Ctxt.InBody,WR);
      if ok then begin
        if Ctxt.OutHead='' then begin // <>'' if set via TServiceCustomAnswer
          WR.Add(']','}');
          Ctxt.OutStatus := HTTP_SUCCESS;
        end;
        Ctxt.OutBody := WR.Text;
      end else
        Ctxt.OutStatus := HTTP_SERVERERROR;
      if frames='[1]' then // call after the last method of the jumbo frame
        Call(callback.Factory.MethodIndexCurrentFrameCallback,frames,nil);
    finally
      WR.Free;
    end;
  except
    on E: Exception do begin
      Ctxt.OutHead := '';
      Ctxt.OutBody := ObjectToJSONDebug(E);
      Ctxt.OutStatus := HTTP_SERVERERROR;
    end;
  end;
end;

{$ifdef LVCL} // SyncObjs.TEvent not available in LVCL yet

function TSQLRestClientURI.ServerRemoteLog(Sender: TTextWriter; Level: TSynLogInfo;
................................................................................
  State := ServerInternalState; // get revision state from server
  for i := 0 to high(Data) do
    if Data[i]<>nil then
    if TObject(Data[i]).InheritsFrom(TSQLTableJSON) then begin
      T := TSQLTableJSON((Data[i]));
      if (T.QuerySQL<>'') and (T.InternalState<>State) then begin // refresh needed?
        with URI(Model.Root,'GET',@Resp,nil,@T.QuerySQL) do
          if Lo=HTTP_SUCCESS then begin // GET with SQL sent
            if Assigned(OnTableUpdate) then
              OnTableUpdate(T,tusPrepare);
            TRefreshed := false;
            if not T.UpdateFrom(Resp,TRefreshed,PCurrentRow) then
              result := false else // mark error retrieving new content
              T.fInternalState := Hi;
            if TRefreshed then
................................................................................
    if SQLWhere<>'' then begin
      if U<>'' then
        U := U+'&where=' else
        U := U+'?where=';
      U := U+UrlEncode(SQLWhere);
    end;
    with URI(Model.URI[TSQLRecordClass(Tables[0])]+U,'GET',@Resp) do
      if Lo<>HTTP_SUCCESS then
        exit else
        InternalState := Hi;
    result := TSQLTableJSON.CreateFromTables([Tables[0]],SQL,Resp); // get data
  end else begin
    // multiple tables -> send SQL statement as HTTP body
    with URI(Model.Root,'GET',@Resp,nil,@SQL) do
      if Lo<>HTTP_SUCCESS then
        exit else
        InternalState := Hi;
    result := TSQLTableJSON.CreateFromTables(Tables,SQL,Resp); // get data
  end;
  result.fInternalState := InternalState;
end;

................................................................................
function TSQLRestClientURI.TransactionBegin(aTable: TSQLRecordClass;
  SessionID: cardinal): boolean;
begin
  result := inherited TransactionBegin(aTable,CONST_AUTHENTICATION_NOT_USED);
  if result then
    // fTransactionActiveSession flag was not already set
    if aTable=nil then
      result := URI(Model.Root,'BEGIN').Lo in [HTTP_SUCCESS,HTTP_NOCONTENT] else
      result := URI(Model.URI[aTable],'BEGIN').Lo in [HTTP_SUCCESS,HTTP_NOCONTENT];
end;

function TSQLRestClientURI.TransactionBeginRetry(aTable: TSQLRecordClass;
  Retries: integer): boolean;
begin
  if Retries>50 then
    Retries := 50; // avoid loop for more than 10 seconds
................................................................................
  aTable: TSQLRecordClass; aID: TID; aResponseHead: PRawUTF8): integer;
var url, header: RawUTF8;
    {$ifdef WITHLOG}
    Log: ISynLog; // for Enter auto-leave to work with FPC
    {$endif}
begin
  if self=nil then
    result := HTTP_UNAVAILABLE else begin
    url := Model.getURICallBack(aMethodName,aTable,aID)+
      UrlEncode(aNameValueParameters);
    {$ifdef WITHLOG}
    Log := fLogClass.Enter('CallBackGet %',[url],self);
    {$endif}
    result := URI(url,'GET',@aResponse,@header).Lo;
    if aResponseHead<>nil then
................................................................................
  ProcessOpaqueParam: pointer);
var Call: ^TSQLRestURIParams absolute ProcessOpaqueParam;
begin
  if Call=nil then
    exit;
  InternalURI(Call^);
  if OnIdleBackgroundThreadActive then
    if Call^.OutStatus=HTTP_NOTIMPLEMENTED then begin
      // InternalCheckOpen failed -> force recreate connection
      InternalClose;
      if OnIdleBackgroundThreadActive then
        InternalURI(Call^); // try request again
    end;
end;

................................................................................
    aUserName, aPassword: string;
    StatusMsg: RawUTF8;
    Call: TSQLRestURIParams;
    aRetryOnceOnTimeout, aPasswordHashed: boolean;
label DoRetry;
begin
  if self=nil then begin
    Int64(result) := HTTP_UNAVAILABLE;
    SetLastException(nil,HTTP_UNAVAILABLE);
    exit;
  end;
  aRetryOnceOnTimeout := RetryOnceOnTimeout;
  fLastErrorMessage := '';
  fLastErrorException := nil;
  if fServerTimeStampOffset=0 then
    if not ServerTimeStampSynchronize then begin
      Int64(result) := HTTP_UNAVAILABLE;
      exit; // if /TimeStamp is not available, server is down!
    end;
  Call.Init;
  if (Head<>nil) and (Head^<>'') then
    Call.InHead := Head^;
  if fSessionHttpHeader<>'' then
    Call.InHead := Trim(Call.InHead+#13#10+fSessionHttpHeader);
................................................................................
      Call.InBody := SendData^;
{$ifndef LVCL}
    if Assigned(fOnIdle) then begin
      if fBackgroundThread=nil then
        fBackgroundThread := TSynBackgroundThreadEvent.Create(OnBackgroundProcess,
          OnIdle,FormatUTF8('% "%" background',[self,Model.Root]));
      if not fBackgroundThread.RunAndWait(@Call) then
        Call.OutStatus := HTTP_UNAVAILABLE;
    end else
{$endif}
    begin
      InternalURI(Call);
      if Call.OutStatus=HTTP_NOTIMPLEMENTED then begin // InternalCheckOpen failed
        InternalClose;     // force recreate connection
        InternalURI(Call); // try request again
      end;
    end;
    result.Lo := Call.OutStatus;
    result.Hi := Call.OutInternalState;
    if Head<>nil then
      Head^ := Call.OutHead;
    if Resp<>nil then
      Resp^ := Call.OutBody;
    fLastErrorCode := Call.OutStatus;
    if (Call.OutStatus=HTTP_TIMEOUT) and aRetryOnceOnTimeout then begin
      aRetryOnceOnTimeout := false;
      InternalLog('% % returned "408 Request Timeout" -> RETRY',[method,url],sllError);
      goto DoRetry;
    end;
    if not StatusCodeIsSuccess(Call.OutStatus) then begin
      StatusCodeToErrorMsg(Call.OutStatus,StatusMsg);
      if Call.OutBody='' then
................................................................................
        fLastErrorMessage := StatusMsg else
        fLastErrorMessage := Call.OutBody;
      InternalLog('% % returned % (%) with message  %',
        [method,url,Call.OutStatus,StatusMsg,fLastErrorMessage],sllError);
      if Assigned(fOnFailed) then
        fOnFailed(self,nil,@Call);
    end;
    if (Call.OutStatus<>HTTP_FORBIDDEN) or not Assigned(OnAuthentificationFailed) then
      break;
    // "403 Forbidden" in case of authentication failure -> try relog
    if not OnAuthentificationFailed(Retry+2,aUserName,aPassword,aPasswordHashed) or
       not SetUser(StringToUTF8(aUserName),StringToUTF8(aPassword),aPasswordHashed) then
      break;
  except
    on E: Exception do begin
      Int64(result) := HTTP_NOTIMPLEMENTED; // 501
      SetLastException(E,HTTP_NOTIMPLEMENTED,@Call);
      exit;
    end;
  end;
end;

function TSQLRestClientURI.CallBackGetResult(const aMethodName: RawUTF8;
  const aNameValueParameters: array of const; aTable: TSQLRecordClass; aID: TID): RawUTF8;
var aResponse: RawUTF8;
begin
  if CallBackGet(aMethodName,aNameValueParameters,aResponse,aTable,aID)=HTTP_SUCCESS then
    result := JSONDecode(aResponse) else
    result := '';
end;

function TSQLRestClientURI.CallBackPut(const aMethodName,
  aSentData: RawUTF8; out aResponse: RawUTF8; aTable: TSQLRecordClass;
  aID: TID; aResponseHead: PRawUTF8): integer;
................................................................................
  'MKACTIVITY','MKCALENDAR','CHECKOUT','MERGE','NOTIFY','PATCH','SEARCH','CONNECT');
var u: RawUTF8;
{$ifdef WITHLOG}
   Log: ISynLog; // for Enter auto-leave to work with FPC
{$endif}
begin
  if (self=nil) or (method<Low(NAME)) then
    result := HTTP_UNAVAILABLE else begin
    u := Model.getURICallBack(aMethodName,aTable,aID);
    {$ifdef WITHLOG}
    Log := fLogClass.Enter('Callback %',[u],self);
    {$endif}
    result := URI(u,NAME[method],@aResponse,aResponseHead,@aSentData).Lo;
    InternalLog('% result=% resplen=%',[NAME[method],result,length(aResponse)],
      sllServiceReturn);
................................................................................
  fServicePublishOwnInterfaces := OwnServer.ServicesPublishedInterfaces;
end;

function TSQLRestClientURI.ServiceRetrieveAssociated(const aServiceName: RawUTF8;
  out URI: TSQLRestServerURIDynArray): boolean;
var json: RawUTF8;
begin
  result := (CallBackGet('stat',['findservice',aServiceName],json)=HTTP_SUCCESS) and
    (DynArrayLoadJSON(URI,pointer(json),TypeInfo(TSQLRestServerURIDynArray))<>nil);
end;

function TSQLRestClientURI.ServiceRetrieveAssociated(const aInterface: TGUID;
  out URI: TSQLRestServerURIDynArray): boolean;
var fact: TInterfaceFactory;
begin
................................................................................
function TSQLRestClientURI.EngineAdd(TableModelIndex: integer;
  const SentData: RawUTF8): TID;
var P: PUTF8Char;
    url, Head: RawUTF8;
begin
  result := 0;
  url := Model.URI[Model.Tables[TableModelIndex]];
  if URI(url,'POST',nil,@Head,@SentData).Lo<>HTTP_CREATED then
    exit; // response must be '201 Created'
  P := pointer(Head); // we need to check the headers
  if P<>nil then
  repeat
    // find ID from 'Location: Member Entry URI' header entry
    if IdemPChar(P,'LOCATION:') then begin // 'Location: root/People/11012' e.g.
      inc(P,9);
................................................................................
  until false;
end;

function TSQLRestClientURI.EngineDelete(TableModelIndex: integer; ID: TID): boolean;
var url: RawUTF8;
begin
  url := Model.getURIID(Model.Tables[TableModelIndex],ID);
  result := URI(url,'DELETE').Lo in [HTTP_SUCCESS,HTTP_NOCONTENT];
end;

function TSQLRestClientURI.EngineDeleteWhere(TableModelIndex: Integer;
  const SQLWhere: RawUTF8; const IDs: TIDDynArray): boolean;
var url: RawUTF8;
begin  // ModelRoot/TableName?where=WhereClause to delete members
  url := Model.getURI(Model.Tables[TableModelIndex])+'?where='+UrlEncode(SQLWhere);
  result := URI(url,'DELETE').Lo in [HTTP_SUCCESS,HTTP_NOCONTENT];
end;

function TSQLRestClientURI.EngineList(const SQL: RawUTF8;
  ForceAJAX: Boolean; ReturnedRowCount: PPtrInt): RawUTF8;
begin
  if (self=nil) or (SQL='') or (ReturnedRowCount<>nil) or
     (URI(Model.Root,'GET',@result,nil,@SQL).Lo<>HTTP_SUCCESS) then
    result := '';
end;

function TSQLRestClientURI.ClientRetrieve(TableModelIndex: integer; ID: TID;
  ForUpdate: boolean; var InternalState: cardinal; var Resp: RawUTF8): boolean;
begin
  if cardinal(TableModelIndex)<=cardinal(Model.fTablesMax) then
  with URIGet(Model.Tables[TableModelIndex],ID,Resp,ForUpdate) do
    if Lo=HTTP_SUCCESS then begin
      InternalState := Hi;
      result := true;
    end else
      result := false else
      result := false;
end;

................................................................................
  BlobField: PPropInfo; out BlobData: TSQLRawBlob): boolean;
var url: RawUTF8;
begin
  if (self=nil) or (aID<=0) or (BlobField=nil) then
    result := false else begin
    // URI is 'ModelRoot/TableName/TableID/BlobFieldName' with GET method
    url := Model.getURICallBack(BlobField^.Name,Model.Tables[TableModelIndex],aID);
    result := URI(url,'GET',@BlobData).Lo=HTTP_SUCCESS;
  end;
end;

function TSQLRestClientURI.EngineUpdate(TableModelIndex: integer; ID: TID;
  const SentData: RawUTF8): boolean;
var url: RawUTF8;
begin
  url := Model.getURIID(Model.Tables[TableModelIndex],ID);
  result := URI(url,'PUT',nil,nil,@SentData).Lo in [HTTP_SUCCESS,HTTP_NOCONTENT];
end;

function TSQLRestClientURI.EngineUpdateBlob(TableModelIndex: integer; aID: TID;
  BlobField: PPropInfo; const BlobData: TSQLRawBlob): boolean;
var url, Head: RawUTF8;
begin
  Head := 'Content-Type: application/octet-stream';
  if (self=nil) or (aID<=0) or (BlobField=nil) then
    result := false else begin
    // PUT ModelRoot/TableName/TableID/BlobFieldName
    FormatUTF8('%/%/%',[Model.URI[Model.Tables[TableModelIndex]],aID,BlobField^.Name],url);
    result := URI(url,'PUT',nil,@Head,@BlobData).Lo in [HTTP_SUCCESS,HTTP_NOCONTENT];
  end;
end;

function TSQLRestClientURI.EngineUpdateField(TableModelIndex: integer;
  const SetFieldName, SetValue, WhereFieldName, WhereValue: RawUTF8): boolean;
var url: RawUTF8;
begin
  if TableModelIndex<0 then
    result := false else begin
    // PUT ModelRoot/TableName?setname=..&set=..&wherename=..&where=..
    FormatUTF8('%?setname=%&set=%&wherename=%&where=%',
      [Model.URI[Model.Tables[TableModelIndex]],
       SetFieldName,UrlEncode(SetValue),WhereFieldName,UrlEncode(WhereValue)],url);
    result := URI(url,'PUT').Lo in [HTTP_SUCCESS,HTTP_NOCONTENT];
  end;
end;

function TSQLRestClientURI.EngineBatchSend(Table: TSQLRecordClass; const Data: RawUTF8;
  var Results: TIDDynArray; ExpectedResultsCount: integer): integer;
var Resp: RawUTF8;
    R: PUTF8Char;
    i: integer;
begin // TSQLRest.BatchSend() ensured that Batch contains some data
  try
    // URI is 'ModelRoot/Batch' or 'ModelRoot/Batch/TableName' with PUT method
    result := URI(Model.getURICallBack('Batch',Table,0),'PUT',@Resp,nil,@Data).Lo;
    if result<>HTTP_SUCCESS then
      exit;
    // returned Resp shall be an array of integers: '[200,200,...]'
    R := pointer(Resp);
    if R<>nil then
      while not (R^ in ['[',#0]) do inc(R);
    result := HTTP_BADREQUEST;
    if (R=nil) or (R^<>'[') then
      // invalid response
      exit;
    SetLength(Results,ExpectedResultsCount);
    if IdemPChar(R,'["OK"]') then begin // to save bandwith if no adding
      for i := 0 to ExpectedResultsCount-1 do
        Results[i] := HTTP_SUCCESS;
    end else begin
      inc(R); // jump first '['
      for i := 0 to ExpectedResultsCount-1 do begin
        Results[i] := GetJSONInt64Var(R);
        while R^ in [#1..' '] do inc(R);
        case R^ of
          ',': inc(R);
................................................................................
          ']': break;
          else exit;
        end;
      end;
      if R^<>']' then
        exit;
    end;
    result := HTTP_SUCCESS; // returns OK
  finally
    BatchAbort;
  end;
end;

procedure TSQLRestClientURI.BatchAbort;
begin
................................................................................
begin
  if self<>nil then
  try
    result := BatchSend(fBatchCurrent,Results);
  finally
    FreeAndNil(fBatchCurrent);
  end else
    result := HTTP_BADREQUEST;
end;


{ TSQLRestServer }

const
  ServerPipeNamePrefix: TFileName = '\\.\pipe\mORMot_';
................................................................................
      GetMem(result,L);
    MoveFast(pointer(s)^,result^,L);
  end;
end;
var call: TSQLRestURIParams;
begin
  if GlobalURIRequestServer=nil then begin
    Int64(result) := HTTP_NOTIMPLEMENTED; // 501
    exit;
  end;
  call.Init;
  call.Url := url;
  call.Method := method;
  call.LowLevelConnectionID := PtrInt(GlobalURIRequestServer);
  call.InHead := 'RemoteIP: 127.0.0.1';
................................................................................
      Magic: cardinal;
      Status: cardinal;
      InternalState: cardinal;
    end;
    Data: TCopyDataStruct;
    Header, ResStr: RawUTF8;
begin
  Msg.Result := HTTP_NOTFOUND;
  if (self=nil) or (Msg.From=0) then
    exit;
  input := PCopyDataStruct(Msg.CopyDataStruct);
  P := input^.lpData;
  if (P=nil) or (input^.cbData<=7) then
    exit;
  if PCardinal(P)^<>MAGIC_SYN then
    exit; // invalid layout: a broadcasted WM_COPYDATA message? :(
  inc(P,4);
  // #1 is a field delimiter below, since Get*Item() functions return nil for #0
  Msg.Result := HTTP_SUCCESS; // Send something back
  call.Init;
  call.Url := GetNextItem(P,#1);
  call.Method := GetNextItem(P,#1);
  call.InHead := GetNextItem(P,#1);
  call.LowLevelConnectionID := Msg.From;
  Header := 'RemoteIP: 127.0.0.1';
  if call.InHead='' then
................................................................................
      result := fRecordVersionMax;
      Writer.Free;
      break;
    end else
    try
      fAcquireExecution[execORMWrite].Safe.Lock;
      fRecordVersionDeleteIgnore := true;
      if BatchSend(Writer,IDs)=HTTP_SUCCESS then begin
        InternalLog('%.RecordVersionSynchronize Added=% Updated=% Deleted=% on %',
          [ClassType,Writer.AddCount,Writer.UpdateCount,Writer.DeleteCount,Master],sllDebug);
        if ChunkRowLimit=0 then begin
          result := fRecordVersionMax;
          break;
        end;
      end else begin
................................................................................
begin
  {$ifdef WITHLOG}
  Log.Log(sllUserAuth,'AuthenticationFailed(%) for % (session=%)',[GetEnumName(
    TypeInfo(TNotifyAuthenticationFailedReason),ord(Reason))^,Call^.Url,Session],self);
  {$endif}
  // 401 Unauthorized response MUST include a WWW-Authenticate header,
  // which is not what we used, so here we won't send 401 error code but 403
  Call.OutStatus := HTTP_FORBIDDEN;
  // call the notification event
  if Assigned(Server.OnAuthenticationFailed) then
    Server.OnAuthenticationFailed(Server,Reason,nil,self);
end;

destructor TSQLRestAcquireExecution.Destroy;
begin
................................................................................
procedure TimeOut;
begin
  {$ifdef WITHLOG}
  Log.Log(sllServer,'TimeOut %.Execute(%) after % ms',[self,ToText(Command)^,
    Server.fAcquireExecution[Command].LockedTimeOut],self);
  {$endif}
  if Call<>nil then
    Call^.OutStatus := HTTP_TIMEOUT; // 408 Request Time-out
end;
var Method: TThreadMethod;
    Start64: Int64;
begin
  with Server.fAcquireExecution[Command] do begin
    case Command of
      execSOAByMethod:
................................................................................
        ClassFieldNamesAllPropsAsText(SettingsStorage.ClassType,true)]);
      exit;
    end;
  end;
  ObjectToVariant(SettingsStorage,config,[woDontStoreDefault]);
  if URIBlobFieldName<>'' then
    config := TDocVariantData(config).GetValueByPath(URIBlobFieldName);
  ReturnsJson(config,HTTP_SUCCESS,true,twJsonEscape,true);
end;

procedure StatsAddSizeForCall(Stats: TSynMonitorInputOutput; const Call: TSQLRestURIParams);
begin
  Stats.AddSize( // rough estimation
    length(Call.Url)+length(Call.Method)+length(Call.InHead)+length(Call.InBody)+12,
    length(Call.OutHead)+length(Call.OutBody)+16);
................................................................................
        exit;
      end;
      ServiceExecution := @Service.fExecution[ServiceMethodIndex];
    end;
    end;
    if (Session>CONST_AUTHENTICATION_NOT_USED) and (ServiceExecution<>nil) and
       (SessionGroup-1 in ServiceExecution.Denied) then begin
      Error('Unauthorized method',HTTP_NOTALLOWED);
      exit;
    end;
    // if we reached here, we have to run the service method
    Service.ExecuteMethod(self);
  end;
var xml: RawUTF8;
    m: integer;
................................................................................
            Static := nil;
            if SQLisSelect then begin
              TableIndexes := Server.Model.GetTableIndexesFromSQLSelect(SQL);
              if TableIndexes=nil then begin
                // check for SELECT without any known table
                if not (reSQLSelectWithoutTable in
                   Call.RestAccessRights^.AllowRemoteExecute) then begin
                  Call.OutStatus := HTTP_NOTALLOWED;
                  exit;
                end;
              end else begin
                // check for SELECT with one (or several JOINed) tables
                for i := 0 to high(TableIndexes) do
                  if not (TableIndexes[i] in Call.RestAccessRights^.GET) then begin
                    Call.OutStatus := HTTP_NOTALLOWED;
                    exit;
                  end;
                // use the first static table (poorman's JOIN)
                Static := Server.InternalAdaptSQL(TableIndexes[0],SQL);
              end;
            end;
            if Static<>nil then  begin
................................................................................
            if Call.OutBody<>'' then begin // got JSON list '[{...}]' ?
              if (SQLSelect<>'') and (length(TableIndexes)=1) then begin
                InternalSetTableFromTableIndex(TableIndexes[0]);
                opt := ClientSQLRecordOptions;
                if opt<>[] then
                  ConvertOutBodyAsPlainJSON(SQLSelect,opt);
              end;
              Call.OutStatus := HTTP_SUCCESS;  // 200 OK
              if not SQLisSelect then // accurate fStats.NotifyORM(Method) below
                Method := TSQLURIMethod(IdemPCharArray(SQLBegin(pointer(SQL)),
                  ['INSERT','UPDATE','DELETE'])+2); // -1+2 -> mGET=1
            end;
          end;
        end;
      end;
    end else
    // here, Table<>nil and TableIndex in [0..MAX_SQLTABLES-1]
    if not (TableIndex in Call.RestAccessRights^.GET) then // check User Access
      Call.OutStatus := HTTP_NOTALLOWED else begin
      if TableID>0 then begin
        // GET ModelRoot/TableName/TableID[/BlobFieldName] to retrieve one member,
        // with or w/out locking, or a specified BLOB field content
        if Method=mLOCK then // Safe.Lock is to be followed by PUT -> check user
          if not (TableIndex in Call.RestAccessRights^.PUT) then
            Call.OutStatus := HTTP_NOTALLOWED else
            if Server.Model.Lock(TableIndex,TableID) then
              Method := mGET; // mark successfully locked
        if Method<>mLOCK then
          if URIBlobFieldName<>'' then begin
            // GET ModelRoot/TableName/TableID/BlobFieldName: retrieve BLOB field content
            Blob := Table.RecordProps.BlobFieldPropFromRawUTF8(URIBlobFieldName);
            if Blob<>nil then begin
              if TableEngine.EngineRetrieveBlob(TableIndex,
                   TableID,Blob,TSQLRawBlob(Call.OutBody)) then begin
                Call.OutHead := GetMimeContentTypeHeader(Call.OutBody);
                Call.OutStatus := HTTP_SUCCESS; // 200 OK
              end else
                Call.OutStatus := HTTP_NOTFOUND;
            end;
          end else begin
            // GET ModelRoot/TableName/TableID: retrieve a member content, JSON encoded
            Call.OutBody := Server.fCache.Retrieve(TableIndex,TableID);
            if Call.OutBody='' then begin
              // get JSON object '{...}'
              if Static<>nil then
................................................................................
                rec := Table.CreateFrom(Call.OutBody); // cached? -> make private
                try
                  Call.OutBody := rec.GetJSONValues(true,true,soSelect,nil,opt);
                finally
                  rec.Free;
                end;
              end;
              Call.OutStatus := HTTP_SUCCESS;
            end else // 200 OK
              Call.OutStatus := HTTP_NOTFOUND;
          end;
      end else
      // ModelRoot/TableName with 'select=..&where=' or YUI paging
      if Method<>mLOCK then begin // Safe.Lock not available here
        SQLSelect := 'RowID'; // if no select is specified (i.e. ModelRoot/TableName)
        // all IDs of this table are returned to the client
        SQLTotalRowsCount := 0;
................................................................................
        SQL := Server.Model.TableProps[TableIndex].
          SQLFromSelectWhere(SQLSelect,trim(SQLWhere));
        Call.OutBody := Server.InternalListRawUTF8(TableIndex,SQL);
        if Call.OutBody<>'' then begin // got JSON list '[{...}]' ?
          opt := ClientSQLRecordOptions;
          if opt<>[] then
            ConvertOutBodyAsPlainJSON(SQLSelect,opt);
          Call.OutStatus := HTTP_SUCCESS;  // 200 OK
          if Server.URIPagingParameters.SendTotalRowsCountFmt<>'' then
            // insert "totalRows":% optional value to the JSON output
            if Server.NoAJAXJSON or (ClientKind=ckFramework) then begin
              P := pointer(Call.OutBody);
              L := length(Call.OutBody);
              P := NotExpandedBufferRowCountPos(P,P+L);
              j := 0;
................................................................................
              if SQLTotalRowsCount=0 then // avoid sending fields array
                Call.OutBody := '[]' else
                Call.OutBody := trim(Call.OutBody);
              Call.OutBody := '{"values":'+Call.OutBody+
                FormatUTF8(Server.URIPagingParameters.SendTotalRowsCountFmt,[SQLTotalRowsCount])+'}';
            end;
        end else
          Call.OutStatus := HTTP_NOTFOUND;
      end;
    end;
    if Call.OutStatus=HTTP_SUCCESS then
      Server.fStats.NotifyORM(Method);
  end;
  mUNLOCK: begin
    // ModelRoot/TableName/TableID to unlock a member
    if not (TableIndex in Call.RestAccessRights^.PUT) then
      Call.OutStatus := HTTP_NOTALLOWED else
    if (Table<>nil) and (TableID>0) and
       Server.Model.UnLock(Table,TableID) then
      Call.OutStatus := HTTP_SUCCESS; // 200 OK
  end;
  mSTATE: begin
    // STATE method for TSQLRestClientServerInternalState
    // this method is called with Root (-> Table=nil -> Static=nil)
    // we need a specialized method in order to avoid fStats.Invalid increase
    Call.OutStatus := HTTP_SUCCESS;
    for i := 0 to high(Server.fStaticData) do
      if (Server.fStaticData[i]<>nil) and
         Server.fStaticData[i].InheritsFrom(TSQLRestStorage) then
        if TSQLRestStorage(Server.fStaticData[i]).RefreshedAndModified then begin
          inc(Server.InternalState); // force refresh
          break;
        end;
................................................................................
begin
  if MethodIndex=Server.fPublishedMethodBatchIndex then begin
    ExecuteSOAByMethod; // run the BATCH process in execORMWrite context
    exit;
  end;
  if not Call.RestAccessRights^.CanExecuteORMWrite(
     Method,Table,TableIndex,TableID,self) then begin
    Call.OutStatus := HTTP_FORBIDDEN;
    exit;
  end;
  case Method of
  mPOST: // POST=ADD=INSERT
    if Table=nil then begin
      // ModelRoot with free SQL statement sent as UTF-8 (only for Admin group)
      // see e.g. TSQLRestClientURI.EngineExecute
      if reSQL in Call.RestAccessRights^.AllowRemoteExecute then
        if (Call.InBody<>'') and
           (not (GotoNextNotSpace(Pointer(Call.InBody))^ in [#0,'[','{'])) and
           Server.EngineExecute(Call.InBody) then begin
          Call.OutStatus := HTTP_SUCCESS; // 200 OK
        end else
        Call.OutStatus := HTTP_FORBIDDEN;
    end else begin
      // ModelRoot/TableName with possible JSON SentData: create a new member
      // here, Table<>nil, TableID<0 and TableIndex in [0..MAX_SQLTABLES-1]
      if rsoComputeFieldsBeforeWriteOnServerSide in Server.Options then
        ComputeInBodyFields(seAdd);
      TableID := TableEngine.EngineAdd(TableIndex,Call.InBody);
      if TableID<>0 then begin
        Call.OutStatus := HTTP_CREATED; // 201 Created
        Call.OutHead := 'Location: '+URI+'/'+Int64ToUtf8(TableID);
        if rsoAddUpdateReturnsContent in Server.Options then begin
          Server.fCache.NotifyDeletion(TableIndex,TableID);
          Call.OutBody := TableEngine.EngineRetrieve(TableIndex,TableID);
          Server.fCache.Notify(TableIndex,TableID,Call.OutBody,soInsert);
        end else
          Server.fCache.Notify(TableIndex,TableID,Call.InBody,soInsert);
................................................................................
          if OK then begin // flush (no CreateTime in JSON)
            Server.fCache.NotifyDeletion(TableIndex,TableID);
            if rsoAddUpdateReturnsContent in Server.Options then
              Call.OutBody := TableEngine.EngineRetrieve(TableIndex,TableID);
          end;
        end;
        if OK then
          Call.OutStatus := HTTP_SUCCESS; // 200 OK
      end else
      Call.OutStatus := HTTP_FORBIDDEN;
    end else
    if Parameters<>nil then begin // e.g. from TSQLRestClient.EngineUpdateField
      // PUT ModelRoot/TableName?setname=..&set=..&wherename=..&where=..
      repeat
        UrlDecodeValue(Parameters,'SETNAME=',SQLSelect);
        UrlDecodeValue(Parameters,'SET=',SQLDir);
        UrlDecodeValue(Parameters,'WHERENAME=',SQLSort);
        UrlDecodeValue(Parameters,'WHERE=',SQLWhere,@Parameters);
      until Parameters=nil;
      if (SQLSelect<>'') and (SQLDir<>'') and (SQLSort<>'') and (SQLWhere<>'') then
        if TableEngine.EngineUpdateField(TableIndex,SQLSelect,SQLDir,SQLSort,SQLWhere) then begin
          if rsoAddUpdateReturnsContent in Server.Options then
            Call.OutBody := TableEngine.EngineRetrieve(TableIndex,TableID);
          Call.OutStatus := HTTP_SUCCESS; // 200 OK
        end;
    end;
  mDELETE:
    if TableID>0 then
      // ModelRoot/TableName/TableID to delete a member
      if not Server.RecordCanBeUpdated(Table,TableID,seDelete,@CustomErrorMsg) then
        Call.OutStatus := HTTP_FORBIDDEN else begin
        if TableEngine.EngineDelete(TableIndex,TableID) and
           Server.AfterDeleteForceCoherency(TableIndex,TableID) then begin
          Call.OutStatus := HTTP_SUCCESS; // 200 OK
          Server.fCache.NotifyDeletion(TableIndex,TableID);
        end;
      end else
    if Parameters<>nil then begin
      // ModelRoot/TableName?where=WhereClause to delete members
      repeat
        if UrlDecodeValue(Parameters,'WHERE=',SQLWhere,@Parameters) then begin
          SQLWhere := trim(SQLWhere);
          if SQLWhere<>'' then begin
            if Server.Delete(Table,SQLWhere) then
              Call.OutStatus := HTTP_SUCCESS; // 200 OK
          end;
          break;
        end;
      until Parameters=nil;
    end;
  mBEGIN: begin      // BEGIN TRANSACTION
    // TSQLVirtualTableJSON/External will rely on SQLite3 module
................................................................................
      if (Static<>nil) and (StaticKind=sVirtualTable) then
        Static.TransactionBegin(Table,Session) else
      if (Static=nil) and (Server.fTransactionTable<>nil) then begin
        Static := Server.StaticVirtualTable[Server.fTransactionTable];
        if Static<>nil then
          Static.TransactionBegin(Table,Session);
      end;
      Call.OutStatus := HTTP_SUCCESS; // 200 OK
    end;
  end;
  mEND: begin        // END=COMMIT
    // this method is called with Root (-> Table=nil -> Static=nil)
    // mEND logic is just the opposite of mBEGIN: release static, then main
    if (Static<>nil) and (StaticKind=sVirtualTable) then
      Static.Commit(Session,false) else
    if (Static=nil) and (Server.fTransactionTable<>nil) then begin
      Static := Server.StaticVirtualTable[Server.fTransactionTable];
      if Static<>nil then
        Static.Commit(Session,false);
    end;
    Server.Commit(Session,false);
    Call.OutStatus := HTTP_SUCCESS; // 200 OK
  end;
  mABORT: begin      // ABORT=ROLLBACK
    // this method is called with Root (-> Table=nil -> Static=nil)
    // mABORT logic is just the opposite of mBEGIN: release static, then main
    if (Static<>nil) and (StaticKind=sVirtualTable) then
      Static.RollBack(Session) else
    if (Static=nil) and (Server.fTransactionTable<>nil) then begin
      Static := Server.StaticVirtualTable[Server.fTransactionTable];
      if Static<>nil then
        Static.RollBack(Session);
    end;
    Server.RollBack(Session);
    Call.OutStatus := HTTP_SUCCESS; // 200 OK
  end;
  end;
  if StatusCodeIsSuccess(Call.OutStatus) then
    Server.fStats.NotifyORM(Method);
end;

procedure TSQLRestServerURIContext.FillInput(const LogInputIdent: RawUTF8);
................................................................................
  if HandleErrorAsRegularResult or StatusCodeIsSuccess(Status) then begin
    Call.OutStatus := Status;
    Call.OutBody := Result;
    if CustomHeader<>'' then
      Call.OutHead := CustomHeader else
      if Call.OutHead='' then
        Call.OutHead := JSON_CONTENT_TYPE_HEADER_VAR;
    if Handle304NotModified and (Status=HTTP_SUCCESS) and
       (Length(Result)>64) then begin
      clientHash := FindIniNameValue(pointer(Call.InHead),'IF-NONE-MATCH: ');
      serverHash := '"'+crc32cUTF8ToHex(Result)+'"';
      if clientHash<>serverHash then
        Call.OutHead := Call.OutHead+#13#10'ETag: '+serverHash else begin
        Call.OutBody := ''; // save bandwidth for "304 Not Modified"
        Call.OutStatus := HTTP_NOTMODIFIED;
      end;
    end;
  end else
    Error(Result,Status);
end;

procedure TSQLRestServerURIContext.Returns(Value: TObject; Status: integer;
................................................................................
begin
  if FileName='' then
    FileTime := 0 else
    FileTime := FileAgeToDateTime(FileName);
  if FileTime=0 then
    if Error404Redirect<>'' then
      Redirect(Error404Redirect) else
      Error('',HTTP_NOTFOUND) else begin
    if Call.OutHead<>'' then
      Call.OutHead := Call.OutHead+#13#10;
    if ContentType<>'' then
      Call.OutHead := Call.OutHead+HEADER_CONTENT_TYPE+ContentType else
      Call.OutHead := Call.OutHead+GetMimeContentTypeHeader('',FileName);
    Call.OutStatus := HTTP_SUCCESS;
    if Handle304NotModified then begin
      clientHash := FindIniNameValue(pointer(Call.InHead),'IF-NONE-MATCH: ');
      serverHash := '"'+DateTimeToIso8601(FileTime,false)+'"';
      Call.OutHead := Call.OutHead+#13#10'ETag: '+serverHash;
      if clientHash=serverHash then begin
        Call.OutStatus := HTTP_NOTMODIFIED;
        exit;
      end;
    end;
    // Content-Type: appears twice: 1st to notify static file, 2nd for mime type
    Call.OutHead := STATICFILE_CONTENT_TYPE_HEADER+#13#10+Call.OutHead;
    StringToUTF8(FileName,Call.OutBody); // body=filename for STATICFILE_CONTENT
    if AttachmentFileName<>'' then
................................................................................
  ReturnFile(fileName,Handle304NotModified,'','',Error404Redirect);
end;

procedure TSQLRestServerURIContext.Redirect(const NewLocation: RawUTF8;
  PermanentChange: boolean);
begin
  if PermanentChange then
    Call.OutStatus := HTTP_MOVEDPERMANENTLY else
    Call.OutStatus := HTTP_TEMPORARYREDIRECT;
  Call.OutHead := 'Location: '+NewLocation;
end;

procedure TSQLRestServerURIContext.Returns(const NameValuePairs: array of const;
  Status: integer; Handle304NotModified,HandleErrorAsRegularResult: boolean);
begin
  Returns(JSONEncode(NameValuePairs),Status,'',Handle304NotModified,
................................................................................
  Log := fLogClass.Enter('URI(% % inlen=%)',[Call.Method,Call.Url,length(Call.InBody)],self);
{$else}
begin
{$endif}
  QueryPerformanceCounter(timeStart);
  fStats.AddCurrentRequestCount(1);
  Call.OutInternalState := InternalState; // other threads may change it
  Call.OutStatus := HTTP_BADREQUEST; // default error code is 400 BAD REQUEST
  Ctxt := ServicesRouting.Create(self,Call);
  try
    {$ifdef WITHLOG}
    Ctxt.Log := Log.Instance;
    {$endif}
    if fShutdownRequested then
      Ctxt.Error('Server is shutting down',HTTP_UNAVAILABLE) else
    if Ctxt.Method=mNone then
      Ctxt.Error('Unknown VERB') else
    // 1. decode URI
    if not Ctxt.URIDecodeREST then
      Ctxt.Error('Invalid Root',HTTP_NOTFOUND) else
    if (RootRedirectGet<>'') and (Ctxt.Method=mGet) and
       (Call.Url=Model.Root) and (Call.InBody='') then
      Ctxt.Redirect(RootRedirectGet) else begin
      Ctxt.URIDecodeSOAByMethod;
      if (Ctxt.MethodIndex<0) and (Ctxt.URI<>'') then
        Ctxt.URIDecodeSOAByInterface;
      // 2. handle security
................................................................................
          Ctxt.Command := execORMWrite;
        if (not Assigned(OnBeforeURI)) or OnBeforeURI(Ctxt) then
          Ctxt.ExecuteCommand;
      except
        on E: Exception do
          if (not Assigned(OnErrorURI)) or OnErrorURI(Ctxt,E) then
            // return 500 internal server error
            Ctxt.Error(E,'',[],HTTP_SERVERERROR);
      end;
    end;
    // 4. returns expected result to the client and update Server statistics
    if StatusCodeIsSuccess(Call.OutStatus) then begin
      outcomingfile := false;
      if Call.OutBody<>'' then begin
        len := length(Call.OutHead);
        outcomingfile := (len>=25) and (Call.OutHead[15]='!') and
          IdemPChar(pointer(Call.OutHead),STATICFILE_CONTENT_TYPE_HEADER_UPPPER);
      end else // Call.OutBody=''
        if (Call.OutStatus=HTTP_SUCCESS) and
           (rsoHtml200WithNoBodyReturns204 in fOptions) then
          Call.OutStatus := HTTP_NOCONTENT;
      fStats.ProcessSuccess(outcomingfile);
    end else begin
      fStats.ProcessErrorNumber(Call.OutStatus);
      if Call.OutBody='' then // if no custom error message, compute it now as JSON
        Ctxt.Error(Ctxt.CustomErrorMsg,Call.OutStatus);
    end;
    StatsAddSizeForCall(fStats,Call);
................................................................................
  info: TDocVariantData;
begin
  if IdemPropNameU(Ctxt.URIBlobFieldName,'info') then begin
    info.InitFast;
    InternalInfo(info);
    Ctxt.Returns(info.ToJSON('','',jsonHumanReadable));
  end else
    Ctxt.Returns(Int64ToUtf8(ServerTimeStamp),HTTP_SUCCESS,TEXT_CONTENT_TYPE_HEADER);
end;

procedure TSQLRestServer.CacheFlush(Ctxt: TSQLRestServerURIContext);
begin
  case Ctxt.Method of
  mGET: begin
    if Ctxt.Table=nil then
................................................................................
    Ctxt.Error('PUT/POST only');
    exit;
  end;
  try
    EngineBatchSend(Ctxt.Table,Ctxt.Call.InBody,TIDDynArray(Results),0);
  except
    on E: Exception do begin
      Ctxt.Error(E,'did break % BATCH process',[Ctxt.Table],HTTP_SERVERERROR);
      exit;
    end;
  end;
  // send back operation status array
  Ctxt.Call.OutStatus := HTTP_SUCCESS;
  for i := 0 to length(Results)-1 do
    if Results[i]<>HTTP_SUCCESS then begin
      Ctxt.Call.OutBody := Int64DynArrayToCSV(Results,length(Results),'[',']');
      exit;
    end;
  Ctxt.Call.OutBody := '["OK"]';  // to save bandwith if no adding
end;

function ServerNonce(Previous: boolean): RawUTF8;
................................................................................
          RunningBatchTable := RunTable;
          RunningBatchURIMethod := URIMethod;
        end;
        if Count>=length(Results) then
          SetLength(Results,Count+256+Count shr 3);
      end;
      // process CRUD method operation
      Results[Count] := HTTP_NOTMODIFIED;
      case URIMethod of
      mDELETE: begin
        OK := EngineDelete(RunTableIndex,ID);
        if OK then begin
          if fCache<>nil then
            fCache.NotifyDeletion(RunTableIndex,ID);
          if (RunningBatchRest<>nil) or
             AfterDeleteForceCoherency(RunTableIndex,ID) then
            Results[Count] := HTTP_SUCCESS; // 200 OK
        end;
      end;
      mPOST: begin
        ID := EngineAdd(RunTableIndex,Value);
        Results[Count] := ID;
        if (ID<>0) and (fCache<>nil) then
          fCache.Notify(RunTableIndex,ID,Value,soInsert);
      end;
      mPUT: begin
        OK := EngineUpdate(RunTableIndex,ID,Value);
        if OK then begin
          Results[Count] := HTTP_SUCCESS; // 200 OK
          if fCache<>nil then // JSON Value may be uncomplete -> delete from cache
            fCache.NotifyDeletion(RunTableIndex,ID);
        end;
      end;
      else raise EORMBatchException.CreateUTF8('%.EngineBatchSend: Unknown "%" method',
        [self,Method]);
      end;
................................................................................
      raise EORMBatchException.CreateUTF8('%.EngineBatchSend: % Truncated',[self,Table]);
    while not (Sent^ in ['}',#0]) do inc(Sent);
    if Sent^<>'}' then
      raise EORMBatchException.CreateUTF8('%.EngineBatchSend(%): Missing }',[self,Table]);
  end;
  // if we reached here, process was OK
  SetLength(Results,Count);
  result := HTTP_SUCCESS;
end;

function CurrentServiceContext: TServiceRunningContext;
begin
  result := ServiceContext;
end;

................................................................................
  {$else}
  aDLL := LoadLibrary(pointer(DllName));
  {$endif}
  {$endif}
  if aDLL=0 then
    raise ECommunicationException.CreateUTF8('%.Create: LoadLibrary(%)',[self,DllName]);
  aRequest := GetProcAddress(aDLL,'URIRequest');
  if (@aRequest=nil) or (aRequest(nil,nil,nil,nil,nil).Lo<>HTTP_NOTFOUND) then begin
    FreeLibrary(aDLL);
    raise ECommunicationException.CreateUTF8(
      '%.Create: % doesn''t export a valid URIRequest() function',[self,DllName]);
  end;
  Create(aModel,aRequest);
  fLibraryHandle := aDLL;
end;
................................................................................
end;

procedure TSQLRestClientURIDll.InternalURI(var Call: TSQLRestURIParams);
var result: Int64Rec;
    pHead, pResp: PUTF8Char;
begin
  if @Func=nil then begin
    Call.OutStatus := HTTP_NOTIMPLEMENTED; // 501 (no valid application or library)
    exit;
  end;
  pResp := nil;
  pHead := nil;
  try
    result := Func(pointer(Call.Url),pointer(Call.Method),pointer(Call.InBody),
      @pResp,@pHead);
................................................................................
  fSafe.Enter;
  try
    if Assigned(fRedirectedServer) then
      fRedirectedServer.URI(Call) else
    if Assigned(fRedirectedClient) then
      // hook to access InternalURI() protected method
      TSQLRestClientRedirect(fRedirectedClient).InternalURI(Call) else
      Call.OutStatus := HTTP_GATEWAYTIMEOUT;
  finally
    fSafe.Leave;
  end;
end;


{$ifdef MSWINDOWS}
................................................................................
    {$ifdef WITHLOG}
    Log: ISynLog;
    {$endif}
begin
  {$ifdef WITHLOG}
  Log := fLogClass.Enter(self);
  {$endif}
  Call.OutStatus := HTTP_NOTIMPLEMENTED; // 501 (no valid application or library)
  fSafe.Enter;
  try
    if InternalCheckOpen then
    try
      Card := MAGIC_SYN; // magic word
      if FileWrite(fServerPipe,Card,4)<>4 then begin
        SleepHiRes(0);
................................................................................
          FileRead(fServerPipe,Call.OutStatus,sizeof(cardinal));
          FileRead(fServerPipe,Call.OutInternalState,sizeof(cardinal));
          Call.OutHead := ReadString(fServerPipe);
          Call.OutBody := ReadString(fServerPipe);
          exit;
        end else
        SleepHiRes(i);
      Call.OutStatus := HTTP_TIMEOUT; // 408 Request Timeout Error
{$else}
      if FileRead(fServerPipe,Call.OutStatus,sizeof(cardinal))=sizeof(cardinal) then begin
        // FileRead() waits till response arrived (or pipe is broken)
        FileRead(fServerPipe,Call.OutInternalState,sizeof(cardinal));
        Call.OutHead := ReadString(fServerPipe);
        Call.OutBody := ReadString(fServerPipe);
      end else
        Call.OutStatus := HTTP_NOTFOUND;
{$endif}
     except
       on E: Exception do begin // error in ReadString()
         InternalLog('% for PipeName=%',[E,fPipeName],sllLastError);
         Call.OutStatus := HTTP_NOTIMPLEMENTED; // 501 (no valid application or library)
         WriteString(fServerPipe,''); // try to notify the server of client logout
         FileClose(fServerPipe);
         fServerPipe := 0;
       end;
     end;
  finally
    fSafe.Leave;
................................................................................
    Safe.UnLock;
  end;
end;

function TServicesPublishedInterfacesList.RegisterFromServer(Client: TSQLRestClientURI): boolean;
var json: RawUTF8;
begin
  result := Client.CallBackGet('stat',['findservice','*'],json)=HTTP_SUCCESS;
  if result and (json<>'') then
    RegisterFromServerJSON(json);
end;

procedure TServicesPublishedInterfacesList.RegisterFromServerJSON(
  var PublishedJson: RawUTF8);
var tix: Int64;
................................................................................

procedure TSQLRestStorageShard.InternalBatchStop;
var i: integer;
begin
  try
    for i := 0 to high(fShardBatch) do
      if fShardBatch[i]<>nil then
        if fShards[i].BatchSend(fShardBatch[i])<>HTTP_SUCCESS then
          InternalLog('%.InternalBatchStop(%): %.BatchSend failed for shard #%',
            [ClassType,fStoredClass,fShards[i].ClassType,i],sllWarning);
  finally
    ObjArrayClear(fShardBatch);
    StorageUnLock;
  end;
end;
................................................................................
    Log: ISynLog;
    {$endif}
begin
  {$ifdef WITHLOG}
  Log := fLogClass.Enter(self);
  {$endif}
  if (fClientWindow=0) or not InternalCheckOpen then begin
    Call.OutStatus := HTTP_NOTIMPLEMENTED; // 501
    InternalLog('InternalCheckOpen failure',sllClient);
    exit;
  end;
  // 1. send request
  // #1 is a field delimiter below, since Get*Item() functions return nil for #0
  SetString(Msg,PAnsiChar(@MAGIC_SYN),4);
  Msg := Msg+Call.Url+#1+Call.Method+#1+Call.InHead+#1+Call.InBody;
................................................................................
        if not DoNotProcessMessages then
          while PeekMessage(aMsg,0,0,0,PM_REMOVE) do begin
            TranslateMessage(aMsg);
            DispatchMessage(aMsg);
          end;
        SleepHiRes(0);
        if GetTickCount64>Finished64 then begin
          Call.OutStatus := HTTP_TIMEOUT; // 408 Request Timeout Error
          exit;
        end;
      until fCurrentResponse<>#0;
    end;
    // 3. return answer to caller
    if length(fCurrentResponse)<=sizeof(Int64) then
      Call.OutStatus := HTTP_NOTIMPLEMENTED else begin
      P := pointer(fCurrentResponse);
      if PCardinal(P)^<>MAGIC_SYN then // broadcasted WM_COPYDATA message? :(
        Call.OutStatus := 0 else begin
        Call.OutStatus := PIntegerArray(P)[1];
        Call.OutInternalState := PIntegerArray(P)[2];
        inc(P,sizeof(integer)*3);
      end;
      if Call.OutStatus=0 then
        Call.OutStatus := HTTP_NOTFOUND else begin
        Call.OutHead := GetNextItem(P,#1);
        if P<>nil then
          SetString(Call.OutBody,P,length(fCurrentResponse)-(P-pointer(fCurrentResponse)));
      end;
    end;
  finally
    fSafe.Leave;
................................................................................
end;

procedure TSQLRestClientURIMessage.WMCopyData(var Msg: TWMCopyData);
begin
  if (self=nil) or (Msg.From<>fServerWindow) or
     (PCopyDataStruct(Msg.CopyDataStruct)^.dwData<>fServerWindow) then
    exit;
  Msg.Result := HTTP_SUCCESS; // Send something back
  if fCurrentResponse=#0 then // expect some response?
    SetString(fCurrentResponse,PAnsiChar(PCopyDataStruct(Msg.CopyDataStruct)^.lpData),
      PCopyDataStruct(Msg.CopyDataStruct)^.cbData);
end;

function TSQLRestClientURIMessage.InternalCheckOpen: boolean;
begin
................................................................................
end;

class function TSQLRestServerAuthentication.ClientGetSessionKey(
  Sender: TSQLRestClientURI; User: TSQLAuthUser; const aNameValueParameters: array of const): RawUTF8;
var resp: RawUTF8;
    values: TPUtf8CharDynArray;
begin
  if (Sender.CallBackGet('Auth',aNameValueParameters,resp)<>HTTP_SUCCESS) or
     (JSONDecode(pointer(resp),['result','data','server','version',
       'logonid','logonname','logondisplay','logongroup'],values)=nil) then begin
    Sender.fSessionData := '';
    result := '';
  end else begin
    SetString(result,values[0],StrLen(values[0]));
    Base64ToBin(PAnsiChar(values[1]),StrLen(values[1]),Sender.fSessionData);
................................................................................
        Ctxt.AuthenticationFailed(afInvalidPassword);
    finally
      U.Free;
    end else
      Ctxt.AuthenticationFailed(afUnknownUser);
  end else begin
    Ctxt.Call.OutHead := 'WWW-Authenticate: Basic realm="mORMot Server"';;
    Ctxt.Error('',HTTP_UNAUTHORIZED); // will popup for credentials in browser
  end;
end;


{$ifdef SSPIAUTH}

{ TSQLRestServerAuthenticationSSPI }
................................................................................
  InDataEnc := Ctxt.InputUTF8['Data'];
  if InDataEnc='' then begin
    // client is browser and used HTTP headers to send auth data
    InDataEnc := FindIniNameValue(pointer(Ctxt.Call.InHead),SECPKGNAMEHTTPAUTHORIZATION);
    if InDataEnc = '' then begin
      // no auth data sent, reply with supported auth methods
      Ctxt.Call.OutHead := SECPKGNAMEHTTPWWWAUTHENTICATE;
      Ctxt.Call.OutStatus := HTTP_UNAUTHORIZED;
      StatusCodeToErrorMsg(Ctxt.Call.OutStatus, Ctxt.Call.OutBody);
      exit;
    end;
    BrowserAuth := True;
  end else
    BrowserAuth := False;
  CtxArr.InitSpecific(TypeInfo(TSecContextDynArray),fSSPIAuthContexts,djInt64);
................................................................................
    SecCtxIdx := CtxArr.New; // add a new entry to fSSPIAuthContexts[]
    InvalidateSecContext(fSSPIAuthContexts[SecCtxIdx],ConnectionID);
  end;
  // call SSPI provider
  if ServerSSPIAuth(fSSPIAuthContexts[SecCtxIdx], Base64ToBin(InDataEnc), OutData) then begin
    if BrowserAuth then begin
      Ctxt.Call.OutHead := (SECPKGNAMEHTTPWWWAUTHENTICATE+' ')+BinToBase64(OutData);
      Ctxt.Call.OutStatus := HTTP_UNAUTHORIZED;
      StatusCodeToErrorMsg(Ctxt.Call.OutStatus, Ctxt.Call.OutBody);
    end else
      Ctxt.Returns(['result','','data',BinToBase64(OutData)]);
    exit; // 1st call: send back OutData to the client
  end;
  // 2nd call: user was authenticated -> release used context
  ServerSSPIAuthUser(fSSPIAuthContexts[SecCtxIdx],UserName);
................................................................................
      if Session<>nil then
        with Session.User do
        if BrowserAuth then
          Ctxt.Returns(JSONEncode(['result',Session.fPrivateSalt,
            'logonid',IDValue,'logonname',LogonName,'logondisplay',DisplayName,
            'logongroup',GroupRights.IDValue,
            'server',ExeVersion.ProgramName,'version',ExeVersion.Version.Detailed]),
            HTTP_SUCCESS,(SECPKGNAMEHTTPWWWAUTHENTICATE+' ')+BinToBase64(OutData)) else
          Ctxt.Returns([
            'result',BinToBase64(SecEncrypt(fSSPIAuthContexts[SecCtxIdx],Session.fPrivateSalt)),
            'logonid',IDValue,'logonname',LogonName,'logondisplay',DisplayName,
            'logongroup',GroupRights.ID,'server',ExeVersion.ProgramName,
            'version',ExeVersion.Version.Detailed,'data',BinToBase64(OutData)]);
    finally
      User.Free;
................................................................................

  function GetFullMethodName: RawUTF8;
  begin
    if cardinal(Ctxt.ServiceMethodIndex)<fInterface.fMethodsCount then
      result := fInterface.fMethods[Ctxt.ServiceMethodIndex].InterfaceDotMethodName else
      result := fInterface.fInterfaceName;
  end;
  procedure Error(const Msg: RawUTF8; Status: integer=HTTP_BADREQUEST);
  begin
    Ctxt.Error('(%) % for %',[ToText(InstanceCreation)^,Msg,GetFullMethodName],Status);
  end;
  function StatsCreate: TSynMonitorInputOutput;
  begin
    result := TSynMonitorInputOutput.Create(GetFullMethodName);
  end;
................................................................................
      else
        if Ctxt.Session>CONST_AUTHENTICATION_NOT_USED then
          case InstanceCreation of // authenticated user -> handle context
          sicPerSession: Inst.InstanceID := Ctxt.Session;
          sicPerUser:    Inst.InstanceID := Ctxt.SessionUser;
          sicPerGroup:   Inst.InstanceID := Ctxt.SessionGroup;
          end else begin
            Error('mode expects an authenticated session',HTTP_UNAUTHORIZED);
            exit;
          end;
      end;
      if InternalInstanceRetrieve(Inst,Ctxt.ServiceMethodIndex) then begin
        Ctxt.Success; // was SERVICE_METHODINDEX_FREEINSTANCE
        exit;         // {"method":"_free_", "params":[], "id":1234}
      end;
    end;
  end;
  if Inst.Instance=nil then begin
    Error('instance not found or deprecated',HTTP_BADREQUEST);
    exit;
  end;
  Ctxt.ServiceInstanceID := Inst.InstanceID;
  // 2. call method implementation
  if (Ctxt.ServiceExecution=nil) or
     (cardinal(Ctxt.ServiceMethodIndex)>=fInterface.fMethodsCount) then begin
    Error('ServiceExecution=nil',HTTP_SERVERERROR);
    exit;
  end;
  if mlInterfaces in TSQLRestServer(Rest).StatLevels then begin
    stats := fStats[Ctxt.ServiceMethodIndex];
    if stats=nil then begin
      stats := StatsCreate;
      fStats[Ctxt.ServiceMethodIndex] := stats;
................................................................................
          MultiEventMerge(exec.fOnExecute,fOnExecute);
        if Ctxt.ServiceExecution.LogRest<>nil then
          exec.AddInterceptor(OnLogRestExecuteMethod);
        if exec.ExecuteJson([instancePtr],Ctxt.ServiceParameters,WR,Ctxt.ForceServiceResultAsJSONObject) then begin
          Ctxt.Call.OutHead := exec.ServiceCustomAnswerHead;
          Ctxt.Call.OutStatus := exec.ServiceCustomAnswerStatus;
        end else begin
          Error('execution failed (probably due to bad input parameters)',HTTP_NOTACCEPTABLE);
          exit; // wrong request
        end;
      finally
        if dolock then
          LeaveCriticalSection(fInstanceLock);
      end;
      if Ctxt.Call.OutHead='' then begin // <>'' for TServiceCustomAnswer
        Ctxt.ServiceResultEnd(WR,Inst.InstanceID);
        Ctxt.Call.OutHead := JSON_CONTENT_TYPE_HEADER_VAR;
        Ctxt.Call.OutStatus := HTTP_SUCCESS;
      end;
      WR.SetText(Ctxt.Call.OutBody);
    finally
      Ctxt.fThreadServer^.Factory := nil;
      WR.Free;
    end;
  finally
................................................................................
      // handle custom content (not JSON array/object answer)
      if ArgsResultIsServiceCustomAnswer then
        with PServiceCustomAnswer(fValues[ArgsResultIndex])^ do
        if Header<>'' then begin
          fServiceCustomAnswerHead := Header;
          Res.ForceContent(Content);
          if Status=0 then // Values[]=@Records[] is filled with 0 by default
            fServiceCustomAnswerStatus := HTTP_SUCCESS else
            fServiceCustomAnswerStatus := Status;
          Result := true;
          exit;
        end;
      // write the '{"result":[...' array or object
      for a := ArgsOutFirst to ArgsOutLast do
      with Args[a] do
................................................................................
    result := InternalInvoke(
      aMethod.URI,aParams,aResult,aErrorMsg,aClientDrivenID,aServiceCustomAnswer);
end;

class function TServiceFactoryClient.GetErrorMessage(status: integer): RawUTF8;
begin
  case status of
    HTTP_UNAVAILABLE: result := 'Check the communication parameters';
    HTTP_NOTIMPLEMENTED: result := 'Server not reachable';
    HTTP_NOTALLOWED: result := 'Method forbidden for this User group';
    HTTP_UNAUTHORIZED: result := 'No active session';
    HTTP_NOTACCEPTABLE: result := 'Invalid input parameters';
    else result := '';
  end;
end;

function TServiceFactoryClient.InternalInvoke(const aMethod: RawUTF8;
  const aParams: RawUTF8; aResult: PRawUTF8; aErrorMsg: PRawUTF8;
  aClientDrivenID: PCardinal; aServiceCustomAnswer: PServiceCustomAnswer;

Changes to SQLite3/mORMotDDD.pas.

2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
....
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
  CqrsSetResultSuccessIf(ndx>=0);
end;

procedure TDDDRepositoryRestCommand.InternalCommit;
begin
  if fBatch.Count=0 then
    CqrsSetResult(cqrsBadRequest) else begin
    CqrsSetResultSuccessIf(Factory.Rest.BatchSend(fBatch,fBatchResults)=HTML_SUCCESS);
    FreeAndNil(fBatch);
  end;
end;

procedure TDDDRepositoryRestCommand.InternalRollback;
begin
  FreeAndNil(fBatch);
................................................................................
    doc: TDocVariantData;
    valid: Boolean;
    status: variant;
    res: TCQRSResult;
    cmd: integer;
begin
  result.Header := JSON_CONTENT_TYPE_HEADER_VAR;
  result.Status := HTML_SUCCESS;
  if SQL='' then
    exit;
  if SQL[1]='#' then begin
    cmd := IdemPCharArray(@SQL[2],['STATE','SETTINGS','VERSION','COMPUTER','LOG',
      'CHAT','STARTDAEMON','STOPDAEMON','RESTARTDAEMON','HELP','INFO']);
    case cmd of
    0: begin






|







 







|







2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
....
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
  CqrsSetResultSuccessIf(ndx>=0);
end;

procedure TDDDRepositoryRestCommand.InternalCommit;
begin
  if fBatch.Count=0 then
    CqrsSetResult(cqrsBadRequest) else begin
    CqrsSetResultSuccessIf(Factory.Rest.BatchSend(fBatch,fBatchResults)=HTTP_SUCCESS);
    FreeAndNil(fBatch);
  end;
end;

procedure TDDDRepositoryRestCommand.InternalRollback;
begin
  FreeAndNil(fBatch);
................................................................................
    doc: TDocVariantData;
    valid: Boolean;
    status: variant;
    res: TCQRSResult;
    cmd: integer;
begin
  result.Header := JSON_CONTENT_TYPE_HEADER_VAR;
  result.Status := HTTP_SUCCESS;
  if SQL='' then
    exit;
  if SQL[1]='#' then begin
    cmd := IdemPCharArray(@SQL[2],['STATE','SETTINGS','VERSION','COMPUTER','LOG',
      'CHAT','STARTDAEMON','STOPDAEMON','RESTARTDAEMON','HELP','INFO']);
    case cmd of
    0: begin

Changes to SQLite3/mORMotHttpClient.pas.

500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
...
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
...
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
      fSafe.Leave;
    end;
    Call.OutStatus := res.Lo;
    Call.OutInternalState := res.Hi;
    Call.OutHead := Head;
    Call.OutBody := Content;
  end else
    Call.OutStatus := HTML_NOTIMPLEMENTED; // 501
{$ifdef WITHLOG}
  with Call do
    fLogFamily.SynLog.Log(sllClient,'% % status=% state=%',
      [method,url,OutStatus,OutInternalState],self);
{$endif}
end;

................................................................................
    raise EServiceException.CreateUTF8('Missing %.WebSocketsUpgrade() call',[self]);
  if FakeCallbackID=0 then begin
    result := true;
    exit;
  end;
  body := FormatUTF8('{"%":%}',[Factory.InterfaceTypeInfo^.Name,FakeCallbackID]);
  head := 'Sec-WebSockets-REST: NonBlocking';
  result := CallBack(mPOST,'CacheFlush/_callback_',body,resp,nil,0,@head)=HTML_SUCCESS;
end;

function TSQLHttpClientWebsockets.CallbackRequest(Ctxt: THttpServerRequest): cardinal;
var params: TSQLRestURIParams;
begin
  if (Ctxt=nil) or
     ((Ctxt.InContentType<>'') and
      not IdemPropNameU(Ctxt.InContentType,JSON_CONTENT_TYPE)) then begin
    result := HTML_BADREQUEST;
    exit;
  end;
  params.Init(Ctxt.URL,Ctxt.Method,Ctxt.InHeaders,Ctxt.InContent);
  InternalNotificationMethodExecute(params);
  Ctxt.OutContent := params.OutBody;
  Ctxt.OutCustomHeaders := params.OutHead;
  Ctxt.OutContentType := params.OutBodyType;
................................................................................
end;

function TSQLHttpClientRequest.InternalRequest(const url, method: RawUTF8;
  var Header, Data, DataType: RawUTF8): Int64Rec;
var OutHeader, OutData: RawByteString;
begin
  if fRequest=nil then
    result.Lo := HTML_NOTIMPLEMENTED else begin
    result.Lo := fRequest.Request(url,method,KeepAliveMS,Header,Data,DataType,
      SockString(OutHeader),SockString(OutData));
    result.Hi := GetCardinal(pointer(
      FindIniNameValue(pointer(OutHeader),'SERVER-INTERNALSTATE: ')));
    Header := OutHeader;
    Data := OutData;
  end;






|







 







|








|







 







|







500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
...
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
...
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
      fSafe.Leave;
    end;
    Call.OutStatus := res.Lo;
    Call.OutInternalState := res.Hi;
    Call.OutHead := Head;
    Call.OutBody := Content;
  end else
    Call.OutStatus := HTTP_NOTIMPLEMENTED; // 501
{$ifdef WITHLOG}
  with Call do
    fLogFamily.SynLog.Log(sllClient,'% % status=% state=%',
      [method,url,OutStatus,OutInternalState],self);
{$endif}
end;

................................................................................
    raise EServiceException.CreateUTF8('Missing %.WebSocketsUpgrade() call',[self]);
  if FakeCallbackID=0 then begin
    result := true;
    exit;
  end;
  body := FormatUTF8('{"%":%}',[Factory.InterfaceTypeInfo^.Name,FakeCallbackID]);
  head := 'Sec-WebSockets-REST: NonBlocking';
  result := CallBack(mPOST,'CacheFlush/_callback_',body,resp,nil,0,@head)=HTTP_SUCCESS;
end;

function TSQLHttpClientWebsockets.CallbackRequest(Ctxt: THttpServerRequest): cardinal;
var params: TSQLRestURIParams;
begin
  if (Ctxt=nil) or
     ((Ctxt.InContentType<>'') and
      not IdemPropNameU(Ctxt.InContentType,JSON_CONTENT_TYPE)) then begin
    result := HTTP_BADREQUEST;
    exit;
  end;
  params.Init(Ctxt.URL,Ctxt.Method,Ctxt.InHeaders,Ctxt.InContent);
  InternalNotificationMethodExecute(params);
  Ctxt.OutContent := params.OutBody;
  Ctxt.OutCustomHeaders := params.OutHead;
  Ctxt.OutContentType := params.OutBodyType;
................................................................................
end;

function TSQLHttpClientRequest.InternalRequest(const url, method: RawUTF8;
  var Header, Data, DataType: RawUTF8): Int64Rec;
var OutHeader, OutData: RawByteString;
begin
  if fRequest=nil then
    result.Lo := HTTP_NOTIMPLEMENTED else begin
    result.Lo := fRequest.Request(url,method,KeepAliveMS,Header,Data,DataType,
      SockString(OutHeader),SockString(OutData));
    result.Hi := GetCardinal(pointer(
      FindIniNameValue(pointer(OutHeader),'SERVER-INTERNALSTATE: ')));
    Header := OutHeader;
    Data := OutData;
  end;

Changes to SQLite3/mORMotHttpServer.pas.

862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
...
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
...
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
....
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
    P: PUTF8Char;
    host,redirect: RawUTF8;
    match: TSQLRestModelMatch;
begin
  if ((Ctxt.URL='') or (Ctxt.URL='/')) and (Ctxt.Method='GET') then
    if fRootRedirectToURI[Ctxt.UseSSL]<>'' then begin
      Ctxt.OutCustomHeaders := 'Location: '+fRootRedirectToURI[Ctxt.UseSSL];
      result := HTML_TEMPORARYREDIRECT;
    end else
      result := HTML_BADREQUEST else
  if (Ctxt.Method='') or (OnlyJSONRequests and
     not IdemPChar(pointer(Ctxt.InContentType),JSON_CONTENT_TYPE_UPPER)) then
    // wrong Input parameters or not JSON request: 400 BAD REQUEST
    result := HTML_BADREQUEST else
  if Ctxt.Method='OPTIONS' then begin
    // handle CORS headers control
    Ctxt.OutCustomHeaders := 'Access-Control-Allow-Headers: '+
      FindIniNameValue(pointer(Ctxt.InHeaders),'ACCESS-CONTROL-REQUEST-HEADERS: ')+
      fAccessControlAllowOriginHeader;
    result := HTML_SUCCESS;
  end else begin
    // compute URI, handling any virtual host domain
    call.Init;
    call.LowLevelConnectionID := Ctxt.ConnectionID;
    if Ctxt.UseSSL then
      include(call.LowLevelFlags,llfSSL);
    if fHosts.Count>0 then begin
................................................................................
        if Ctxt.URL[1]='/' then
          call.Url := host+Ctxt.URL else
          call.Url := host+'/'+Ctxt.URL else
      if Ctxt.URL[1]='/' then
        call.Url := copy(Ctxt.URL,2,maxInt) else
        call.Url := Ctxt.URL;
    // search and call any matching TSQLRestServer instance
    result := HTML_NOTFOUND; // page not found by default (in case of wrong URL)
    for i := 0 to high(fDBServers) do
    with fDBServers[i] do
    if Ctxt.UseSSL=(Security=secSSL) then begin // registered for http or https
      match := Server.Model.URIMatch(call.Url);
      if match=rmNoMatch then
        continue;
      if fRedirectServerRootUriForExactCase and (match=rmMatchWithCaseChange) then begin
        // force redirection to exact Server.Model.Root case sensitivity
        call.OutStatus := HTML_TEMPORARYREDIRECT;
        call.OutHead := 'Location: '+Server.Model.Root+
          copy(call.Url,length(Server.Model.Root)+1,maxInt);
      end else begin
        // call matching TSQLRestServer.URI()
        call.Method := Ctxt.Method;
        call.InHead := Ctxt.InHeaders;
        call.InBody := Ctxt.InContent;
................................................................................
        Ctxt.OutContentType := GetNextLine(P+14,P);
        call.OutHead := P;
      end else
        // default content type is JSON
        Ctxt.OutContentType := JSON_CONTENT_TYPE_VAR;
      // handle HTTP redirection over virtual hosts
      if (host<>'') and
         ((result=HTML_MOVEDPERMANENTLY) or (result=HTML_TEMPORARYREDIRECT)) then begin
        redirect := FindIniNameValue(P,'LOCATION: ');
        len := length(host);
        if (length(redirect)>len) and (redirect[len+1]='/') and
           IdemPropNameU(host,pointer(redirect),len) then
          // host/method -> method on same domain
          call.OutHead := 'Location: '+copy(redirect,len+1,maxInt);
      end;
................................................................................
      // aConnection.InheritsFrom(TSynThread) may raise an exception
      // -> checked in WebSocketsCallback/IsActiveWebSocket
      ctxt := THttpServerRequest.Create(nil,aConnectionID,nil);
      try
        ctxt.Prepare(FormatUTF8('%/%/%',[aSender.Model.Root,
          aInterfaceDotMethodName,aFakeCallID]),'POST','','['+aParams+']','');
        status := fHttpServer.Callback(ctxt,aResult=nil);
        if status=HTML_SUCCESS then begin
          if aResult<>nil then
            aResult^ := Ctxt.OutContent;
          result := true;
        end else
          if aErrorMsg<>nil then
            aErrorMsg^ := FormatUTF8('%.Callback(%) received status=% from %',
              [fHttpServer,aConnectionID,status,ctxt.URL]);






|

|



|





|







 







|








|







 







|







 







|







862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
...
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
...
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
....
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
    P: PUTF8Char;
    host,redirect: RawUTF8;
    match: TSQLRestModelMatch;
begin
  if ((Ctxt.URL='') or (Ctxt.URL='/')) and (Ctxt.Method='GET') then
    if fRootRedirectToURI[Ctxt.UseSSL]<>'' then begin
      Ctxt.OutCustomHeaders := 'Location: '+fRootRedirectToURI[Ctxt.UseSSL];
      result := HTTP_TEMPORARYREDIRECT;
    end else
      result := HTTP_BADREQUEST else
  if (Ctxt.Method='') or (OnlyJSONRequests and
     not IdemPChar(pointer(Ctxt.InContentType),JSON_CONTENT_TYPE_UPPER)) then
    // wrong Input parameters or not JSON request: 400 BAD REQUEST
    result := HTTP_BADREQUEST else
  if Ctxt.Method='OPTIONS' then begin
    // handle CORS headers control
    Ctxt.OutCustomHeaders := 'Access-Control-Allow-Headers: '+
      FindIniNameValue(pointer(Ctxt.InHeaders),'ACCESS-CONTROL-REQUEST-HEADERS: ')+
      fAccessControlAllowOriginHeader;
    result := HTTP_SUCCESS;
  end else begin
    // compute URI, handling any virtual host domain
    call.Init;
    call.LowLevelConnectionID := Ctxt.ConnectionID;
    if Ctxt.UseSSL then
      include(call.LowLevelFlags,llfSSL);
    if fHosts.Count>0 then begin
................................................................................
        if Ctxt.URL[1]='/' then
          call.Url := host+Ctxt.URL else
          call.Url := host+'/'+Ctxt.URL else
      if Ctxt.URL[1]='/' then
        call.Url := copy(Ctxt.URL,2,maxInt) else
        call.Url := Ctxt.URL;
    // search and call any matching TSQLRestServer instance
    result := HTTP_NOTFOUND; // page not found by default (in case of wrong URL)
    for i := 0 to high(fDBServers) do
    with fDBServers[i] do
    if Ctxt.UseSSL=(Security=secSSL) then begin // registered for http or https
      match := Server.Model.URIMatch(call.Url);
      if match=rmNoMatch then
        continue;
      if fRedirectServerRootUriForExactCase and (match=rmMatchWithCaseChange) then begin
        // force redirection to exact Server.Model.Root case sensitivity
        call.OutStatus := HTTP_TEMPORARYREDIRECT;
        call.OutHead := 'Location: '+Server.Model.Root+
          copy(call.Url,length(Server.Model.Root)+1,maxInt);
      end else begin
        // call matching TSQLRestServer.URI()
        call.Method := Ctxt.Method;
        call.InHead := Ctxt.InHeaders;
        call.InBody := Ctxt.InContent;
................................................................................
        Ctxt.OutContentType := GetNextLine(P+14,P);
        call.OutHead := P;
      end else
        // default content type is JSON
        Ctxt.OutContentType := JSON_CONTENT_TYPE_VAR;
      // handle HTTP redirection over virtual hosts
      if (host<>'') and
         ((result=HTTP_MOVEDPERMANENTLY) or (result=HTTP_TEMPORARYREDIRECT)) then begin
        redirect := FindIniNameValue(P,'LOCATION: ');
        len := length(host);
        if (length(redirect)>len) and (redirect[len+1]='/') and
           IdemPropNameU(host,pointer(redirect),len) then
          // host/method -> method on same domain
          call.OutHead := 'Location: '+copy(redirect,len+1,maxInt);
      end;
................................................................................
      // aConnection.InheritsFrom(TSynThread) may raise an exception
      // -> checked in WebSocketsCallback/IsActiveWebSocket
      ctxt := THttpServerRequest.Create(nil,aConnectionID,nil);
      try
        ctxt.Prepare(FormatUTF8('%/%/%',[aSender.Model.Root,
          aInterfaceDotMethodName,aFakeCallID]),'POST','','['+aParams+']','');
        status := fHttpServer.Callback(ctxt,aResult=nil);
        if status=HTTP_SUCCESS then begin
          if aResult<>nil then
            aResult^ := Ctxt.OutContent;
          result := true;
        end else
          if aErrorMsg<>nil then
            aErrorMsg^ := FormatUTF8('%.Callback(%) received status=% from %',
              [fHttpServer,aConnectionID,status,ctxt.URL]);

Changes to SQLite3/mORMotMVC.pas.

303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
...
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
...
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
....
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
....
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
....
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
....
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
....
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
....
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
....
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
....
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
  TMVCAction = record
    /// the method name to be executed
    RedirectToMethodName: RawUTF8;
    /// may contain a JSON object which will be used to specify parameters
    // to the specified method
    RedirectToMethodParameters: RawUTF8;
    /// which HTML status code should be returned
    // - if RedirectMethodName is set, will return 307 HTML_TEMPORARYREDIRECT
    // by default, but you can set here the expected HTML status code, e.g.
    // 201 HTML_CREATED or 404 HTML_NOTFOUND
    ReturnedStatus: cardinal;
  end;

  TMVCApplication = class;

  /// abtract MVC rendering execution context
  // - you shoud not execute this abstract class, but any of the inherited class
................................................................................
  // - those error are external errors which should be notified to the client
  // - can be used to change the default view, e.g. on application error
  EMVCApplication = class(ESynException)
  protected
    fAction: TMVCAction;
  public
    /// same as calling TMVCApplication.GotoView()
    // - HTML_TEMPORARYREDIRECT will change the URI, but HTML_SUCCESS won't
    constructor CreateGotoView(const aMethod: RawUTF8;
      const aParametersNameValuePairs: array of const;
      aStatus: cardinal=HTML_TEMPORARYREDIRECT);
    /// same as calling TMVCApplication.GotoError()
    constructor CreateGotoError(const aErrorMessage: string;
      aErrorCode: integer=HTML_BADREQUEST); overload;
    /// same as calling TMVCApplication.GotoError()
    constructor CreateGotoError(aHtmlErrorCode: integer); overload;
    /// same as calling TMVCApplication.GotoDefault
    // - HTML_TEMPORARYREDIRECT will change the URI, but HTML_SUCCESS won't
    constructor CreateDefault(aStatus: cardinal=HTML_TEMPORARYREDIRECT);
  end;

  /// defines the main and error pages for the ViewModel of one application
  IMVCApplication = interface(IInvokable)
    ['{C48718BF-861B-448A-B593-8012DB51E15D}']
    /// the default main page
    // - whole data context is retrieved and returned as a TDocVariant
................................................................................
    /// generic IMVCApplication implementation
    procedure Error(var Msg: RawUTF8; var Scope: variant);
    /// every view will have this data context transmitted as "main":...
    function GetViewInfo(MethodIndex: integer): variant; virtual;
    /// compute the data context e.g. for the /mvc-info URI
    function GetMvcInfo: variant; virtual;
    /// wrappers to redirect to IMVCApplication standard methods
    // - if status is HTML_TEMPORARYREDIRECT, it will change the URI
    // whereas HTML_SUCCESS would just render the view for the current URI
    class procedure GotoView(var Action: TMVCAction; const MethodName: RawUTF8;
      const ParametersNameValuePairs: array of const;
      Status: cardinal=HTML_TEMPORARYREDIRECT);
    class procedure GotoError(var Action: TMVCAction; const Msg: string;
      ErrorCode: integer=HTML_BADREQUEST); overload;
    class procedure GotoError(var Action: TMVCAction; ErrorCode: integer); overload;
    class procedure GotoDefault(var Action: TMVCAction;
      Status: cardinal=HTML_TEMPORARYREDIRECT);
  public
    /// initialize the instance of the MVC/MVVM application
    // - define the associated REST instance, and the interface definition for
    // application commands
    // - is not defined as constructor, since this TInjectableObject may
    // expect injection using the CreateInjected() constructor
    procedure Start(aRestModel: TSQLRest; aInterface: PTypeInfo); virtual;
................................................................................
begin
  GotoView(Action,'Error',['Msg',Msg],ErrorCode);
end;

class procedure TMVCApplication.GotoError(var Action: TMVCAction;
  ErrorCode: integer);
begin
  if ErrorCode<=HTML_CONTINUE then
    ErrorCode := HTML_BADREQUEST;
  GotoView(Action,'Error',['Msg',StatusCodeToErrorMsg(ErrorCode)],ErrorCode);
end;

class procedure TMVCApplication.GotoDefault(var Action: TMVCAction; Status: cardinal);
begin
  Action.ReturnedStatus := Status;
  Action.RedirectToMethodName := 'Default';
................................................................................
var action: TMVCAction;
    exec: TServiceMethodExecute;
    isAction: boolean;
    WR: TTextWriter;
    methodOutput: RawUTF8;
    renderContext: variant;
begin
  action.ReturnedStatus := HTML_SUCCESS;
  fMethodIndex := aMethodIndex;
  try
    if fMethodIndex>=0 then begin
      repeat
        try
          isAction := fApplication.fFactory.Methods[fMethodIndex].ArgsResultIsServiceCustomAnswer;
          WR := TJSONSerializer.CreateOwnedStream;
................................................................................
        except
          on E: EMVCApplication do
            action := E.fAction;
        end; // lower level exceptions will be handled below
        fInput := action.RedirectToMethodParameters;
        fMethodIndex := fApplication.fFactory.FindMethodIndex(action.RedirectToMethodName);
        if action.ReturnedStatus=0 then
          action.ReturnedStatus := HTML_SUCCESS else
        if (action.ReturnedStatus=HTML_TEMPORARYREDIRECT) or
           (action.ReturnedStatus=HTML_FOUND) or
           (action.ReturnedStatus=HTML_SEEOTHER) or
           (action.ReturnedStatus=HTML_MOVEDPERMANENTLY) then
          if Redirects(action) then // if redirection is implemented
            exit else
            action.ReturnedStatus := HTML_SUCCESS; // fallback is to handle here
      until fMethodIndex<0; // loop to handle redirection
    end;
    // if we reached here, there was a wrong URI -> render the 404 error page
    CommandError('notfound',true,HTML_NOTFOUND);
  except
    on E: Exception do
      CommandError('exception',
        ObjectToVariantDebug(E,'%.ExecuteCommand',[self]),HTML_SERVERERROR);
  end;
end;

function TMVCRendererAbstract.Redirects(const action: TMVCAction): boolean;
begin
  result := false;
end; // indicates redirection did not happen -> caller should do it manually
................................................................................
  if (publishMvcInfo in fPublishOptions) and
     IdemPropNameU(rawMethodName,MVCINFO_URI) then begin
    if fMvcInfoCache='' then begin
      mvcinfo := fApplication.GetMvcInfo;
      mvcinfo.viewsFolder := fViews.ViewTemplateFolder;
      fMvcInfoCache := TSynMustache.Parse(MUSTACHE_MVCINFO).Render(mvcinfo);
    end;
    Ctxt.Returns(fMvcInfoCache,HTML_SUCCESS,HTML_CONTENT_TYPE_HEADER,True);
  end else
  if (publishStatic in fPublishOptions) and
     IdemPropNameU(rawMethodName,STATIC_URI) then begin
    // code below will use a local in-memory cache, but would do the same as:
    // Ctxt.ReturnFileFromFolder(fViews.ViewTemplateFolder+STATIC_URI);
    static := fStaticCache.Value(rawFormat,#0);
    if static=#0 then begin
................................................................................
        static := StringFromFile(fViews.ViewTemplateFolder+STATIC_URI+PathDelim+staticFileName);
        if static<>'' then
          static := GetMimeContentType(nil,0,staticFileName)+#0+static;
      end;
      fStaticCache.Add(rawFormat,static);
    end;
    if static='' then
      Ctxt.Error('',HTML_NOTFOUND) else begin
      Split(static,#0,rawFormat,static);
      Ctxt.Returns(static,HTML_SUCCESS,HEADER_CONTENT_TYPE+rawFormat,True);
    end;
    exit;
  end else begin
     if IdemPropNameU(rawFormat,'json') then
      rendererClass := TMVCRendererJSON else
      rendererClass := TMVCRendererFromViews;
    renderer := rendererClass.Create(self);
................................................................................
                FlattenAsNestedObject(RawUTF8(method^.Args[method^.ArgsInFirst].ParamName^));
            end;
            renderer.fInput := ToJSON;
          end;
        end;
        renderer.ExecuteCommand(methodIndex);
      end else
        renderer.CommandError('notfound',true,HTML_NOTFOUND);
      Ctxt.Returns(renderer.Output.Content,renderer.Output.Status,
        renderer.Output.Header,True,true);
    finally
      renderer.Free;
    end;
  end;
end;
................................................................................
  fRun := aRun;
  inherited Create(fRun.Application);
end;

procedure TMVCRendererReturningData.ExecuteCommand(aMethodIndex: integer);
  procedure SetOutputValue(const aValue: RawUTF8);
  begin
    fOutput.Status := HTML_SUCCESS;
    Split(aValue,#0,fOutput.Header,RawUTF8(fOutput.Content));
  end;
  function RetrievedFromInputValues(const aKey: RawUTF8;
    const aInputValues: TSynNameValue): boolean;
  var i: integer;
  begin
    i := aInputValues.Find(aKey);
................................................................................
  if fCacheCurrent<>noCache then
  try
    fRun.fCacheLocker.Enter;
    with fRun.fCache[aMethodIndex] do begin
      inc(fCacheCurrentSec,TimeOutSeconds);
      case fCacheCurrent of
      rootCache:
        if fOutput.Status=HTML_SUCCESS then begin
          RootValue := fOutput.Header+#0+fOutput.Content;
          RootValueExpirationTime := fCacheCurrentSec;
        end else
          RootValue := '';
      inputCache:
        if fOutput.Status=HTML_SUCCESS then
          InputValues.Add(fCacheCurrentInputValueKey,fOutput.Header+#0+fOutput.Content,fCacheCurrentSec) else
          InputValues.Add(fCacheCurrentInputValueKey,'');
      end;
    end;
  finally
    fRun.fCacheLocker.Leave;
  end;






|

|







 







|


|


|



|
|







 







|
|


|

|


|







 







|
|







 







|







 







|
|
|
|
|


|



|



|







 







|







 







|

|







 







|







 







|







 







|





|







303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
...
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
...
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
....
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
....
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
....
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
....
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
....
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
....
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
....
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
....
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
  TMVCAction = record
    /// the method name to be executed
    RedirectToMethodName: RawUTF8;
    /// may contain a JSON object which will be used to specify parameters
    // to the specified method
    RedirectToMethodParameters: RawUTF8;
    /// which HTML status code should be returned
    // - if RedirectMethodName is set, will return 307 HTTP_TEMPORARYREDIRECT
    // by default, but you can set here the expected HTML status code, e.g.
    // 201 HTTP_CREATED or 404 HTTP_NOTFOUND
    ReturnedStatus: cardinal;
  end;

  TMVCApplication = class;

  /// abtract MVC rendering execution context
  // - you shoud not execute this abstract class, but any of the inherited class
................................................................................
  // - those error are external errors which should be notified to the client
  // - can be used to change the default view, e.g. on application error
  EMVCApplication = class(ESynException)
  protected
    fAction: TMVCAction;
  public
    /// same as calling TMVCApplication.GotoView()
    // - HTTP_TEMPORARYREDIRECT will change the URI, but HTTP_SUCCESS won't
    constructor CreateGotoView(const aMethod: RawUTF8;
      const aParametersNameValuePairs: array of const;
      aStatus: cardinal=HTTP_TEMPORARYREDIRECT);
    /// same as calling TMVCApplication.GotoError()
    constructor CreateGotoError(const aErrorMessage: string;
      aErrorCode: integer=HTTP_BADREQUEST); overload;
    /// same as calling TMVCApplication.GotoError()
    constructor CreateGotoError(aHtmlErrorCode: integer); overload;
    /// same as calling TMVCApplication.GotoDefault
    // - HTTP_TEMPORARYREDIRECT will change the URI, but HTTP_SUCCESS won't
    constructor CreateDefault(aStatus: cardinal=HTTP_TEMPORARYREDIRECT);
  end;

  /// defines the main and error pages for the ViewModel of one application
  IMVCApplication = interface(IInvokable)
    ['{C48718BF-861B-448A-B593-8012DB51E15D}']
    /// the default main page
    // - whole data context is retrieved and returned as a TDocVariant
................................................................................
    /// generic IMVCApplication implementation
    procedure Error(var Msg: RawUTF8; var Scope: variant);
    /// every view will have this data context transmitted as "main":...
    function GetViewInfo(MethodIndex: integer): variant; virtual;
    /// compute the data context e.g. for the /mvc-info URI
    function GetMvcInfo: variant; virtual;
    /// wrappers to redirect to IMVCApplication standard methods
    // - if status is HTTP_TEMPORARYREDIRECT, it will change the URI
    // whereas HTTP_SUCCESS would just render the view for the current URI
    class procedure GotoView(var Action: TMVCAction; const MethodName: RawUTF8;
      const ParametersNameValuePairs: array of const;
      Status: cardinal=HTTP_TEMPORARYREDIRECT);
    class procedure GotoError(var Action: TMVCAction; const Msg: string;
      ErrorCode: integer=HTTP_BADREQUEST); overload;
    class procedure GotoError(var Action: TMVCAction; ErrorCode: integer); overload;
    class procedure GotoDefault(var Action: TMVCAction;
      Status: cardinal=HTTP_TEMPORARYREDIRECT);
  public
    /// initialize the instance of the MVC/MVVM application
    // - define the associated REST instance, and the interface definition for
    // application commands
    // - is not defined as constructor, since this TInjectableObject may
    // expect injection using the CreateInjected() constructor
    procedure Start(aRestModel: TSQLRest; aInterface: PTypeInfo); virtual;
................................................................................
begin
  GotoView(Action,'Error',['Msg',Msg],ErrorCode);
end;

class procedure TMVCApplication.GotoError(var Action: TMVCAction;
  ErrorCode: integer);
begin
  if ErrorCode<=HTTP_CONTINUE then
    ErrorCode := HTTP_BADREQUEST;
  GotoView(Action,'Error',['Msg',StatusCodeToErrorMsg(ErrorCode)],ErrorCode);
end;

class procedure TMVCApplication.GotoDefault(var Action: TMVCAction; Status: cardinal);
begin
  Action.ReturnedStatus := Status;
  Action.RedirectToMethodName := 'Default';
................................................................................
var action: TMVCAction;
    exec: TServiceMethodExecute;
    isAction: boolean;
    WR: TTextWriter;
    methodOutput: RawUTF8;
    renderContext: variant;
begin
  action.ReturnedStatus := HTTP_SUCCESS;
  fMethodIndex := aMethodIndex;
  try
    if fMethodIndex>=0 then begin
      repeat
        try
          isAction := fApplication.fFactory.Methods[fMethodIndex].ArgsResultIsServiceCustomAnswer;
          WR := TJSONSerializer.CreateOwnedStream;
................................................................................
        except
          on E: EMVCApplication do
            action := E.fAction;
        end; // lower level exceptions will be handled below
        fInput := action.RedirectToMethodParameters;
        fMethodIndex := fApplication.fFactory.FindMethodIndex(action.RedirectToMethodName);
        if action.ReturnedStatus=0 then
          action.ReturnedStatus := HTTP_SUCCESS else
        if (action.ReturnedStatus=HTTP_TEMPORARYREDIRECT) or
           (action.ReturnedStatus=HTTP_FOUND) or
           (action.ReturnedStatus=HTTP_SEEOTHER) or
           (action.ReturnedStatus=HTTP_MOVEDPERMANENTLY) then
          if Redirects(action) then // if redirection is implemented
            exit else
            action.ReturnedStatus := HTTP_SUCCESS; // fallback is to handle here
      until fMethodIndex<0; // loop to handle redirection
    end;
    // if we reached here, there was a wrong URI -> render the 404 error page
    CommandError('notfound',true,HTTP_NOTFOUND);
  except
    on E: Exception do
      CommandError('exception',
        ObjectToVariantDebug(E,'%.ExecuteCommand',[self]),HTTP_SERVERERROR);
  end;
end;

function TMVCRendererAbstract.Redirects(const action: TMVCAction): boolean;
begin
  result := false;
end; // indicates redirection did not happen -> caller should do it manually
................................................................................
  if (publishMvcInfo in fPublishOptions) and
     IdemPropNameU(rawMethodName,MVCINFO_URI) then begin
    if fMvcInfoCache='' then begin
      mvcinfo := fApplication.GetMvcInfo;
      mvcinfo.viewsFolder := fViews.ViewTemplateFolder;
      fMvcInfoCache := TSynMustache.Parse(MUSTACHE_MVCINFO).Render(mvcinfo);
    end;
    Ctxt.Returns(fMvcInfoCache,HTTP_SUCCESS,HTML_CONTENT_TYPE_HEADER,True);
  end else
  if (publishStatic in fPublishOptions) and
     IdemPropNameU(rawMethodName,STATIC_URI) then begin
    // code below will use a local in-memory cache, but would do the same as:
    // Ctxt.ReturnFileFromFolder(fViews.ViewTemplateFolder+STATIC_URI);
    static := fStaticCache.Value(rawFormat,#0);
    if static=#0 then begin
................................................................................
        static := StringFromFile(fViews.ViewTemplateFolder+STATIC_URI+PathDelim+staticFileName);
        if static<>'' then
          static := GetMimeContentType(nil,0,staticFileName)+#0+static;
      end;
      fStaticCache.Add(rawFormat,static);
    end;
    if static='' then
      Ctxt.Error('',HTTP_NOTFOUND) else begin
      Split(static,#0,rawFormat,static);
      Ctxt.Returns(static,HTTP_SUCCESS,HEADER_CONTENT_TYPE+rawFormat,True);
    end;
    exit;
  end else begin
     if IdemPropNameU(rawFormat,'json') then
      rendererClass := TMVCRendererJSON else
      rendererClass := TMVCRendererFromViews;
    renderer := rendererClass.Create(self);
................................................................................
                FlattenAsNestedObject(RawUTF8(method^.Args[method^.ArgsInFirst].ParamName^));
            end;
            renderer.fInput := ToJSON;
          end;
        end;
        renderer.ExecuteCommand(methodIndex);
      end else
        renderer.CommandError('notfound',true,HTTP_NOTFOUND);
      Ctxt.Returns(renderer.Output.Content,renderer.Output.Status,
        renderer.Output.Header,True,true);
    finally
      renderer.Free;
    end;
  end;
end;
................................................................................
  fRun := aRun;
  inherited Create(fRun.Application);
end;

procedure TMVCRendererReturningData.ExecuteCommand(aMethodIndex: integer);
  procedure SetOutputValue(const aValue: RawUTF8);
  begin
    fOutput.Status := HTTP_SUCCESS;
    Split(aValue,#0,fOutput.Header,RawUTF8(fOutput.Content));
  end;
  function RetrievedFromInputValues(const aKey: RawUTF8;
    const aInputValues: TSynNameValue): boolean;
  var i: integer;
  begin
    i := aInputValues.Find(aKey);
................................................................................
  if fCacheCurrent<>noCache then
  try
    fRun.fCacheLocker.Enter;
    with fRun.fCache[aMethodIndex] do begin
      inc(fCacheCurrentSec,TimeOutSeconds);
      case fCacheCurrent of
      rootCache:
        if fOutput.Status=HTTP_SUCCESS then begin
          RootValue := fOutput.Header+#0+fOutput.Content;
          RootValueExpirationTime := fCacheCurrentSec;
        end else
          RootValue := '';
      inputCache:
        if fOutput.Status=HTTP_SUCCESS then
          InputValues.Add(fCacheCurrentInputValueKey,fOutput.Header+#0+fOutput.Content,fCacheCurrentSec) else
          InputValues.Add(fCacheCurrentInputValueKey,'');
      end;
    end;
  finally
    fRun.fCacheLocker.Leave;
  end;

Changes to SQLite3/mORMotWrappers.pas.

1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
....
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
       [result,templateTitle,savedName,templateExt,uri,uri,uri]);
    until FindNext(SR)<>0;
    FindClose(SR);
    result := FormatUTF8('%</ul><p>You can also retrieve the corresponding '+
      '<a href=/%/wrapper/context>template context</a>.<hr><p>Generated by a '+
      '<a href=http://mormot.net>Synopse <i>mORMot</i> '+SYNOPSE_FRAMEWORK_VERSION+
      '</a> server.',[result,root]);
    Ctxt.Returns(result,HTML_SUCCESS,HTML_CONTENT_TYPE_HEADER);
    exit;
  end else
    FindClose(SR);
  Split(Ctxt.URIBlobFieldName,'/',templateName,unitName);
  Split(unitName,'.',unitName,templateExt);
  if PosEx('.',templateExt)>0 then begin // see as text
    if IdemPropNameU(Split(templateExt,'.',templateExt),'mustache') then
................................................................................
    head := TEXT_CONTENT_TYPE_HEADER;
  end else // download as file
    head := HEADER_CONTENT_TYPE+'application/'+LowerCase(templateExt);
  templateName := templateName+'.'+templateExt+'.mustache';
  template := AnyTextFileToRawUTF8(
    Path[templateFound]+PathDelim+UTF8ToString(templateName),true);
  if template='' then begin
    Ctxt.Error(templateName,HTML_NOTFOUND);
    exit;
  end;
  if unitName='' then
    result := template else begin
    context.templateName := templateName;
    context.filename := unitName;
    result := TSynMustache.Parse(template).Render(context,nil,
      TSynMustache.HelpersGetStandardList,nil,true);
  end;
  Ctxt.Returns(result,HTML_SUCCESS,head);
end;

function WrapperFromModel(aServer: TSQLRestServer; const aMustacheTemplate,
  aFileName: RawUTF8; aPort: integer; aHelpers: TSynMustacheHelpers;
  aContext: PVariant): RawUTF8;
var context: variant;
begin






|







 







|









|







1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
....
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
       [result,templateTitle,savedName,templateExt,uri,uri,uri]);
    until FindNext(SR)<>0;
    FindClose(SR);
    result := FormatUTF8('%</ul><p>You can also retrieve the corresponding '+
      '<a href=/%/wrapper/context>template context</a>.<hr><p>Generated by a '+
      '<a href=http://mormot.net>Synopse <i>mORMot</i> '+SYNOPSE_FRAMEWORK_VERSION+
      '</a> server.',[result,root]);
    Ctxt.Returns(result,HTTP_SUCCESS,HTML_CONTENT_TYPE_HEADER);
    exit;
  end else
    FindClose(SR);
  Split(Ctxt.URIBlobFieldName,'/',templateName,unitName);
  Split(unitName,'.',unitName,templateExt);
  if PosEx('.',templateExt)>0 then begin // see as text
    if IdemPropNameU(Split(templateExt,'.',templateExt),'mustache') then
................................................................................
    head := TEXT_CONTENT_TYPE_HEADER;
  end else // download as file
    head := HEADER_CONTENT_TYPE+'application/'+LowerCase(templateExt);
  templateName := templateName+'.'+templateExt+'.mustache';
  template := AnyTextFileToRawUTF8(
    Path[templateFound]+PathDelim+UTF8ToString(templateName),true);
  if template='' then begin
    Ctxt.Error(templateName,HTTP_NOTFOUND);
    exit;
  end;
  if unitName='' then
    result := template else begin
    context.templateName := templateName;
    context.filename := unitName;
    result := TSynMustache.Parse(template).Render(context,nil,
      TSynMustache.HelpersGetStandardList,nil,true);
  end;
  Ctxt.Returns(result,HTTP_SUCCESS,head);
end;

function WrapperFromModel(aServer: TSQLRestServer; const aMustacheTemplate,
  aFileName: RawUTF8; aPort: integer; aHelpers: TSynMustacheHelpers;
  aContext: PVariant): RawUTF8;
var context: variant;
begin

Changes to SynBidirSock.pas.

508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
    /// finalize the context
    // - will release the TWebSocketProtocol associated instance
    destructor Destroy; override;
    /// will push a request or notification to the other end of the connection
    // - caller should set the aRequest with the outgoing parameters, and
    // optionally receive a response from the other end
    // - the request may be sent in blocking or non blocking mode
    // - returns the HTML status code (HTML_SUCCESS=200 for success)
    function NotifyCallback(aRequest: THttpServerRequest;
      aMode: TWebSocketProcessNotifyCallback): cardinal; virtual;
    /// the settings currently used during the WebSockets process
    // - defined as a pointer so that you may be able to change the values
    function Settings: PWebSocketProcessSettings; {$ifdef HASINLINE}inline;{$endif}
    /// the associated communication socket
    // - on the server side, is a THttpServerSocket






|







508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
    /// finalize the context
    // - will release the TWebSocketProtocol associated instance
    destructor Destroy; override;
    /// will push a request or notification to the other end of the connection
    // - caller should set the aRequest with the outgoing parameters, and
    // optionally receive a response from the other end
    // - the request may be sent in blocking or non blocking mode
    // - returns the HTML status code (HTTP_SUCCESS=200 for success)
    function NotifyCallback(aRequest: THttpServerRequest;
      aMode: TWebSocketProcessNotifyCallback): cardinal; virtual;
    /// the settings currently used during the WebSockets process
    // - defined as a pointer so that you may be able to change the values
    function Settings: PWebSocketProcessSettings; {$ifdef HASINLINE}inline;{$endif}
    /// the associated communication socket
    // - on the server side, is a THttpServerSocket

Changes to SynSelfTests.pas.

7476
7477
7478
7479
7480
7481
7482
7483
7484
7485
7486
7487
7488
7489
7490
7491
7492
7493
....
7587
7588
7589
7590
7591
7592
7593
7594
7595
7596
7597
7598
7599
7600
7601
....
9289
9290
9291
9292
9293
9294
9295
9296
9297
9298
9299
9300
9301
9302
9303
....
9448
9449
9450
9451
9452
9453
9454
9455
9456
9457
9458
9459
9460
9461
9462
.....
10351
10352
10353
10354
10355
10356
10357
10358
10359
10360
10361
10362
10363
10364
10365
.....
10372
10373
10374
10375
10376
10377
10378
10379
10380
10381
10382
10383
10384
10385
10386
10387
10388
.....
10925
10926
10927
10928
10929
10930
10931
10932
10933
10934
10935
10936
10937
10938
10939
.....
10987
10988
10989
10990
10991
10992
10993
10994
10995
10996
10997
10998
10999
11000
11001
11002
11003
11004
11005
11006
11007
11008
11009
11010
11011
.....
11056
11057
11058
11059
11060
11061
11062
11063
11064
11065
11066
11067
11068
11069
11070
11071
11072
.....
11079
11080
11081
11082
11083
11084
11085
11086
11087
11088
11089
11090
11091
11092
11093
.....
11558
11559
11560
11561
11562
11563
11564
11565
11566
11567
11568
11569
11570
11571
11572
11573
11574
.....
13388
13389
13390
13391
13392
13393
13394
13395
13396
13397
13398
13399
13400
13401
13402
13403
13404
13405
.....
14414
14415
14416
14417
14418
14419
14420
14421
14422
14423
14424
14425
14426
14427
14428
.....
14492
14493
14494
14495
14496
14497
14498
14499
14500
14501
14502
14503
14504
14505
14506
          end;
          Client.Commit;
          Check(Client.BatchStart(TSQLRecordTest,1000));
          for i := 100 to 9999 do begin
            R.FillWith(i);
            Check(Client.BatchAdd(R,true,false,ALL_FIELDS)=i-100);
          end;
          Check(Client.BatchSend(IDs)=HTML_SUCCESS);
          Check(Length(IDs)=9900);
          Check(not FileExists('fullmem.data'));
          Check(Client.CallBackPut('Flush','',dummy)=HTML_SUCCESS);
          Check(FileExists('fullmem.data'));
          Check(Client.Retrieve(200,R));
          R.CheckWith(self,200);
        finally
          R.Free;
        end;
      finally
................................................................................
          R.CheckWith(self,110,10);
          Batch := TSQLRestBatch.Create(Server,TSQLRecordTest,30);
          try
            for i := 10000 to 10099 do begin
              R.FillWith(i);
              Check(Batch.Add(R,true,false,ALL_FIELDS)=i-10000);
            end;
            Check(Server.BatchSend(Batch,IDs)=HTML_SUCCESS);
          finally
            Batch.Free;
          end;
        finally
          R.Free;
        end;
        Check(Length(IDs)=100);
................................................................................
        TTestBidirectionalRemoteConnection(Test).fHttpServer.RemoveServer(Master);
      Master.Free; // test TSQLRestServer.InternalRecordVersionMaxFromExisting
      MasterAccess.Free;
      CreateMaster(false);
      MasterAccess.BatchStart(TSQLRecordPeopleVersioned,10000);
      while Rec.FillOne do // fast add via Batch
        Test.Check(MasterAccess.BatchAdd(Rec,true,true)>=0);
      Test.Check(MasterAccess.BatchSend(IDs)=HTML_SUCCESS);
      Test.Check(n=length(IDs)+10);
      Test.Check(Rec.FillRewind);
      for i := 0 to 9 do
        Test.Check(Rec.FillOne);
      for i := 0 to high(IDs) do
        if Rec.FillOne then
          Test.Check(IDs[i]=Rec.IDValue) else
................................................................................
      end;
      b := TSQLRestBatch.Create(db,TSQLRecordTest,SHARD_RANGE div 3,[boExtendedJSON]);
      try
        for i := 51 to SHARD_MAX do begin
          R.FillWith(i);
          Check(b.Add(R,true,false,ALL_FIELDS)=i-51);
        end;
        Check(db.BatchSend(b)=HTML_SUCCESS);
      finally
        b.Free;
      end;
    finally
      R.Free;
    end;
  finally
................................................................................
              Client.BatchStart(TSQLRecordPeople,5000);
              n := 0;
              while R.FillOne do begin
                R.YearOfBirth := n;
                Client.BatchAdd(R,true);
                inc(n);
              end;
              Check(Client.BatchSend(res)=HTML_SUCCESS);
              Check(length(res)=n);
              Check(length(res)=n);
              for i := 1 to 100 do begin
                R.ClearProperties;
                Check(Client.Retrieve(res[Random(n)],R));
                Check(R.ID<>0);
                Check(res[R.YearOfBirth]=R.ID);
................................................................................
            for i := 0 to high(ids) do begin
              Check(Client.Retrieve(ids[i],R));
              Check(R.YearOfBirth=i);
            end;
            for i := 0 to high(ids) do begin
              Client.BatchStart(TSQLRecordPeople);
              Client.BatchDelete(ids[i]);
              Check(Client.BatchSend(res)=HTML_SUCCESS);
              Check(length(res)=1);
              Check(res[0]=HTML_SUCCESS);
            end;
            for i := 0 to high(ids) do
              Check(not Client.Retrieve(ids[i],R));
            R.ClearProperties;
            for i := 0 to high(ids) do begin
              R.fID := ids[i];
              Check(Client.Update(R),'test locking');
................................................................................
            {$endif}
          end;
          inc(n);
        end;
        Check(aExternalClient.Retrieve(1,RInt1));
        Check(RInt1.fID=1);
        Check(n=fPeopleData.RowCount);
        Check(aExternalClient.BatchSend(BatchID)=HTML_SUCCESS);
        Check(length(BatchID)=n-99);
        Check(aExternalClient.TableHasRows(TSQLRecordPeopleExt));
        Check(aExternalClient.TableRowCount(TSQLRecordPeopleExt)=n);
        Check(aExternalClient.Server.TableHasRows(TSQLRecordPeopleExt));
        Check(aExternalClient.Server.TableRowCount(TSQLRecordPeopleExt)=n);
        Check(RInt.FillRewind);
        while RInt.FillOne do begin
................................................................................
              Check(RExt.CreatedAt<=Updated);
              Check(RExt.LastChange>=Updated);
              {$ifndef NOVARIANTS}
              Check(VariantDynArrayToJSON(RExt.Value)=FormatUTF8('["text",%]',[RExt.YearOfDeath]));
              {$endif}
            end;
          end;
        Check(aExternalClient.BatchSend(BatchIDUpdate)=HTML_SUCCESS);
        Check(length(BatchIDUpdate)=70);
        for i := 1 to BatchID[high(BatchID)] do
          if i and 127=0 then
          if i>4000 then begin
            if aExternalClient.BatchCount=0 then
              aExternalClient.BatchStart(TSQLRecordPeopleExt);
            Check(aExternalClient.BatchDelete(i)>=0,'BatchDelete 1/128 rows');
          end else
            Check(aExternalClient.Delete(TSQLRecordPeopleExt,i),'Delete 1/128 rows');
        Check(aExternalClient.BatchSend(BatchIDUpdate)=HTML_SUCCESS);
        Check(length(BatchIDUpdate)=55);
        n := aExternalClient.TableRowCount(TSQLRecordPeople);
        Check(aExternalClient.Server.TableRowCount(TSQLRecordPeopleExt)=10925);
        Check(aExternalClient.Server.StaticVirtualTable[TSQLRecordPeople]=nil);
        Check(aExternalClient.Server.StaticVirtualTable[TSQLRecordPeopleExt]<>nil);
        Check(aExternalClient.Server.StaticVirtualTable[TSQLRecordOnlyBlob]<>nil);
        {$ifdef WITHUNSAFEBACKUP}
................................................................................
        for i := 0 to high(ids) do begin
          Check(aExternalClient.Retrieve(ids[i],RExt));
          Check(RExt.YearOfBirth=i);
        end;
        for i := 0 to high(ids) do begin
          aExternalClient.BatchStart(TSQLRecordPeopleExt);
          aExternalClient.BatchDelete(ids[i]);
          Check(aExternalClient.BatchSend(BatchID)=HTML_SUCCESS);
          Check(length(BatchID)=1);
          Check(BatchID[0]=HTML_SUCCESS);
        end;
        for i := 0 to high(ids) do
          Check(not aExternalClient.Retrieve(ids[i],RExt));
        RExt.ClearProperties;
        for i := 0 to high(ids) do begin
          RExt.fID := ids[i];
          Check(aExternalClient.Update(RExt),'test locking');
................................................................................
        aExternalClient.BatchStart(TSQLRecordTestJoin,1000);
        for i := 1 to BLOB_MAX do
        if i and 127<>0 then begin
          RJoin.Name := Int32ToUTF8(i);
          RJoin.People := TSQLRecordPeopleExt(i);
          aExternalClient.BatchAdd(RJoin,true);
        end;
        Check(aExternalClient.BatchSend(BatchIDJoined)=HTML_SUCCESS);
        Check(length(BatchIDJoined)=993);
        RJoin.FillPrepare(aExternalClient);
        Check(RJoin.FillTable.RowCount=993);
        i := 1;
        while RJoin.FillOne do begin
          if i and 127=0 then
            inc(i); // deleted item
................................................................................
    for i := 0 to high(ids) do begin
      Check(ClientDist.Retrieve(ids[i],V2));
      Check(V2.YearOfBirth=i);
    end;
    for i := 0 to high(ids) do begin
      ClientDist.BatchStart(TSQLRecordPeople);
      ClientDist.BatchDelete(ids[i]);
      Check(ClientDist.BatchSend(res)=HTML_SUCCESS);
      Check(length(res)=1);
      Check(res[0]=HTML_SUCCESS);
    end;
    for i := 0 to high(ids) do
      Check(not ClientDist.Retrieve(ids[i],V2));
    V2.ClearProperties;
    for i := 0 to high(ids) do begin
      V2.fID := ids[i];
      Check(ClientDist.Update(V2),'test locking');
................................................................................
         CheckFailed(HTTPClient.Services.Info(TypeInfo(ITestPerThread)).Get(Inst.CT)) then
        exit;
      Inst.ExpectedSessionID := HTTPClient.SessionID;
      HTTPClient.Retrieve('LogonName=?',[],[HTTPClient.SessionUser.LogonName],HTTPClient.SessionUser);
      Inst.ExpectedUserID := HTTPClient.SessionUser.ID;
      Inst.ExpectedGroupID := HTTPClient.SessionUser.GroupRights.ID;
      //SetOptions(false{$ifndef LVCL},true,[optExecInMainThread]{$endif});
      Check(HTTPClient.CallBackGet('stat',['findservice','toto'],json)=HTML_SUCCESS);
      Check(json='[]');
      for i := 0 to High(SERVICES) do begin
        Check(HTTPClient.CallBackGet('stat',['findservice',SERVICES[i]],json)=HTML_SUCCESS);
        Check(json<>'[]');
        Check(HTTPClient.ServiceRetrieveAssociated(SERVICES[i],URI));
        Check(length(URI)=1);
        Check(URI[0].Port=HTTP_DEFAULTPORT);
        Check(URI[0].Root=fClient.Model.Root);
      end;
      Check(HTTPClient.ServiceRetrieveAssociated(IComplexNumber,URI));
................................................................................
  result := _ObjFast(['a',a,'b',b,'c',c]);
end;

function TBidirServer.TestRestCustom(a: integer): TServiceCustomAnswer;
begin
  result.Header := BINARY_CONTENT_TYPE_HEADER;
  result.Content := Int32ToUtf8(a)+#0#1;
  result.Status := HTML_SUCCESS;
end;

function TBidirServer.TestCallback(d: Integer; const callback: IBidirCallback): boolean;
begin
  fCallback := callback;
  result := d<>0;
end;
................................................................................
        continue;
      check(v.a=a);
      check(v.b=b);
      check(v.c=c);
    end;
  for a := -10 to 10 do begin
    res := I.TestRestCustom(a);
    check(res.Status=HTML_SUCCESS);
    check(GetInteger(pointer(res.Content))=a);
    check(res.Content[Length(res.Content)]=#1);
  end;
end;

procedure TTestBidirectionalRemoteConnection.TestCallback(Rest: TSQLRest);
var I: IBidirService;






|


|







 







|







 







|







 







|







 







|







 







|

|







 







|







 







|









|







 







|

|







 







|







 







|

|







 







|


|







 







|







 







|







7476
7477
7478
7479
7480
7481
7482
7483
7484
7485
7486
7487
7488
7489
7490
7491
7492
7493
....
7587
7588
7589
7590
7591
7592
7593
7594
7595
7596
7597
7598
7599
7600
7601
....
9289
9290
9291
9292
9293
9294
9295
9296
9297
9298
9299
9300
9301
9302
9303
....
9448
9449
9450
9451
9452
9453
9454
9455
9456
9457
9458
9459
9460
9461
9462
.....
10351
10352
10353
10354
10355
10356
10357
10358
10359
10360
10361
10362
10363
10364
10365
.....
10372
10373
10374
10375
10376
10377
10378
10379
10380
10381
10382
10383
10384
10385
10386
10387
10388
.....
10925
10926
10927
10928
10929
10930
10931
10932
10933
10934
10935
10936
10937
10938
10939
.....
10987
10988
10989
10990
10991
10992
10993
10994
10995
10996
10997
10998
10999
11000
11001
11002
11003
11004
11005
11006
11007
11008
11009
11010
11011
.....
11056
11057
11058
11059
11060
11061
11062
11063
11064
11065
11066
11067
11068
11069
11070
11071
11072
.....
11079
11080
11081
11082
11083
11084
11085
11086
11087
11088
11089
11090
11091
11092
11093
.....
11558
11559
11560
11561
11562
11563
11564
11565
11566
11567
11568
11569
11570
11571
11572
11573
11574
.....
13388
13389
13390
13391
13392
13393
13394
13395
13396
13397
13398
13399
13400
13401
13402
13403
13404
13405
.....
14414
14415
14416
14417
14418
14419
14420
14421
14422
14423
14424
14425
14426
14427
14428
.....
14492
14493
14494
14495
14496
14497
14498
14499
14500
14501
14502
14503
14504
14505
14506
          end;
          Client.Commit;
          Check(Client.BatchStart(TSQLRecordTest,1000));
          for i := 100 to 9999 do begin
            R.FillWith(i);
            Check(Client.BatchAdd(R,true,false,ALL_FIELDS)=i-100);
          end;
          Check(Client.BatchSend(IDs)=HTTP_SUCCESS);
          Check(Length(IDs)=9900);
          Check(not FileExists('fullmem.data'));
          Check(Client.CallBackPut('Flush','',dummy)=HTTP_SUCCESS);
          Check(FileExists('fullmem.data'));
          Check(Client.Retrieve(200,R));
          R.CheckWith(self,200);
        finally
          R.Free;
        end;
      finally
................................................................................
          R.CheckWith(self,110,10);
          Batch := TSQLRestBatch.Create(Server,TSQLRecordTest,30);
          try
            for i := 10000 to 10099 do begin
              R.FillWith(i);
              Check(Batch.Add(R,true,false,ALL_FIELDS)=i-10000);
            end;
            Check(Server.BatchSend(Batch,IDs)=HTTP_SUCCESS);
          finally
            Batch.Free;
          end;
        finally
          R.Free;
        end;
        Check(Length(IDs)=100);
................................................................................
        TTestBidirectionalRemoteConnection(Test).fHttpServer.RemoveServer(Master);
      Master.Free; // test TSQLRestServer.InternalRecordVersionMaxFromExisting
      MasterAccess.Free;
      CreateMaster(false);
      MasterAccess.BatchStart(TSQLRecordPeopleVersioned,10000);
      while Rec.FillOne do // fast add via Batch
        Test.Check(MasterAccess.BatchAdd(Rec,true,true)>=0);
      Test.Check(MasterAccess.BatchSend(IDs)=HTTP_SUCCESS);
      Test.Check(n=length(IDs)+10);
      Test.Check(Rec.FillRewind);
      for i := 0 to 9 do
        Test.Check(Rec.FillOne);
      for i := 0 to high(IDs) do
        if Rec.FillOne then
          Test.Check(IDs[i]=Rec.IDValue) else
................................................................................
      end;
      b := TSQLRestBatch.Create(db,TSQLRecordTest,SHARD_RANGE div 3,[boExtendedJSON]);
      try
        for i := 51 to SHARD_MAX do begin
          R.FillWith(i);
          Check(b.Add(R,true,false,ALL_FIELDS)=i-51);
        end;
        Check(db.BatchSend(b)=HTTP_SUCCESS);
      finally
        b.Free;
      end;
    finally
      R.Free;
    end;
  finally
................................................................................
              Client.BatchStart(TSQLRecordPeople,5000);
              n := 0;
              while R.FillOne do begin
                R.YearOfBirth := n;
                Client.BatchAdd(R,true);
                inc(n);
              end;
              Check(Client.BatchSend(res)=HTTP_SUCCESS);
              Check(length(res)=n);
              Check(length(res)=n);
              for i := 1 to 100 do begin
                R.ClearProperties;
                Check(Client.Retrieve(res[Random(n)],R));
                Check(R.ID<>0);
                Check(res[R.YearOfBirth]=R.ID);
................................................................................
            for i := 0 to high(ids) do begin
              Check(Client.Retrieve(ids[i],R));
              Check(R.YearOfBirth=i);
            end;
            for i := 0 to high(ids) do begin
              Client.BatchStart(TSQLRecordPeople);
              Client.BatchDelete(ids[i]);
              Check(Client.BatchSend(res)=HTTP_SUCCESS);
              Check(length(res)=1);
              Check(res[0]=HTTP_SUCCESS);
            end;
            for i := 0 to high(ids) do
              Check(not Client.Retrieve(ids[i],R));
            R.ClearProperties;
            for i := 0 to high(ids) do begin
              R.fID := ids[i];
              Check(Client.Update(R),'test locking');
................................................................................
            {$endif}
          end;
          inc(n);
        end;
        Check(aExternalClient.Retrieve(1,RInt1));
        Check(RInt1.fID=1);
        Check(n=fPeopleData.RowCount);
        Check(aExternalClient.BatchSend(BatchID)=HTTP_SUCCESS);
        Check(length(BatchID)=n-99);
        Check(aExternalClient.TableHasRows(TSQLRecordPeopleExt));
        Check(aExternalClient.TableRowCount(TSQLRecordPeopleExt)=n);
        Check(aExternalClient.Server.TableHasRows(TSQLRecordPeopleExt));
        Check(aExternalClient.Server.TableRowCount(TSQLRecordPeopleExt)=n);
        Check(RInt.FillRewind);
        while RInt.FillOne do begin
................................................................................
              Check(RExt.CreatedAt<=Updated);
              Check(RExt.LastChange>=Updated);
              {$ifndef NOVARIANTS}
              Check(VariantDynArrayToJSON(RExt.Value)=FormatUTF8('["text",%]',[RExt.YearOfDeath]));
              {$endif}
            end;
          end;
        Check(aExternalClient.BatchSend(BatchIDUpdate)=HTTP_SUCCESS);
        Check(length(BatchIDUpdate)=70);
        for i := 1 to BatchID[high(BatchID)] do
          if i and 127=0 then
          if i>4000 then begin
            if aExternalClient.BatchCount=0 then
              aExternalClient.BatchStart(TSQLRecordPeopleExt);
            Check(aExternalClient.BatchDelete(i)>=0,'BatchDelete 1/128 rows');
          end else
            Check(aExternalClient.Delete(TSQLRecordPeopleExt,i),'Delete 1/128 rows');
        Check(aExternalClient.BatchSend(BatchIDUpdate)=HTTP_SUCCESS);
        Check(length(BatchIDUpdate)=55);
        n := aExternalClient.TableRowCount(TSQLRecordPeople);
        Check(aExternalClient.Server.TableRowCount(TSQLRecordPeopleExt)=10925);
        Check(aExternalClient.Server.StaticVirtualTable[TSQLRecordPeople]=nil);
        Check(aExternalClient.Server.StaticVirtualTable[TSQLRecordPeopleExt]<>nil);
        Check(aExternalClient.Server.StaticVirtualTable[TSQLRecordOnlyBlob]<>nil);
        {$ifdef WITHUNSAFEBACKUP}
................................................................................
        for i := 0 to high(ids) do begin
          Check(aExternalClient.Retrieve(ids[i],RExt));
          Check(RExt.YearOfBirth=i);
        end;
        for i := 0 to high(ids) do begin
          aExternalClient.BatchStart(TSQLRecordPeopleExt);
          aExternalClient.BatchDelete(ids[i]);
          Check(aExternalClient.BatchSend(BatchID)=HTTP_SUCCESS);
          Check(length(BatchID)=1);
          Check(BatchID[0]=HTTP_SUCCESS);
        end;
        for i := 0 to high(ids) do
          Check(not aExternalClient.Retrieve(ids[i],RExt));
        RExt.ClearProperties;
        for i := 0 to high(ids) do begin
          RExt.fID := ids[i];
          Check(aExternalClient.Update(RExt),'test locking');
................................................................................
        aExternalClient.BatchStart(TSQLRecordTestJoin,1000);
        for i := 1 to BLOB_MAX do
        if i and 127<>0 then begin
          RJoin.Name := Int32ToUTF8(i);
          RJoin.People := TSQLRecordPeopleExt(i);
          aExternalClient.BatchAdd(RJoin,true);
        end;
        Check(aExternalClient.BatchSend(BatchIDJoined)=HTTP_SUCCESS);
        Check(length(BatchIDJoined)=993);
        RJoin.FillPrepare(aExternalClient);
        Check(RJoin.FillTable.RowCount=993);
        i := 1;
        while RJoin.FillOne do begin
          if i and 127=0 then
            inc(i); // deleted item
................................................................................
    for i := 0 to high(ids) do begin
      Check(ClientDist.Retrieve(ids[i],V2));
      Check(V2.YearOfBirth=i);
    end;
    for i := 0 to high(ids) do begin
      ClientDist.BatchStart(TSQLRecordPeople);
      ClientDist.BatchDelete(ids[i]);
      Check(ClientDist.BatchSend(res)=HTTP_SUCCESS);
      Check(length(res)=1);
      Check(res[0]=HTTP_SUCCESS);
    end;
    for i := 0 to high(ids) do
      Check(not ClientDist.Retrieve(ids[i],V2));
    V2.ClearProperties;
    for i := 0 to high(ids) do begin
      V2.fID := ids[i];
      Check(ClientDist.Update(V2),'test locking');
................................................................................
         CheckFailed(HTTPClient.Services.Info(TypeInfo(ITestPerThread)).Get(Inst.CT)) then
        exit;
      Inst.ExpectedSessionID := HTTPClient.SessionID;
      HTTPClient.Retrieve('LogonName=?',[],[HTTPClient.SessionUser.LogonName],HTTPClient.SessionUser);
      Inst.ExpectedUserID := HTTPClient.SessionUser.ID;
      Inst.ExpectedGroupID := HTTPClient.SessionUser.GroupRights.ID;
      //SetOptions(false{$ifndef LVCL},true,[optExecInMainThread]{$endif});
      Check(HTTPClient.CallBackGet('stat',['findservice','toto'],json)=HTTP_SUCCESS);
      Check(json='[]');
      for i := 0 to High(SERVICES) do begin
        Check(HTTPClient.CallBackGet('stat',['findservice',SERVICES[i]],json)=HTTP_SUCCESS);
        Check(json<>'[]');
        Check(HTTPClient.ServiceRetrieveAssociated(SERVICES[i],URI));
        Check(length(URI)=1);
        Check(URI[0].Port=HTTP_DEFAULTPORT);
        Check(URI[0].Root=fClient.Model.Root);
      end;
      Check(HTTPClient.ServiceRetrieveAssociated(IComplexNumber,URI));
................................................................................
  result := _ObjFast(['a',a,'b',b,'c',c]);
end;

function TBidirServer.TestRestCustom(a: integer): TServiceCustomAnswer;
begin
  result.Header := BINARY_CONTENT_TYPE_HEADER;
  result.Content := Int32ToUtf8(a)+#0#1;
  result.Status := HTTP_SUCCESS;
end;

function TBidirServer.TestCallback(d: Integer; const callback: IBidirCallback): boolean;
begin
  fCallback := callback;
  result := d<>0;
end;
................................................................................
        continue;
      check(v.a=a);
      check(v.b=b);
      check(v.c=c);
    end;
  for a := -10 to 10 do begin
    res := I.TestRestCustom(a);
    check(res.Status=HTTP_SUCCESS);
    check(GetInteger(pointer(res.Content))=a);
    check(res.Content[Length(res.Content)]=#1);
  end;
end;

procedure TTestBidirectionalRemoteConnection.TestCallback(Rest: TSQLRest);
var I: IBidirService;

Changes to SynopseCommit.inc.

1
'1.18.2803'
|
1
'1.18.2804'