Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
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: |
6064a072aad7e71b10ba72967c057f1b |
User & Date: | ab 2016-07-27 14:48:22 |
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 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><b><i>mORMot</i> Web Test</b></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><b><i>mORMot</i> Web Test</b></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('.\www\index.html','.\www\debug\index.html',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://*/* https://*/* tel:* sms:* mailto:* geo:*</AllowIntent> </Cordova> </VendorSpecific> <Options> <PostBuild> <Enabled>0</Enabled> <Script>//CopyFile('.\www\index.html','.\www\debug\index.html',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'
|