#1 2013-11-26 12:46:21

EMartin
Member
From: Buenos Aires - Argentina
Registered: 2013-01-09
Posts: 332

Server side validation returning all errors

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

#2 2013-11-26 12:59:31

chapa
Member
Registered: 2012-04-30
Posts: 117

Re: Server side validation returning all errors

+1 for such functionality. Server side validating should be part of the framework.

Offline

#3 2013-11-26 14:49:27

ab
Administrator
From: France
Registered: 2010-06-21
Posts: 14,182
Website

Re: Server side validation returning all errors

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

#4 2013-11-26 15:15:00

EMartin
Member
From: Buenos Aires - Argentina
Registered: 2013-01-09
Posts: 332

Re: Server side validation returning all errors

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

#5 2013-11-27 11:03:40

EMartin
Member
From: Buenos Aires - Argentina
Registered: 2013-01-09
Posts: 332

Re: Server side validation returning all errors

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

Board footer

Powered by FluxBB