mirror of
https://github.com/danieleteti/delphimvcframework.git
synced 2024-11-15 15:55:54 +01:00
Merge remote-tracking branch 'upstream/master' into new_restclient
This commit is contained in:
commit
fdb7ad30a3
114
README.md
114
README.md
@ -1,8 +1,6 @@
|
||||
|
||||
![](https://img.shields.io/badge/current%20dmvcframework%20version-3.2.0--boron-blue?style=for-the-badge)
|
||||
![DelphiMVCFramework Logo](docs/dmvcframework_logofacebook.png)
|
||||
![](https://img.shields.io/badge/next%20dmvcframework%20version-3.2.1--carbon-red)![GitHub All Releases](https://img.shields.io/github/downloads/danieleteti/delphimvcframework/total?label=releases%20download)
|
||||
|
||||
![](https://img.shields.io/badge/We%20are%20working%20on%20dmvcframework%20new%20version-3.2.1--carbon-red)![GitHub All Releases](https://img.shields.io/github/downloads/danieleteti/delphimvcframework/total?label=releases%20download)
|
||||
|
||||
# DelphiMVCFramework 3.2.0-boron is [here](https://github.com/danieleteti/delphimvcframework/releases/tag/v3_2_0_boron)!
|
||||
|
||||
@ -385,7 +383,117 @@ Congratulations to Daniele Teti and all the staff for the excellent work!" -- Ma
|
||||
- Fixed! [issue388](https://github.com/danieleteti/delphimvcframework/issues/388)
|
||||
- Fixed! Has been patched a serious security bug affecting deployment configurations which uses internal WebServer to serve static files (do not affect all Apache, IIS or proxied deployments). Thanks to **Stephan Munz** to have discovered it. *Update to dmvcframework-3.2-RC5+ is required for all such kind of deployments.*
|
||||
|
||||
## Changes in upcoming version (3.2.1-carbon)
|
||||
|
||||
### Bug Fixes and Improvements
|
||||
|
||||
- [docExpansion parameter for Swagger](https://github.com/danieleteti/delphimvcframework/issues/408)
|
||||
|
||||
- New `Context: TWebContext` parameter in JSON-RPC Hooks
|
||||
|
||||
```delphi
|
||||
{ Called before any actual routing }
|
||||
procedure OnBeforeRoutingHook(const Context: TWebContext; const JSON: TJsonObject);
|
||||
{ Called after routing and before the actual remote method invocation }
|
||||
procedure OnBeforeCallHook(const Context: TWebContext; const JSON: TJsonObject);
|
||||
{ Called after actual remote method invocation, even if the method raised an exception }
|
||||
procedure OnAfterCallHook(const Context: TWebContext; const JSON: TJsonObject);
|
||||
```
|
||||
|
||||
- When a JSON-RPC Request returns a `System.Boolean` the `result` will be a JSON `true` or `false` and no `1` or `0` as it was in the `3.2.0-boron`.
|
||||
|
||||
- `IMVCJSONRPCExecutor.ExecuteNotification` returns a `IJSONRPCResponse`. In case of error response contains information about the error, in case of successful execution the response is a [Null Object](https://en.wikipedia.org/wiki/Null_object_pattern).
|
||||
|
||||
- Added `foReadOnly` and `foWriteOnly` as field options in `MVCTableField` attribute (used by `TMVCActiveRecord`). Currently available field options are:
|
||||
|
||||
- *foPrimaryKey* { it's the primary key of the mapped table }
|
||||
- *foAutoGenerated* { not written, read - similar to foReadOnly but is reloaded after insert and update }
|
||||
- *foTransient* { never stored nor read - managed only at run-time }
|
||||
- *foReadOnly* { not written, read }
|
||||
- *foWriteOnly* { written, not read }
|
||||
|
||||
Now it is possible to declare entities like the followings (or with any other combinations):
|
||||
|
||||
```delphi
|
||||
[MVCNameCase(ncLowerCase)]
|
||||
[MVCTable('articles')]
|
||||
TArticleWithWriteOnlyFields = class(TCustomEntity)
|
||||
private
|
||||
[MVCTableField('ID', [foPrimaryKey, foAutoGenerated])]
|
||||
fID: NullableInt32;
|
||||
[MVCTableField('description', [foWriteOnly])]
|
||||
fDescription: string;
|
||||
[MVCTableField('price', [foWriteOnly])]
|
||||
fPrice: Integer;
|
||||
public
|
||||
property ID: NullableInt32 read fID write fID;
|
||||
property Description: string read fDescription write fDescription;
|
||||
property Price: Integer read fPrice write fPrice;
|
||||
end;
|
||||
|
||||
[MVCNameCase(ncLowerCase)]
|
||||
[MVCTable('articles')]
|
||||
TArticleWithReadOnlyFields = class(TCustomEntity)
|
||||
private
|
||||
[MVCTableField('ID', [foPrimaryKey, foReadOnly])]
|
||||
fID: NullableInt32;
|
||||
[MVCTableField('code', [foTransient])]
|
||||
fCode: NullableString;
|
||||
[MVCTableField('description', [foReadOnly])]
|
||||
fDescrizione: string;
|
||||
[MVCTableField('price', [foReadOnly])]
|
||||
fPrice: Currency;
|
||||
public
|
||||
property ID: NullableInt32 read fID write fID;
|
||||
property Code: NullableString read fCode write fCode;
|
||||
property Description: string read fDescription write fDescription;
|
||||
property Price: Currency read fPrice write fPrice;
|
||||
end;
|
||||
```
|
||||
|
||||
- Added the ability to deserialize an object starting from an arbitrary node in the JSON (or other format) present in the request body.
|
||||
|
||||
```delphi
|
||||
procedure TBooksController.CreateBook;
|
||||
var
|
||||
lBook: TBook;
|
||||
begin
|
||||
//this call deserialize a TBook instance
|
||||
//starting from the 'book' node of
|
||||
//the request body
|
||||
lBook := Context.Request.BodyAs<TBook>('book');
|
||||
try
|
||||
lBook.Insert;
|
||||
Render201Created('/api/books/' + lBook.ID.ToString);
|
||||
finally
|
||||
lBook.Free;
|
||||
end;
|
||||
end;
|
||||
```
|
||||
|
||||
- Improved the primary key type handling for manual handling in MVCActiveRecord.
|
||||
|
||||
```delphi
|
||||
procedure TMyBaseEntity.OnBeforeInsert;
|
||||
begin
|
||||
inherited;
|
||||
//regardless the name of the PK field
|
||||
//the following code fills the PK with a GUID
|
||||
//Inheriting the other entities from this, all
|
||||
//will inherit this behavior.
|
||||
SetPK(TValue.From<NullableString>(TGUID.NewGuid.ToString));
|
||||
|
||||
//if the PK was a simple string, the code
|
||||
//should be like the following
|
||||
//SetPK(TGUID.NewGuid.ToString);
|
||||
end;
|
||||
|
||||
```
|
||||
|
||||
- Improved `activerecord_showcase` sample.
|
||||
|
||||
## Roadmap
|
||||
|
||||
DelphiMVCFramework roadmap is always updated as-soon-as the features planned are implemented. Check the roadmap [here](roadmap.md).
|
||||
|
||||
## Trainings, consultancy or custom development service
|
||||
|
@ -1,25 +0,0 @@
|
||||
Daniele Teti
|
||||
Role : Founder, Lead Developer
|
||||
email : d.teti@bittime.it
|
||||
blog : www.danieleteti.it
|
||||
company : www.bittimeprofessionals.it
|
||||
country : ITALY
|
||||
|
||||
Daniele Spinetti
|
||||
Role : Expert, Senior Developer
|
||||
email : d.spinetti@bittime.it
|
||||
blog : www.danielespinetti.it
|
||||
company : www.bittimeprofessionals.it
|
||||
country : ITALY
|
||||
|
||||
Nick Hodges
|
||||
Role : Expert, Senior Developer
|
||||
blog : www.nickhodges.com, www.codingindelphi.com/blog/
|
||||
company : www.embarcadero.com
|
||||
country : USA
|
||||
|
||||
Ezequiel Juliano Müller
|
||||
Role : Expert, Senior Developer
|
||||
email : ezequieljuliano@gmail.com
|
||||
blog : www.ezequieljuliano.com.br
|
||||
country : BRAZIL
|
@ -10,7 +10,7 @@ As we are in the detailed planning stages for these features, we will share addi
|
||||
- (DONE) Implement Strongly Typed Actions
|
||||
- (DONE) Implement Custom Authentication and Authorization Middleware
|
||||
- (DONE) Use a middleware to implement response compression for console type projects
|
||||
- (INITIAL)Implement Swagger support (we need a good self contained YAML parser/generator...)
|
||||
- (DONE)Implement Swagger support
|
||||
- (DONE) Linux support
|
||||
- (DONE) Update Mapper framework to make it extensible and configurable
|
||||
- The default mapper interface will be the same as the current version so that no breaking changes happened
|
||||
@ -25,7 +25,7 @@ As we are in the detailed planning stages for these features, we will share addi
|
||||
- New samples with specific web related use cases:
|
||||
- WebWorkers
|
||||
- (DONE) Angular2+
|
||||
- React
|
||||
- (DONE) React
|
||||
- (DONE) Create "Custom Authentication and Authorization" demo
|
||||
- (CANCELED) Complete the [DevGuide](https://danieleteti.gitbooks.io/delphimvcframework/content/) on gitbooks
|
||||
- Improve the session mechanism to allows more flexibility
|
||||
|
@ -46,9 +46,9 @@ type
|
||||
[MVCTable('articles')]
|
||||
TArticle = class(TCustomEntity)
|
||||
private
|
||||
[MVCTableField('ID')]
|
||||
[MVCTableField('ID', [foPrimaryKey, foAutoGenerated])]
|
||||
fID: NullableInt32;
|
||||
[MVCTableField('code')]
|
||||
[MVCTableField('code', [foTransient])]
|
||||
fCodice: NullableString;
|
||||
[MVCTableField('description')]
|
||||
fDescrizione: string;
|
||||
@ -63,6 +63,41 @@ type
|
||||
property Price: Currency read fPrezzo write fPrezzo;
|
||||
end;
|
||||
|
||||
[MVCNameCase(ncLowerCase)]
|
||||
[MVCTable('articles')]
|
||||
TArticleWithWriteOnlyFields = class(TCustomEntity)
|
||||
private
|
||||
[MVCTableField('ID', [foPrimaryKey, foAutoGenerated, foReadOnly])]
|
||||
fID: NullableInt32;
|
||||
[MVCTableField('description', [foWriteOnly])]
|
||||
fDescrizione: string;
|
||||
[MVCTableField('price', [foWriteOnly])]
|
||||
fPrice: Integer;
|
||||
public
|
||||
property ID: NullableInt32 read fID write fID;
|
||||
property Description: string read fDescrizione write fDescrizione;
|
||||
property Price: Integer read fPrice write fPrice;
|
||||
end;
|
||||
|
||||
[MVCNameCase(ncLowerCase)]
|
||||
[MVCTable('articles')]
|
||||
TArticleWithReadOnlyFields = class(TCustomEntity)
|
||||
private
|
||||
[MVCTableField('ID', [foPrimaryKey, foReadOnly])]
|
||||
fID: NullableInt32;
|
||||
[MVCTableField('code', [foTransient])]
|
||||
fCodice: NullableString;
|
||||
[MVCTableField('description', [foReadOnly])]
|
||||
fDescrizione: string;
|
||||
[MVCTableField('price', [foReadOnly])]
|
||||
fPrezzo: Currency;
|
||||
public
|
||||
property ID: NullableInt32 read fID write fID;
|
||||
property Code: NullableString read fCodice write fCodice;
|
||||
property Description: string read fDescrizione write fDescrizione;
|
||||
property Price: Currency read fPrezzo write fPrezzo;
|
||||
end;
|
||||
|
||||
TOrder = class;
|
||||
|
||||
[MVCNameCase(ncLowerCase)]
|
||||
@ -108,7 +143,6 @@ type
|
||||
fID: Integer;
|
||||
[MVCTableField('code', [foTransient])]
|
||||
fCode: string;
|
||||
[MVCTableField('', [foTransient])]
|
||||
fFormattedCode: string;
|
||||
[MVCTableField('description')]
|
||||
fCompanyName: string;
|
||||
@ -238,6 +272,12 @@ type
|
||||
property Note: string read fNote write fNote;
|
||||
end;
|
||||
|
||||
[MVCTable('customers_with_code')]
|
||||
TCustomerPlainWithClientPK = class(TCustomerWithCode)
|
||||
protected
|
||||
procedure OnBeforeInsert; override;
|
||||
end;
|
||||
|
||||
[MVCNameCase(ncLowerCase)]
|
||||
[MVCTable('orders')]
|
||||
TOrder = class(TCustomEntity)
|
||||
@ -350,7 +390,7 @@ type
|
||||
implementation
|
||||
|
||||
uses
|
||||
System.SysUtils, Data.DB, MVCFramework.Logger;
|
||||
System.SysUtils, Data.DB, MVCFramework.Logger, System.Rtti;
|
||||
|
||||
constructor TArticle.Create;
|
||||
begin
|
||||
@ -459,7 +499,7 @@ end;
|
||||
constructor TNullablesTest.Create;
|
||||
begin
|
||||
inherited Create;
|
||||
// ff_blob := TMemoryStream.Create;
|
||||
ff_blob := TMemoryStream.Create;
|
||||
end;
|
||||
|
||||
destructor TNullablesTest.Destroy;
|
||||
@ -476,4 +516,16 @@ begin
|
||||
Log.Info(ClassName + ' | ' + SQL, 'sql_trace');
|
||||
end;
|
||||
|
||||
{ TCustomerPlainWithClientPK }
|
||||
|
||||
procedure TCustomerPlainWithClientPK.OnBeforeInsert;
|
||||
begin
|
||||
inherited;
|
||||
SetPK(TValue.From<NullableString>(TGUID.NewGuid.ToString
|
||||
.Replace('{', '')
|
||||
.Replace('-', '')
|
||||
.Replace('}', '')
|
||||
.Substring(0, 20)));
|
||||
end;
|
||||
|
||||
end.
|
||||
|
@ -2,7 +2,7 @@ object MainForm: TMainForm
|
||||
Left = 0
|
||||
Top = 0
|
||||
Caption = 'TMVCActiveRecord - ShowCase'
|
||||
ClientHeight = 587
|
||||
ClientHeight = 640
|
||||
ClientWidth = 635
|
||||
Color = clBtnFace
|
||||
Font.Charset = DEFAULT_CHARSET
|
||||
@ -15,7 +15,7 @@ object MainForm: TMainForm
|
||||
OnShow = FormShow
|
||||
DesignSize = (
|
||||
635
|
||||
587)
|
||||
640)
|
||||
PixelsPerInch = 96
|
||||
TextHeight = 13
|
||||
object btnCRUD: TButton
|
||||
@ -40,7 +40,7 @@ object MainForm: TMainForm
|
||||
Left = 135
|
||||
Top = 8
|
||||
Width = 492
|
||||
Height = 571
|
||||
Height = 624
|
||||
Anchors = [akLeft, akTop, akRight, akBottom]
|
||||
Ctl3D = True
|
||||
DoubleBuffered = True
|
||||
@ -57,6 +57,7 @@ object MainForm: TMainForm
|
||||
TabOrder = 2
|
||||
WantReturns = False
|
||||
WordWrap = False
|
||||
ExplicitHeight = 571
|
||||
end
|
||||
object btnRelations: TButton
|
||||
Left = 8
|
||||
@ -158,6 +159,24 @@ object MainForm: TMainForm
|
||||
TabOrder = 13
|
||||
OnClick = btnCountWithRQLClick
|
||||
end
|
||||
object btnReadAndWriteOnly: TButton
|
||||
Left = 8
|
||||
Top = 523
|
||||
Width = 121
|
||||
Height = 33
|
||||
Caption = 'R/O, R/W'
|
||||
TabOrder = 14
|
||||
OnClick = btnReadAndWriteOnlyClick
|
||||
end
|
||||
object btnClientGeneratedPK: TButton
|
||||
Left = 8
|
||||
Top = 562
|
||||
Width = 121
|
||||
Height = 33
|
||||
Caption = 'Client Generated PKs'
|
||||
TabOrder = 15
|
||||
OnClick = btnClientGeneratedPKClick
|
||||
end
|
||||
object FDConnection1: TFDConnection
|
||||
Left = 176
|
||||
Top = 56
|
||||
|
@ -42,6 +42,8 @@ type
|
||||
btnCRUDWithStringPKs: TButton;
|
||||
btnWithSpaces: TButton;
|
||||
btnCountWithRQL: TButton;
|
||||
btnReadAndWriteOnly: TButton;
|
||||
btnClientGeneratedPK: TButton;
|
||||
procedure btnCRUDClick(Sender: TObject);
|
||||
procedure btnInheritanceClick(Sender: TObject);
|
||||
procedure btnMultiThreadingClick(Sender: TObject);
|
||||
@ -58,6 +60,8 @@ type
|
||||
procedure btnCRUDWithStringPKsClick(Sender: TObject);
|
||||
procedure btnWithSpacesClick(Sender: TObject);
|
||||
procedure btnCountWithRQLClick(Sender: TObject);
|
||||
procedure btnReadAndWriteOnlyClick(Sender: TObject);
|
||||
procedure btnClientGeneratedPKClick(Sender: TObject);
|
||||
private
|
||||
procedure Log(const Value: string);
|
||||
procedure LoadCustomers;
|
||||
@ -88,6 +92,19 @@ const
|
||||
CompanySuffix: array [0 .. 5] of string = ('Corp.', 'Inc.', 'Ltd.', 'Srl', 'SPA', 'doo');
|
||||
Stuff: array [0 .. 4] of string = ('Burger', 'GAS', 'Motors', 'House', 'Boats');
|
||||
|
||||
procedure TMainForm.btnClientGeneratedPKClick(Sender: TObject);
|
||||
var
|
||||
lCustomer: TCustomerPlainWithClientPK;
|
||||
begin
|
||||
Log('** OnBeforeInsert and SetPK');
|
||||
lCustomer := TCustomerPlainWithClientPK.Create();
|
||||
try
|
||||
lCustomer.Store;
|
||||
finally
|
||||
lCustomer.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TMainForm.btnCountWithRQLClick(Sender: TObject);
|
||||
var
|
||||
lRQL: string;
|
||||
@ -137,7 +154,7 @@ begin
|
||||
Log('There are ' + TMVCActiveRecord.Count<TCustomer>().ToString + ' row/s for entity ' + TCustomer.ClassName);
|
||||
lCustomer := TCustomer.Create;
|
||||
try
|
||||
Log('Entity ' + TCustomer.ClassName + ' is mapped to table ' + lCustomer.TableName);
|
||||
Log('Entity ' + TCustomer.ClassName + ' is mapped to table ' + lCustomer.TableName);
|
||||
lCustomer.CompanyName := 'Google Inc.';
|
||||
lCustomer.City := 'Montain View, CA';
|
||||
lCustomer.Note := 'Hello there!';
|
||||
@ -449,7 +466,12 @@ begin
|
||||
lTest.f_int2 := 2;
|
||||
lTest.f_int4 := 4;
|
||||
lTest.f_int8 := 8;
|
||||
lTest.f_blob := TStringStream.Create('Hello World');
|
||||
with TStreamWriter.Create(lTest.f_blob) do
|
||||
try
|
||||
write('Hello World');
|
||||
finally
|
||||
Free;
|
||||
end;
|
||||
lTest.Insert;
|
||||
Log('Inserting nulls');
|
||||
finally
|
||||
@ -474,8 +496,7 @@ begin
|
||||
lTest.f_int2 := lTest.f_int2.Value + 2;
|
||||
lTest.f_int4 := lTest.f_int4.Value + 4;
|
||||
lTest.f_int8 := lTest.f_int8.Value + 8;
|
||||
lTest.f_blob.Free;
|
||||
lTest.f_blob := nil;
|
||||
lTest.f_blob.Size := 0;
|
||||
lTest.Update;
|
||||
finally
|
||||
lTest.Free;
|
||||
@ -494,7 +515,7 @@ begin
|
||||
Assert(not lTest.f_float4.HasValue);
|
||||
Assert(not lTest.f_float8.HasValue);
|
||||
Assert(not lTest.f_bool.HasValue);
|
||||
Assert(not Assigned(lTest.f_blob), 'Blob contains a value when should not');
|
||||
Assert(lTest.f_blob.Size = 0, 'Blob contains a value when should not');
|
||||
TMVCActiveRecord.DeleteRQL(TNullablesTest, 'eq(f_int2,4)');
|
||||
finally
|
||||
lTest.Free;
|
||||
@ -572,6 +593,65 @@ begin
|
||||
|
||||
end;
|
||||
|
||||
procedure TMainForm.btnReadAndWriteOnlyClick(Sender: TObject);
|
||||
var
|
||||
lArtWO, lArtWO2: TArticleWithWriteOnlyFields;
|
||||
lArtRO: TArticleWithReadOnlyFields;
|
||||
lID: NullableInt32;
|
||||
lArt: TArticle;
|
||||
begin
|
||||
lArtWO := TArticleWithWriteOnlyFields.Create();
|
||||
try
|
||||
lArtWO.Description := 'Description1';
|
||||
lArtWO.Price := 12;
|
||||
lArtWO.Insert;
|
||||
Log('Stored TArticleWithWriteOnlyFields');
|
||||
lID := lArtWO.ID;
|
||||
|
||||
lArt := TMVCActiveRecord.GetByPK<TArticle>(lID);
|
||||
try
|
||||
Assert(lArtWO.Description = lArt.Description);
|
||||
Assert(lArtWO.Price = lArt.Price);
|
||||
Log('Check Stored version of TArticleWithWriteOnlyFields');
|
||||
|
||||
Log('Reading data using TArticleWithReadOnlyFields');
|
||||
lArtRO := TMVCActiveRecord.GetByPK<TArticleWithReadOnlyFields>(lID);
|
||||
try
|
||||
Assert(lArtRO.Description = lArt.Description);
|
||||
Assert(lArtRO.Price = lArt.Price);
|
||||
Log('Check Read data of TArticleWithWriteOnlyFields using TArticleWithReadOnlyFields');
|
||||
finally
|
||||
lArtRO.Free;
|
||||
end;
|
||||
|
||||
Log('Reading data using TArticleWithWriteOnlyFields (???)');
|
||||
lArtWO2 := TMVCActiveRecord.GetByPK<TArticleWithWriteOnlyFields>(lID);
|
||||
try
|
||||
Assert(lArtWO2.ID.ValueOrDefault = lID.ValueOrDefault);
|
||||
Assert(lArtWO2.Description = '');
|
||||
Assert(lArtWO2.Price = 0);
|
||||
finally
|
||||
lArtWO2.Free;
|
||||
end;
|
||||
finally
|
||||
lArt.Free;
|
||||
end;
|
||||
|
||||
lArtRO := TArticleWithReadOnlyFields.Create();
|
||||
try
|
||||
lArtRO.Description := 'Description1';
|
||||
lArtRO.Price := 12;
|
||||
ShowMessage('Now an exception will be raised...');
|
||||
lArtRO.Insert; // exception here :-)
|
||||
finally
|
||||
lArtRO.Free;
|
||||
end;
|
||||
|
||||
finally
|
||||
lArtWO.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TMainForm.btnRelationsClick(Sender: TObject);
|
||||
var
|
||||
lCustomer: TCustomerEx;
|
||||
@ -818,7 +898,7 @@ begin
|
||||
lCustomer := TCustomerWithTransient.Create;
|
||||
try
|
||||
{
|
||||
'Code' and City will not be persisted because defined as 'transient'
|
||||
'Code' will not be persisted because defined as 'transient'
|
||||
}
|
||||
lCustomer.Code := '1234';
|
||||
lCustomer.CompanyName := 'Google Inc.';
|
||||
|
@ -163,13 +163,13 @@
|
||||
</Excluded_Packages>
|
||||
</Delphi.Personality>
|
||||
<Deployment Version="3">
|
||||
<DeployFile LocalName="bin\activerecord_showcase.exe" Configuration="BUILD" Class="ProjectOutput">
|
||||
<DeployFile LocalName="bin\activerecord_showcase.exe" Configuration="Debug" Class="ProjectOutput">
|
||||
<Platform Name="Win32">
|
||||
<RemoteName>activerecord_showcase.exe</RemoteName>
|
||||
<Overwrite>true</Overwrite>
|
||||
</Platform>
|
||||
</DeployFile>
|
||||
<DeployFile LocalName="bin\activerecord_showcase.exe" Configuration="Debug" Class="ProjectOutput">
|
||||
<DeployFile LocalName="bin\activerecord_showcase.exe" Configuration="BUILD" Class="ProjectOutput">
|
||||
<Platform Name="Win32">
|
||||
<RemoteName>activerecord_showcase.exe</RemoteName>
|
||||
<Overwrite>true</Overwrite>
|
||||
|
@ -160,8 +160,11 @@
|
||||
<Source Name="MainSource">AuthenticateAuthorize.dpr</Source>
|
||||
</Source>
|
||||
<Excluded_Packages>
|
||||
<Excluded_Packages Name="$(BDSBIN)\dcloffice2k170.bpl">Microsoft Office 2000 Beispiele für gekapselte Komponenten für Automatisierungsserver</Excluded_Packages>
|
||||
<Excluded_Packages Name="$(BDSBIN)\dclofficexp170.bpl">Microsoft Office XP Beispiele für gekapselte Komponenten für Automation Server</Excluded_Packages>
|
||||
<Excluded_Packages Name="$(BDSBIN)\bcboffice2k260.bpl">Embarcadero C++Builder Office 2000 Servers Package</Excluded_Packages>
|
||||
<Excluded_Packages Name="$(BDSBIN)\bcbofficexp260.bpl">Embarcadero C++Builder Office XP Servers Package</Excluded_Packages>
|
||||
<Excluded_Packages Name="$(BDSBIN)\dcloffice2k260.bpl">Microsoft Office 2000 Sample Automation Server Wrapper Components</Excluded_Packages>
|
||||
<Excluded_Packages Name="$(BDSBIN)\dclofficexp260.bpl">Microsoft Office XP Sample Automation Server Wrapper Components</Excluded_Packages>
|
||||
<Excluded_Packages Name="C:\Program Files (x86)\FastReports\LibD26\dclfrxtee26.bpl">FastReport 6.0 Tee Components</Excluded_Packages>
|
||||
</Excluded_Packages>
|
||||
</Delphi.Personality>
|
||||
<Deployment Version="3">
|
||||
|
@ -24,9 +24,10 @@ implementation
|
||||
|
||||
{ TMVCAuthorization }
|
||||
|
||||
procedure TAuthenticationSample.OnAuthentication(const AContext: TWebContext; const UserName: string; const Password: string;
|
||||
UserRoles: System.Generics.Collections.TList<System.string>;
|
||||
var IsValid: Boolean; const SessionData: TSessionData);
|
||||
procedure TAuthenticationSample.OnAuthentication(const AContext: TWebContext; const UserName: string;
|
||||
const Password: string;
|
||||
UserRoles: System.Generics.Collections.TList<System.string>;
|
||||
var IsValid: Boolean; const SessionData: TSessionData);
|
||||
begin
|
||||
IsValid := UserName.Equals(Password); // hey!, this is just a demo!!!
|
||||
if IsValid then
|
||||
@ -53,9 +54,9 @@ end;
|
||||
|
||||
procedure TAuthenticationSample.OnAuthorization
|
||||
(const AContext: TWebContext; UserRoles
|
||||
: System.Generics.Collections.TList<System.string>;
|
||||
const ControllerQualifiedClassName: string; const ActionName: string;
|
||||
var IsAuthorized: Boolean);
|
||||
: System.Generics.Collections.TList<System.string>;
|
||||
const ControllerQualifiedClassName: string; const ActionName: string;
|
||||
var IsAuthorized: Boolean);
|
||||
begin
|
||||
IsAuthorized := False;
|
||||
if ActionName = 'Logout' then
|
||||
@ -69,7 +70,7 @@ begin
|
||||
end;
|
||||
|
||||
procedure TAuthenticationSample.OnRequest(const AContext: TWebContext; const ControllerQualifiedClassName: string;
|
||||
const ActionName: string; var AuthenticationRequired: Boolean);
|
||||
const ActionName: string; var AuthenticationRequired: Boolean);
|
||||
begin
|
||||
AuthenticationRequired := ControllerQualifiedClassName =
|
||||
'AppControllerU.TAdminController';
|
||||
|
@ -49,7 +49,9 @@ begin
|
||||
.AddMiddleware(TMVCBasicAuthenticationMiddleware.Create(TAuthenticationSample.Create))
|
||||
.AddMiddleware(TMVCStaticFilesMiddleware.Create(
|
||||
'/', { StaticFilesPath }
|
||||
'..\..\www' { DocumentRoot }
|
||||
'..\..\www', { DocumentRoot }
|
||||
'index.html',
|
||||
False { not serving a SPA }
|
||||
));
|
||||
end;
|
||||
|
||||
|
Binary file not shown.
@ -2,7 +2,7 @@ object Form10: TForm10
|
||||
Left = 0
|
||||
Top = 0
|
||||
Caption = 'Form10'
|
||||
ClientHeight = 448
|
||||
ClientHeight = 484
|
||||
ClientWidth = 831
|
||||
Color = clBtnFace
|
||||
Font.Charset = DEFAULT_CHARSET
|
||||
@ -14,9 +14,28 @@ object Form10: TForm10
|
||||
OnCreate = FormCreate
|
||||
PixelsPerInch = 96
|
||||
TextHeight = 13
|
||||
object Label2: TLabel
|
||||
AlignWithMargins = True
|
||||
Left = 3
|
||||
Top = 3
|
||||
Width = 825
|
||||
Height = 39
|
||||
Align = alTop
|
||||
Alignment = taCenter
|
||||
Caption =
|
||||
'Please use the demo available in samples\jsonrpc_with_published_' +
|
||||
'objects\'
|
||||
Font.Charset = DEFAULT_CHARSET
|
||||
Font.Color = clWindowText
|
||||
Font.Height = -21
|
||||
Font.Name = 'Tahoma'
|
||||
Font.Style = []
|
||||
ParentFont = False
|
||||
Layout = tlCenter
|
||||
end
|
||||
object GroupBox1: TGroupBox
|
||||
Left = 8
|
||||
Top = 16
|
||||
Top = 48
|
||||
Width = 815
|
||||
Height = 124
|
||||
Caption = 'Simple Types'
|
||||
@ -164,7 +183,7 @@ object Form10: TForm10
|
||||
end
|
||||
object GroupBox2: TGroupBox
|
||||
Left = 8
|
||||
Top = 146
|
||||
Top = 178
|
||||
Width = 489
|
||||
Height = 159
|
||||
Caption = 'Returning Objects'
|
||||
@ -202,7 +221,7 @@ object Form10: TForm10
|
||||
end
|
||||
object GroupBox3: TGroupBox
|
||||
Left = 509
|
||||
Top = 146
|
||||
Top = 178
|
||||
Width = 314
|
||||
Height = 294
|
||||
Caption = 'Returning Datasets'
|
||||
@ -239,7 +258,7 @@ object Form10: TForm10
|
||||
end
|
||||
object GroupBox4: TGroupBox
|
||||
Left = 8
|
||||
Top = 311
|
||||
Top = 343
|
||||
Width = 489
|
||||
Height = 129
|
||||
Caption = 'Passing Objects as parameters'
|
||||
@ -298,7 +317,7 @@ object Form10: TForm10
|
||||
object DataSource1: TDataSource
|
||||
DataSet = FDMemTable1
|
||||
Left = 767
|
||||
Top = 184
|
||||
Top = 216
|
||||
end
|
||||
object FDMemTable1: TFDMemTable
|
||||
FetchOptions.AssignedValues = [evMode]
|
||||
@ -309,7 +328,7 @@ object Form10: TForm10
|
||||
UpdateOptions.CheckRequired = False
|
||||
UpdateOptions.AutoCommitUpdates = True
|
||||
Left = 767
|
||||
Top = 248
|
||||
Top = 280
|
||||
object FDMemTable1Code: TIntegerField
|
||||
FieldName = 'Code'
|
||||
end
|
||||
|
@ -66,6 +66,7 @@ type
|
||||
btnInvalid2: TButton;
|
||||
btnNotification: TButton;
|
||||
btnInvalidMethod: TButton;
|
||||
Label2: TLabel;
|
||||
procedure btnSubstractClick(Sender: TObject);
|
||||
procedure btnReverseStringClick(Sender: TObject);
|
||||
procedure edtGetCustomersClick(Sender: TObject);
|
||||
@ -138,25 +139,25 @@ end;
|
||||
|
||||
procedure TForm10.btnInvalid1Click(Sender: TObject);
|
||||
var
|
||||
lReq: IJSONRPCRequest;
|
||||
lReq: IJSONRPCNotification;
|
||||
lResp: IJSONRPCResponse;
|
||||
begin
|
||||
lReq := TJSONRPCRequest.Create;
|
||||
lReq := TJSONRPCNotification.Create;
|
||||
lReq.Method := 'invalidmethod1';
|
||||
lReq.Params.Add(1);
|
||||
lResp := FExecutor.ExecuteRequest(lReq);
|
||||
lResp := FExecutor.ExecuteNotification(lReq);
|
||||
ShowMessage(lResp.Error.ErrMessage);
|
||||
end;
|
||||
|
||||
procedure TForm10.btnInvalid2Click(Sender: TObject);
|
||||
var
|
||||
lReq: IJSONRPCRequest;
|
||||
lReq: IJSONRPCNotification;
|
||||
lResp: IJSONRPCResponse;
|
||||
begin
|
||||
lReq := TJSONRPCRequest.Create;
|
||||
lReq := TJSONRPCNotification.Create;
|
||||
lReq.Method := 'invalidmethod2';
|
||||
lReq.Params.Add(1);
|
||||
lResp := FExecutor.ExecuteRequest(lReq);
|
||||
lResp := FExecutor.ExecuteNotification(lReq);
|
||||
ShowMessage(lResp.Error.ErrMessage);
|
||||
end;
|
||||
|
||||
|
@ -420,6 +420,37 @@ object MainForm: TMainForm
|
||||
end
|
||||
end
|
||||
end
|
||||
object TabSheet3: TTabSheet
|
||||
Caption = 'Hooks Demo'
|
||||
ImageIndex = 2
|
||||
object btnDoNothing: TButton
|
||||
Left = 24
|
||||
Top = 24
|
||||
Width = 145
|
||||
Height = 33
|
||||
Caption = 'Do Nothing'
|
||||
TabOrder = 0
|
||||
OnClick = btnDoNothingClick
|
||||
end
|
||||
object btnDoNothingError: TButton
|
||||
Left = 24
|
||||
Top = 63
|
||||
Width = 145
|
||||
Height = 33
|
||||
Caption = 'Do Nothing With Errors'
|
||||
TabOrder = 1
|
||||
OnClick = btnDoNothingErrorClick
|
||||
end
|
||||
object btnNotExistent: TButton
|
||||
Left = 24
|
||||
Top = 102
|
||||
Width = 145
|
||||
Height = 33
|
||||
Caption = 'Invalid Method'
|
||||
TabOrder = 2
|
||||
OnClick = btnNotExistentClick
|
||||
end
|
||||
end
|
||||
end
|
||||
object DataSource1: TDataSource
|
||||
DataSet = FDMemTable1
|
||||
|
@ -81,6 +81,10 @@ type
|
||||
Edit2: TEdit;
|
||||
btnSubtractWithNamedParams: TButton;
|
||||
Edit3: TEdit;
|
||||
TabSheet3: TTabSheet;
|
||||
btnDoNothing: TButton;
|
||||
btnDoNothingError: TButton;
|
||||
btnNotExistent: TButton;
|
||||
procedure btnSubstractClick(Sender: TObject);
|
||||
procedure btnReverseStringClick(Sender: TObject);
|
||||
procedure edtGetCustomersClick(Sender: TObject);
|
||||
@ -97,9 +101,13 @@ type
|
||||
procedure btnFloatsTestsClick(Sender: TObject);
|
||||
procedure btnWithJSONClick(Sender: TObject);
|
||||
procedure btnSubtractWithNamedParamsClick(Sender: TObject);
|
||||
procedure btnDoNothingClick(Sender: TObject);
|
||||
procedure btnNotExistentClick(Sender: TObject);
|
||||
procedure btnDoNothingErrorClick(Sender: TObject);
|
||||
private
|
||||
FExecutor: IMVCJSONRPCExecutor;
|
||||
FExecutor2: IMVCJSONRPCExecutor;
|
||||
FExecutor3: IMVCJSONRPCExecutor;
|
||||
public
|
||||
{ Public declarations }
|
||||
end;
|
||||
@ -121,6 +129,7 @@ uses
|
||||
|
||||
{$R *.dfm}
|
||||
|
||||
|
||||
procedure TMainForm.btnAddDayClick(Sender: TObject);
|
||||
var
|
||||
lReq: IJSONRPCRequest;
|
||||
@ -160,6 +169,22 @@ begin
|
||||
|
||||
end;
|
||||
|
||||
procedure TMainForm.btnDoNothingClick(Sender: TObject);
|
||||
var
|
||||
lReq: IJSONRPCNotification;
|
||||
begin
|
||||
lReq := TJSONRPCNotification.Create('DoSomething');
|
||||
FExecutor3.ExecuteNotification(lReq);
|
||||
end;
|
||||
|
||||
procedure TMainForm.btnDoNothingErrorClick(Sender: TObject);
|
||||
var
|
||||
lReq: IJSONRPCNotification;
|
||||
begin
|
||||
lReq := TJSONRPCNotification.Create('DoSomethingWithError');
|
||||
FExecutor3.ExecuteNotification(lReq);
|
||||
end;
|
||||
|
||||
procedure TMainForm.btnFloatsTestsClick(Sender: TObject);
|
||||
var
|
||||
lReq: IJSONRPCRequest;
|
||||
@ -209,25 +234,25 @@ end;
|
||||
|
||||
procedure TMainForm.btnInvalid1Click(Sender: TObject);
|
||||
var
|
||||
lReq: IJSONRPCRequest;
|
||||
lReq: IJSONRPCNotification;
|
||||
lResp: IJSONRPCResponse;
|
||||
begin
|
||||
lReq := TJSONRPCRequest.Create;
|
||||
lReq := TJSONRPCNotification.Create;
|
||||
lReq.Method := 'invalidmethod1';
|
||||
lReq.Params.Add(1);
|
||||
lResp := FExecutor.ExecuteRequest(lReq);
|
||||
lResp := FExecutor.ExecuteNotification(lReq);
|
||||
ShowMessage(lResp.Error.ErrMessage);
|
||||
end;
|
||||
|
||||
procedure TMainForm.btnInvalid2Click(Sender: TObject);
|
||||
var
|
||||
lReq: IJSONRPCRequest;
|
||||
lReq: IJSONRPCNotification;
|
||||
lResp: IJSONRPCResponse;
|
||||
begin
|
||||
lReq := TJSONRPCRequest.Create;
|
||||
lReq := TJSONRPCNotification.Create;
|
||||
lReq.Method := 'invalidmethod2';
|
||||
lReq.Params.Add(1);
|
||||
lResp := FExecutor.ExecuteRequest(lReq);
|
||||
lResp := FExecutor.ExecuteNotification(lReq);
|
||||
ShowMessage(lResp.Error.ErrMessage);
|
||||
end;
|
||||
|
||||
@ -361,6 +386,14 @@ begin
|
||||
ShowMessage(lPerson.ToJSON(False));
|
||||
end;
|
||||
|
||||
procedure TMainForm.btnNotExistentClick(Sender: TObject);
|
||||
var
|
||||
lReq: IJSONRPCNotification;
|
||||
begin
|
||||
lReq := TJSONRPCNotification.Create('blablabla');
|
||||
FExecutor3.ExecuteNotification(lReq);
|
||||
end;
|
||||
|
||||
procedure TMainForm.edtGetCustomersClick(Sender: TObject);
|
||||
var
|
||||
lReq: IJSONRPCRequest;
|
||||
@ -379,6 +412,8 @@ procedure TMainForm.FormCreate(Sender: TObject);
|
||||
begin
|
||||
FExecutor := TMVCJSONRPCExecutor.Create('http://localhost:8080/jsonrpc');
|
||||
FExecutor2 := TMVCJSONRPCExecutor.Create('http://localhost:8080/rpcdatamodule');
|
||||
FExecutor3 := TMVCJSONRPCExecutor.Create('http://localhost:8080/jsonrpchooks');
|
||||
|
||||
dtNextMonday.Date := Date;
|
||||
|
||||
// these are the methods to handle http headers in JSONRPC
|
||||
@ -389,6 +424,7 @@ begin
|
||||
FExecutor.ClearHTTPHeaders;
|
||||
Assert(FExecutor.HTTPHeadersCount = 0);
|
||||
FExecutor.AddHTTPHeader(TNetHeader.Create('x-token', TGUID.NewGuid.ToString));
|
||||
PageControl1.ActivePageIndex := 0;
|
||||
end;
|
||||
|
||||
end.
|
||||
|
@ -59,6 +59,12 @@ begin
|
||||
Result := TdmMain.Create(nil);
|
||||
end, '/rpcdatamodule');
|
||||
|
||||
FMVC.PublishObject(
|
||||
function: TObject
|
||||
begin
|
||||
Result := TMyObjectWithHooks.Create;
|
||||
end, '/jsonrpchooks');
|
||||
|
||||
FMVC.AddMiddleware(TCORSMiddleware.Create());
|
||||
end;
|
||||
|
||||
|
@ -52,10 +52,10 @@ type
|
||||
function GetCustomersDataset: TFDMemTable;
|
||||
function GetPeopleDataset: TFDMemTable;
|
||||
public
|
||||
procedure OnBeforeCall(const JSONRequest: TJDOJsonObject);
|
||||
procedure OnBeforeRouting(const JSON: TJDOJsonObject);
|
||||
procedure OnBeforeSendResponse(
|
||||
const JSONResponse: TJDOJsonObject);
|
||||
procedure OnBeforeCall(const Context: TWebContext; const JSONRequest: TJDOJsonObject);
|
||||
procedure OnBeforeRouting(const Context: TWebContext; const JSON: TJDOJsonObject);
|
||||
procedure OnAfterCallHook(
|
||||
const Context: TWebContext; const JSONResponse: TJDOJsonObject);
|
||||
public
|
||||
[MVCDoc('You know, returns aValue1 - aValue2')]
|
||||
function Subtract(Value1, Value2: Integer): Integer;
|
||||
@ -79,6 +79,17 @@ type
|
||||
|
||||
end;
|
||||
|
||||
TMyObjectWithHooks = class
|
||||
public
|
||||
// hooks
|
||||
procedure OnBeforeRoutingHook(const Context: TWebContext; const JSON: TJsonObject);
|
||||
procedure OnBeforeCallHook(const Context: TWebContext; const JSON: TJsonObject);
|
||||
procedure OnAfterCallHook(const Context: TWebContext; const JSON: TJsonObject);
|
||||
// dummy method
|
||||
procedure DoSomething;
|
||||
procedure DoSomethingWithError;
|
||||
end;
|
||||
|
||||
TUtils = class sealed
|
||||
class function JSONObjectAs<T: constructor, class>(const JSON: TJsonObject): T;
|
||||
end;
|
||||
@ -300,26 +311,53 @@ end;
|
||||
|
||||
{ TMyObjectWithHooks }
|
||||
|
||||
procedure TMyObject.OnBeforeCall(const JSONRequest: TJDOJsonObject);
|
||||
procedure TMyObject.OnBeforeCall(const Context: TWebContext; const JSONRequest: TJDOJsonObject);
|
||||
begin
|
||||
Log.Info('TMyObjectWithHooks.OnBeforeCall >> ', 'jsonrpc');
|
||||
Log.Info(JSONRequest.ToJSON(false), 'jsonrpc');
|
||||
Log.Info('TMyObjectWithHooks.OnBeforeCall << ', 'jsonrpc');
|
||||
end;
|
||||
|
||||
procedure TMyObject.OnBeforeRouting(const JSON: TJDOJsonObject);
|
||||
procedure TMyObject.OnBeforeRouting(const Context: TWebContext; const JSON: TJDOJsonObject);
|
||||
begin
|
||||
Log.Info('TMyObjectWithHooks.OnBeforeRouting >> ', 'jsonrpc');
|
||||
Log.Info(JSON.ToJSON(false), 'jsonrpc');
|
||||
Log.Info('TMyObjectWithHooks.OnBeforeRouting << ', 'jsonrpc');
|
||||
end;
|
||||
|
||||
procedure TMyObject.OnBeforeSendResponse(
|
||||
const JSONResponse: TJDOJsonObject);
|
||||
procedure TMyObject.OnAfterCallHook(
|
||||
const Context: TWebContext; const JSONResponse: TJDOJsonObject);
|
||||
begin
|
||||
Log.Info('TMyObjectWithHooks.OnBeforeSendResponse >> ', 'jsonrpc');
|
||||
Log.Info(JSONResponse.ToJSON(false), 'jsonrpc');
|
||||
Log.Info('TMyObjectWithHooks.OnBeforeSendResponse << ', 'jsonrpc');
|
||||
end;
|
||||
|
||||
{ TMyObjectWithHooks }
|
||||
|
||||
procedure TMyObjectWithHooks.DoSomething;
|
||||
begin
|
||||
// do nothing
|
||||
end;
|
||||
|
||||
procedure TMyObjectWithHooks.DoSomethingWithError;
|
||||
begin
|
||||
raise Exception.Create('Boom');
|
||||
end;
|
||||
|
||||
procedure TMyObjectWithHooks.OnAfterCallHook(const Context: TWebContext; const JSON: TJsonObject);
|
||||
begin
|
||||
// do nothing
|
||||
end;
|
||||
|
||||
procedure TMyObjectWithHooks.OnBeforeCallHook(const Context: TWebContext; const JSON: TJsonObject);
|
||||
begin
|
||||
// do nothing
|
||||
end;
|
||||
|
||||
procedure TMyObjectWithHooks.OnBeforeRoutingHook(const Context: TWebContext; const JSON: TJsonObject);
|
||||
begin
|
||||
// do nothing
|
||||
end;
|
||||
|
||||
end.
|
||||
|
@ -6,6 +6,7 @@ uses
|
||||
System.SysUtils,
|
||||
MVCFramework.Logger,
|
||||
MVCFramework.Commons,
|
||||
MVCFramework.Console,
|
||||
MVCFramework.REPLCommandsHandlerU,
|
||||
Web.ReqMulti,
|
||||
Web.WebReq,
|
||||
@ -65,12 +66,17 @@ begin
|
||||
{ more info about ListenQueue
|
||||
http://www.indyproject.org/docsite/html/frames.html?frmname=topic&frmfile=TIdCustomTCPServer_ListenQueue.html }
|
||||
LServer.ListenQueue := 200;
|
||||
|
||||
SaveColors;
|
||||
TextColor(Yellow);
|
||||
WriteLn('Write "quit" or "exit" to shutdown the server');
|
||||
RestoreSavedColors;
|
||||
repeat
|
||||
if lCmd.IsEmpty then
|
||||
begin
|
||||
SaveColors;
|
||||
TextColor(Green);
|
||||
Write('-> ');
|
||||
RestoreSavedColors;
|
||||
ReadLn(lCmd)
|
||||
end;
|
||||
try
|
||||
@ -85,7 +91,10 @@ begin
|
||||
end;
|
||||
THandleCommandResult.Unknown:
|
||||
begin
|
||||
SaveColors;
|
||||
TextColor(Red);
|
||||
REPLEmit('Unknown command: ' + lCmd);
|
||||
RestoreSavedColors;
|
||||
end;
|
||||
end;
|
||||
finally
|
||||
@ -101,6 +110,7 @@ end;
|
||||
begin
|
||||
ReportMemoryLeaksOnShutdown := True;
|
||||
IsMultiThread := True;
|
||||
TextColor(TConsoleColor.White);
|
||||
try
|
||||
if WebRequestHandler <> nil then
|
||||
WebRequestHandler.WebModuleClass := WebModuleClass;
|
||||
|
@ -58,7 +58,12 @@ type
|
||||
end;
|
||||
|
||||
TMVCActiveRecordClass = class of TMVCActiveRecord;
|
||||
TMVCActiveRecordFieldOption = (foPrimaryKey, foAutoGenerated, foTransient);
|
||||
TMVCActiveRecordFieldOption = (
|
||||
foPrimaryKey, { it's the primary key of the mapped table }
|
||||
foAutoGenerated, { not written, read - similar to readonly }
|
||||
foTransient, { not written, not read }
|
||||
foReadOnly, { not written, read }
|
||||
foWriteOnly); { written, not read }
|
||||
TMVCActiveRecordFieldOptions = set of TMVCActiveRecordFieldOption;
|
||||
TMVCEntityAction = (eaCreate, eaRetrieve, eaUpdate, eaDelete);
|
||||
TMVCEntityActions = set of TMVCEntityAction;
|
||||
@ -81,7 +86,7 @@ type
|
||||
|
||||
TFieldInfo = class
|
||||
public
|
||||
// TableName: string;
|
||||
// TableName: string;
|
||||
FieldName: string;
|
||||
FieldOptions: TMVCActiveRecordFieldOptions;
|
||||
DataTypeName: string;
|
||||
@ -91,11 +96,13 @@ type
|
||||
|
||||
TFieldsMap = class(TObjectDictionary<TRTTIField, TFieldInfo>)
|
||||
private
|
||||
fNonTransientFieldsCount: Integer;
|
||||
fWritableFieldsCount: Integer;
|
||||
fReadableFieldsCount: Integer;
|
||||
public
|
||||
constructor Create;
|
||||
procedure EndUpdates;
|
||||
property NonTransientFieldsCount: Integer read fNonTransientFieldsCount;
|
||||
property WritableFieldsCount: Integer read fWritableFieldsCount;
|
||||
property ReadableFieldsCount: Integer read fWritableFieldsCount;
|
||||
function GetInfoByFieldName(const FieldName: string): TFieldInfo;
|
||||
end;
|
||||
|
||||
@ -155,7 +162,7 @@ type
|
||||
function GetPrimaryKeyIsAutogenerated: Boolean;
|
||||
procedure SetPrimaryKeyIsAutogenerated(const Value: Boolean);
|
||||
function GetPrimaryKeyFieldType: TFieldType;
|
||||
procedure SetTableName(const Value: String);
|
||||
procedure SetTableName(const Value: string);
|
||||
protected
|
||||
fRTTIType: TRttiInstanceType;
|
||||
fProps: TArray<TRTTIField>;
|
||||
@ -282,7 +289,7 @@ type
|
||||
procedure AddChildren(const ChildObject: TObject);
|
||||
procedure RemoveChildren(const ChildObject: TObject);
|
||||
[MVCDoNotSerialize]
|
||||
property TableName: String read fTableName write SetTableName;
|
||||
property TableName: string read fTableName write SetTableName;
|
||||
[MVCDoNotSerialize]
|
||||
property PrimaryKeyIsAutogenerated: Boolean read GetPrimaryKeyIsAutogenerated write SetPrimaryKeyIsAutogenerated;
|
||||
class function GetByPK(const aClass: TMVCActiveRecordClass; const aValue: int64;
|
||||
@ -452,7 +459,7 @@ type
|
||||
// end-capabilities
|
||||
function CreateSQLWhereByRQL(const RQL: string; const Mapping: TMVCFieldsMapping;
|
||||
const UseArtificialLimit: Boolean = True;
|
||||
const UseFilterOnly: Boolean = False
|
||||
const UseFilterOnly: Boolean = false
|
||||
): string; virtual; abstract;
|
||||
function CreateSelectSQL(const TableName: string; const Map: TFieldsMap;
|
||||
const PKFieldName: string; const PKOptions: TMVCActiveRecordFieldOptions): string; virtual; abstract;
|
||||
@ -603,6 +610,13 @@ begin
|
||||
lName := aName.ToLower;
|
||||
lConnKeyName := GetKeyName(lName);
|
||||
|
||||
{ If the transaction is not started, initialize TxIsolation as ReadCommitted }
|
||||
if aConnection.Transaction = nil then
|
||||
begin
|
||||
{ needed for Delphi 10.4 Sydney+ }
|
||||
aConnection.TxOptions.Isolation := TFDTxIsolation.xiReadCommitted;
|
||||
end;
|
||||
|
||||
fMREW.BeginWrite;
|
||||
try
|
||||
lConnHolder := TConnHolder.Create;
|
||||
@ -955,7 +969,7 @@ begin
|
||||
begin
|
||||
fPrimaryKeyFieldType := ftLargeInt;
|
||||
end
|
||||
else if lPrimaryFieldTypeAsStr.EndsWith('integer') then
|
||||
else if lPrimaryFieldTypeAsStr.EndsWith('integer') or lPrimaryFieldTypeAsStr.EndsWith('int32') then
|
||||
begin
|
||||
fPrimaryKeyFieldType := ftInteger;
|
||||
end
|
||||
@ -975,9 +989,8 @@ begin
|
||||
continue;
|
||||
end;
|
||||
|
||||
{ TODO -oDanieleT -cGeneral : Definire TFieldInfo per tute le info del field }
|
||||
lFieldInfo := TFieldInfo.Create;
|
||||
//lFieldInfo.TableName := fTableName;
|
||||
// lFieldInfo.TableName := fTableName;
|
||||
lFieldInfo.FieldName := MVCTableFieldAttribute(lAttribute).FieldName;
|
||||
lFieldInfo.FieldOptions := MVCTableFieldAttribute(lAttribute).FieldOptions;
|
||||
lFieldInfo.DataTypeName := MVCTableFieldAttribute(lAttribute).DataTypeName;
|
||||
@ -1010,10 +1023,11 @@ begin
|
||||
OnValidation(TMVCEntityAction.eaCreate);
|
||||
OnBeforeInsert;
|
||||
OnBeforeInsertOrUpdate;
|
||||
if fMap.NonTransientFieldsCount = 0 then
|
||||
if fMap.WritableFieldsCount = 0 then
|
||||
begin
|
||||
raise EMVCActiveRecord.CreateFmt
|
||||
('Cannot insert an entity if all fields are transient. Class [%s] mapped on table [%s]', [ClassName, fTableName]);
|
||||
('Cannot insert an entity if all fields are not writable or transient. Class [%s] mapped on table [%s]',
|
||||
[ClassName, fTableName]);
|
||||
end;
|
||||
if (foAutoGenerated in fPrimaryKeyOptions) then
|
||||
begin
|
||||
@ -1049,7 +1063,7 @@ begin
|
||||
lSQL := Self.SQLGenerator.CreateSelectCount(fTableName);
|
||||
if not RQL.IsEmpty then
|
||||
begin
|
||||
lSQL := lSQL + fSQLGenerator.CreateSQLWhereByRQL(RQL, GetMapping, False, True);
|
||||
lSQL := lSQL + fSQLGenerator.CreateSQLWhereByRQL(RQL, GetMapping, false, True);
|
||||
end;
|
||||
Result := GetScalar(lSQL, []);
|
||||
end;
|
||||
@ -1135,19 +1149,24 @@ class function TMVCActiveRecordHelper.GetByPK<T>(const aValue: int64;
|
||||
const RaiseExceptionIfNotFound: Boolean = True): T;
|
||||
var
|
||||
lActiveRecord: TMVCActiveRecord;
|
||||
lLoaded: Boolean;
|
||||
begin
|
||||
Result := T.Create;
|
||||
lActiveRecord := TMVCActiveRecord(Result);
|
||||
if not lActiveRecord.LoadByPK(aValue) then
|
||||
|
||||
try
|
||||
lLoaded := lActiveRecord.LoadByPK(aValue);
|
||||
except
|
||||
FreeAndNil(Result);
|
||||
raise;
|
||||
end;
|
||||
|
||||
if not lLoaded then
|
||||
begin
|
||||
Result.Free;
|
||||
FreeAndNil(Result);
|
||||
if RaiseExceptionIfNotFound then
|
||||
begin
|
||||
raise EMVCActiveRecordNotFound.Create('Data not found');
|
||||
end
|
||||
else
|
||||
begin
|
||||
Result := nil;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
@ -2176,23 +2195,14 @@ begin
|
||||
if fPrimaryKey.GetValue(Self).Kind = tkRecord then
|
||||
begin
|
||||
lPKValue := fPrimaryKey.GetValue(Self);
|
||||
if lPKValue.IsType<NullableInt32> then
|
||||
if lPKValue.IsType<NullableInt32> and aValue.IsType<NullableInt32>() then
|
||||
begin
|
||||
if aValue.IsType<UInt32> then
|
||||
begin
|
||||
lPKValue := TValue.From<NullableInt32>(IntToNullableInt(aValue.AsInteger));
|
||||
end;
|
||||
//
|
||||
// if aValue.AsType<NullableInt32>().HasValue then
|
||||
// begin
|
||||
// lPKValue := aValue;
|
||||
// end
|
||||
// else
|
||||
// begin
|
||||
// lPKValue.AsType<NullableInt32>().Clear;
|
||||
// end;
|
||||
end
|
||||
else if lPKValue.IsType<NullableInt64> then
|
||||
else if lPKValue.IsType<NullableInt64> and aValue.IsType<NullableInt64>() then
|
||||
begin
|
||||
if aValue.AsType<NullableInt64>().HasValue then
|
||||
begin
|
||||
@ -2203,7 +2213,18 @@ begin
|
||||
lPKValue.AsType<NullableInt64>().Clear;
|
||||
end;
|
||||
end
|
||||
else if lPKValue.IsType<NullableUInt32> then
|
||||
else if lPKValue.IsType<NullableString> and aValue.IsType<NullableString>() then
|
||||
begin
|
||||
if aValue.AsType<NullableString>().HasValue then
|
||||
begin
|
||||
lPKValue := aValue;
|
||||
end
|
||||
else
|
||||
begin
|
||||
lPKValue.AsType<NullableString>().Clear;
|
||||
end;
|
||||
end
|
||||
else if lPKValue.IsType<NullableUInt32> and aValue.IsType<NullableUInt32>() then
|
||||
begin
|
||||
if aValue.AsType<NullableUInt32>().HasValue then
|
||||
begin
|
||||
@ -2214,7 +2235,7 @@ begin
|
||||
lPKValue.AsType<NullableUInt32>().Clear;
|
||||
end;
|
||||
end
|
||||
else if lPKValue.IsType<NullableUInt64> then
|
||||
else if lPKValue.IsType<NullableUInt64> and aValue.IsType<NullableUInt64>() then
|
||||
begin
|
||||
if aValue.AsType<NullableUInt64>().HasValue then
|
||||
begin
|
||||
@ -2226,7 +2247,9 @@ begin
|
||||
end;
|
||||
end
|
||||
else
|
||||
raise EMVCActiveRecord.Create('Invalid type for primary key');
|
||||
begin
|
||||
raise EMVCActiveRecord.Create('Invalid type for primary key [HINT] Double check if TypeInfo(PK) is equal to TypeInfo(Value)');
|
||||
end;
|
||||
fPrimaryKey.SetValue(Self, lPKValue);
|
||||
end
|
||||
else
|
||||
@ -2247,7 +2270,7 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TMVCActiveRecord.SetTableName(const Value: String);
|
||||
procedure TMVCActiveRecord.SetTableName(const Value: string);
|
||||
begin
|
||||
fTableName := Value;
|
||||
end;
|
||||
@ -2361,7 +2384,7 @@ begin
|
||||
OnValidation(TMVCEntityAction.eaUpdate);
|
||||
OnBeforeUpdate;
|
||||
OnBeforeInsertOrUpdate;
|
||||
if fMap.NonTransientFieldsCount = 0 then
|
||||
if fMap.WritableFieldsCount = 0 then
|
||||
begin
|
||||
raise EMVCActiveRecord.CreateFmt
|
||||
('Cannot update an entity if all fields are transient. Class [%s] mapped on table [%s]', [ClassName, fTableName]);
|
||||
@ -2378,7 +2401,7 @@ begin
|
||||
begin
|
||||
fChildren := TObjectList<TObject>.Create(True);
|
||||
end;
|
||||
if not(fChildren.Contains(ChildObject)) and (not (ChildObject = Self)) then
|
||||
if not(fChildren.Contains(ChildObject)) and (not(ChildObject = Self)) then
|
||||
begin
|
||||
fChildren.Add(ChildObject);
|
||||
end;
|
||||
@ -2659,7 +2682,14 @@ begin
|
||||
Result := Copy(Result, 1, Length(Result) - Length(Delimiter));
|
||||
if not PKFieldName.IsEmpty then
|
||||
begin
|
||||
Result := GetFieldNameForSQL(PKFieldName) + ', ' + Result;
|
||||
if not Result.IsEmpty then
|
||||
begin
|
||||
Result := GetFieldNameForSQL(PKFieldName) + ', ' + Result
|
||||
end
|
||||
else
|
||||
begin
|
||||
Result := GetFieldNameForSQL(PKFieldName)
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
@ -2744,20 +2774,27 @@ end;
|
||||
constructor TFieldsMap.Create;
|
||||
begin
|
||||
inherited Create([doOwnsValues]);
|
||||
fNonTransientFieldsCount := 0;
|
||||
fWritableFieldsCount := -1;
|
||||
fReadableFieldsCount := -1;
|
||||
end;
|
||||
|
||||
procedure TFieldsMap.EndUpdates;
|
||||
var
|
||||
lPair: TPair<TRTTIField, TFieldInfo>;
|
||||
begin
|
||||
fNonTransientFieldsCount := 0;
|
||||
fWritableFieldsCount := 0;
|
||||
fReadableFieldsCount := 0;
|
||||
for lPair in Self do
|
||||
begin
|
||||
lPair.Value.EndUpdates;
|
||||
if not(foTransient in lPair.Value.FieldOptions) then
|
||||
// if not(foTransient in lPair.Value.FieldOptions) then
|
||||
if lPair.Value.Writeable then
|
||||
begin
|
||||
Inc(fNonTransientFieldsCount);
|
||||
Inc(fWritableFieldsCount);
|
||||
end;
|
||||
if lPair.Value.Readable then
|
||||
begin
|
||||
Inc(fReadableFieldsCount);
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
@ -2781,8 +2818,18 @@ end;
|
||||
|
||||
procedure TFieldInfo.EndUpdates;
|
||||
begin
|
||||
Writeable := (not FieldName.IsEmpty) and (not((foAutoGenerated in FieldOptions) or (foTransient in FieldOptions)));
|
||||
Readable := not(foTransient in FieldOptions) and (not FieldName.IsEmpty);
|
||||
if FieldName.IsEmpty then
|
||||
begin
|
||||
Writeable := false;
|
||||
Readable := false;
|
||||
end
|
||||
else
|
||||
begin
|
||||
// Writeable := (not (foReadOnly in FieldOptions)) and (not((foAutoGenerated in FieldOptions) or (foTransient in FieldOptions)));
|
||||
Writeable := ((FieldOptions * [foReadOnly, foTransient, foAutoGenerated]) = []);
|
||||
// Readable := (not (foWriteOnly in FieldOptions)) and (not(foTransient in FieldOptions));
|
||||
Readable := (FieldOptions * [foWriteOnly, foTransient]) = [];
|
||||
end;
|
||||
end;
|
||||
|
||||
initialization
|
||||
|
@ -38,7 +38,8 @@ type
|
||||
IMVCJSONRPCExecutor = interface
|
||||
['{55415094-9D28-4707-AEC5-5FCF925E82BC}']
|
||||
function ExecuteRequest(const aJSONRPCRequest: IJSONRPCRequest): IJSONRPCResponse;
|
||||
procedure ExecuteNotification(const aJSONRPCNotification: IJSONRPCNotification);
|
||||
function ExecuteNotification(const aJSONRPCNotification: IJSONRPCNotification): IJSONRPCResponse;
|
||||
function HTTPResponse: IHTTPResponse;
|
||||
// Http headers handling
|
||||
procedure AddHTTPHeader(const aNetHeader: TNetHeader);
|
||||
procedure ClearHTTPHeaders;
|
||||
@ -56,17 +57,19 @@ type
|
||||
|
||||
TMVCJSONRPCExecutor = class(TInterfacedObject, IMVCJSONRPCExecutor)
|
||||
private
|
||||
FURL: string;
|
||||
FHTTP: THTTPClient;
|
||||
FRaiseExceptionOnError: Boolean;
|
||||
FHTTPRequestHeaders: TList<TNetHeader>;
|
||||
fURL: string;
|
||||
fHTTP: THTTPClient;
|
||||
fRaiseExceptionOnError: Boolean;
|
||||
fHTTPRequestHeaders: TList<TNetHeader>;
|
||||
fHTTPResponse: IHTTPResponse;
|
||||
fOnReceiveResponse: TProc<IJSONRPCObject, IJSONRPCObject>;
|
||||
fOnSendCommand: TProc<IJSONRPCObject>;
|
||||
function GetHTTPRequestHeaders: TList<TNetHeader>;
|
||||
protected
|
||||
function HTTPResponse: IHTTPResponse;
|
||||
function InternalExecute(const aJSONRPCObject: IJSONRPCObject): IJSONRPCResponse;
|
||||
function ExecuteRequest(const aJSONRPCRequest: IJSONRPCRequest): IJSONRPCResponse;
|
||||
procedure ExecuteNotification(const aJSONRPCNotification: IJSONRPCNotification);
|
||||
function ExecuteNotification(const aJSONRPCNotification: IJSONRPCNotification): IJSONRPCResponse;
|
||||
// Http headers handling
|
||||
procedure AddHTTPHeader(const aNetHeader: TNetHeader);
|
||||
procedure ClearHTTPHeaders;
|
||||
@ -128,26 +131,26 @@ end;
|
||||
|
||||
procedure TMVCJSONRPCExecutor.ClearHTTPHeaders;
|
||||
begin
|
||||
if Assigned(FHTTPRequestHeaders) then
|
||||
if Assigned(fHTTPRequestHeaders) then
|
||||
begin
|
||||
FHTTPRequestHeaders.Clear;
|
||||
fHTTPRequestHeaders.Clear;
|
||||
end;
|
||||
end;
|
||||
|
||||
function TMVCJSONRPCExecutor.ConfigureHTTPClient(
|
||||
const aConfigProc: TProc<THTTPClient>): IMVCJSONRPCExecutor;
|
||||
begin
|
||||
aConfigProc(FHTTP);
|
||||
aConfigProc(fHTTP);
|
||||
end;
|
||||
|
||||
constructor TMVCJSONRPCExecutor.Create(const aURL: string; const aRaiseExceptionOnError: Boolean = True);
|
||||
begin
|
||||
inherited Create;
|
||||
FRaiseExceptionOnError := aRaiseExceptionOnError;
|
||||
FURL := aURL;
|
||||
FHTTP := THTTPClient.Create;
|
||||
FHTTP.ResponseTimeout := MaxInt;
|
||||
FHTTPRequestHeaders := nil;
|
||||
fRaiseExceptionOnError := aRaiseExceptionOnError;
|
||||
fURL := aURL;
|
||||
fHTTP := THTTPClient.Create;
|
||||
fHTTP.ResponseTimeout := MaxInt;
|
||||
fHTTPRequestHeaders := nil;
|
||||
SetOnReceiveResponse(nil)
|
||||
.SetOnReceiveData(nil)
|
||||
.SetOnNeedClientCertificate(nil)
|
||||
@ -156,15 +159,24 @@ end;
|
||||
|
||||
destructor TMVCJSONRPCExecutor.Destroy;
|
||||
begin
|
||||
FHTTP.Free;
|
||||
FHTTPRequestHeaders.Free;
|
||||
fHTTP.Free;
|
||||
fHTTPRequestHeaders.Free;
|
||||
inherited;
|
||||
end;
|
||||
|
||||
procedure TMVCJSONRPCExecutor.ExecuteNotification(const aJSONRPCNotification: IJSONRPCNotification);
|
||||
function TMVCJSONRPCExecutor.ExecuteNotification(const aJSONRPCNotification: IJSONRPCNotification): IJSONRPCResponse;
|
||||
// var
|
||||
// lResp: IJSONRPCResponse;
|
||||
begin
|
||||
if InternalExecute(aJSONRPCNotification as TJSONRPCObject) <> nil then
|
||||
raise EMVCJSONRPCException.Create('A "notification" cannot returns a response. Use ExecuteRequest instead.');
|
||||
Result := InternalExecute(aJSONRPCNotification as TJSONRPCObject);
|
||||
// if Assigned(lResp) then
|
||||
// begin
|
||||
//
|
||||
// end;
|
||||
// if InternalExecute(aJSONRPCNotification as TJSONRPCObject) <> nil then
|
||||
// begin
|
||||
// raise EMVCJSONRPCException.Create('A "notification" cannot returns a response. Use ExecuteRequest instead.');
|
||||
// end;
|
||||
end;
|
||||
|
||||
function TMVCJSONRPCExecutor.ExecuteRequest(const aJSONRPCRequest: IJSONRPCRequest): IJSONRPCResponse;
|
||||
@ -174,18 +186,18 @@ end;
|
||||
|
||||
function TMVCJSONRPCExecutor.GetHTTPRequestHeaders: TList<TNetHeader>;
|
||||
begin
|
||||
if not Assigned(FHTTPRequestHeaders) then
|
||||
if not Assigned(fHTTPRequestHeaders) then
|
||||
begin
|
||||
FHTTPRequestHeaders := TList<TNetHeader>.Create;
|
||||
fHTTPRequestHeaders := TList<TNetHeader>.Create;
|
||||
end;
|
||||
Result := FHTTPRequestHeaders;
|
||||
Result := fHTTPRequestHeaders;
|
||||
end;
|
||||
|
||||
function TMVCJSONRPCExecutor.HTTPHeadersCount: Integer;
|
||||
begin
|
||||
if Assigned(FHTTPRequestHeaders) then
|
||||
if Assigned(fHTTPRequestHeaders) then
|
||||
begin
|
||||
Result := FHTTPRequestHeaders.Count;
|
||||
Result := fHTTPRequestHeaders.Count;
|
||||
end
|
||||
else
|
||||
begin
|
||||
@ -201,9 +213,9 @@ var
|
||||
lCustomHeaders: TNetHeaders;
|
||||
begin
|
||||
lCustomHeaders := [];
|
||||
if Assigned(FHTTPRequestHeaders) then
|
||||
if Assigned(fHTTPRequestHeaders) then
|
||||
begin
|
||||
lCustomHeaders := FHTTPRequestHeaders.ToArray;
|
||||
lCustomHeaders := fHTTPRequestHeaders.ToArray;
|
||||
end;
|
||||
|
||||
Result := nil;
|
||||
@ -214,37 +226,49 @@ begin
|
||||
begin
|
||||
fOnSendCommand(aJSONRPCObject);
|
||||
end;
|
||||
lHttpResp := FHTTP.Post(FURL, lSS, nil, [TNetHeader.Create('content-type', 'application/json;charset=utf8'),
|
||||
fHTTPResponse := nil;
|
||||
lHttpResp := fHTTP.Post(fURL, lSS, nil, [TNetHeader.Create('content-type', 'application/json;charset=utf8'),
|
||||
TNetHeader.Create('accept', 'application/json;charset=utf8')] + lCustomHeaders);
|
||||
if (lHttpResp.StatusCode <> HTTP_STATUS.NoContent) then
|
||||
fHTTPResponse := lHttpResp;
|
||||
if lHttpResp.StatusCode = HTTP_STATUS.NoContent then
|
||||
begin
|
||||
lJSONRPCResponse := TJSONRPCNullResponse.Create;
|
||||
end
|
||||
else
|
||||
begin
|
||||
lJSONRPCResponse := TJSONRPCResponse.Create;
|
||||
lJSONRPCResponse.AsJSONString := lHttpResp.ContentAsString;
|
||||
if Assigned(fOnReceiveResponse) then
|
||||
begin
|
||||
fOnReceiveResponse(aJSONRPCObject, lJSONRPCResponse);
|
||||
end;
|
||||
if Assigned(lJSONRPCResponse.Error) and FRaiseExceptionOnError then
|
||||
raise EMVCJSONRPCException.CreateFmt('[REMOTE EXCEPTION][%d]: %s',
|
||||
[lJSONRPCResponse.Error.Code, lJSONRPCResponse.Error.ErrMessage]);
|
||||
Result := lJSONRPCResponse;
|
||||
end;
|
||||
if Assigned(fOnReceiveResponse) then
|
||||
begin
|
||||
fOnReceiveResponse(aJSONRPCObject, lJSONRPCResponse);
|
||||
end;
|
||||
fHTTPResponse := lHttpResp;
|
||||
if Assigned(lJSONRPCResponse.Error) and fRaiseExceptionOnError then
|
||||
raise EMVCJSONRPCException.CreateFmt('[REMOTE EXCEPTION][%d]: %s',
|
||||
[lJSONRPCResponse.Error.Code, lJSONRPCResponse.Error.ErrMessage]);
|
||||
Result := lJSONRPCResponse;
|
||||
finally
|
||||
lSS.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
function TMVCJSONRPCExecutor.HTTPResponse: IHTTPResponse;
|
||||
begin
|
||||
Result := fHTTPResponse;
|
||||
end;
|
||||
|
||||
function TMVCJSONRPCExecutor.SetOnNeedClientCertificate(const aOnNeedClientCertificate: TNeedClientCertificateEvent)
|
||||
: IMVCJSONRPCExecutor;
|
||||
begin
|
||||
FHTTP.OnNeedClientCertificate := aOnNeedClientCertificate;
|
||||
fHTTP.OnNeedClientCertificate := aOnNeedClientCertificate;
|
||||
Result := Self;
|
||||
end;
|
||||
|
||||
function TMVCJSONRPCExecutor.SetOnReceiveData(
|
||||
const aOnReceiveData: TReceiveDataEvent): IMVCJSONRPCExecutor;
|
||||
begin
|
||||
FHTTP.OnReceiveData := aOnReceiveData;
|
||||
fHTTP.OnReceiveData := aOnReceiveData;
|
||||
Result := Self;
|
||||
end;
|
||||
|
||||
@ -265,7 +289,7 @@ end;
|
||||
function TMVCJSONRPCExecutor.SetOnValidateServerCertificate(const aOnValidateServerCertificate
|
||||
: TValidateCertificateEvent): IMVCJSONRPCExecutor;
|
||||
begin
|
||||
FHTTP.OnValidateServerCertificate := aOnValidateServerCertificate;
|
||||
fHTTP.OnValidateServerCertificate := aOnValidateServerCertificate;
|
||||
Result := Self;
|
||||
end;
|
||||
|
||||
|
@ -58,9 +58,9 @@ const
|
||||
const
|
||||
JSONRPC_HOOKS_ON_BEFORE_ROUTING = 'OnBeforeRoutingHook';
|
||||
JSONRPC_HOOKS_ON_BEFORE_CALL = 'OnBeforeCallHook';
|
||||
JSONRPC_HOOKS_ON_BEFORE_SEND_RESPONSE = 'OnBeforeSendResponseHook';
|
||||
JSONRPC_HOOKS_ON_AFTER_CALL = 'OnAfterCallHook';
|
||||
JSONRPC_HOOKS_METHOD_NAMES: array [0 .. 2] of string = (JSONRPC_HOOKS_ON_BEFORE_ROUTING,
|
||||
JSONRPC_HOOKS_ON_BEFORE_CALL, JSONRPC_HOOKS_ON_BEFORE_SEND_RESPONSE);
|
||||
JSONRPC_HOOKS_ON_BEFORE_CALL, JSONRPC_HOOKS_ON_AFTER_CALL);
|
||||
|
||||
{
|
||||
http://www.jsonrpc.org/historical/json-rpc-over-http.html#response-codes
|
||||
@ -261,8 +261,8 @@ type
|
||||
FResult: TValue;
|
||||
FError: TJSONRPCResponseError;
|
||||
FID: TValue;
|
||||
function GetResult: TValue;
|
||||
protected
|
||||
function GetResult: TValue;
|
||||
function GetJSON: TJDOJsonObject; override;
|
||||
procedure SetJSON(const JSON: TJDOJsonObject); override;
|
||||
procedure SetID(const Value: TValue);
|
||||
@ -281,6 +281,29 @@ type
|
||||
destructor Destroy; override;
|
||||
end;
|
||||
|
||||
TJSONRPCNullResponse = class(TJSONRPCObject, IJSONRPCResponse)
|
||||
private
|
||||
FError: TJSONRPCResponseError;
|
||||
procedure RaiseErrorForNullObject;
|
||||
protected
|
||||
function GetJSONString: string; override;
|
||||
procedure SetJsonString(const Value: string); override;
|
||||
function GetJSON: TJDOJsonObject; override;
|
||||
procedure SetJSON(const JSON: TJDOJsonObject); override;
|
||||
procedure SetID(const Value: TValue);
|
||||
procedure SetResult(const Value: TValue);
|
||||
procedure SetError(const Value: TJSONRPCResponseError);
|
||||
function GetError: TJSONRPCResponseError;
|
||||
function GetID: TValue;
|
||||
function GetResult: TValue;
|
||||
function ResultAsJSONObject: TJDOJsonObject;
|
||||
function ResultAsJSONArray: TJDOJsonArray;
|
||||
function IsError: Boolean;
|
||||
property Result: TValue read GetResult write SetResult;
|
||||
property Error: TJSONRPCResponseError read GetError write SetError;
|
||||
property RequestID: TValue read GetID write SetID;
|
||||
end;
|
||||
|
||||
EMVCJSONRPCInvalidVersion = class(Exception)
|
||||
|
||||
end;
|
||||
@ -357,7 +380,7 @@ type
|
||||
function CanBeRemotelyInvoked(const RTTIMethod: TRTTIMethod): Boolean;
|
||||
procedure ForEachInvokableMethod(const aProc: TProc<TRTTIMethod>);
|
||||
procedure TryToCallMethod(const aRTTIType: TRttiType; const MethodName: string;
|
||||
const Parameter: TJDOJsonObject; const ParameterName: string);
|
||||
const Parameter: TJDOJsonObject);
|
||||
public
|
||||
[MVCPath]
|
||||
[MVCHTTPMethods([httpPOST])]
|
||||
@ -984,6 +1007,7 @@ var
|
||||
lClass: TJSONRPCProxyGeneratorClass;
|
||||
lGenerator: TJSONRPCProxyGenerator;
|
||||
lRTTI: TRTTIContext;
|
||||
lContentType: string;
|
||||
begin
|
||||
lLanguage := Context.Request.Params['language'].ToLower;
|
||||
if lLanguage.IsEmpty then
|
||||
@ -991,6 +1015,15 @@ begin
|
||||
lLanguage := 'delphi';
|
||||
end;
|
||||
|
||||
if Context.Request.QueryStringParamExists('content-type') then
|
||||
begin
|
||||
lContentType := Context.Request.Params['content-type'];
|
||||
end
|
||||
else
|
||||
begin
|
||||
lContentType := 'text/plain';
|
||||
end;
|
||||
|
||||
if not Assigned(GProxyGeneratorsRegister) then
|
||||
begin
|
||||
raise EMVCJSONRPCException.Create
|
||||
@ -1013,7 +1046,7 @@ begin
|
||||
lGenerator.VisitMethod(aRTTIMethod);
|
||||
end);
|
||||
lGenerator.EndGeneration();
|
||||
Context.Response.ContentType := 'text/plain';
|
||||
Context.Response.ContentType := lContentType;
|
||||
Render(lGenerator.GetCode);
|
||||
finally
|
||||
lRTTI.Free;
|
||||
@ -1069,7 +1102,11 @@ var
|
||||
lReqID: TValue;
|
||||
lJSON: TJDOJsonObject;
|
||||
lJSONResp: TJDOJsonObject;
|
||||
lBeforeCallHookHasBeenInvoked: Boolean;
|
||||
lAfterCallHookHasBeenInvoked: Boolean;
|
||||
begin
|
||||
lBeforeCallHookHasBeenInvoked := False;
|
||||
lAfterCallHookHasBeenInvoked := False;
|
||||
lRTTIType := nil;
|
||||
lReqID := TValue.Empty;
|
||||
SetLength(lParamsToInject, 0);
|
||||
@ -1079,20 +1116,21 @@ begin
|
||||
lJSON := StrToJSONObject(Context.Request.Body);
|
||||
try
|
||||
if not Assigned(lJSON) then
|
||||
begin
|
||||
raise EMVCJSONRPCParseError.Create;
|
||||
end;
|
||||
lRTTIType := lRTTI.GetType(fRPCInstance.ClassType);
|
||||
TryToCallMethod(lRTTIType, JSONRPC_HOOKS_ON_BEFORE_ROUTING, lJSON, 'JSON');
|
||||
lJSONRPCReq := CreateRequest(lJSON);
|
||||
lMethod := lJSONRPCReq.Method;
|
||||
|
||||
if SameText(lMethod, JSONRPC_HOOKS_ON_BEFORE_ROUTING) or
|
||||
SameText(lMethod, JSONRPC_HOOKS_ON_BEFORE_CALL) or
|
||||
SameText(lMethod, JSONRPC_HOOKS_ON_BEFORE_SEND_RESPONSE) then
|
||||
if IsReservedMethodName(lMethod) then
|
||||
begin
|
||||
raise EMVCJSONRPCInvalidRequest.Create
|
||||
('Requested method name is reserved and cannot be called remotely');
|
||||
raise EMVCJSONRPCInvalidRequest.CreateFmt
|
||||
('Requested method name [%s] is reserved and cannot be called remotely', [lMethod]);
|
||||
end;
|
||||
|
||||
TryToCallMethod(lRTTIType, JSONRPC_HOOKS_ON_BEFORE_ROUTING, lJSON);
|
||||
|
||||
if lJSONRPCReq.RequestType = TJSONRPCRequestType.Request then
|
||||
begin
|
||||
if lJSONRPCReq.RequestID.IsEmpty then
|
||||
@ -1108,14 +1146,6 @@ begin
|
||||
|
||||
if Assigned(lRTTIMethod) then
|
||||
begin
|
||||
if not CanBeRemotelyInvoked(lRTTIMethod) then
|
||||
begin
|
||||
LogW(Format
|
||||
('Method "%s" cannot be called. Only public functions or procedures can be called. ',
|
||||
[lMethod]));
|
||||
raise EMVCJSONRPCMethodNotFound.Create(lMethod);
|
||||
end;
|
||||
|
||||
if (lJSONRPCReq.RequestType = TJSONRPCRequestType.Request) and
|
||||
(lRTTIMethod.MethodKind <> mkFunction) then
|
||||
begin
|
||||
@ -1130,6 +1160,14 @@ begin
|
||||
('Cannot call a function using a JSON-RPC notification. [HINT] Use requests for functions and notifications for procedures');
|
||||
end;
|
||||
|
||||
if not CanBeRemotelyInvoked(lRTTIMethod) then
|
||||
begin
|
||||
LogW(Format
|
||||
('Method [%s] cannot remotely invoked. Only public functions or procedures can be called.',
|
||||
[lMethod]));
|
||||
raise EMVCJSONRPCMethodNotFound.Create(lMethod);
|
||||
end;
|
||||
|
||||
try
|
||||
lJSONRPCReq.FillParameters(lJSON, lRTTIMethod);
|
||||
except
|
||||
@ -1140,9 +1178,13 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
lJSONResp := nil;
|
||||
// try
|
||||
TryToCallMethod(lRTTIType, JSONRPC_HOOKS_ON_BEFORE_CALL, lJSON);
|
||||
lBeforeCallHookHasBeenInvoked := True;
|
||||
try
|
||||
TryToCallMethod(lRTTIType, JSONRPC_HOOKS_ON_BEFORE_CALL, lJSON, 'JSONRequest');
|
||||
LogD('[JSON-RPC][CALL][' + CALL_TYPE[lRTTIMethod.MethodKind] + '] ' + lRTTIMethod.Name);
|
||||
LogD('[JSON-RPC][CALL][' + CALL_TYPE[lRTTIMethod.MethodKind] + '][' + fRPCInstance.ClassName + '.' +
|
||||
lRTTIMethod.Name + ']');
|
||||
lRes := lRTTIMethod.Invoke(fRPCInstance, lJSONRPCReq.Params.ToArray);
|
||||
except
|
||||
on E: EInvalidCast do
|
||||
@ -1165,21 +1207,30 @@ begin
|
||||
lJSONRPCResponse := CreateResponse(lJSONRPCReq.RequestID, lRes);
|
||||
ResponseStatus(200);
|
||||
lJSONResp := lJSONRPCResponse.AsJSON;
|
||||
try
|
||||
TryToCallMethod(lRTTIType, JSONRPC_HOOKS_ON_BEFORE_SEND_RESPONSE, lJSONResp,
|
||||
'JSONResponse');
|
||||
Render(lJSONResp);
|
||||
except
|
||||
try
|
||||
lJSONResp.Free;
|
||||
except
|
||||
// do nothing
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
else
|
||||
raise EMVCJSONRPCException.Create('Invalid RequestType');
|
||||
end;
|
||||
|
||||
// finally
|
||||
// if lBeforeCallHookHasBeenInvoked then
|
||||
// begin
|
||||
// TryToCallMethod(lRTTIType, JSONRPC_HOOKS_ON_AFTER_CALL, lJSONResp);
|
||||
// lAfterCallHookHasBeenInvoked := True;
|
||||
// end;
|
||||
// if lJSONResp <> nil then
|
||||
// begin
|
||||
// try
|
||||
// Render(lJSONResp);
|
||||
// except
|
||||
// try
|
||||
// lJSONResp.Free;
|
||||
// except
|
||||
// // do nothing
|
||||
// end;
|
||||
// end;
|
||||
// end;
|
||||
// end;
|
||||
end
|
||||
else
|
||||
begin
|
||||
@ -1187,9 +1238,8 @@ begin
|
||||
[lMethod, fRPCInstance.QualifiedClassName]));
|
||||
raise EMVCJSONRPCMethodNotFound.Create(lMethod);
|
||||
end;
|
||||
|
||||
finally
|
||||
lJSON.Free;
|
||||
FreeAndNil(lJSON);
|
||||
end;
|
||||
except
|
||||
on E: EMVCJSONRPCErrorResponse do
|
||||
@ -1218,28 +1268,33 @@ begin
|
||||
JSONRPC_ERR_SERVER_ERROR_LOWERBOUND .. JSONRPC_ERR_SERVER_ERROR_UPPERBOUND:
|
||||
ResponseStatus(500);
|
||||
end;
|
||||
lJSON := CreateError(lReqID, E.JSONRPCErrorCode, E.Message);
|
||||
try
|
||||
TryToCallMethod(lRTTIType, JSONRPC_HOOKS_ON_BEFORE_SEND_RESPONSE, lJSON, 'JSONResponse');
|
||||
Render(lJSON, False);
|
||||
finally
|
||||
lJSON.Free;
|
||||
end;
|
||||
lJSONResp := CreateError(lReqID, E.JSONRPCErrorCode, E.Message);
|
||||
LogE(Format('[JSON-RPC][CLS %s][ERR %d][MSG "%s"]', [E.ClassName, E.JSONRPCErrorCode,
|
||||
E.Message]));
|
||||
end;
|
||||
on Ex: Exception do // use another name for exception variable, otherwise E is nil!!
|
||||
begin
|
||||
lJSON := CreateError(lReqID, 0, Ex.Message);
|
||||
try
|
||||
TryToCallMethod(lRTTIType, JSONRPC_HOOKS_ON_BEFORE_SEND_RESPONSE, lJSON, 'JSONResponse');
|
||||
Render(lJSON, False);
|
||||
finally
|
||||
lJSON.Free;
|
||||
end;
|
||||
lJSONResp := CreateError(lReqID, 0, Ex.Message);
|
||||
LogE(Format('[JSON-RPC][CLS %s][MSG "%s"]', [Ex.ClassName, Ex.Message]));
|
||||
end;
|
||||
end; // except
|
||||
|
||||
if lBeforeCallHookHasBeenInvoked and (not lAfterCallHookHasBeenInvoked) then
|
||||
begin
|
||||
try
|
||||
TryToCallMethod(lRTTIType, JSONRPC_HOOKS_ON_AFTER_CALL, lJSONResp);
|
||||
except
|
||||
on E: Exception do
|
||||
begin
|
||||
FreeAndNil(lJSONResp);
|
||||
if E is EMVCJSONRPCErrorResponse then
|
||||
lJSONResp := CreateError(lReqID, EMVCJSONRPCErrorResponse(E).JSONRPCErrorCode, E.Message)
|
||||
else
|
||||
lJSONResp := CreateError(lReqID, 0, E.Message);
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
Render(lJSONResp, True);
|
||||
finally
|
||||
lRTTI.Free;
|
||||
end;
|
||||
@ -1257,12 +1312,13 @@ begin
|
||||
end;
|
||||
|
||||
procedure TMVCJSONRPCController.TryToCallMethod(const aRTTIType: TRttiType;
|
||||
const MethodName: string; const Parameter: TJDOJsonObject; const ParameterName: string);
|
||||
const MethodName: string; const Parameter: TJDOJsonObject);
|
||||
var
|
||||
lHookMethod: TRTTIMethod;
|
||||
lHookParam: TRttiParameter;
|
||||
lHookParamParamType: string;
|
||||
lHookParamName: string;
|
||||
lHookSecondParam: TRttiParameter;
|
||||
lHookSecondParamType: string;
|
||||
lHookFirstParam: TRttiParameter;
|
||||
lHookFirstParamType: string;
|
||||
begin
|
||||
if not Assigned(aRTTIType) then
|
||||
begin
|
||||
@ -1271,20 +1327,38 @@ begin
|
||||
lHookMethod := aRTTIType.GetMethod(MethodName);
|
||||
if Assigned(lHookMethod) then
|
||||
begin
|
||||
if (Length(lHookMethod.GetParameters) <> 1) then
|
||||
if (Length(lHookMethod.GetParameters) <> 2) then
|
||||
begin
|
||||
raise EMVCJSONRPCException.CreateFmt('Invalid signature for [%s] Hook method [HINT: procedure '
|
||||
+ '%s.%s(const %s: TJDOJsonObject)', [MethodName, fRPCInstance.ClassName, MethodName,
|
||||
ParameterName]);
|
||||
lHookParam := lHookMethod.GetParameters[0];
|
||||
lHookParamParamType := lHookParam.ParamType.ToString.ToLower;
|
||||
lHookParamName := lHookParam.Name.ToLower;
|
||||
if ((lHookParamParamType <> 'tjdojsonobject') and (lHookParamParamType <> 'tjsonobject')) or
|
||||
(lHookParam.Flags * [pfConst, pfAddress] <> [pfConst, pfAddress]) or
|
||||
(lHookParamName <> ParameterName.ToLower) then
|
||||
+ '%s.%s(const Context: TWebContext; const Value: TJDOJsonObject)',
|
||||
[MethodName, fRPCInstance.ClassName, MethodName]);
|
||||
end;
|
||||
|
||||
lHookFirstParam := lHookMethod.GetParameters[0];
|
||||
lHookSecondParam := lHookMethod.GetParameters[1];
|
||||
|
||||
lHookFirstParamType := lHookFirstParam.ParamType.ToString.ToLower;
|
||||
lHookSecondParamType := lHookSecondParam.ParamType.ToString.ToLower;
|
||||
|
||||
if (lHookMethod.MethodKind <> mkProcedure) then
|
||||
raise EMVCJSONRPCException.CreateFmt('Invalid signature for [%s] Hook method [HINT: Hook methods MUST have the following signature "procedure '
|
||||
+ '%s.%s(const Context: TWebContext; const Value: TJDOJsonObject)"',
|
||||
[MethodName, fRPCInstance.ClassName, MethodName]);
|
||||
|
||||
if ((lHookSecondParamType <> 'tjdojsonobject') and (lHookSecondParamType <> 'tjsonobject')) or
|
||||
(lHookSecondParam.Flags * [pfConst, pfAddress] <> [pfConst, pfAddress]) then
|
||||
raise EMVCJSONRPCException.CreateFmt('Invalid signature for [%s] Hook method [HINT: procedure '
|
||||
+ '%s.%s(const %s: TJDOJsonObject)', [MethodName, fRPCInstance.ClassName, MethodName,
|
||||
ParameterName]);
|
||||
lHookMethod.Invoke(fRPCInstance, [Parameter])
|
||||
+ '%s.%s(const Context: TWebContext; const Value: TJDOJsonObject)',
|
||||
[MethodName, fRPCInstance.ClassName, MethodName]);
|
||||
|
||||
if (lHookFirstParamType <> 'twebcontext') or
|
||||
(lHookFirstParam.Flags * [pfConst, pfAddress] <> [pfConst, pfAddress]) then
|
||||
raise EMVCJSONRPCException.CreateFmt('Invalid signature for [%s] Hook method [HINT: procedure '
|
||||
+ '%s.%s(const Context: TWebContext; const Value: TJDOJsonObject)',
|
||||
[MethodName, fRPCInstance.ClassName, MethodName]);
|
||||
|
||||
LogD('[JSON-RPC][HOOK][' + fRPCInstance.ClassName + '.' + MethodName + ']');
|
||||
lHookMethod.Invoke(fRPCInstance, [Self.Context, Parameter])
|
||||
end;
|
||||
end;
|
||||
|
||||
@ -1387,6 +1461,7 @@ end;
|
||||
constructor TJSONRPCRequest.Create;
|
||||
begin
|
||||
inherited Create;
|
||||
Self.FID := TValue.Empty;
|
||||
end;
|
||||
|
||||
destructor TJSONRPCRequest.Destroy;
|
||||
@ -1798,24 +1873,32 @@ end;
|
||||
function TJSONRPCRequest.GetJSON: TJDOJsonObject;
|
||||
begin
|
||||
Result := inherited GetJSON;
|
||||
if not FID.IsEmpty then
|
||||
begin
|
||||
if FID.IsType<string> then
|
||||
try
|
||||
if not FID.IsEmpty then
|
||||
begin
|
||||
Result.S[JSONRPC_ID] := FID.AsString;
|
||||
end
|
||||
else if FID.IsType<Int32> then
|
||||
begin
|
||||
Result.I[JSONRPC_ID] := FID.AsInteger;
|
||||
end
|
||||
else if FID.IsType<Int64> then
|
||||
begin
|
||||
Result.I[JSONRPC_ID] := FID.AsInt64;
|
||||
if FID.IsType<string> then
|
||||
begin
|
||||
Result.S[JSONRPC_ID] := FID.AsString;
|
||||
end
|
||||
else if FID.IsType<Int32> then
|
||||
begin
|
||||
Result.I[JSONRPC_ID] := FID.AsInteger;
|
||||
end
|
||||
else if FID.IsType<Int64> then
|
||||
begin
|
||||
Result.I[JSONRPC_ID] := FID.AsInt64;
|
||||
end
|
||||
else
|
||||
raise EMVCJSONRPCException.Create('ID can be only Int32, Int64 or String');
|
||||
end
|
||||
else
|
||||
raise EMVCJSONRPCException.Create('ID can be only Int32, Int64 or String');
|
||||
begin
|
||||
raise EMVCJSONRPCException.Create('ID cannot be empty in a JSON-RPC request');
|
||||
end;
|
||||
except
|
||||
Result.Free;
|
||||
raise;
|
||||
end;
|
||||
|
||||
end;
|
||||
|
||||
{ TJSONRPCProxyGenerator }
|
||||
@ -2032,6 +2115,81 @@ begin
|
||||
fJSONRPCErrorCode := ErrCode;
|
||||
end;
|
||||
|
||||
{ TJSONRPCNullResponse }
|
||||
|
||||
function TJSONRPCNullResponse.GetError: TJSONRPCResponseError;
|
||||
begin
|
||||
Result := FError;
|
||||
end;
|
||||
|
||||
function TJSONRPCNullResponse.GetID: TValue;
|
||||
begin
|
||||
RaiseErrorForNullObject;
|
||||
end;
|
||||
|
||||
function TJSONRPCNullResponse.GetJSON: TJDOJsonObject;
|
||||
begin
|
||||
Result := nil;
|
||||
RaiseErrorForNullObject;
|
||||
end;
|
||||
|
||||
function TJSONRPCNullResponse.GetJSONString: string;
|
||||
begin
|
||||
RaiseErrorForNullObject;
|
||||
end;
|
||||
|
||||
function TJSONRPCNullResponse.GetResult: TValue;
|
||||
begin
|
||||
RaiseErrorForNullObject;
|
||||
end;
|
||||
|
||||
function TJSONRPCNullResponse.IsError: Boolean;
|
||||
begin
|
||||
Result := False;
|
||||
end;
|
||||
|
||||
procedure TJSONRPCNullResponse.RaiseErrorForNullObject;
|
||||
begin
|
||||
raise EMVCJSONRPCException.Create('Invalid Call for NULL object');
|
||||
end;
|
||||
|
||||
function TJSONRPCNullResponse.ResultAsJSONArray: TJDOJsonArray;
|
||||
begin
|
||||
Result := nil;
|
||||
RaiseErrorForNullObject;
|
||||
end;
|
||||
|
||||
function TJSONRPCNullResponse.ResultAsJSONObject: TJDOJsonObject;
|
||||
begin
|
||||
Result := nil;
|
||||
RaiseErrorForNullObject;
|
||||
end;
|
||||
|
||||
procedure TJSONRPCNullResponse.SetError(const Value: TJSONRPCResponseError);
|
||||
begin
|
||||
FError := Value;
|
||||
end;
|
||||
|
||||
procedure TJSONRPCNullResponse.SetID(const Value: TValue);
|
||||
begin
|
||||
RaiseErrorForNullObject;
|
||||
end;
|
||||
|
||||
procedure TJSONRPCNullResponse.SetJSON(const JSON: TJDOJsonObject);
|
||||
begin
|
||||
RaiseErrorForNullObject;
|
||||
end;
|
||||
|
||||
procedure TJSONRPCNullResponse.SetJsonString(const Value: string);
|
||||
begin
|
||||
RaiseErrorForNullObject;
|
||||
end;
|
||||
|
||||
procedure TJSONRPCNullResponse.SetResult(const Value: TValue);
|
||||
begin
|
||||
RaiseErrorForNullObject;
|
||||
end;
|
||||
|
||||
initialization
|
||||
|
||||
finalization
|
||||
|
@ -36,21 +36,83 @@ uses
|
||||
|
||||
type
|
||||
|
||||
{$SCOPEDENUMS ON}
|
||||
|
||||
{$SCOPEDENUMS ON}
|
||||
TJWTCheckableClaim = (ExpirationTime, NotBefore, IssuedAt);
|
||||
TJWTCheckableClaims = set of TJWTCheckableClaim;
|
||||
|
||||
TJWTRegisteredClaimNames = class sealed
|
||||
public
|
||||
const
|
||||
/// <summary>
|
||||
/// The "iss" (issuer) claim identifies the principal that issued the
|
||||
/// JWT. The processing of this claim is generally application specific.
|
||||
/// The "iss" value is a case-sensitive string containing a StringOrURI
|
||||
/// value. Use of this claim is OPTIONAL.
|
||||
/// </summary>
|
||||
Issuer: string = 'iss';
|
||||
/// <summary>
|
||||
/// The "sub" (subject) claim identifies the principal that is the
|
||||
/// subject of the JWT. The claims in a JWT are normally statements
|
||||
/// about the subject. The subject value MUST either be scoped to be
|
||||
/// locally unique in the context of the issuer or be globally unique.
|
||||
/// The processing of this claim is generally application specific. The
|
||||
/// "sub" value is a case-sensitive string containing a StringOrURI
|
||||
/// value. Use of this claim is OPTIONAL.
|
||||
/// </summary>
|
||||
Subject: string = 'sub';
|
||||
/// <summary>
|
||||
/// The "aud" (audience) claim identifies the recipients that the JWT is
|
||||
/// intended for. Each principal intended to process the JWT MUST
|
||||
/// identify itself with a value in the audience claim. If the principal
|
||||
/// processing the claim does not identify itself with a value in the
|
||||
/// "aud" claim when this claim is present, then the JWT MUST be
|
||||
/// rejected. In the general case, the "aud" value is an array of case-
|
||||
/// sensitive strings, each containing a StringOrURI value. In the
|
||||
/// special case when the JWT has one audience, the "aud" value MAY be a
|
||||
/// single case-sensitive string containing a StringOrURI value. The
|
||||
/// interpretation of audience values is generally application specific.
|
||||
/// Use of this claim is OPTIONAL.
|
||||
/// </summary>
|
||||
Audience: string = 'aud';
|
||||
/// <summary>
|
||||
/// The "exp" (expiration time) claim identifies the expiration time on
|
||||
/// or after which the JWT MUST NOT be accepted for processing. The
|
||||
/// processing of the "exp" claim requires that the current date/time
|
||||
/// MUST be before the expiration date/time listed in the "exp" claim.
|
||||
/// Implementers MAY provide for some small leeway, usually no more than
|
||||
/// a few minutes, to account for clock skew. Its value MUST be a number
|
||||
/// containing a NumericDate value. Use of this claim is OPTIONAL.
|
||||
/// </summary>
|
||||
ExpirationTime: string = 'exp';
|
||||
/// <summary>
|
||||
/// The "nbf" (not before) claim identifies the time before which the JWT
|
||||
/// MUST NOT be accepted for processing. The processing of the "nbf"
|
||||
/// claim requires that the current date/time MUST be after or equal to
|
||||
/// the not-before date/time listed in the "nbf" claim. Implementers MAY
|
||||
/// provide for some small leeway, usually no more than a few minutes, to
|
||||
/// account for clock skew. Its value MUST be a number containing a
|
||||
/// NumericDate value. Use of this claim is OPTIONAL.
|
||||
/// </summary>
|
||||
NotBefore: string = 'nbf';
|
||||
/// <summary>
|
||||
/// The "iat" (issued at) claim identifies the time at which the JWT was
|
||||
/// issued. This claim can be used to determine the age of the JWT. Its
|
||||
/// value MUST be a number containing a NumericDate value. Use of this
|
||||
/// claim is OPTIONAL.
|
||||
/// </summary>
|
||||
IssuedAt: string = 'iat';
|
||||
/// <summary>
|
||||
/// The "jti" (JWT ID) claim provides a unique identifier for the JWT.
|
||||
/// The identifier value MUST be assigned in a manner that ensures that
|
||||
/// there is a negligible probability that the same value will be
|
||||
/// accidentally assigned to a different data object; if the application
|
||||
/// uses multiple issuers, collisions MUST be prevented among values
|
||||
/// produced by different issuers as well. The "jti" claim can be used
|
||||
/// to prevent the JWT from being replayed. The "jti" value is a case-
|
||||
/// sensitive string. Use of this claim is OPTIONAL.
|
||||
/// </summary>
|
||||
JWT_ID: string = 'jti';
|
||||
|
||||
Names: array [0 .. 6] of string = (
|
||||
'iss',
|
||||
'sub',
|
||||
@ -213,7 +275,8 @@ type
|
||||
/// ExpirationTime will be incremented by LiveValidityWindowInSeconds seconds automatically
|
||||
/// if the remaining seconds are less than the LiveValidityWindowInSeconds.
|
||||
/// </summary>
|
||||
property LiveValidityWindowInSeconds: Cardinal read GetLiveValidityWindowInSeconds write SetLiveValidityWindowInSeconds;
|
||||
property LiveValidityWindowInSeconds: Cardinal read GetLiveValidityWindowInSeconds
|
||||
write SetLiveValidityWindowInSeconds;
|
||||
end;
|
||||
|
||||
implementation
|
||||
@ -478,7 +541,7 @@ begin
|
||||
try
|
||||
lPayload := TJDOJSONObject.Create;
|
||||
try
|
||||
lHeader.S['alg'] := HMACAlgorithm;
|
||||
lHeader.S['alg'] := HMACAlgorithm;
|
||||
lHeader.S['typ'] := 'JWT';
|
||||
for lRegClaimName in TJWTRegisteredClaimNames.Names do
|
||||
begin
|
||||
@ -495,7 +558,7 @@ begin
|
||||
|
||||
for lCustomClaimName in FCustomClaims.Keys do
|
||||
begin
|
||||
lPayload.S[lCustomClaimName] := FCustomClaims[lCustomClaimName];
|
||||
lPayload.S[lCustomClaimName] := FCustomClaims[lCustomClaimName];
|
||||
end;
|
||||
|
||||
lHeaderEncoded := URLSafeB64encode(lHeader.ToString, False, IndyTextEncoding_UTF8);
|
||||
@ -623,8 +686,8 @@ begin
|
||||
begin
|
||||
lIsRegistered := False;
|
||||
|
||||
lName := lJPayload.Names[I];
|
||||
lValue := lJPayload.Items[I].Value;
|
||||
lName := lJPayload.Names[i];
|
||||
lValue := lJPayload.Items[i].Value;
|
||||
|
||||
// if is a registered claim, load it in the proper dictionary...
|
||||
for j := 0 to high(TJWTRegisteredClaimNames.Names) do
|
||||
|
@ -94,7 +94,7 @@ type
|
||||
const AActionName: string; var AHandled: Boolean);
|
||||
protected
|
||||
procedure OnBeforeControllerAction(AContext: TWebContext; const AControllerQualifiedClassName: string;
|
||||
const AActionName: string; var AHandled: Boolean);
|
||||
const AActionName: string; var AHandled: Boolean); override;
|
||||
public
|
||||
constructor Create(const AAuthenticationHandler: IMVCAuthenticationHandler;
|
||||
const ALoginUrl: string = '/system/users/logged'); override;
|
||||
|
@ -61,12 +61,12 @@ type
|
||||
const AActionName: string;
|
||||
const AHandled: Boolean
|
||||
);
|
||||
procedure OnAfterRouting(AContext: TWebContext; const AHandled: Boolean);
|
||||
public
|
||||
constructor Create(
|
||||
const AAuthenticationHandler: IMVCAuthenticationHandler;
|
||||
const ARealm: string = 'DelphiMVCFramework REALM'
|
||||
); virtual;
|
||||
procedure OnAfterRouting(AContext: TWebContext; const AHandled: Boolean);
|
||||
end;
|
||||
|
||||
TMVCCustomAuthenticationMiddleware = class(TInterfacedObject, IMVCMiddleware)
|
||||
@ -84,7 +84,7 @@ type
|
||||
const AControllerQualifiedClassName: string;
|
||||
const AActionName: string;
|
||||
var AHandled: Boolean
|
||||
);
|
||||
); virtual;
|
||||
|
||||
procedure OnAfterControllerAction(
|
||||
AContext: TWebContext;
|
||||
@ -96,7 +96,6 @@ type
|
||||
const AHandled: Boolean
|
||||
);
|
||||
|
||||
|
||||
procedure SendResponse(AContext: TWebContext; var AHandled: Boolean; AHttpStatus: Word = HTTP_STATUS.Unauthorized);
|
||||
procedure DoLogin(AContext: TWebContext; var AHandled: Boolean);
|
||||
procedure DoLogout(AContext: TWebContext; var AHandled: Boolean);
|
||||
@ -149,15 +148,18 @@ procedure TMVCBasicAuthenticationMiddleware.OnBeforeControllerAction(
|
||||
if AContext.Request.ClientPreferHTML then
|
||||
begin
|
||||
AContext.Response.ContentType := TMVCMediaType.TEXT_HTML;
|
||||
AContext.Response.RawWebResponse.Content := Format(CONTENT_HTML_FORMAT, [CONTENT_401_NOT_AUTHORIZED, AContext.Config[TMVCConfigKey.ServerName]]);
|
||||
AContext.Response.RawWebResponse.Content :=
|
||||
Format(CONTENT_HTML_FORMAT, [CONTENT_401_NOT_AUTHORIZED, AContext.Config[TMVCConfigKey.ServerName]]);
|
||||
end
|
||||
else
|
||||
begin
|
||||
AContext.Response.ContentType := TMVCMediaType.TEXT_PLAIN;
|
||||
AContext.Response.RawWebResponse.Content := CONTENT_401_NOT_AUTHORIZED + sLineBreak + AContext.Config[TMVCConfigKey.ServerName];
|
||||
AContext.Response.RawWebResponse.Content := CONTENT_401_NOT_AUTHORIZED + sLineBreak + AContext.Config
|
||||
[TMVCConfigKey.ServerName];
|
||||
end;
|
||||
AContext.Response.StatusCode := HTTP_STATUS.Unauthorized;
|
||||
AContext.Response.SetCustomHeader('WWW-Authenticate', 'Basic realm=' + QuotedStr(FRealm));
|
||||
AContext.SessionStop(False);
|
||||
AHandled := True;
|
||||
end;
|
||||
|
||||
@ -167,21 +169,30 @@ procedure TMVCBasicAuthenticationMiddleware.OnBeforeControllerAction(
|
||||
if AContext.Request.ClientPreferHTML then
|
||||
begin
|
||||
AContext.Response.ContentType := TMVCMediaType.TEXT_HTML;
|
||||
AContext.Response.RawWebResponse.Content := Format(CONTENT_HTML_FORMAT, [CONTENT_403_FORBIDDEN, AContext.Config[TMVCConfigKey.ServerName]]);
|
||||
AContext.Response.RawWebResponse.Content :=
|
||||
Format(CONTENT_HTML_FORMAT, [CONTENT_403_FORBIDDEN, AContext.Config[TMVCConfigKey.ServerName]]);
|
||||
end
|
||||
else if AContext.Request.ContentMediaType.StartsWith(TMVCMediaType.APPLICATION_JSON) then
|
||||
begin
|
||||
AContext.Response.ContentType := TMVCMediaType.APPLICATION_JSON;
|
||||
AContext.Response.RawWebResponse.Content :=
|
||||
'{"status":"error", "message":"' + CONTENT_403_FORBIDDEN.Replace('"', '\"') + '"}';
|
||||
end
|
||||
else
|
||||
begin
|
||||
AContext.Response.ContentType := TMVCMediaType.TEXT_PLAIN;
|
||||
AContext.Response.RawWebResponse.Content := CONTENT_403_FORBIDDEN + sLineBreak + AContext.Config[TMVCConfigKey.ServerName];
|
||||
AContext.Response.RawWebResponse.Content := CONTENT_403_FORBIDDEN + sLineBreak + AContext.Config
|
||||
[TMVCConfigKey.ServerName];
|
||||
end;
|
||||
AContext.Response.StatusCode := HTTP_STATUS.Forbidden;
|
||||
AContext.Response.ReasonString := AContext.Config[TMVCConfigKey.ServerName];
|
||||
AHandled := True;
|
||||
end;
|
||||
|
||||
var
|
||||
AuthRequired: Boolean;
|
||||
IsValid, IsAuthorized: Boolean;
|
||||
AuthHeader: string;
|
||||
AuthHeader, Token: string;
|
||||
AuthPieces: TArray<string>;
|
||||
RolesList: TList<string>;
|
||||
SessionData: TSessionData;
|
||||
@ -199,9 +210,15 @@ begin
|
||||
if not IsValid then
|
||||
begin
|
||||
AuthHeader := AContext.Request.Headers['Authorization'];
|
||||
AuthHeader := TMVCSerializerHelper.DecodeString(AuthHeader.Remove(0, 'Basic'.Length).Trim);
|
||||
if AuthHeader.IsEmpty or (not AuthHeader.StartsWith('Basic ', True)) then
|
||||
begin
|
||||
SendWWWAuthenticate;
|
||||
Exit;
|
||||
end;
|
||||
Token := AuthHeader.Remove(0, 'Basic '.Length).Trim;
|
||||
AuthHeader := TMVCSerializerHelper.DecodeString(Token);
|
||||
AuthPieces := AuthHeader.Split([':']);
|
||||
if AuthHeader.IsEmpty or (Length(AuthPieces) <> 2) then
|
||||
if Length(AuthPieces) <> 2 then
|
||||
begin
|
||||
SendWWWAuthenticate;
|
||||
Exit;
|
||||
@ -211,7 +228,8 @@ begin
|
||||
try
|
||||
SessionData := TSessionData.Create;
|
||||
try
|
||||
FAuthenticationHandler.OnAuthentication(AContext, AuthPieces[0], AuthPieces[1], RolesList, IsValid, SessionData);
|
||||
FAuthenticationHandler.OnAuthentication(AContext, AuthPieces[0], AuthPieces[1], RolesList, IsValid,
|
||||
SessionData);
|
||||
if IsValid then
|
||||
begin
|
||||
AContext.LoggedUser.Roles.AddRange(RolesList);
|
||||
@ -232,7 +250,8 @@ begin
|
||||
|
||||
IsAuthorized := False;
|
||||
if IsValid then
|
||||
FAuthenticationHandler.OnAuthorization(AContext, AContext.LoggedUser.Roles, AControllerQualifiedClassName, AActionName, IsAuthorized);
|
||||
FAuthenticationHandler.OnAuthorization(AContext, AContext.LoggedUser.Roles, AControllerQualifiedClassName,
|
||||
AActionName, IsAuthorized);
|
||||
|
||||
if IsAuthorized then
|
||||
AHandled := False
|
||||
@ -241,7 +260,9 @@ begin
|
||||
if IsValid then
|
||||
Send403Forbidden
|
||||
else
|
||||
begin
|
||||
SendWWWAuthenticate;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
@ -249,7 +270,7 @@ procedure TMVCBasicAuthenticationMiddleware.OnBeforeRouting(
|
||||
AContext: TWebContext;
|
||||
var AHandled: Boolean);
|
||||
begin
|
||||
// Implement as needed
|
||||
AHandled := False;
|
||||
end;
|
||||
|
||||
{ TMVCCustomAuthenticationMiddleware }
|
||||
@ -281,7 +302,8 @@ begin
|
||||
AHandled := True;
|
||||
AContext.Response.StatusCode := HTTP_STATUS.BadRequest;
|
||||
AContext.Response.ContentType := TMVCMediaType.APPLICATION_JSON;
|
||||
AContext.Response.RawWebResponse.Content := '{"status":"KO", "message":"username and password are mandatory in the body request as json object"}';
|
||||
AContext.Response.RawWebResponse.Content :=
|
||||
'{"status":"error", "message":"username and password are mandatory in the body request as json object"}';
|
||||
Exit;
|
||||
end;
|
||||
|
||||
@ -393,7 +415,8 @@ begin
|
||||
end;
|
||||
|
||||
IsAuthorized := False;
|
||||
FAuthenticationHandler.OnAuthorization(AContext, AContext.LoggedUser.Roles, AControllerQualifiedClassName, AActionName, IsAuthorized);
|
||||
FAuthenticationHandler.OnAuthorization(AContext, AContext.LoggedUser.Roles, AControllerQualifiedClassName,
|
||||
AActionName, IsAuthorized);
|
||||
if IsAuthorized then
|
||||
AHandled := False
|
||||
else
|
||||
@ -412,7 +435,8 @@ begin
|
||||
begin
|
||||
AHandled := False;
|
||||
|
||||
if (AContext.Request.HTTPMethod = httpPOST) and (AContext.Request.ContentType.StartsWith(TMVCMediaType.APPLICATION_JSON)) then
|
||||
if (AContext.Request.HTTPMethod = httpPOST) and
|
||||
(AContext.Request.ContentType.StartsWith(TMVCMediaType.APPLICATION_JSON)) then
|
||||
DoLogin(AContext, AHandled);
|
||||
|
||||
if (AContext.Request.HTTPMethod = httpDELETE) then
|
||||
@ -433,7 +457,8 @@ begin
|
||||
if AContext.Request.ClientPreferHTML then
|
||||
begin
|
||||
AContext.Response.ContentType := TMVCMediaType.TEXT_HTML;
|
||||
AContext.Response.RawWebResponse.Content := Format(CONTENT_HTML_FORMAT, [IntToStr(AHttpStatus), AContext.Config[TMVCConfigKey.ServerName]]);
|
||||
AContext.Response.RawWebResponse.Content :=
|
||||
Format(CONTENT_HTML_FORMAT, [IntToStr(AHttpStatus), AContext.Config[TMVCConfigKey.ServerName]]);
|
||||
end
|
||||
else
|
||||
begin
|
||||
|
@ -167,7 +167,14 @@ end;
|
||||
|
||||
function TRQLPostgreSQLCompiler.RQLLimitToSQL(const aRQLLimit: TRQLLimit): string;
|
||||
begin
|
||||
Result := Format(' /*limit*/ LIMIT %d OFFSET %d', [aRQLLimit.Count, aRQLLimit.Start]);
|
||||
if aRQLLimit.Start = 0 then
|
||||
begin
|
||||
Result := Format(' /*limit*/ LIMIT %d', [aRQLLimit.Count]);
|
||||
end
|
||||
else
|
||||
begin
|
||||
Result := Format(' /*limit*/ LIMIT %d OFFSET %d', [aRQLLimit.Count, aRQLLimit.Start]);
|
||||
end;
|
||||
end;
|
||||
|
||||
function TRQLPostgreSQLCompiler.RQLLogicOperatorToSQL(const aRQLFIlter: TRQLLogicOperator): string;
|
||||
|
@ -221,10 +221,15 @@ begin
|
||||
LMethods := LRttiType.GetMethods; {do not use GetDeclaredMethods because JSON-RPC rely on this!!}
|
||||
for LMethod in LMethods do
|
||||
begin
|
||||
if (LMethod.MethodKind <> mkProcedure) or LMethod.IsClassMethod then
|
||||
if LMethod.Visibility <> mvPublic then //2020-08-08
|
||||
Continue;
|
||||
if (LMethod.MethodKind <> mkProcedure) {or LMethod.IsClassMethod} then
|
||||
Continue;
|
||||
|
||||
LAttributes := LMethod.GetAttributes;
|
||||
if Length(LAttributes) = 0 then
|
||||
Continue;
|
||||
|
||||
for LAtt in LAttributes do
|
||||
begin
|
||||
if LAtt is MVCPathAttribute then
|
||||
@ -240,7 +245,7 @@ begin
|
||||
FControllerClazz := LControllerDelegate.Clazz;
|
||||
FControllerCreateAction := LControllerDelegate.CreateAction;
|
||||
LProduceAttribute := GetAttribute<MVCProducesAttribute>(LAttributes);
|
||||
if Assigned(LProduceAttribute) then
|
||||
if LProduceAttribute <> nil then
|
||||
begin
|
||||
AResponseContentMediaType := LProduceAttribute.Value;
|
||||
AResponseContentCharset := LProduceAttribute.Charset;
|
||||
@ -396,7 +401,7 @@ var
|
||||
FoundOneAttProduces: Boolean;
|
||||
begin
|
||||
Result := False;
|
||||
if AAccept = '*/*' then
|
||||
if AAccept.Contains('*/*') then //2020-08-08
|
||||
begin
|
||||
Exit(True);
|
||||
end;
|
||||
|
@ -91,6 +91,7 @@ var
|
||||
lPKInInsert: Boolean;
|
||||
begin
|
||||
lPKInInsert := (not PKFieldName.IsEmpty) and (not(TMVCActiveRecordFieldOption.foAutoGenerated in PKOptions));
|
||||
lPKInInsert := lPKInInsert and (not(TMVCActiveRecordFieldOption.foReadOnly in PKOptions));
|
||||
lSB := TStringBuilder.Create;
|
||||
try
|
||||
lSB.Append('INSERT INTO ' + TableName + '(');
|
||||
|
@ -63,6 +63,7 @@ var
|
||||
lPKInInsert: Boolean;
|
||||
begin
|
||||
lPKInInsert := (not PKFieldName.IsEmpty); // and (not(TMVCActiveRecordFieldOption.foAutoGenerated in PKOptions));
|
||||
lPKInInsert := lPKInInsert and (not(TMVCActiveRecordFieldOption.foReadOnly in PKOptions));
|
||||
lSB := TStringBuilder.Create;
|
||||
try
|
||||
lSB.Append('INSERT INTO ' + TableName + '(');
|
||||
|
@ -88,6 +88,7 @@ var
|
||||
lPKInInsert: Boolean;
|
||||
begin
|
||||
lPKInInsert := (not PKFieldName.IsEmpty) and (not(TMVCActiveRecordFieldOption.foAutoGenerated in PKOptions));
|
||||
lPKInInsert := lPKInInsert and (not(TMVCActiveRecordFieldOption.foReadOnly in PKOptions));
|
||||
lSB := TStringBuilder.Create;
|
||||
try
|
||||
lSB.Append('INSERT INTO ' + TableName + '(');
|
||||
|
@ -88,6 +88,7 @@ var
|
||||
lPKInInsert: Boolean;
|
||||
begin
|
||||
lPKInInsert := (not PKFieldName.IsEmpty) and (not(TMVCActiveRecordFieldOption.foAutoGenerated in PKOptions));
|
||||
lPKInInsert := lPKInInsert and (not(TMVCActiveRecordFieldOption.foReadOnly in PKOptions));
|
||||
lSB := TStringBuilder.Create;
|
||||
try
|
||||
lSB.Append('INSERT INTO ' + TableName + '(');
|
||||
|
@ -99,6 +99,7 @@ var
|
||||
lPKInInsert: Boolean;
|
||||
begin
|
||||
lPKInInsert := (not PKFieldName.IsEmpty) and (not(TMVCActiveRecordFieldOption.foAutoGenerated in PKOptions));
|
||||
lPKInInsert := lPKInInsert and (not(TMVCActiveRecordFieldOption.foReadOnly in PKOptions));
|
||||
lSB := TStringBuilder.Create;
|
||||
try
|
||||
lSB.Append('INSERT INTO ' + GetTableNameForSQL(TableName) + ' (');
|
||||
@ -146,6 +147,11 @@ function TMVCSQLGeneratorPostgreSQL.CreateSelectByPKSQL(
|
||||
const Map: TFieldsMap; const PKFieldName: string;
|
||||
const PKOptions: TMVCActiveRecordFieldOptions): string;
|
||||
begin
|
||||
if PKFieldName.IsEmpty then
|
||||
begin
|
||||
raise EMVCActiveRecord.Create('No primary key provided. [HINT] Define a primary key field adding foPrimaryKey in field options.');
|
||||
end;
|
||||
|
||||
Result := CreateSelectSQL(TableName, Map, PKFieldName, PKOptions) + ' WHERE ' +
|
||||
GetFieldNameForSQL(PKFieldName) + '= :' + GetParamNameForSQL(PKFieldName); // IntToStr(PrimaryKeyValue);
|
||||
end;
|
||||
|
@ -87,6 +87,7 @@ var
|
||||
lPKInInsert: Boolean;
|
||||
begin
|
||||
lPKInInsert := (not PKFieldName.IsEmpty) and (not(TMVCActiveRecordFieldOption.foAutoGenerated in PKOptions));
|
||||
lPKInInsert := lPKInInsert and (not(TMVCActiveRecordFieldOption.foReadOnly in PKOptions));
|
||||
lSB := TStringBuilder.Create;
|
||||
try
|
||||
lSB.Append('INSERT INTO ' + TableName + ' (');
|
||||
|
@ -736,7 +736,7 @@ var
|
||||
Attrs: TArray<TCustomAttribute>;
|
||||
Attr: TCustomAttribute;
|
||||
begin
|
||||
{ TODO -oDanieleT -cGeneral : in un rendering di una lista, quante volte viene chiamata questa funzione?}
|
||||
{ TODO -oDanieleT -cGeneral : in un rendering di una lista, quante volte viene chiamata questa funzione? }
|
||||
{ Tante volte, ma eliminando tutta la logica si guadagnerebbe al massiom il 6% nel caso tipico, forse non vale la pena di aggiungere una cache apposita }
|
||||
Result := AProperty.Name;
|
||||
|
||||
@ -1006,6 +1006,7 @@ var
|
||||
lInternalStream: TStream;
|
||||
lSStream: TStringStream;
|
||||
lValue: TValue;
|
||||
lStrValue: string;
|
||||
{$IF not Defined(TokyoOrBetter)}
|
||||
lFieldValue: string;
|
||||
{$ENDIF}
|
||||
@ -1023,7 +1024,35 @@ begin
|
||||
case AField.DataType of
|
||||
ftString, ftWideString:
|
||||
begin
|
||||
aRTTIField.SetValue(AObject, AField.AsString);
|
||||
// mysql tinytext is identified as string, but raises an Invalid Class Cast
|
||||
// so we need to do some more checks...
|
||||
case aRTTIField.FieldType.TypeKind of
|
||||
tkString, tkUString:
|
||||
begin
|
||||
aRTTIField.SetValue(AObject, AField.AsString);
|
||||
end;
|
||||
tkClass: { mysql - maps a tiny field, identified as string, into a TStream }
|
||||
begin
|
||||
lInternalStream := aRTTIField.GetValue(AObject).AsObject as TStream;
|
||||
if lInternalStream = nil then
|
||||
begin
|
||||
raise EMVCException.CreateFmt('Property target for %s field is nil. [HINT] Initialize the stream before load data', [AField.FieldName]);
|
||||
end;
|
||||
lInternalStream.Size := 0;
|
||||
lStrValue := AField.AsString;
|
||||
if not lStrValue.IsEmpty then
|
||||
begin
|
||||
lInternalStream.Write(lStrValue, Length(lStrValue));
|
||||
lInternalStream.Position := 0;
|
||||
end;
|
||||
end
|
||||
else
|
||||
begin
|
||||
raise EMVCException.CreateFmt('Unsupported FieldType (%d) for field %s',
|
||||
[Ord(AField.DataType), AField.FieldName]);
|
||||
end;
|
||||
end;
|
||||
// aRTTIField.SetValue(AObject, AField.AsString);
|
||||
end;
|
||||
ftLargeint, ftAutoInc:
|
||||
begin
|
||||
|
@ -118,7 +118,8 @@ type
|
||||
const ASerializedObject: string;
|
||||
const AObject: TObject;
|
||||
const AType: TMVCSerializationType = stDefault;
|
||||
const AIgnoredAttributes: TMVCIgnoredList = nil
|
||||
const AIgnoredAttributes: TMVCIgnoredList = nil;
|
||||
const ARootNode: String = ''
|
||||
); overload;
|
||||
|
||||
procedure DeserializeObject(
|
||||
|
@ -135,7 +135,8 @@ type
|
||||
const SerializationAction: TMVCDatasetSerializationAction = nil): string;
|
||||
|
||||
procedure DeserializeObject(const ASerializedObject: string; const AObject: TObject;
|
||||
const AType: TMVCSerializationType = stDefault; const AIgnoredAttributes: TMVCIgnoredList = []); overload;
|
||||
const AType: TMVCSerializationType = stDefault; const AIgnoredAttributes: TMVCIgnoredList = [];
|
||||
const ARootNode: string = ''); overload;
|
||||
|
||||
procedure DeserializeObject(const ASerializedObject: string; const AObject: IInterface;
|
||||
const AType: TMVCSerializationType = stDefault; const AIgnoredAttributes: TMVCIgnoredList = []); overload;
|
||||
@ -194,6 +195,18 @@ uses
|
||||
MVCFramework.DataSet.Utils,
|
||||
MVCFramework.Nullables;
|
||||
|
||||
function SelectRootNodeOrWholeObject(const RootNode: string; const JSONObject: TJsonObject): TJsonObject; inline;
|
||||
begin
|
||||
if RootNode.IsEmpty then
|
||||
begin
|
||||
Result := JSONObject
|
||||
end
|
||||
else
|
||||
begin
|
||||
Result := JSONObject.O[RootNode];
|
||||
end;
|
||||
end;
|
||||
|
||||
{ TMVCJsonDataObjectsSerializer }
|
||||
|
||||
procedure TMVCJsonDataObjectsSerializer.AfterConstruction;
|
||||
@ -2013,7 +2026,7 @@ end;
|
||||
|
||||
procedure TMVCJsonDataObjectsSerializer.DeserializeObject(const ASerializedObject: string;
|
||||
const AObject: TObject;
|
||||
const AType: TMVCSerializationType; const AIgnoredAttributes: TMVCIgnoredList);
|
||||
const AType: TMVCSerializationType; const AIgnoredAttributes: TMVCIgnoredList; const ARootNode: string);
|
||||
var
|
||||
JSONObject: TJDOJsonObject;
|
||||
JsonBase: TJsonBaseObject;
|
||||
@ -2041,11 +2054,12 @@ begin
|
||||
try
|
||||
if GetTypeSerializers.ContainsKey(AObject.ClassInfo) then
|
||||
begin
|
||||
GetTypeSerializers.Items[AObject.ClassInfo].DeserializeRoot(JSONObject, AObject, []);
|
||||
GetTypeSerializers.Items[AObject.ClassInfo].DeserializeRoot(SelectRootNodeOrWholeObject(ARootNode, JSONObject),
|
||||
AObject, [])
|
||||
end
|
||||
else
|
||||
begin
|
||||
JsonObjectToObject(JSONObject, AObject, GetSerializationType(AObject, AType), AIgnoredAttributes);
|
||||
JsonObjectToObject(SelectRootNodeOrWholeObject(ARootNode, JSONObject), AObject, GetSerializationType(AObject, AType), AIgnoredAttributes);
|
||||
end;
|
||||
finally
|
||||
JSONObject.Free;
|
||||
@ -2097,9 +2111,9 @@ begin
|
||||
{$IFDEF NEXTGEN}
|
||||
lTypeName := PChar(Pointer(Value.TypeInfo.Name))
|
||||
{$ELSE}
|
||||
lTypeName := String(Value.TypeInfo.Name);
|
||||
lTypeName := string(Value.TypeInfo.Name);
|
||||
{$ENDIF}
|
||||
if (lTypeName = 'TDate') or (lTypeName = 'TDateTime') or (lTypeName = 'TTime') then
|
||||
if (lTypeName = 'TDate') or (lTypeName = 'TDateTime') or (lTypeName = 'TTime') then
|
||||
begin
|
||||
JSON.D[KeyName] := Value.AsExtended;
|
||||
end
|
||||
@ -2118,8 +2132,15 @@ begin
|
||||
end;
|
||||
tkEnumeration:
|
||||
begin
|
||||
Value.TryAsOrdinal(lOrdinalValue);
|
||||
JSON.I[KeyName] := lOrdinalValue;
|
||||
if (Value.TypeInfo = System.TypeInfo(Boolean)) then
|
||||
begin
|
||||
JSON.B[KeyName] := Value.AsBoolean;
|
||||
end
|
||||
else
|
||||
begin
|
||||
Value.TryAsOrdinal(lOrdinalValue);
|
||||
JSON.I[KeyName] := lOrdinalValue;
|
||||
end;
|
||||
end;
|
||||
tkClass, tkInterface:
|
||||
begin
|
||||
|
@ -80,7 +80,7 @@ uses
|
||||
Web.WebReq,
|
||||
LoggerPro,
|
||||
IdGlobal,
|
||||
IdGlobalProtocols,
|
||||
IdGlobalProtocols,
|
||||
Swag.Doc,
|
||||
Swag.Common.Types,
|
||||
MVCFramework.Commons,
|
||||
@ -88,7 +88,7 @@ uses
|
||||
|
||||
type
|
||||
|
||||
TSessionData = TDictionary<String, String>;
|
||||
TSessionData = TDictionary<string, string>;
|
||||
TMVCCustomData = TSessionData;
|
||||
TMVCBaseViewEngine = class;
|
||||
TMVCViewEngineClass = class of TMVCBaseViewEngine;
|
||||
@ -348,11 +348,11 @@ type
|
||||
function ContentParam(const AName: string): string;
|
||||
function Cookie(const AName: string): string;
|
||||
function Body: string;
|
||||
function BodyAs<T: class, constructor>: T;
|
||||
function BodyAs<T: class, constructor>(const RootNode: string = ''): T;
|
||||
function BodyAsListOf<T: class, constructor>: TObjectList<T>;
|
||||
procedure BodyFor<T: class, constructor>(const AObject: T);
|
||||
procedure BodyForListOf<T: class, constructor>(const AObjectList: TObjectList<T>);
|
||||
// function HeaderNames: TArray<String>;
|
||||
// function HeaderNames: TArray<String>;
|
||||
property RawWebRequest: TWebRequest read FWebRequest;
|
||||
property ContentMediaType: string read FContentMediaType;
|
||||
property ContentType: string read FContentType;
|
||||
@ -606,7 +606,7 @@ type
|
||||
FContentCharset: string;
|
||||
FResponseStream: TStringBuilder;
|
||||
function ToMVCList(const AObject: TObject; AOwnsObject: Boolean = False): IMVCList;
|
||||
public
|
||||
public { this must be public because of entity processors }
|
||||
function GetContentType: string;
|
||||
function GetStatusCode: Integer;
|
||||
procedure SetContentType(const AValue: string);
|
||||
@ -756,16 +756,17 @@ type
|
||||
property StatusCode: Integer read GetStatusCode write SetStatusCode;
|
||||
property ViewModelList: TMVCViewDataObject read GetViewModel;
|
||||
property ViewDataSetList: TMVCViewDataSet read GetViewDataSets;
|
||||
public
|
||||
constructor Create; virtual;
|
||||
destructor Destroy; override;
|
||||
|
||||
// procedure PushToView(const AModelName: string; const AModel: string);
|
||||
procedure PushObjectToView(const aModelName: string; const AModel: TObject); deprecated 'Use "ViewData"';
|
||||
procedure PushDataSetToView(const aModelName: string; const ADataSet: TDataSet); deprecated 'Use "ViewDataSet"';
|
||||
|
||||
property ViewData[const aModelName: string]: TObject read GetViewData write SetViewData;
|
||||
property ViewDataset[const aDataSetName: string]: TDataSet read GetViewDataset write SetViewDataset;
|
||||
|
||||
public
|
||||
constructor Create; virtual;
|
||||
destructor Destroy; override;
|
||||
|
||||
end;
|
||||
|
||||
TMVCControllerClazz = class of TMVCController;
|
||||
@ -854,7 +855,7 @@ type
|
||||
FConfig: TMVCConfig;
|
||||
FConfigCache_MaxRequestSize: Int64;
|
||||
FConfigCache_ExposeServerSignature: Boolean;
|
||||
FConfigCache_ServerSignature: String;
|
||||
FConfigCache_ServerSignature: string;
|
||||
FConfigCache_ExposeXPoweredBy: Boolean;
|
||||
FSerializers: TDictionary<string, IMVCSerializer>;
|
||||
FMiddlewares: TList<IMVCMiddleware>;
|
||||
@ -1144,7 +1145,7 @@ begin
|
||||
Result := FBody;
|
||||
end;
|
||||
|
||||
function TMVCWebRequest.BodyAs<T>: T;
|
||||
function TMVCWebRequest.BodyAs<T>(const RootNode: string): T;
|
||||
var
|
||||
Obj: TObject;
|
||||
lSerializer: IMVCSerializer;
|
||||
@ -1154,7 +1155,7 @@ begin
|
||||
begin
|
||||
Obj := TMVCSerializerHelper.CreateObject(TClass(T).QualifiedClassName);
|
||||
try
|
||||
lSerializer.DeserializeObject(Body, Obj);
|
||||
lSerializer.DeserializeObject(Body, Obj, TMVCSerializationType.stDefault, nil, RootNode);
|
||||
Result := Obj as T;
|
||||
except
|
||||
on E: Exception do
|
||||
@ -1209,19 +1210,19 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
//function TMVCWebRequest.HeaderNames: TArray<String>;
|
||||
//var
|
||||
// lHeaderList: TIdHeaderList;
|
||||
// I: Integer;
|
||||
//begin
|
||||
// EnsureINDY;
|
||||
// lHeaderList := THackIdHTTPAppRequest(TMVCIndyWebRequest(Self).RawWebRequest).FRequestInfo.RawHeaders;
|
||||
// SetLength(Result, lHeaderList.Count);
|
||||
// for I := 0 to Pred(lHeaderList.Count) do
|
||||
// begin
|
||||
// Result[I] := lHeaderList.Names[I];
|
||||
// end;
|
||||
//end;
|
||||
// function TMVCWebRequest.HeaderNames: TArray<String>;
|
||||
// var
|
||||
// lHeaderList: TIdHeaderList;
|
||||
// I: Integer;
|
||||
// begin
|
||||
// EnsureINDY;
|
||||
// lHeaderList := THackIdHTTPAppRequest(TMVCIndyWebRequest(Self).RawWebRequest).FRequestInfo.RawHeaders;
|
||||
// SetLength(Result, lHeaderList.Count);
|
||||
// for I := 0 to Pred(lHeaderList.Count) do
|
||||
// begin
|
||||
// Result[I] := lHeaderList.Names[I];
|
||||
// end;
|
||||
// end;
|
||||
|
||||
procedure TMVCWebRequest.BodyForListOf<T>(const AObjectList: TObjectList<T>);
|
||||
var
|
||||
@ -1335,7 +1336,7 @@ end;
|
||||
|
||||
procedure TMVCWebRequest.EnsureINDY;
|
||||
begin
|
||||
if not (Self is TMVCIndyWebRequest) then
|
||||
if not(Self is TMVCIndyWebRequest) then
|
||||
begin
|
||||
raise EMVCException.Create(http_status.InternalServerError, 'Method available only in INDY implementation');
|
||||
end;
|
||||
@ -2111,7 +2112,8 @@ begin
|
||||
|
||||
if ARequest.ContentLength > FConfigCache_MaxRequestSize then
|
||||
begin
|
||||
raise EMVCException.CreateFmt(HTTP_STATUS.RequestEntityTooLarge, 'Request size exceeded the max allowed size [%d KiB] (1)',
|
||||
raise EMVCException.CreateFmt(http_status.RequestEntityTooLarge,
|
||||
'Request size exceeded the max allowed size [%d KiB] (1)',
|
||||
[(FConfigCache_MaxRequestSize div 1024)]);
|
||||
end;
|
||||
|
||||
@ -2121,7 +2123,8 @@ begin
|
||||
// Double check for malicious content-length header
|
||||
if ARequest.ContentLength > FConfigCache_MaxRequestSize then
|
||||
begin
|
||||
raise EMVCException.CreateFmt(HTTP_STATUS.RequestEntityTooLarge, 'Request size exceeded the max allowed size [%d KiB] (2)',
|
||||
raise EMVCException.CreateFmt(http_status.RequestEntityTooLarge,
|
||||
'Request size exceeded the max allowed size [%d KiB] (2)',
|
||||
[(FConfigCache_MaxRequestSize div 1024)]);
|
||||
end;
|
||||
{$ENDIF}
|
||||
@ -2155,7 +2158,7 @@ begin
|
||||
begin
|
||||
Log.ErrorFmt('[%s] %s (Custom message: "%s")',
|
||||
[Ex.Classname, Ex.Message, 'Cannot create controller'], LOGGERPRO_TAG);
|
||||
raise EMVCException.Create(HTTP_STATUS.InternalServerError, 'Cannot create controller');
|
||||
raise EMVCException.Create(http_status.InternalServerError, 'Cannot create controller');
|
||||
end;
|
||||
end;
|
||||
lSelectedController.Engine := Self;
|
||||
@ -2208,14 +2211,14 @@ begin
|
||||
begin
|
||||
if Config[TMVCConfigKey.AllowUnhandledAction] = 'false' then
|
||||
begin
|
||||
lContext.Response.StatusCode := HTTP_STATUS.NotFound;
|
||||
lContext.Response.StatusCode := http_status.NotFound;
|
||||
lContext.Response.ReasonString := 'Not Found';
|
||||
fOnRouterLog(lRouter, rlsRouteNotFound, lContext);
|
||||
raise EMVCException.Create(
|
||||
lContext.Response.ReasonString,
|
||||
lContext.Request.HTTPMethodAsString + ' ' + lContext.Request.PathInfo,
|
||||
0,
|
||||
HTTP_STATUS.NotFound
|
||||
http_status.NotFound
|
||||
);
|
||||
end
|
||||
else
|
||||
@ -2262,12 +2265,12 @@ begin
|
||||
LOGGERPRO_TAG);
|
||||
if Assigned(lSelectedController) then
|
||||
begin
|
||||
lSelectedController.ResponseStatus(HTTP_STATUS.InternalServerError);
|
||||
lSelectedController.ResponseStatus(http_status.InternalServerError);
|
||||
lSelectedController.Render(EIO);
|
||||
end
|
||||
else
|
||||
begin
|
||||
SendRawHTTPStatus(lContext, HTTP_STATUS.InternalServerError,
|
||||
SendRawHTTPStatus(lContext, http_status.InternalServerError,
|
||||
Format('[%s] %s', [EIO.Classname, EIO.Message]), EIO.Classname);
|
||||
end;
|
||||
end;
|
||||
@ -2280,12 +2283,12 @@ begin
|
||||
[Ex.Classname, Ex.Message, 'Global Action Exception Handler'], LOGGERPRO_TAG);
|
||||
if Assigned(lSelectedController) then
|
||||
begin
|
||||
lSelectedController.ResponseStatus(HTTP_STATUS.InternalServerError);
|
||||
lSelectedController.ResponseStatus(http_status.InternalServerError);
|
||||
lSelectedController.Render(Ex);
|
||||
end
|
||||
else
|
||||
begin
|
||||
SendRawHTTPStatus(lContext, HTTP_STATUS.InternalServerError,
|
||||
SendRawHTTPStatus(lContext, http_status.InternalServerError,
|
||||
Format('[%s] %s', [Ex.Classname, Ex.Message]), Ex.Classname);
|
||||
end;
|
||||
end;
|
||||
@ -2303,12 +2306,12 @@ begin
|
||||
if Assigned(lSelectedController) then
|
||||
begin
|
||||
{ middlewares *must* not raise unhandled exceptions }
|
||||
lSelectedController.ResponseStatus(HTTP_STATUS.InternalServerError);
|
||||
lSelectedController.ResponseStatus(http_status.InternalServerError);
|
||||
lSelectedController.Render(Ex);
|
||||
end
|
||||
else
|
||||
begin
|
||||
SendRawHTTPStatus(lContext, HTTP_STATUS.InternalServerError,
|
||||
SendRawHTTPStatus(lContext, http_status.InternalServerError,
|
||||
Format('[%s] %s', [Ex.Classname, Ex.Message]), Ex.Classname);
|
||||
end;
|
||||
end;
|
||||
@ -2402,7 +2405,7 @@ var
|
||||
lQualifiedName: string;
|
||||
begin
|
||||
if AContext.Request.SegmentParamsCount <> Length(AActionFormalParams) then
|
||||
raise EMVCException.CreateFmt(HTTP_STATUS.BadRequest,
|
||||
raise EMVCException.CreateFmt(http_status.BadRequest,
|
||||
'Parameters count mismatch (expected %d actual %d) for action "%s"',
|
||||
[Length(AActionFormalParams), AContext.Request.SegmentParamsCount, AActionName]);
|
||||
|
||||
@ -2413,7 +2416,7 @@ begin
|
||||
|
||||
if not AContext.Request.SegmentParam(lParamName, lStrValue) then
|
||||
raise EMVCException.CreateFmt
|
||||
(HTTP_STATUS.BadRequest, 'Invalid parameter %s for action %s (Hint: Here parameters names are case-sensitive)',
|
||||
(http_status.BadRequest, 'Invalid parameter %s for action %s (Hint: Here parameters names are case-sensitive)',
|
||||
[lParamName, AActionName]);
|
||||
|
||||
case AActionFormalParams[I].ParamType.TypeKind of
|
||||
@ -2423,7 +2426,7 @@ begin
|
||||
except
|
||||
on E: Exception do
|
||||
begin
|
||||
raise EMVCException.CreateFmt(HTTP_STATUS.BadRequest,
|
||||
raise EMVCException.CreateFmt(http_status.BadRequest,
|
||||
'Invalid Integer value for param [%s] - [CLASS: %s][MSG: %s]',
|
||||
[AActionFormalParams[I].name, E.Classname, E.Message]);
|
||||
end;
|
||||
@ -2434,7 +2437,7 @@ begin
|
||||
except
|
||||
on E: Exception do
|
||||
begin
|
||||
raise EMVCException.CreateFmt(HTTP_STATUS.BadRequest,
|
||||
raise EMVCException.CreateFmt(http_status.BadRequest,
|
||||
'Invalid Int64 value for param [%s] - [CLASS: %s][MSG: %s]',
|
||||
[AActionFormalParams[I].name, E.Classname, E.Message]);
|
||||
end;
|
||||
@ -2455,7 +2458,7 @@ begin
|
||||
except
|
||||
on E: Exception do
|
||||
begin
|
||||
raise EMVCException.CreateFmt(HTTP_STATUS.BadRequest,
|
||||
raise EMVCException.CreateFmt(http_status.BadRequest,
|
||||
'Invalid TDate value for param [%s] - [CLASS: %s][MSG: %s]',
|
||||
[AActionFormalParams[I].name, E.Classname, E.Message]);
|
||||
end;
|
||||
@ -2470,7 +2473,7 @@ begin
|
||||
except
|
||||
on E: Exception do
|
||||
begin
|
||||
raise EMVCException.CreateFmt(HTTP_STATUS.BadRequest,
|
||||
raise EMVCException.CreateFmt(http_status.BadRequest,
|
||||
'Invalid TDateTime value for param [%s] - [CLASS: %s][MSG: %s]',
|
||||
[AActionFormalParams[I].name, E.Classname, E.Message]);
|
||||
end;
|
||||
@ -2485,7 +2488,7 @@ begin
|
||||
except
|
||||
on E: Exception do
|
||||
begin
|
||||
raise EMVCException.CreateFmt(HTTP_STATUS.BadRequest,
|
||||
raise EMVCException.CreateFmt(http_status.BadRequest,
|
||||
'Invalid TTime value for param [%s] - [CLASS: %s][MSG: %s]',
|
||||
[AActionFormalParams[I].name, E.Classname, E.Message]);
|
||||
end;
|
||||
@ -2498,7 +2501,7 @@ begin
|
||||
except
|
||||
on E: Exception do
|
||||
begin
|
||||
raise EMVCException.CreateFmt(HTTP_STATUS.BadRequest,
|
||||
raise EMVCException.CreateFmt(http_status.BadRequest,
|
||||
'Invalid Float value for param [%s] - [CLASS: %s][MSG: %s]',
|
||||
[AActionFormalParams[I].name, E.Classname, E.Message]);
|
||||
end;
|
||||
@ -2516,14 +2519,14 @@ begin
|
||||
else
|
||||
begin
|
||||
raise EMVCException.CreateFmt
|
||||
(HTTP_STATUS.BadRequest,
|
||||
(http_status.BadRequest,
|
||||
'Invalid boolean value for parameter %s. Boolean parameters accepts only "true"/"false" or "1"/"0".',
|
||||
[lParamName]);
|
||||
end;
|
||||
end
|
||||
else
|
||||
begin
|
||||
raise EMVCException.CreateFmt(HTTP_STATUS.BadRequest, 'Invalid type for parameter %s. Allowed types are ' +
|
||||
raise EMVCException.CreateFmt(http_status.BadRequest, 'Invalid type for parameter %s. Allowed types are ' +
|
||||
ALLOWED_TYPED_ACTION_PARAMETERS_TYPES, [lParamName]);
|
||||
end;
|
||||
end;
|
||||
@ -2543,7 +2546,7 @@ begin
|
||||
end
|
||||
else
|
||||
begin
|
||||
raise EMVCException.CreateFmt(HTTP_STATUS.BadRequest, 'Invalid type for parameter %s. Allowed types are ' +
|
||||
raise EMVCException.CreateFmt(http_status.BadRequest, 'Invalid type for parameter %s. Allowed types are ' +
|
||||
ALLOWED_TYPED_ACTION_PARAMETERS_TYPES, [lParamName]);
|
||||
end;
|
||||
end;
|
||||
@ -2610,7 +2613,7 @@ end;
|
||||
|
||||
procedure TMVCEngine.HTTP404(const AContext: TWebContext);
|
||||
begin
|
||||
AContext.Response.SetStatusCode(HTTP_STATUS.NotFound);
|
||||
AContext.Response.SetStatusCode(http_status.NotFound);
|
||||
AContext.Response.SetContentType(BuildContentType(TMVCMediaType.TEXT_PLAIN,
|
||||
AContext.Config[TMVCConfigKey.DefaultContentCharset]));
|
||||
AContext.Response.SetReasonString('Not Found');
|
||||
@ -2619,7 +2622,7 @@ end;
|
||||
|
||||
procedure TMVCEngine.HTTP500(const AContext: TWebContext; const AReasonString: string);
|
||||
begin
|
||||
AContext.Response.SetStatusCode(HTTP_STATUS.InternalServerError);
|
||||
AContext.Response.SetStatusCode(http_status.InternalServerError);
|
||||
AContext.Response.SetContentType(BuildContentType(TMVCMediaType.TEXT_PLAIN,
|
||||
AContext.Config[TMVCConfigKey.DefaultContentCharset]));
|
||||
AContext.Response.SetReasonString('Internal server error');
|
||||
@ -2680,7 +2683,7 @@ begin
|
||||
|
||||
if IsShuttingDown then
|
||||
begin
|
||||
AResponse.StatusCode := HTTP_STATUS.ServiceUnavailable;
|
||||
AResponse.StatusCode := http_status.ServiceUnavailable;
|
||||
AResponse.ContentType := TMVCMediaType.TEXT_PLAIN;
|
||||
AResponse.Content := 'Server is shutting down';
|
||||
AHandled := True;
|
||||
@ -2695,10 +2698,10 @@ begin
|
||||
begin
|
||||
Log.ErrorFmt('[%s] %s', [E.Classname, E.Message], LOGGERPRO_TAG);
|
||||
|
||||
AResponse.StatusCode:= HTTP_STATUS.InternalServerError; // default is Internal Server Error
|
||||
AResponse.StatusCode := http_status.InternalServerError; // default is Internal Server Error
|
||||
if E is EMVCException then
|
||||
begin
|
||||
AResponse.StatusCode:= (E as EMVCException).HttpErrorCode;
|
||||
AResponse.StatusCode := (E as EMVCException).HTTPErrorCode;
|
||||
end;
|
||||
|
||||
AResponse.Content := E.Message;
|
||||
@ -2741,7 +2744,7 @@ procedure TMVCEngine.ResponseErrorPage(const AException: Exception; const AReque
|
||||
const AResponse: TWebResponse);
|
||||
begin
|
||||
AResponse.SetCustomHeader('x-mvc-error', AException.Classname + ': ' + AException.Message);
|
||||
AResponse.StatusCode := HTTP_STATUS.OK;
|
||||
AResponse.StatusCode := http_status.OK;
|
||||
|
||||
begin
|
||||
AResponse.ContentType := TMVCMediaType.TEXT_PLAIN;
|
||||
@ -2755,7 +2758,8 @@ class function TMVCEngine.SendSessionCookie(const AContext: TWebContext): string
|
||||
var
|
||||
SId: string;
|
||||
begin
|
||||
SId := StringReplace(StringReplace(StringReplace(GUIDToString(TGUID.NewGuid), '}', '', []), '{', '', []), '-', '',
|
||||
SId := StringReplace(StringReplace(StringReplace('DT' + GUIDToString(TGUID.NewGuid), '}', '',
|
||||
[]), '{', '', []), '-', '',
|
||||
[rfReplaceAll]);
|
||||
Result := SendSessionCookie(AContext, SId);
|
||||
end;
|
||||
@ -3132,7 +3136,7 @@ begin
|
||||
begin
|
||||
raise EMVCException.Create('Cannot send 202 without provide an HREF');
|
||||
end;
|
||||
ResponseStatus(HTTP_STATUS.Accepted, Reason);
|
||||
ResponseStatus(http_status.Accepted, Reason);
|
||||
Render(TMVCAcceptedResponse.Create(HREF, ID));
|
||||
end;
|
||||
|
||||
@ -3142,10 +3146,10 @@ begin
|
||||
begin
|
||||
FContext.Response.CustomHeaders.AddPair('location', Location);
|
||||
end;
|
||||
ResponseStatus(HTTP_STATUS.Created, Reason);
|
||||
{$IF CompilerVersion >= 34}
|
||||
Render(''); //in 10.4 INDY requires something on the content
|
||||
{$ENDIF}
|
||||
ResponseStatus(http_status.Created, Reason);
|
||||
{$IF CompilerVersion >= 34}
|
||||
Render(''); // in 10.4 INDY requires something on the content
|
||||
{$ENDIF}
|
||||
end;
|
||||
|
||||
procedure TMVCRenderer.Render204NoContent(const Location, Reason: string);
|
||||
@ -3154,7 +3158,7 @@ begin
|
||||
begin
|
||||
FContext.Response.CustomHeaders.AddPair('location', Location);
|
||||
end;
|
||||
ResponseStatus(HTTP_STATUS.NoContent, Reason);
|
||||
ResponseStatus(http_status.NoContent, Reason);
|
||||
end;
|
||||
|
||||
procedure TMVCRenderer.ResponseStatus(const AStatusCode: Integer; const AReasonString: string);
|
||||
@ -3442,7 +3446,7 @@ begin
|
||||
// setting up the correct SSE headers
|
||||
SetContentType('text/event-stream');
|
||||
GetContext.Response.SetCustomHeader('Cache-Control', 'no-cache');
|
||||
GetContext.Response.StatusCode := HTTP_STATUS.OK;
|
||||
GetContext.Response.StatusCode := http_status.OK;
|
||||
|
||||
// render the response using SSE compliant data format
|
||||
|
||||
@ -3509,8 +3513,8 @@ begin
|
||||
if AException is EMVCException then
|
||||
ResponseStatus(EMVCException(AException).HTTPErrorCode, AException.Message + ' [' + AException.Classname + ']');
|
||||
|
||||
if (GetContext.Response.StatusCode = HTTP_STATUS.OK) then
|
||||
ResponseStatus(HTTP_STATUS.InternalServerError, AException.Message + ' [' + AException.Classname + ']');
|
||||
if (GetContext.Response.StatusCode = http_status.OK) then
|
||||
ResponseStatus(http_status.InternalServerError, AException.Message + ' [' + AException.Classname + ']');
|
||||
|
||||
if (not GetContext.Request.IsAjax) and (GetContext.Request.ClientPrefer(TMVCMediaType.TEXT_HTML)) then
|
||||
begin
|
||||
|
@ -5,25 +5,29 @@ program DMVCFrameworkTests;
|
||||
{$APPTYPE CONSOLE}
|
||||
{$ENDIF}{$ENDIF}{$STRONGLINKTYPES ON}
|
||||
|
||||
|
||||
uses
|
||||
System.SysUtils,
|
||||
{$IFDEF GUI_TESTRUNNER}
|
||||
{$IFDEF GUI_TESTRUNNER}
|
||||
Vcl.Forms,
|
||||
DUnitX.Loggers.GUI.Vcl,
|
||||
{$ENDIF }
|
||||
{$IFDEF CONSOLE_TESTRUNNER}
|
||||
// Fmx.Forms,
|
||||
// DUNitX.Loggers.GUIX,
|
||||
{$ENDIF }
|
||||
{$IFDEF CONSOLE_TESTRUNNER}
|
||||
DUnitX.Loggers.Console,
|
||||
{$ENDIF }
|
||||
DUnitX.Loggers.Xml.NUnit,
|
||||
{$ENDIF }
|
||||
// DUnitX.Loggers.Xml.NUnit,
|
||||
DUnitX.TestFramework,
|
||||
FrameworkTestsU in 'FrameworkTestsU.pas',
|
||||
LiveServerTestU in 'LiveServerTestU.pas',
|
||||
BOs in 'BOs.pas',
|
||||
TestServerControllerU in '..\TestServer\TestServerControllerU.pas',
|
||||
RESTAdapterTestsU in 'RESTAdapterTestsU.pas',
|
||||
MVCFramework.Tests.WebModule2 in '..\StandaloneServer\MVCFramework.Tests.WebModule2.pas' {TestWebModule2: TWebModule},
|
||||
MVCFramework.Tests.WebModule2
|
||||
in '..\StandaloneServer\MVCFramework.Tests.WebModule2.pas' {TestWebModule2: TWebModule} ,
|
||||
MVCFramework.Tests.StandaloneServer in '..\StandaloneServer\MVCFramework.Tests.StandaloneServer.pas',
|
||||
MVCFramework.Tests.WebModule1 in '..\RESTClient\MVCFramework.Tests.WebModule1.pas' {TestWebModule1: TWebModule},
|
||||
MVCFramework.Tests.WebModule1 in '..\RESTClient\MVCFramework.Tests.WebModule1.pas' {TestWebModule1: TWebModule} ,
|
||||
MVCFramework.Tests.RESTClient in '..\RESTClient\MVCFramework.Tests.RESTClient.pas',
|
||||
MVCFramework.Tests.AppController in '..\RESTClient\MVCFramework.Tests.AppController.pas',
|
||||
BusinessObjectsU in '..\..\..\samples\commons\BusinessObjectsU.pas',
|
||||
@ -37,21 +41,24 @@ uses
|
||||
JsonDataObjects in '..\..\..\sources\JsonDataObjects.pas',
|
||||
Serializers.JsonDataObjectsTestU in 'Serializers.JsonDataObjectsTestU.pas',
|
||||
MVCFramework.Tests.Serializer.Entities in '..\..\common\MVCFramework.Tests.Serializer.Entities.pas',
|
||||
MVCFramework.Tests.Serializer.EntitiesModule in '..\..\common\MVCFramework.Tests.Serializer.EntitiesModule.pas' {EntitiesModule: TDataModule},
|
||||
MVCFramework.Tests.Serializer.EntitiesModule
|
||||
in '..\..\common\MVCFramework.Tests.Serializer.EntitiesModule.pas' {EntitiesModule: TDataModule} ,
|
||||
MVCFramework.Tests.Serializer.Intf in '..\..\common\MVCFramework.Tests.Serializer.Intf.pas',
|
||||
MVCFramework.Serializer.JsonDataObjects.OptionalCustomTypes in '..\..\..\sources\MVCFramework.Serializer.JsonDataObjects.OptionalCustomTypes.pas',
|
||||
MVCFramework.Serializer.JsonDataObjects.OptionalCustomTypes
|
||||
in '..\..\..\sources\MVCFramework.Serializer.JsonDataObjects.OptionalCustomTypes.pas',
|
||||
ActiveRecordTestsU in 'ActiveRecordTestsU.pas',
|
||||
TestConstsU in 'TestConstsU.pas';
|
||||
|
||||
{$R *.RES}
|
||||
{$IFDEF CONSOLE_TESTRUNNER}
|
||||
|
||||
|
||||
procedure MainConsole();
|
||||
var
|
||||
runner: ITestRunner;
|
||||
results: IRunResults;
|
||||
logger: ITestLogger;
|
||||
// nunitLogger: ITestLogger;
|
||||
// nunitLogger: ITestLogger;
|
||||
begin
|
||||
try
|
||||
// Check command line options, will exit if invalid
|
||||
@ -90,14 +97,17 @@ end;
|
||||
{$ENDIF}
|
||||
{$IFDEF GUI_TESTRUNNER}
|
||||
|
||||
|
||||
procedure MainGUI;
|
||||
begin
|
||||
Application.Initialize;
|
||||
Application.CreateForm(TGUIVCLTestRunner, GUIVCLTestRunner);
|
||||
// Application.CreateForm(TGUIXTestRunner, GUIXTestRunner);
|
||||
Application.Run;
|
||||
end;
|
||||
{$ENDIF}
|
||||
|
||||
|
||||
begin
|
||||
ReportMemoryLeaksOnShutdown := True;
|
||||
{$IFDEF CONSOLE_TESTRUNNER}
|
||||
|
@ -4,7 +4,7 @@
|
||||
<ProjectVersion>19.0</ProjectVersion>
|
||||
<FrameworkType>VCL</FrameworkType>
|
||||
<Base>True</Base>
|
||||
<Config Condition="'$(Config)'==''">GUI</Config>
|
||||
<Config Condition="'$(Config)'==''">CONSOLE</Config>
|
||||
<Platform Condition="'$(Platform)'==''">Win32</Platform>
|
||||
<TargetedPlatforms>1</TargetedPlatforms>
|
||||
<AppType>Console</AppType>
|
||||
|
@ -33,7 +33,6 @@ uses
|
||||
System.DateUtils,
|
||||
System.Hash;
|
||||
|
||||
|
||||
type
|
||||
|
||||
TBaseServerTest = class(TObject)
|
||||
@ -224,12 +223,17 @@ type
|
||||
protected
|
||||
FExecutor: IMVCJSONRPCExecutor;
|
||||
FExecutor2: IMVCJSONRPCExecutor;
|
||||
FExecutor3: IMVCJSONRPCExecutor;
|
||||
public
|
||||
[Setup]
|
||||
procedure Setup;
|
||||
[Test]
|
||||
procedure TestRequestWithoutParams;
|
||||
[Test]
|
||||
procedure TestNotificationWithoutParams;
|
||||
[Test]
|
||||
procedure TestNotificationWhichRaisesError;
|
||||
[Test]
|
||||
procedure TestRequestToNotFoundMethod;
|
||||
[Test]
|
||||
procedure TestRequestWithParams_I_I_ret_I;
|
||||
@ -249,6 +253,26 @@ type
|
||||
procedure TestRequestWithParams_I_I_ret_A;
|
||||
[Test]
|
||||
procedure TestRequestWithParams_DT_T_ret_DT;
|
||||
// hooks tests
|
||||
[Test]
|
||||
procedure TestHooks;
|
||||
[Test]
|
||||
procedure TestHooksWhenMethodRaisesError;
|
||||
[Test]
|
||||
procedure TestHooksWhenOnAfterCallHookRaisesError;
|
||||
[Test]
|
||||
procedure TestHooksNotif;
|
||||
[Test]
|
||||
procedure TestHooksNotifWhenOnBeforeRoutingHookRaisesError;
|
||||
[Test]
|
||||
procedure TestHooksNotifWhenOnBeforeCallHookRaisesError;
|
||||
[Test]
|
||||
procedure TestHooksNotifWhenOnAfterCallHookRaisesError;
|
||||
[Test]
|
||||
procedure TestHooksWhenOnBeforeCallHookRaisesError;
|
||||
[Test]
|
||||
procedure TestHooksWhenOnBeforeRoutingHookRaisesError;
|
||||
|
||||
end;
|
||||
|
||||
implementation
|
||||
@ -278,7 +302,7 @@ uses
|
||||
{$ENDIF}
|
||||
, TestConstsU;
|
||||
|
||||
function GetServer: String;
|
||||
function GetServer: string;
|
||||
begin
|
||||
Result := 'http://' + TEST_SERVER_ADDRESS + ':9999';
|
||||
end;
|
||||
@ -1876,6 +1900,123 @@ procedure TJSONRPCServerTest.Setup;
|
||||
begin
|
||||
FExecutor := TMVCJSONRPCExecutor.Create('http://' + TEST_SERVER_ADDRESS + ':9999/jsonrpc', false);
|
||||
FExecutor2 := TMVCJSONRPCExecutor.Create('http://' + TEST_SERVER_ADDRESS + ':9999/jsonrpcclass', false);
|
||||
FExecutor3 := TMVCJSONRPCExecutor.Create('http://' + TEST_SERVER_ADDRESS + ':9999/jsonrpcclass1', false);
|
||||
end;
|
||||
|
||||
procedure TJSONRPCServerTest.TestHooks;
|
||||
begin
|
||||
var lRequest1: IJSONRPCRequest := TJSONRPCRequest.Create(1234, 'request1');
|
||||
var lResp := FExecutor3.ExecuteRequest(lRequest1);
|
||||
Assert.areEqual('OnBeforeRoutingHook|OnBeforeCallHook|OnAfterCallHook',
|
||||
FExecutor3.HTTPResponse.HeaderValue['x-history']);
|
||||
end;
|
||||
|
||||
procedure TJSONRPCServerTest.TestHooksNotif;
|
||||
begin
|
||||
var lNotif: IJSONRPCNotification := TJSONRPCNotification.Create('Notif1');
|
||||
var lResp := FExecutor3.ExecuteNotification(lNotif);
|
||||
Assert.areEqual('OnBeforeRoutingHook|OnBeforeCallHook|OnAfterCallHook', FExecutor3.HTTPResponse.HeaderValue['x-history']);
|
||||
Assert.IsFalse(lResp.IsError);
|
||||
Assert.WillRaise(
|
||||
procedure
|
||||
begin
|
||||
lResp.AsJSONString;
|
||||
end, EMVCJSONRPCException);
|
||||
end;
|
||||
|
||||
procedure TJSONRPCServerTest.TestHooksNotifWhenOnAfterCallHookRaisesError;
|
||||
begin
|
||||
var lNotif: IJSONRPCNotification := TJSONRPCNotification.Create('error_OnAfterCallHook');
|
||||
var lResp: IJSONRPCResponse := FExecutor3.ExecuteNotification(lNotif);
|
||||
Assert.areEqual('', FExecutor3.HTTPResponse.HeaderValue['x-history']);
|
||||
Assert.isTrue(lResp.IsError);
|
||||
Assert.WillNotRaise(
|
||||
procedure
|
||||
begin
|
||||
lResp.AsJSONString;
|
||||
end, EMVCJSONRPCException);
|
||||
end;
|
||||
|
||||
procedure TJSONRPCServerTest.TestHooksNotifWhenOnBeforeCallHookRaisesError;
|
||||
begin
|
||||
var lNotif: IJSONRPCNotification := TJSONRPCNotification.Create('error_OnBeforeCallHook');
|
||||
var lResp: IJSONRPCResponse := FExecutor3.ExecuteNotification(lNotif);
|
||||
Assert.areEqual('', FExecutor3.HTTPResponse.HeaderValue['x-history']);
|
||||
Assert.isTrue(lResp.IsError);
|
||||
Assert.WillNotRaise(
|
||||
procedure
|
||||
begin
|
||||
lResp.AsJSONString;
|
||||
end, EMVCJSONRPCException);
|
||||
end;
|
||||
|
||||
procedure TJSONRPCServerTest.TestHooksNotifWhenOnBeforeRoutingHookRaisesError;
|
||||
begin
|
||||
var lNotif: IJSONRPCNotification := TJSONRPCNotification.Create('error_OnBeforeRoutingHook');
|
||||
var lResp: IJSONRPCResponse := FExecutor3.ExecuteNotification(lNotif);
|
||||
Assert.areEqual('', FExecutor3.HTTPResponse.HeaderValue['x-history']);
|
||||
Assert.isTrue(lResp.IsError);
|
||||
Assert.WillNotRaise(
|
||||
procedure
|
||||
begin
|
||||
lResp.AsJSONString;
|
||||
end, EMVCJSONRPCException);
|
||||
end;
|
||||
|
||||
procedure TJSONRPCServerTest.TestHooksWhenMethodRaisesError;
|
||||
begin
|
||||
var lRequest1: IJSONRPCRequest := TJSONRPCRequest.Create(1234, 'RequestWithError');
|
||||
var lResp := FExecutor3.ExecuteRequest(lRequest1);
|
||||
Assert.areEqual('OnBeforeRoutingHook|OnBeforeCallHook|OnAfterCallHook|error',
|
||||
FExecutor3.HTTPResponse.HeaderValue['x-history']);
|
||||
Assert.isTrue(lResp.IsError, 'Method raised error but response is not an error');
|
||||
end;
|
||||
|
||||
procedure TJSONRPCServerTest.TestHooksWhenOnAfterCallHookRaisesError;
|
||||
begin
|
||||
var lRequest1: IJSONRPCRequest := TJSONRPCRequest.Create(1234, 'error_OnAfterCallHook');
|
||||
var lResp := FExecutor3.ExecuteRequest(lRequest1);
|
||||
Assert.isTrue(lResp.IsError, lResp.ToString(true));
|
||||
Assert.areEqual(lResp.Error.ErrMessage, 'error_OnAfterCallHook');
|
||||
end;
|
||||
|
||||
procedure TJSONRPCServerTest.TestHooksWhenOnBeforeCallHookRaisesError;
|
||||
begin
|
||||
var lRequest1: IJSONRPCRequest := TJSONRPCRequest.Create(1234, 'error_OnBeforeCallHook');
|
||||
var lResp := FExecutor3.ExecuteRequest(lRequest1);
|
||||
Assert.isTrue(lResp.IsError, lResp.ToString(true));
|
||||
Assert.areEqual(lResp.Error.ErrMessage, 'error_OnBeforeCallHook');
|
||||
|
||||
end;
|
||||
|
||||
procedure TJSONRPCServerTest.TestHooksWhenOnBeforeRoutingHookRaisesError;
|
||||
begin
|
||||
var lRequest1: IJSONRPCRequest := TJSONRPCRequest.Create(1234, 'error_OnBeforeRoutingHook');
|
||||
var lResp := FExecutor3.ExecuteRequest(lRequest1);
|
||||
Assert.isTrue(lResp.IsError, lResp.ToString(true));
|
||||
Assert.areEqual(lResp.Error.ErrMessage, 'error_OnBeforeRoutingHook');
|
||||
end;
|
||||
|
||||
procedure TJSONRPCServerTest.TestNotificationWhichRaisesError;
|
||||
var
|
||||
lReq: IJSONRPCNotification;
|
||||
begin
|
||||
lReq := TJSONRPCNotification.Create;
|
||||
lReq.Method := 'NotifWithError';
|
||||
var lResp := FExecutor3.ExecuteNotification(lReq);
|
||||
Assert.IsTrue(lResp.IsError);
|
||||
Assert.Contains(lResp.Error.ErrMessage, 'BOOM NOTIF');
|
||||
end;
|
||||
|
||||
procedure TJSONRPCServerTest.TestNotificationWithoutParams;
|
||||
var
|
||||
lReq: IJSONRPCNotification;
|
||||
begin
|
||||
lReq := TJSONRPCNotification.Create;
|
||||
lReq.Method := 'mynotify';
|
||||
FExecutor.ExecuteNotification(lReq);
|
||||
FExecutor2.ExecuteNotification(lReq);
|
||||
Assert.Pass();
|
||||
end;
|
||||
|
||||
procedure TJSONRPCServerTest.TestRequestToNotFoundMethod;
|
||||
@ -1978,13 +2119,15 @@ end;
|
||||
|
||||
procedure TJSONRPCServerTest.TestRequestWithoutParams;
|
||||
var
|
||||
lReq: IJSONRPCNotification;
|
||||
lReq: IJSONRPCRequest;
|
||||
lResp: IJSONRPCResponse;
|
||||
begin
|
||||
lReq := TJSONRPCNotification.Create;
|
||||
lReq.Method := 'mynotify';
|
||||
FExecutor.ExecuteNotification(lReq);
|
||||
FExecutor2.ExecuteNotification(lReq);
|
||||
Assert.Pass();
|
||||
lReq := TJSONRPCRequest.Create;
|
||||
lReq.Method := 'MyRequest';
|
||||
lReq.RequestID := 1234;
|
||||
lResp := FExecutor.ExecuteRequest(lReq);
|
||||
Assert.isFalse(lResp.IsError);
|
||||
Assert.isTrue(lResp.Result.AsBoolean);
|
||||
end;
|
||||
|
||||
procedure TJSONRPCServerTest.TestRequestWithParams_I_I_ret_I;
|
||||
|
@ -10,6 +10,7 @@ type
|
||||
public
|
||||
function Subtract(Value1, Value2: Int64): Integer;
|
||||
procedure MyNotify;
|
||||
function MyRequest: Boolean;
|
||||
function Add(Value1, Value2, Value3: Int64): TJsonObject;
|
||||
function GetListFromTo(aFrom, aTo: Int64): TJsonArray;
|
||||
function MultiplyString(aString: string; Multiplier: Int64): string;
|
||||
@ -25,6 +26,25 @@ type
|
||||
function AddTimeToDateTime(aDateTime: TDateTime; aTime: TTime): TDateTime;
|
||||
end;
|
||||
|
||||
TTestJSONRPCHookClass = class(TObject)
|
||||
private
|
||||
fJSONReq: TJsonObject;
|
||||
fHistory: string;
|
||||
fJSONRPCKind: TJSONRPCRequestType;
|
||||
public
|
||||
procedure OnBeforeRoutingHook(const Context: TWebContext; const JSON: TJsonObject);
|
||||
procedure OnBeforeCallHook(const Context: TWebContext; const JSON: TJsonObject);
|
||||
procedure OnAfterCallHook(const Context: TWebContext; const JSON: TJsonObject);
|
||||
function error_OnBeforeRoutingHook: Boolean;
|
||||
function error_OnBeforeCallHook: Boolean;
|
||||
function error_OnAfterCallHook: Boolean;
|
||||
|
||||
procedure Notif1;
|
||||
procedure NotifWithError;
|
||||
function Request1: string;
|
||||
function RequestWithError: string;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
uses
|
||||
@ -65,6 +85,11 @@ begin
|
||||
Self.ClassName;
|
||||
end;
|
||||
|
||||
function TTestJSONRPCController.MyRequest: Boolean;
|
||||
begin
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
function TTestJSONRPCController.Subtract(Value1, Value2: Int64): Integer;
|
||||
begin
|
||||
Result := Value1 - Value2;
|
||||
@ -116,4 +141,100 @@ begin
|
||||
Result := Value1 - Value2;
|
||||
end;
|
||||
|
||||
{ TTestJSONRPCHookClass }
|
||||
|
||||
function TTestJSONRPCHookClass.error_OnAfterCallHook: Boolean;
|
||||
begin
|
||||
// do nothing
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
function TTestJSONRPCHookClass.error_OnBeforeCallHook: Boolean;
|
||||
begin
|
||||
// do nothing
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
function TTestJSONRPCHookClass.error_OnBeforeRoutingHook: Boolean;
|
||||
begin
|
||||
// do nothing
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
procedure TTestJSONRPCHookClass.Notif1;
|
||||
begin
|
||||
// do nothing
|
||||
end;
|
||||
|
||||
procedure TTestJSONRPCHookClass.NotifWithError;
|
||||
begin
|
||||
raise Exception.Create('BOOM NOTIF');
|
||||
end;
|
||||
|
||||
procedure TTestJSONRPCHookClass.OnAfterCallHook(const Context: TWebContext; const JSON: TJsonObject);
|
||||
begin
|
||||
try
|
||||
if SameText(fJSONReq.S['method'], 'error_OnAfterCallHook') then
|
||||
raise Exception.Create('error_OnAfterCallHook');
|
||||
|
||||
fHistory := fHistory + '|OnAfterCallHook';
|
||||
|
||||
// do nothing
|
||||
if fJSONRPCKind = TJSONRPCRequestType.Request then
|
||||
begin
|
||||
Assert(Assigned(JSON));
|
||||
LogD('TTestJSONRPCHookClass.OnAfterCallHook: ' + JSON.ToJSON());
|
||||
end
|
||||
else
|
||||
begin
|
||||
if Assigned(JSON) then
|
||||
Assert(JSON.Contains('error'), 'ERROR! Notification has a response but is not an error');
|
||||
LogD('TTestJSONRPCHookClass.OnAfterCallHook: Param is nil');
|
||||
end;
|
||||
if Assigned(JSON) then
|
||||
if JSON.Contains('error') then
|
||||
fHistory := fHistory + '|error';
|
||||
Context.Response.CustomHeaders.Values['x-history'] := fHistory;
|
||||
finally
|
||||
FreeAndNil(fJSONReq);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TTestJSONRPCHookClass.OnBeforeCallHook(const Context: TWebContext; const JSON: TJsonObject);
|
||||
begin
|
||||
if SameText(JSON.S['method'], 'error_OnBeforeCallHook') then
|
||||
raise Exception.Create('error_OnBeforeCallHook');
|
||||
|
||||
fHistory := fHistory + '|OnBeforeCallHook';
|
||||
Assert(Assigned(JSON), 'JSON not assigned in OnBeforeCallHook');
|
||||
LogD('TTestJSONRPCHookClass.OnBeforeCallHook: ' + JSON.ToJSON());
|
||||
end;
|
||||
|
||||
procedure TTestJSONRPCHookClass.OnBeforeRoutingHook(const Context: TWebContext; const JSON: TJsonObject);
|
||||
begin
|
||||
fJSONReq := JSON.Clone;
|
||||
|
||||
if SameText(JSON.S['method'], 'error_OnBeforeRoutingHook') then
|
||||
raise Exception.Create('error_OnBeforeRoutingHook');
|
||||
|
||||
fHistory := 'OnBeforeRoutingHook';
|
||||
// do nothing
|
||||
Assert(Assigned(JSON), 'JSON not assigned in OnBeforeRoutingHook');
|
||||
LogD('TTestJSONRPCHookClass.OnBeforeRoutingHook: ' + JSON.ToJSON());
|
||||
if JSON.Contains('id') then
|
||||
fJSONRPCKind := TJSONRPCRequestType.Request
|
||||
else
|
||||
fJSONRPCKind := TJSONRPCRequestType.Notification;
|
||||
end;
|
||||
|
||||
function TTestJSONRPCHookClass.Request1: string;
|
||||
begin
|
||||
Result := 'empty';
|
||||
end;
|
||||
|
||||
function TTestJSONRPCHookClass.RequestWithError: string;
|
||||
begin
|
||||
raise Exception.Create('BOOM REQUEST');
|
||||
end;
|
||||
|
||||
end.
|
||||
|
@ -92,6 +92,11 @@ begin
|
||||
begin
|
||||
Result := TTestJSONRPCClass.Create
|
||||
end, '/jsonrpcclass')
|
||||
.PublishObject(
|
||||
function: TObject
|
||||
begin
|
||||
Result := TTestJSONRPCHookClass.Create
|
||||
end, '/jsonrpcclass1')
|
||||
.AddController(TTestFaultController) // this will raise an exception
|
||||
.AddController(TTestFault2Controller,
|
||||
function: TMVCController
|
||||
|
Loading…
Reference in New Issue
Block a user