xtool/contrib/mORMot/SQLite3/Samples/ThirdPartyDemos/EMartin/TSynRestDataset/SynRestMidasVCL.pas

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.