You are not logged in.
Pages: 1
Hi,
First of all, here, in Argentina, we say that Carlos Gardel (tango singer legendary) "Cada día canta mejor/Every day sings better" and I can say that mORMot framework "Every day is better".
After a time I came back to use/try mORMot framework, and time ago I did make changes to the framework implementing server side validation. I want share this changes to know if this the best way and can be part of framework. I want server side validation thinking in web client application where the request (data, not authentication) can be changed despite the local validations (I know this is a paranoiac defensive programming) and I want return all errors where the error description and the field index are reported, in this way request round trip is saved when there are many errors. Also this information is used for display the error message and highlighting the field in the user interface.
Ok, these are the changes, all in mORMot.pas:
TSQLRecord = ...
...
protected
...
function Validate(aRest: TSQLRest; const aFields: array of RawUTF8; // just as reference in the code
aInvalidFieldIndex: PInteger=nil): string; overload;
//>>EMartin: Validate fields and return error list. Invoked from server side validate.
function ValidateAll(aRest: TSQLRest; const aFields: TSQLFieldBits=[0..MAX_SQLFIELDS-1]): string; overload; virtual;
//<<EMartin
...
end;
...
TSQLRestServer = ...
private
...
fPublishedMethods: TDynArrayHashed; // just as reference in the code
fServerSideValidate: Boolean; //EMartin
protected
...
function InternalUpdateEvent(aEvent: TSQLEvent; aTable: TSQLRecordClass; aID: integer; // just as reference in the code
aIsBlobFields: PSQLFieldBits): boolean; virtual;
///>>EMartin: this method will validate logic business rules on TSQLRecord from server side
function Validate(const aURI: TSQLRestServerURIContext): Boolean; virtual;
//<<EMartin
public
...
property SessionClass: TAuthSessionClass read fSessionClass write fSessionClass; // just as reference in the code
//>>EMartin: this property indicates whether execute TSQLValidate from server
// side
property ServerSideValidate: Boolean read fServerSideValidate write fServerSideValidate;
//<<EMartin
...
end;
...
implementation
...
//>>EMartin
function TSQLRecord.ValidateAll(aRest: TSQLRest; const aFields: TSQLFieldBits=[0..MAX_SQLFIELDS-1]): string;
var f, i: integer;
Value: RawUTF8;
Validate: TSynValidate;
ValidateRest: TSynValidateRest absolute Validate;
ErrMsg: String;
Writer: TTextWriter;
begin
result := '';
if (self=nil) or IsZero(aFields) then
// avoid GPF and handle case if no field was selected
exit;
Writer := TTextWriter.CreateOwnedStream;
with RecordProps do
for f := 0 to Fields.Count-1 do
if Fields.List[f].SQLFieldType in COPIABLE_FIELDS then begin
if (Filters<>nil) and (Filters[f]<>nil) then
for i := 0 to Filters[f].Count-1 do begin
Validate := TSynValidate(Filters[f].List[i]);
if Validate.InheritsFrom(TSynValidate) then begin
if Value='' then
Fields.List[f].GetValueVar(self,false,Value,nil);
if Validate.InheritsFrom(TSynValidateRest) then begin
// set additional parameters
ValidateRest.fProcessRec := self;
ValidateRest.fProcessRest := aRest;
end;
if not Validate.Process(f,Value,ErrMsg) then begin
// TSynValidate process failed -> add error to list
if ErrMsg='' then
// no custom message -> show a default message
ErrMsg := format(sValidationFailed,[
GetCaptionFromClass(Validate.ClassType)]);
with Writer do
begin
if (Text <> '') then
AddShort(',');
AddShort('{"error":"');
AddJSONEscapeString(ErrMsg);
AddShort('","fieldName":"');
AddJSONEscape(pointer(Fields.List[f].Name));
AddShort('","fieldIndex":');
Add(f);
AddShort('}');
end;
end;
end;
end;
Value := '';
end;
Writer.SetText(Value);
Writer.Free;
Result := UTF8ToString(Value);
end;
//<<EMartin
...
TSQLRestServerURIContext.ExecuteORMWrite ...
...
begin
...
// here, Table<>nil and TableIndex in [0..MAX_SQLTABLES-1]
if not (TableIndex in Call.RestAccessRights^.POST) then // check User
Call.OutStatus := HTML_NOTALLOWED else
if (Server.ServerSideValidate and not Server.Validate(Self)) then //EMartin Call.outBody will contain error messages
Call.OutStatus := HTML_BADREQUEST else //EMartin
...
// PUT ModelRoot/TableName/ID[/BlobFieldName] to update member/BLOB content
if not (TableIndex in Call.RestAccessRights^.PUT) then // check User
Call.OutStatus := HTML_NOTALLOWED else
if not Server.RecordCanBeUpdated(Table,ID,seUpdate,@CustomErrorMsg) then
Call.OutStatus := HTML_NOTMODIFIED else
if (Server.ServerSideValidate and not Server.Validate(Self)) then //EMartin Call.outBody will contain error messages
Call.OutStatus := HTML_BADREQUEST else begin //EMartin browser will treat as error
...
// ModelRoot/TableName/ID to delete a member
if not (TableIndex in Call.RestAccessRights^.DELETE) then // check User
Call.OutStatus := HTML_NOTALLOWED else
if not Server.RecordCanBeUpdated(Table,ID,seDelete,@CustomErrorMsg)
or (Server.ServerSideValidate and not Server.Validate(Self)) then //EMartin Call.outBody will contain error messages
Call.OutStatus := HTML_BADREQUEST else begin
...
end;
...
//>>EMartin
function TSQLRestServer.Validate(const aURI: TSQLRestServerURIContext): Boolean;
var
SQLRec: TSQLRecord;
Decoder: TJSONObjectDecoder;
I: Integer;
ModifiedFields: TSQLFieldBits;
lErrors: String;
begin
Result := True;
if (aURI.Call^.InBody = '') then
Exit;
SQLRec := aURI.Table.Create;
try
Retrieve(aURI.ID, SQLRec);
Decoder.Decode(aURI.Call^.InBody,nil,pNonQuoted);
for I := Low(Decoder.FieldNames) to High(Decoder.FieldNames) do
if (SQLRec.GetFieldValue(Decoder.FieldNames[i]) <> Decoder.FieldValues[i]) then
begin
SQLRec.SetFieldValue(Decoder.FieldNames[i], Pointer(Decoder.FieldValues[i]));
Include(ModifiedFields, I);
end;
if (ModifiedFields <> []) then
begin
lErrors := SQLRec.ValidateAll(Self, ModifiedFields);
Result := (lErrors = '');
if not Result then
with TTextWriter.CreateOwnedStream do
begin
AddShort('{"errors":[');
AddString(StringToUTF8(lErrors));
AddShort(']}');
SetText(aURI.Call^.OutBody);
Free;
end;
end;
finally
SQLRec.Free;
end;
end;
//<<EMartin
...
With
TSQLServerRest.ServerSideValidate := true;
the server side validation is enabled and when validation fail this is the response:
{"errors":[{"error":"Expect at least 5 characters","fieldName":"Description","fieldIndex":1},{"error":"Category and subcategory cannot be the same","fieldName":"Parent","fieldIndex":6}]}
I did make the changes with the revision 8d9f29394d60ee87.
That's all.
Best regards.
Esteban.
Esteban
Offline
+1 for such functionality. Server side validating should be part of the framework.
Offline
AFAIR there is already validation and filtering available in the framework, since years.
With some built-in validators and filters.
On both client and server sides, if needed (e.g. mORMot's auto-generated UI units do handle directly those rules).
See the SAD pdf about this feature.
You can take a look at this blog article from 2011 - http://blog.synopse.info/post/2011/06/0 … ilter-data
Offline
Yes, I know and I used part of TSynFilterOrValidate, but when the errors are many the validation abort on the first fail.
I forgot put this code. In this code I can validate comparing to fields in the same record (maybe I can make the same inheriting from TSynFilterOrValidate ?):
TSQLCategory = class(TSQLRecord)
private
fDescription: RawUTF8;
fTime: TModTime;
fSQLCategory: TSQLCategory;
fLanguage: TSQLLanguage;
fSortOrder: Integer;
fStatus: TSQLStatus;
fImage: TSQLImage;
public
function ValidateAll(aRest: TSQLRest; const aFields: TSQLFieldBits=[0..MAX_SQLFIELDS-1]): string; override;
published
property ModTime: TModTime read fTime write fTime;
property Description: RawUTF8 read fDescription write fDescription;
property Language: TSQLLanguage read fLanguage write fLanguage;
property SortOrder: Integer read fSortOrder write fSortOrder;
property Image: TSQLImage read fImage write fImage;
property Status: TSQLStatus read fStatus write fStatus;
property Parent: TSQLCategory read fSQLCategory write fSQLCategory;
end;
implementation
{ TSQLCategory }
function TSQLCategory.ValidateAll(aRest: TSQLRest;
const aFields: TSQLFieldBits): string;
var
lParent: TSQLPropInfo;
lResultID: Integer;
lResultParent: RawUTF8;
lFieldName: PUTF8Char;
begin
Result := inherited;
lResultID := GetID;
lResultParent := '';
if (lResultID > 0) then
begin
lFieldName := 'Parent';
lParent := RecordProps.Fields.ByName(lFieldName);
if (lParent <> nil) then
lParent.GetValueVar(self, False, lResultParent, nil);
if (lResultID = StrToCurrency(Pointer(lResultParent))) then
with TTextWriter.CreateOwnedStream do
begin
if (Result <> '') then
AddShort(',');
AddShort('{"error":"');
AddJSONEscapeString('Category and subcategory cannot be the same');
AddShort('","fieldName":"');
AddJSONEscapeString('Parent');
AddShort('","fieldIndex":');
Add(lParent.PropertyIndex);
AddShort('}');
Result := Result + UTF8ToString(Text);
Free;
end;
end;
end;
All errors detected reported once. I saw this behavior in Java Spring Framework.
TIA.
Esteban.
Esteban
Offline
Hi AB, then, is there a way of validate and return all errors found to client with the current built-in validators? the client is not Delphi is Kendo UI Web. The code that I put in this thread works for I need, but if there is something more simple would be better.
Esteban
Offline
Pages: 1