{ This file is part of the Free Pascal run time library. Copyright (c) 2019 by Michael Van Canneyt, member of the Free Pascal development team Simple SQLDBRESTBridge JSON dataset component and connection. See the file COPYING.FPC, included in this distribution, for details about the copyright. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. **********************************************************************} unit sqldbrestdataset; {$mode objfpc} interface uses Classes, SysUtils, JS, web, db, JSONDataset, restconnection; Type { TSQLDBRestConnection } TSQLDBRestConnection = Class(TRestConnection) private FConnectionsResourceName: String; FCustomViewResourceName: String; FDataProperty: String; FmetaDataProperty: String; FMetaDataResourceName: String; FonGetResources: TNotifyEvent; FPassword: String; FResourceList: TStrings; FUserName: String; procedure DoResources(Sender: TObject); function DoStoreDataProp: Boolean; function DoStoreMetadata: Boolean; function DoStoreMetadataProp: Boolean; Protected Procedure SetupRequest(aXHR : TJSXMLHttpRequest); override; Function GetUpdateBaseURL(aRequest: TRecordUpdateDescriptor): String; override; Function GetReadBaseURL(aRequest: TDataRequest): String; Override; Public Constructor create(aOwner : TComponent); override; Destructor Destroy; override; Procedure GetResources(OnResult : TNotifyEvent = Nil); Property ResourceList : TStrings Read FResourceList; Published Property OnGetResources : TNotifyEvent Read FonGetResources Write FOnGetResources; Property metaDataProperty : String read FmetaDataProperty Write FmetaDataProperty Stored DoStoreMetadataProp; Property DataProperty : String read FDataProperty Write FDataProperty Stored DoStoreDataProp; Property MetaDataResourceName : String Read FMetaDataResourceName Write FMetaDataResourceName Stored DoStoreMetadata; Property UserName : String Read FUserName Write FUserName; Property Password : String Read FPassword Write FPassword; Property ConnectionsResourceName : String Read FConnectionsResourceName Write FConnectionsResourceName; Property CustomViewResourceName : String Read FCustomViewResourceName Write FCustomViewResourceName; // Published from TRESTCOnnection Property BaseURL; Property PageParam; Property OnGetURL; Property OnUpdateURL; end; { TSQLDBRestDataset } { TQueryParam } TQueryParam = class(TParam) private FEnabled: Boolean; Public Procedure Assign(Source : TPersistent); override; function AsQuery : String; Published Property Enabled : Boolean Read FEnabled Write FEnabled; end; { TQueryParams } TQueryParams = Class(TParams) private function GetP(aIndex : Integer): TQueryParam; procedure SetP(aIndex : Integer; AValue: TQueryParam); Public Property Params[aIndex : Integer] : TQueryParam Read GetP Write SetP; default; end; TGetQueryParamsEvent = Procedure (Sender : TDataset; IsReadURL : Boolean; var QueryString : String) of object; TSQLDBRestDataset = Class(TJSONDataset) private FConnection: TSQLDBRestConnection; FDatabaseConnection: String; FOnGetQueryParams: TGetQueryParamsEvent; FParams: TQueryParams; FResourceName: String; FSQL: TStrings; function CleanSQL: String; function CustomViewResourceName: String; procedure DoSQLChange(Sender: TObject); procedure SetConnection(AValue: TSQLDBRestConnection); procedure SetParams(AValue: TQueryParams); procedure SetResourceName(AValue: String); procedure SetSQL(AValue: TStrings); Protected function MyURL(isRead : Boolean): String; virtual; Function CreateQueryParams : TQueryParams; virtual; function GetURLQueryParams(IsRead : Boolean): string; virtual; function DataPacketReceived(ARequest: TDataRequest): Boolean; override; function GetStringFieldLength(F: TJSObject; AName: String; AIndex: Integer): integer;virtual; function StringToFieldType(S: String): TFieldType; virtual; Function DoGetDataProxy: TDataProxy; override; Procedure MetaDataToFieldDefs; override; Public Constructor Create(aOwner : TComponent); override; Destructor Destroy; override; Class Function DefaultBlobDataToBytes(aValue : JSValue) : TBytes; override; Function ParamByName(const aName : String) : TQueryParam; Published Property Connection: TSQLDBRestConnection Read FConnection Write SetConnection; Property ResourceName : String Read FResourceName Write SetResourceName; Property SQL : TStrings Read FSQL Write SetSQL; property DatabaseConnection : String Read FDatabaseConnection Write FDatabaseConnection; Property Params : TQueryParams Read FParams Write SetParams; Property OnGetQueryParams : TGetQueryParamsEvent Read FOnGetQueryParams Write FOnGetQueryParams; end; implementation Type { TServiceRequest } TServiceRequest = Class(TObject) Private FOnMyDone, FOnDone : TNotifyEvent; FXHR: TJSXMLHttpRequest; function GetResult: String; function GetResultJSON: TJSObject; function GetStatusCode: Integer; function onLoad(Event{%H-}: TEventListenerEvent): boolean; Public Constructor Create(Const aMethod,aURL,aUserName,aPassword : String; aOnDone1 : TNotifyEvent; aOnDone2 : TNotifyEvent = Nil); Procedure Execute; Property RequestResult : String read GetResult; Property ResultJSON : TJSObject read GetResultJSON; Property OnDone : TNotifyEvent Read FOnDone; Property StatusCode : Integer Read GetStatusCode; end; { TQueryParam } procedure TQueryParam.Assign(Source: TPersistent); Var P : TQueryParam absolute Source; begin if Source is TQueryParam then begin FEnabled:=P.Enabled; end; inherited Assign(Source); end; function TQueryParam.AsQuery: String; begin Result:=''; if Enabled then Result:=Name+'='+encodeURIComponent(AsString); end; { TQueryParams } function TQueryParams.GetP(aIndex : Integer): TQueryParam; begin Result:=Items[aIndex] as TQueryParam end; procedure TQueryParams.SetP(aIndex : Integer; AValue: TQueryParam); begin Items[aIndex]:=aValue; end; { TServiceRequest } constructor TServiceRequest.Create(const aMethod,aURL, aUserName, aPassword: String; aOnDone1 : TNotifyEvent; aOnDone2 : TNotifyEvent = Nil); begin FOnMyDone:=aOnDone1; FOnDone:=aOnDone2; FXHR:=TJSXMLHttpRequest.New; FXHR.AddEventListener('load',@onLoad); FXHR.open(aMethod,aURL,true); (* else begin // FXHR.withCredentials := true; FXHR.open(aMethod,aURL,true,aUserName,aPassword); end;*) FXHR.setRequestHeader('Content-Type', 'application/json'); FXHR.setRequestHeader('Authorization', 'Basic '+window.btoa(aUserName+':'+aPassword)); end; procedure TServiceRequest.Execute; begin FXHR.send; end; function TServiceRequest.GetResult: String; begin Result:=FXHR.responseText; end; function TServiceRequest.GetResultJSON: TJSObject; begin if SameText(FXHR.getResponseHeader('Content-Type'),'application/json') then Result:=TJSJSON.parseObject(RequestResult) else Result:=nil; end; function TServiceRequest.GetStatusCode: Integer; begin Result:=FXHR.Status; end; function TServiceRequest.onLoad(Event: TEventListenerEvent): boolean; begin if Assigned(FOnMyDone) then FOnMyDone(Self); Result:=False; end; { TSQLDBRestConnection } function TSQLDBRestConnection.DoStoreMetadata: Boolean; begin Result:=(FMetadataResourceName<>'metadata'); end; function TSQLDBRestConnection.DoStoreMetadataProp: Boolean; begin Result:=(FMetaDataProperty<>'metaData'); end; procedure TSQLDBRestConnection.SetupRequest(aXHR: TJSXMLHttpRequest); begin aXHR.setRequestHeader('Content-Type', 'application/json'); aXHR.setRequestHeader('Accept', 'application/json'); if (UserName<>'') then aXHR.setRequestHeader('Authorization', 'Basic '+window.btoa(UserName+':'+Password)); // Will call the OnSetupHTTPRequest handler inherited SetupRequest(aXHR); end; function TSQLDBRestConnection.GetUpdateBaseURL(aRequest: TRecordUpdateDescriptor): String; Var DS : TSQLDBRestDataset; begin Result:=inherited GetUpdateBaseURL(aRequest); DS:=TSQLDBRestDataset(aRequest.Dataset); Result:=IncludeTrailingPathDelimiter(Result)+DS.MyURL(False); end; function TSQLDBRestConnection.GetReadBaseURL(aRequest: TDataRequest): String; Var DS : TSQLDBRestDataset; begin Result:=inherited GetReadBaseURL(aRequest); DS:=TSQLDBRestDataset(aRequest.Dataset); Result:=IncludeTrailingPathDelimiter(Result)+DS.MyURL(True); end; procedure TSQLDBRestConnection.DoResources(Sender: TObject); Var R : TServiceRequest absolute Sender; J,Res : TJSObject; A : TJSArray; i : Integer; begin FResourceList.Clear; if (R.StatusCode=200) then begin J:=R.ResultJSON; if J=Nil then exit; A:=TJSArray(J.Properties['data']); For I:=0 to A.Length-1 do begin Res:=TJSObject(A[i]); FResourceList.Add(String(Res.Properties['name'])); end; end; If Assigned(R.OnDone) then R.OnDone(Self); If Assigned(OnGetResources) then OnGetResources(Self); end; function TSQLDBRestConnection.DoStoreDataProp: Boolean; begin Result:=(FDataProperty<>'data'); end; constructor TSQLDBRestConnection.create(aOwner: TComponent); begin inherited create(aOwner); FResourceList:=TStringList.Create; FMetaDataResourceName:='metadata'; FmetaDataProperty:='metaData'; FDataProperty:='data'; TStringList(FResourceList).Sorted:=true; end; destructor TSQLDBRestConnection.Destroy; begin FreeAndNil(FResourceList); inherited Destroy; end; procedure TSQLDBRestConnection.GetResources(OnResult: TNotifyEvent); Var aURL : String; R : TServiceRequest; begin aURL:=IncludeTrailingPathDelimiter(BaseURL)+MetaDataResourceName+'?fmt=json'; R:=TServiceRequest.Create('GET',aURL,Self.UserName,Self.Password,@DoResources,OnResult); R.Execute; end; { TSQLDBRestDataset } procedure TSQLDBRestDataset.SetConnection(AValue: TSQLDBRestConnection); begin if FConnection=AValue then Exit; if Assigned(FConnection) then FConnection.RemoveFreeNotification(Self); FConnection:=AValue; if Assigned(FConnection) then FConnection.FreeNotification(Self); end; procedure TSQLDBRestDataset.SetParams(AValue: TQueryParams); begin if FParams=AValue then Exit; FParams.Assign(AValue); end; function TSQLDBRestDataset.GetURLQueryParams(IsRead :Boolean) : string; Procedure AddToResult(aQuery : string); begin if aQuery='' then exit; If Result<>'' then Result:=Result+'&'; Result:=Result+aQuery; end; Var I : Integer; begin Result:=''; if IsRead then begin if SameText(ResourceName,CustomViewResourceName) then AddToResult('SQL'+EncodeURIComponent(CleanSQL)); For I:=0 to Params.Count-1 do AddToResult(Params[I].AsQuery); end; if Assigned(FOnGetQueryParams) then FOnGetQueryParams(Self,IsRead,Result); end; function TSQLDBRestDataset.MyURL(isRead: Boolean): String; Var Qry : String; begin Result:=DatabaseConnection; if (Result<>'') and (Result[Length(Result)]<>'/') then Result:=Result+'/'; Result:=Result+ResourceName; Qry:=GetURLQueryParams(IsRead); if Qry<>'' then Result:=Result+'?'+Qry; end; procedure TSQLDBRestDataset.DoSQLChange(Sender: TObject); begin if Trim(FSQL.Text)<>'' then FResourceName:=CustomViewResourceName; end; procedure TSQLDBRestDataset.SetResourceName(AValue: String); begin if FResourceName=AValue then Exit; CheckInactive; if Not SameText(aValue,CustomViewResourceName) then FSQL.Clear; FResourceName:=AValue; end; function TSQLDBRestDataset.CustomViewResourceName : String; begin if Assigned(Connection) then Result:=Connection.CustomViewResourceName else Result:='customView'; end; function TSQLDBRestDataset.CleanSQL: String; begin Result:=StringReplace(SQL.Text,#13#10,' ',[rfReplaceAll]); Result:=StringReplace(Result,#10,' ',[rfReplaceAll]); Result:=StringReplace(Result,#10,' ',[rfReplaceAll]); end; procedure TSQLDBRestDataset.SetSQL(AValue: TStrings); begin if FSQL=AValue then Exit; FSQL.Assign(AValue); end; function TSQLDBRestDataset.CreateQueryParams: TQueryParams; begin Result:=TQueryParams.Create(Self,TQueryParam); end; class function TSQLDBRestDataset.DefaultBlobDataToBytes(aValue: JSValue): TBytes; begin Result:=BytesOf(Window.atob(String(aValue))); end; function TSQLDBRestDataset.ParamByName(const aName: String): TQueryParam; begin Result:=TQueryParam(Params.ParamByName(aName)); end; function TSQLDBRestDataset.DoGetDataProxy: TDataProxy; begin Result:=Connection.DataProxy; end; function TSQLDBRestDataset.StringToFieldType(S: String): TFieldType; begin if (s='int') then Result:=ftInteger else if (s='bigint') then Result:=ftLargeInt else if (s='float') then Result:=ftFloat else if (s='bool') then Result:=ftBoolean else if (s='date') then Result:=ftDate else if (s='datetime') then Result:=ftDateTime else if (s='time') then Result:=ftTime else if (s='blob') then Result:=ftBlob else if (s='string') then Result:=ftString else if MapUnknownToStringType then Result:=ftString else Raise EJSONDataset.CreateFmt('Unknown JSON data type : %s',[s]); end; function TSQLDBRestDataset.GetStringFieldLength(F: TJSObject; AName: String; AIndex: Integer): integer; Var I,L : Integer; D : JSValue; begin Result:=0; D:=F.Properties['maxLen']; if Not jsIsNan(toNumber(D)) then begin Result:=Trunc(toNumber(D)); if (Result<=0) then Raise EJSONDataset.CreateFmt('Invalid maximum length specifier for field %s',[AName]) end else begin For I:=0 to Rows.Length-1 do begin D:=FieldMapper.GetJSONDataForField(Aname,AIndex,Rows[i]); if isString(D) then begin l:=Length(String(D)); if L>Result then Result:=L; end; end; end; if (Result=0) then Result:=20; end; procedure TSQLDBRestDataset.MetaDataToFieldDefs; Var A : TJSArray; F : TJSObject; I,FS : Integer; N: String; ft: TFieldType; D : JSValue; begin FieldDefs.Clear; D:=Metadata.Properties['fields']; if Not IsArray(D) then Raise EJSONDataset.Create('Invalid metadata object'); A:=TJSArray(D); For I:=0 to A.Length-1 do begin If Not isObject(A[i]) then Raise EJSONDataset.CreateFmt('Field definition %d in metadata is not an object',[i]); F:=TJSObject(A[i]); D:=F.Properties['name']; If Not isString(D) then Raise EJSONDataset.CreateFmt('Field definition %d in has no or invalid name property',[i]); N:=String(D); D:=F.Properties['type']; If IsNull(D) or isUndefined(D) then ft:=ftstring else If Not isString(D) then begin Raise EJSONDataset.CreateFmt('Field definition %d in has invalid type property',[i]) end else begin ft:=StringToFieldType(String(D)); end; if (ft=ftString) then fs:=GetStringFieldLength(F,N,I) else fs:=0; FieldDefs.Add(N,ft,fs); end; end; constructor TSQLDBRestDataset.Create(aOwner: TComponent); begin inherited Create(aOwner); FSQL:=TStringList.Create; TStringList(FSQL).OnChange:=@DoSQLChange; FParams:=CreateQueryParams; end; destructor TSQLDBRestDataset.Destroy; begin FreeAndNil(FSQL); FreeAndnil(FParams); inherited Destroy; end; function TSQLDBRestDataset.DataPacketReceived(ARequest: TDataRequest): Boolean; Var O : TJSObject; A : TJSArray; smetadata,sroot : String; begin Result:=False; If isNull(aRequest.Data) then exit; If isString(aRequest.Data) then O:=TJSOBject(TJSJSON.Parse(String(aRequest.Data))) else if isObject(aRequest.Data) then O:=TJSOBject(aRequest.Data) else DatabaseError('Cannot handle data packet'); sRoot:=Connection.DataProperty; sMetaData:=Connection.metaDataProperty; if (sroot='') then sroot:='data'; if (smetadata='') then smetadata:='metaData'; { if (IDField='') then idField:='id';} if O.hasOwnProperty(sMetaData) and isObject(o[sMetaData]) then begin if not Active then // Load fields from metadata metaData:=TJSObject(o[SMetaData]); { if metaData.hasOwnProperty('idField') and isString(metaData['idField']) then IDField:=string(metaData['idField']);} end; if O.hasOwnProperty(sRoot) and isArray(o[sRoot]) then begin A:=TJSArray(o[sRoot]); Result:=A.Length>0; AddToRows(A); end; end; end.