434 lines
14 KiB
ObjectPascal
434 lines
14 KiB
ObjectPascal
/// fill a VCL TClientDataset from SynRestVCL data access
|
|
// - this unit is a part of the freeware Synopse framework,
|
|
// licensed under a MPL/GPL/LGPL tri-license; version 1.18
|
|
unit SynRestMidasVCL;
|
|
|
|
{
|
|
This file is part of Synopse framework.
|
|
|
|
Synopse framework. Copyright (C) 2022 Arnaud Bouchez
|
|
Synopse Informatique - https://synopse.info
|
|
|
|
*** BEGIN LICENSE BLOCK *****
|
|
Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
|
|
|
The contents of this file are subject to the Mozilla Public License Version
|
|
1.1 (the "License"); you may not use this file except in compliance with
|
|
the License. You may obtain a copy of the License at
|
|
http://www.mozilla.org/MPL
|
|
|
|
Software distributed under the License is distributed on an "AS IS" basis,
|
|
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
for the specific language governing rights and limitations under the License.
|
|
|
|
The Original Code is Synopse mORMot framework.
|
|
|
|
The Initial Developer of the Original Code is Arnaud Bouchez.
|
|
|
|
Portions created by the Initial Developer are Copyright (C) 2022
|
|
the Initial Developer. All Rights Reserved.
|
|
|
|
Contributor(s):
|
|
- Esteban Martin
|
|
- houdw2006
|
|
|
|
Alternatively, the contents of this file may be used under the terms of
|
|
either the GNU General Public License Version 2 or later (the "GPL"), or
|
|
the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
|
in which case the provisions of the GPL or the LGPL are applicable instead
|
|
of those above. If you wish to allow use of your version of this file only
|
|
under the terms of either the GPL or the LGPL, and not to allow others to
|
|
use your version of this file under the terms of the MPL, indicate your
|
|
decision by deleting the provisions above and replace them with the notice
|
|
and other provisions required by the GPL or the LGPL. If you do not delete
|
|
the provisions above, a recipient may use your version of this file under
|
|
the terms of any one of the MPL, the GPL or the LGPL.
|
|
|
|
***** END LICENSE BLOCK *****
|
|
|
|
Version 1.18
|
|
- first public release, corresponding to Synopse mORMot Framework 1.18,
|
|
which is an extraction from former SynRestVCL.pas unit (which is faster
|
|
but read/only)
|
|
- introducing TSynRestDataSet (under Delphi), which allows to apply updates:
|
|
will be used now for overloaded ToClientDataSet() functions result
|
|
- fixed Delphi XE2 compilation issue with SetCommandText declaration
|
|
- bug fix skipping first record
|
|
- fix memory leak adding TSynRestDataset.destroy (by houdw2006)
|
|
|
|
}
|
|
|
|
{$I Synopse.inc} // define HASINLINE CPU32 CPU64 OWNNORMTOUPPER
|
|
|
|
interface
|
|
|
|
uses
|
|
{$ifdef ISDELPHIXE2}System.SysUtils,{$else}SysUtils,{$endif}
|
|
Classes,
|
|
{$ifndef DELPHI5OROLDER}
|
|
Variants,
|
|
{$ifndef FPC}
|
|
MidasLib,
|
|
{$endif}
|
|
{$endif}
|
|
mORMot,
|
|
mORMotHttpClient,
|
|
SynCommons,
|
|
SynTable,
|
|
SynDB,
|
|
SynRestVCL,
|
|
DB,
|
|
{$ifdef FPC}
|
|
BufDataset
|
|
{$else}
|
|
Contnrs,
|
|
DBClient,
|
|
Provider,
|
|
SqlConst
|
|
{$endif};
|
|
|
|
|
|
{$ifdef FPC} { TODO: duplicated code from SynDBMidasVCL }
|
|
type
|
|
/// FPC's pure pascal in-memory buffer is used instead of TClientDataSet
|
|
TClientDataSet = TBufDataset;
|
|
|
|
/// wrapper functions will use FPC's pure pascal in-memory buffer
|
|
TSynRestDataSet = TBufDataset;
|
|
|
|
{$else FPC}
|
|
type
|
|
/// A TSynRestDataset, inherited from TCustomClientDataSet, which allows to apply updates on a TWinHTTP connection.
|
|
// The TSQLModel is required for getting column datatype and size and if the TSQLRecord has defined
|
|
// InternalDefineModel for validations they will be associated to a TField.OnValidate. Similary if the method
|
|
// ComputeBeforeWriteFields is overridden this will be used.
|
|
// - typical usage may be for instance:
|
|
// ! ds := TSynRestDataSet.Create(MainForm);
|
|
// ! ds.Dataset.SQLModel := CreateModel; // The SQLModel is required
|
|
// ! ds.CommandText := 'http://host:port/root/TableName?select=*&where=condition&sort=fieldname';
|
|
// ! ds1.Dataset := ds; // assigning the rest dataset to TDatasource that can be associated a TDBGrid for example.
|
|
// ! ds.Open;
|
|
// ! // ... use ds as usual, including modifications
|
|
// ! ds.ApplyUpdates(0);
|
|
// or using from a service returning a dataset:
|
|
// ! ds := TSynRestDataSet.Create(MainForm);
|
|
// ! ds.Dataset.SQLModel := CreateModel; // The SQLModel is required
|
|
// ! the TSQLRecord associated should be defined with the same structure of the returned array from the service
|
|
// ! ds.CommandText := 'http://host:port/root/ServiceName.Operation?paramname=:paramvalue';
|
|
// ! ds.Params.ParamByName('paramname').Value := 'xyz';
|
|
// ! ds1.Dataset := ds; // assigning the rest dataset to TDatasource that can be associated a TDBGrid for example.
|
|
// ! ds.Open;
|
|
// ! // ... use ds as usual, including modifications
|
|
// ! ds.ApplyUpdates(0);
|
|
TSynRestDataSet = class(TCustomClientDataSet)
|
|
protected
|
|
fDataSet: TSynRestSQLDataset;
|
|
fProvider: TDataSetProvider;
|
|
procedure DoOnFieldValidate(Sender: TField);
|
|
procedure DoOnUpdateError(Sender: TObject; DataSet: TCustomClientDataSet; E: EUpdateError; UpdateKind: TUpdateKind; var Response: TResolverResponse);
|
|
// from TDataSet
|
|
procedure OpenCursor(InfoQuery: Boolean); override;
|
|
{$ifdef ISDELPHI2007ANDUP}
|
|
// from IProviderSupport
|
|
function PSGetCommandText: string; override;
|
|
{$endif}
|
|
{$IFNDEF NEXTGEN}
|
|
{$ifdef ISDELPHIXE}
|
|
procedure SetCommandText(Value: WideString); override;
|
|
{$else ISDELPHIXE}
|
|
procedure SetCommandText(Value: String); override;
|
|
{$endif ISDELPHIXE}
|
|
{$ELSE}
|
|
procedure SetCommandText(Value: String); override;
|
|
{$ENDIF !NEXTGEN}
|
|
procedure SetFieldValidateFromSQLRecordSynValidate;
|
|
public
|
|
/// initialize the instance
|
|
constructor Create(AOwner: TComponent); override;
|
|
/// destroy the instance
|
|
destructor Destroy; override;
|
|
/// initialize the internal TDataSet from a Rest statement result set
|
|
// - Statement will have the form http://host:port/root/tablename or
|
|
// http://host:port/root/servicename.operationname?paramname=:paramalias
|
|
// examples:
|
|
// http://host:port/root/tablename?select=XXX or
|
|
// http://host:port/root/tablename?select=XXX&where=field1=XXX or field2=XXX
|
|
// http://host:port/root/service.operation?param=:param
|
|
procedure From(Statement: RawUTF8; MaxRowCount: cardinal=0);
|
|
procedure FetchParams;
|
|
published
|
|
property CommandText;
|
|
property Active;
|
|
property Aggregates;
|
|
property AggregatesActive;
|
|
property AutoCalcFields;
|
|
property Constraints;
|
|
property DisableStringTrim;
|
|
property FileName;
|
|
property Filter;
|
|
property Filtered;
|
|
property FilterOptions;
|
|
property FieldDefs;
|
|
property IndexDefs;
|
|
property IndexFieldNames;
|
|
property IndexName;
|
|
property FetchOnDemand;
|
|
property MasterFields;
|
|
property MasterSource;
|
|
property ObjectView;
|
|
property PacketRecords;
|
|
property Params;
|
|
property ReadOnly;
|
|
property StoreDefs;
|
|
property BeforeOpen;
|
|
property AfterOpen;
|
|
property BeforeClose;
|
|
property AfterClose;
|
|
property BeforeInsert;
|
|
property AfterInsert;
|
|
property BeforeEdit;
|
|
property AfterEdit;
|
|
property BeforePost;
|
|
property AfterPost;
|
|
property BeforeCancel;
|
|
property AfterCancel;
|
|
property BeforeDelete;
|
|
property AfterDelete;
|
|
property BeforeScroll;
|
|
property AfterScroll;
|
|
property BeforeRefresh;
|
|
property AfterRefresh;
|
|
property OnCalcFields;
|
|
property OnDeleteError;
|
|
property OnEditError;
|
|
property OnFilterRecord;
|
|
property OnNewRecord;
|
|
property OnPostError;
|
|
property OnReconcileError;
|
|
property BeforeApplyUpdates;
|
|
property AfterApplyUpdates;
|
|
property BeforeGetRecords;
|
|
property AfterGetRecords;
|
|
property BeforeRowRequest;
|
|
property AfterRowRequest;
|
|
property BeforeExecute;
|
|
property AfterExecute;
|
|
property BeforeGetParams;
|
|
property AfterGetParams;
|
|
/// the associated SynRestVCL TDataSet, used to retrieve and update data
|
|
property DataSet: TSynRestSQLDataSet read fDataSet;
|
|
end;
|
|
|
|
{$endif FPC}
|
|
|
|
/// Convert JSON array to REST TClientDataset
|
|
// - the dataset is created inside this function
|
|
function JSONToSynRestDataset(const aJSON: RawUTF8; const aSQLModel: TSQLModel): TSynRestDataset;
|
|
|
|
implementation
|
|
|
|
uses
|
|
Dialogs;
|
|
|
|
type
|
|
TSynRestSQLDatasetHack = class(TSynRestSQLDataset);
|
|
TSynValidateRestHack = class(TSynValidateRest);
|
|
|
|
{$ifndef FPC}
|
|
|
|
function JSONToSynRestDataset(const aJSON: RawUTF8; const aSQLModel: TSQLModel): TSynRestDataset;
|
|
var
|
|
lSQLTableJSON: TSQLTableJSON;
|
|
lData: TRawByteStringStream;
|
|
begin
|
|
Result := Nil;
|
|
if (aJSON = '') then
|
|
Exit;
|
|
lSQLTableJSON := TSQLTableJSON.Create('', aJSON);
|
|
lData := TRawByteStringStream.Create('');
|
|
try
|
|
JSONToBinary(lSQLTableJSON, lData);
|
|
Result := TSynRestDataset.Create(Nil);
|
|
Result.Dataset.SQLModel := aSQLModel;
|
|
Result.DataSet.From(lData.DataString);
|
|
finally
|
|
FreeAndNil(lData);
|
|
FreeAndNil(lSQLTableJSON);
|
|
end;
|
|
end;
|
|
|
|
{ TSynRestDataSet }
|
|
|
|
constructor TSynRestDataSet.Create(AOwner: TComponent);
|
|
begin
|
|
inherited Create(AOwner);
|
|
fProvider := TDataSetProvider.Create(Self);
|
|
fProvider.Name := 'InternalProvider'; { Do not localize }
|
|
fProvider.SetSubComponent(True);
|
|
fProvider.Options := fProvider.Options+[poAllowCommandText];
|
|
fProvider.OnUpdateError := DoOnUpdateError;
|
|
SetProvider(fProvider);
|
|
fDataSet := TSynRestSQLDataSet.Create(Self);
|
|
fDataSet.Name := 'InternalDataSet'; { Do not localize }
|
|
fDataSet.SetSubComponent(True);
|
|
fProvider.DataSet := fDataSet;
|
|
end;
|
|
|
|
destructor TSynRestDataSet.Destroy;
|
|
begin
|
|
fProvider.DataSet := nil;
|
|
FreeAndNil(fDataSet);
|
|
FreeAndNil(fProvider);
|
|
inherited;
|
|
end;
|
|
|
|
procedure TSynRestDataSet.DoOnFieldValidate(Sender: TField);
|
|
var
|
|
lRec: TSQLRecord;
|
|
F: Integer; // fields
|
|
V: Integer; // validations
|
|
Validate: TSynValidate;
|
|
Value: RawUTF8;
|
|
lErrMsg: string;
|
|
lFields: TSQLPropInfoList;
|
|
lwasTSynValidateRest: boolean;
|
|
ValidateRest: TSynValidateRest absolute Validate;
|
|
begin
|
|
lRec := TSynRestSQLDatasetHack(fDataset).GetSQLRecordClass.Create;
|
|
try
|
|
lFields := lRec.RecordProps.Fields;
|
|
F := lFields.IndexByName(Sender.FieldName);
|
|
// the field has not validation
|
|
if (Length(lRec.RecordProps.Filters[F]) = 0) then
|
|
Exit;
|
|
|
|
if not (lFields.List[F].SQLFieldType in COPIABLE_FIELDS) then
|
|
Exit;
|
|
|
|
lRec.SetFieldValue(Sender.FieldName, PUTF8Char(VariantToUTF8(Sender.Value)));
|
|
for V := 0 to Length(lRec.RecordProps.Filters[F])-1 do begin
|
|
Validate := TSynValidate(lRec.RecordProps.Filters[F,V]);
|
|
if Validate.InheritsFrom(TSynValidate) then begin
|
|
Value := Sender.Value;
|
|
lwasTSynValidateRest := Validate.InheritsFrom(TSynValidateRest);
|
|
if lwasTSynValidateRest then begin // set additional parameters
|
|
TSynValidateRestHack(ValidateRest).fProcessRec := lRec;
|
|
TSynValidateRestHack(ValidateRest).fProcessRest := Nil; // no Rest for the moment
|
|
end;
|
|
try
|
|
if not Validate.Process(F,Value,lErrMsg) then begin
|
|
if lErrMsg='' then
|
|
// no custom message -> show a default message
|
|
lErrMsg := format(sValidationFailed,[GetCaptionFromClass(Validate.ClassType)])
|
|
else
|
|
raise ESQLRestException.CreateUTF8('Error % on field "%"', [lErrMsg, Sender.DisplayName]);
|
|
end;
|
|
finally
|
|
if lwasTSynValidateRest then begin // reset additional parameters
|
|
TSynValidateRestHack(ValidateRest).fProcessRec := nil;
|
|
TSynValidateRestHack(ValidateRest).fProcessRest := nil;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
finally
|
|
lRec.Free;
|
|
end;
|
|
end;
|
|
|
|
procedure TSynRestDataSet.DoOnUpdateError(Sender: TObject; DataSet: TCustomClientDataSet; E: EUpdateError;
|
|
UpdateKind: TUpdateKind; var Response: TResolverResponse);
|
|
begin
|
|
Response := rrAbort;
|
|
MessageDlg(E.OriginalException.Message, mtError, [mbOK], 0);
|
|
end;
|
|
|
|
procedure TSynRestDataSet.From(Statement: RawUTF8; MaxRowCount: cardinal);
|
|
begin
|
|
fDataSet.From(Statement);
|
|
fDataSet.CommandText := ''; // ensure no SQL execution
|
|
Open;
|
|
fDataSet.CommandText := UTF8ToString(Statement); // assign it AFTER Open
|
|
end;
|
|
|
|
procedure TSynRestDataSet.FetchParams;
|
|
begin
|
|
if not HasAppServer and Assigned(FProvider) then
|
|
SetProvider(FProvider);
|
|
inherited FetchParams;
|
|
end;
|
|
|
|
procedure TSynRestDataSet.OpenCursor(InfoQuery: Boolean);
|
|
begin
|
|
if Assigned(fProvider) then
|
|
SetProvider(fProvider);
|
|
if fProvider.DataSet=self then
|
|
raise ESQLDBException.Create(SCircularProvider);
|
|
inherited OpenCursor(InfoQuery);
|
|
SetFieldValidateFromSQLRecordSynValidate;
|
|
end;
|
|
|
|
{$ifdef ISDELPHI2007ANDUP}
|
|
function TSynRestDataSet.PSGetCommandText: string;
|
|
{$ifdef ISDELPHIXE3}
|
|
var IP: IProviderSupportNG;
|
|
begin
|
|
if Supports(fDataSet, IProviderSupportNG, IP) then
|
|
{$else}
|
|
var IP: IProviderSupport;
|
|
begin
|
|
if Supports(fDataSet, IProviderSupport, IP) then
|
|
{$endif}
|
|
result := IP.PSGetCommandText else
|
|
result := CommandText;
|
|
end;
|
|
{$endif ISDELPHI2007ANDUP}
|
|
|
|
{$IFNDEF NEXTGEN}
|
|
{$ifdef ISDELPHIXE}
|
|
procedure TSynRestDataSet.SetCommandText(Value: WideString);
|
|
{$else ISDELPHIXE}
|
|
procedure TSynRestDataSet.SetCommandText(Value: String);
|
|
{$endif ISDELPHIXE}
|
|
{$ELSE}
|
|
procedure TSynRestDataSet.SetCommandText(Value: String);
|
|
{$ENDIF !NEXTGEN}
|
|
begin
|
|
TSynRestSQLDatasetHack(fDataset).SetCommandText(Value);
|
|
inherited SetCommandText(fDataset.CommandText);
|
|
// with this TSynRestSQLDataset can bind param values
|
|
TSynRestSQLDatasetHack(fDataset).fParams := Params;
|
|
if (Name = '') then
|
|
Name := 'rds' + StringReplaceChars(TSynRestSQLDatasetHack(fDataset).fTableName, '.', '_');
|
|
end;
|
|
|
|
procedure TSynRestDataSet.SetFieldValidateFromSQLRecordSynValidate;
|
|
var
|
|
F: Integer; // dataset fields
|
|
V: Integer; // validation fields
|
|
lProps: TSQLRecordProperties;
|
|
begin
|
|
// if not TSQLRecord associated, nothing to do
|
|
if (TSynRestSQLDatasetHack(fDataset).GetTableName = '') then
|
|
Exit;
|
|
lProps := TSynRestSQLDatasetHack(fDataset).GetSQLRecordClass.RecordProps;
|
|
// if there isn't filters, bye
|
|
if (Length(lProps.Filters) = 0) then
|
|
Exit;
|
|
for F := 0 to Fields.Count-1 do
|
|
begin
|
|
V := lProps.Fields.IndexByName(Fields[F].FieldName);
|
|
if (V > -1) then
|
|
begin
|
|
if (Length(lProps.Filters[V]) > 0) then
|
|
Fields[F].OnValidate := DoOnFieldValidate;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
{$endif FPC}
|
|
|
|
end.
|
|
|
|
|