/// shared DDD Infrastructure: User CQRS Repository via ORM
// - this unit is a part of the freeware Synopse mORMot framework,
// licensed under a MPL/GPL/LGPL tri-license; version 1.18
unit dddInfraRepoUser;

{
    This file is part of Synopse mORMot framework.

    Synopse mORMot framework. Copyright (C) 2022 Arnaud Bouchez
      Synopse Informatique - https://synopse.info

  *** BEGIN LICENSE BLOCK *****
  Version: MPL 1.1/GPL 2.0/LGPL 2.1

  The contents of this file are subject to the Mozilla Public License Version
  1.1 (the "License"); you may not use this file except in compliance with
  the License. You may obtain a copy of the License at
  http://www.mozilla.org/MPL

  Software distributed under the License is distributed on an "AS IS" basis,
  WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  for the specific language governing rights and limitations under the License.

  The Original Code is Synopse mORMot framework.

  The Initial Developer of the Original Code is Arnaud Bouchez.

  Portions created by the Initial Developer are Copyright (C) 2022
  the Initial Developer. All Rights Reserved.

  Contributor(s):

  
  Alternatively, the contents of this file may be used under the terms of
  either the GNU General Public License Version 2 or later (the "GPL"), or
  the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  in which case the provisions of the GPL or the LGPL are applicable instead
  of those above. If you wish to allow use of your version of this file only
  under the terms of either the GPL or the LGPL, and not to allow others to
  use your version of this file under the terms of the MPL, indicate your
  decision by deleting the provisions above and replace them with the notice
  and other provisions required by the GPL or the LGPL. If you do not delete
  the provisions above, a recipient may use your version of this file under
  the terms of any one of the MPL, the GPL or the LGPL.

  ***** END LICENSE BLOCK *****

}

{$I Synopse.inc} // define HASINLINE CPU32 CPU64 OWNNORMTOUPPER

interface

uses
  SysUtils,
  Classes,
  SynCommons,
  SynCrypto,
  SynTests,
  SynTable, // for TSynFilter and TSynValidate
  mORMot,
  mORMotDDD,
  dddDomUserTypes,
  dddDomUserCQRS;


{ *********** Implements User Aggregate CQRS Repository via mORMot's RESTful ORM }

type
  /// implements a User CQRS Repository via mORMot's RESTful ORM
  // - this class will use a supplied TSQLRest instance to persist TUser
  // Aggregate Roots, following the IDomUserCommand CQRS methods
  // - each TUser aggregate will be mapped into a TSQLRecordUser ORM table
  TInfraRepoUser = class(TDDDRepositoryRestCommand,IDomUserCommand,IDomUserQuery)
  public
    function SelectByLogonName(const aLogonName: RawUTF8): TCQRSResult;
    function SelectByEmailValidation(aValidationState: TDomUserEmailValidation): TCQRSResult;
    function SelectByLastName(const aName: TLastName; aStartWith: boolean): TCQRSResult;
    function SelectAll: TCQRSResult;
    function Get(out aAggregate: TUser): TCQRSResult;
    function GetAll(out aAggregates: TUserObjArray): TCQRSResult;
    function GetNext(out aAggregate: TUser): TCQRSResult;
    function Add(const aAggregate: TUser): TCQRSResult;
    function Update(const aUpdatedAggregate: TUser): TCQRSResult;
    function HowManyValidatedEmail: integer;
  end;

  /// implements a Factory of User CQRS Repositories via mORMot's RESTful ORM
  // - this class will associate the TUser Aggregate Root with a TSQLRecordUser
  // ORM table, as managed in a given TSQLRest instance
  TInfraRepoUserFactory = class(TDDDRepositoryRestFactory)
  public
    /// initialize the association with the ORM
    constructor Create(aRest: TSQLRest; aOwner: TDDDRepositoryRestManager=nil); reintroduce;
  	/// perform some tests on this Factory/Repository implementation
    class procedure RegressionTests(test: TSynTestCase);
  end;


{ *********** Person / User / Customer Persistence ORM classes }

type
  /// ORM class able to store a TPerson object
  // - the TPerson.Name property has been flattened to Name_* columns as
  // expected by TDDDRepositoryRestFactory.ComputeMapping
  TSQLRecordPerson = class(TSQLRecord)
  protected
    fFirst: RawUTF8;
    fMiddle: RawUTF8;
    fLast: RawUTF8;
    fBirthDate: TDateTime;
  published
    property Name_First: RawUTF8 read fFirst write fFirst;
    property Name_Middle: RawUTF8 read fMiddle write fMiddle;
    property Name_Last: RawUTF8 read fLast write fLast;
    property Birth: TDateTime read fBirthDate;
  end;

  /// ORM class able to store a TPersonContactable object
  // - the TPersonContactable.Address property has been flattened to Address_*
  // columns as expected by TDDDRepositoryRestFactory.ComputeMapping
  TSQLRecordPersonContactable = class(TSQLRecordPerson)
  protected
    fStreet1: RawUTF8;
    fStreet2: RawUTF8;
    fCityArea: RawUTF8;
    fCity: RawUTF8;
    fRegion: RawUTF8;
    fCode: RawUTF8;
    fCountry: integer;
    fEmail: RawUTF8;
    fPhone1: RawUTF8;
    fPhone2: RawUTF8;
  published
    property Address_Street1: RawUTF8 read fStreet1 write fStreet1;
    property Address_Street2: RawUTF8 read fStreet2 write fStreet2;
    property Address_CityArea: RawUTF8 read fCityArea write fCityArea;
    property Address_City: RawUTF8 read fCity write fCity;
    property Address_Region: RawUTF8 read fRegion write fRegion;
    property Address_Code: RawUTF8 read fCode write fCode;
    property Address_Country: integer read fCountry;
    property Phone1: RawUTF8 read fPhone1 write fPhone1;
    property Phone2: RawUTF8 read fPhone2 write fPhone2;
    property Email: RawUTF8 read fEmail write fEmail;
  end;

  /// ORM class used to persist a TUser domain aggregate
  TSQLRecordUser = class(TSQLRecordPersonContactable)
  protected
    fLogonName: RawUTF8;
    fEmailValidated: TDomUserEmailValidation;
  published
    property LogonName: RawUTF8 read fLogonName write fLogonName stored AS_UNIQUE;
    property EmailValidated: TDomUserEmailValidation read fEmailValidated write fEmailValidated;
  end;


  
implementation

{ TInfraRepoUser }

{ in practice, implementing a I*Command interface mainly consist in calling
  the various TDDDRepositoryRestCommand.ORM*() methods, which would perform
  all process on the REST instance using the TSQLRecordUser table mapped to
  the TUser aggregate root
  - purpose of this I*Command interface is to use the loosely typed
    TDDDRepositoryRestCommand.ORM*() methods to match the exact needs of
    the DDD Aggregate class
  - it would also hide the persistence details so that we would be able
    to ignore e.g. what a primary key is, and avoid the "anemic domain model"
    anti-pattern, which is basically CRUD in disguise }

function TInfraRepoUser.SelectByLogonName(
  const aLogonName: RawUTF8): TCQRSResult;
begin
  result := ORMSelectOne('LogonName=?',[aLogonName],(aLogonName=''));
end;

function TInfraRepoUser.SelectByEmailValidation(
  aValidationState: TDomUserEmailValidation): TCQRSResult;
begin
  result := ORMSelectAll('EmailValidated=?',[ord(aValidationState)]);
end;

function TInfraRepoUser.SelectByLastName(const aName: TLastName;
  aStartWith: boolean): TCQRSResult;
begin
  if aStartWith then
    result := ORMSelectAll('Name_Last LIKE ?',[aName+'%'],(aName='')) else
    result := ORMSelectAll('Name_Last=?',[aName],(aName=''));
end;

function TInfraRepoUser.SelectAll: TCQRSResult;
begin
  result := ORMSelectAll('',[]);
end;

function TInfraRepoUser.Get(out aAggregate: TUser): TCQRSResult;
begin
  result := ORMGetAggregate(aAggregate);
end;

function TInfraRepoUser.GetAll(
  out aAggregates: TUserObjArray): TCQRSResult;
begin
  result := ORMGetAllAggregates(aAggregates);
end;

function TInfraRepoUser.GetNext(out aAggregate: TUser): TCQRSResult;
begin
  result := ORMGetNextAggregate(aAggregate);
end;

function TInfraRepoUser.Add(const aAggregate: TUser): TCQRSResult;
begin
  result := ORMAdd(aAggregate);
end;

function TInfraRepoUser.Update(
  const aUpdatedAggregate: TUser): TCQRSResult;
begin
  result := ORMUpdate(aUpdatedAggregate);
end;

function TInfraRepoUser.HowManyValidatedEmail: integer;
begin
  if ORMSelectCount('EmailValidated=%',[ord(evValidated)],[],result)<>cqrsSuccess then
    result := 0;
end;


{ TInfraRepoUserFactory }

constructor TInfraRepoUserFactory.Create(aRest: TSQLRest;
  aOwner: TDDDRepositoryRestManager);
begin
  inherited Create(IDomUserCommand,TInfraRepoUser,TUser,aRest,TSQLRecordUser,aOwner);
  AddFilterOrValidate(['*'],TSynFilterTrim.Create);
  AddFilterOrValidate(['LogonName'],TSynValidateNonVoidText.Create);
end;

class procedure TInfraRepoUserFactory.RegressionTests(test: TSynTestCase);

procedure TestOne(Rest: TSQLRest);
const MAX=1000;
      MOD_EMAILVALID=ord(high(TDomUserEmailValidation))+1;
var cmd: IDomUserCommand;
    qry: IDomUserQuery;
    user: TUser;
    users: TUserObjArray;
    i,usersCount: integer;
    itext: RawUTF8;
    v: TDomUserEmailValidation;
    count: array[TDomUserEmailValidation] of integer;
    msg: string;
begin
  test.Check(Rest.Services.Resolve(IDomUserCommand,cmd));
  user := TUser.Create;
  try
    for i := 1 to MAX do begin
      UInt32ToUtf8(i,itext);
      user.LogonName := '  '+itext; // left '  ' to test TSynFilterTrim.Create
      user.EmailValidated := TDomUserEmailValidation(i mod MOD_EMAILVALID);
      user.Name.Last := 'Last'+itext;
      user.Name.First := 'First'+itext;
      user.Address.Street1 := 'Street '+itext;
      user.Address.Country.Alpha2 := 'fr';
      user.Phone1 := itext;
      test.check(cmd.Add(user)=cqrsSuccess);
    end;
    test.check(cmd.Commit=cqrsSuccess);
  finally
    user.Free;
  end; 
  user := TUser.Create;
  try
    test.Check(Rest.Services.Resolve(IDomUserQuery,qry));
    test.Check(qry.GetCount=0);
    for i := 1 to MAX do begin
      UInt32ToUtf8(i,itext);
      test.Check(qry.SelectByLogonName(itext)=cqrsSuccess);
      test.Check(qry.GetCount=1);
      test.Check(qry.Get(user)=cqrsSuccess);
      test.Check(qry.GetCount=1);
      test.Check(user.LogonName=itext);
      test.Check(user.EmailValidated=TDomUserEmailValidation(i mod MOD_EMAILVALID));
      test.Check(user.Name.Last='Last'+itext);
      test.Check(user.Name.First='First'+itext);
      test.Check(user.Address.Street1='Street '+itext);
      test.Check(user.Address.Country.Alpha2='FR');
      test.Check(user.Phone1=itext);
    end;
    test.Check(Rest.Services.Resolve(IDomUserCommand,cmd));
    try
      usersCount := 0;
      for v := low(TDomUserEmailValidation) to high(TDomUserEmailValidation) do begin
        test.Check(cmd.SelectByEmailValidation(v)=cqrsSuccess);
        ObjArrayClear(users); // should be done, otherwise memory leak
        test.Check(cmd.GetAll(users)=cqrsSuccess);
        test.Check(length(users)>=MAX div MOD_EMAILVALID);
        count[v] := length(users);
        inc(usersCount,length(users));
        for i := 0 to high(users) do begin
          test.Check(users[i].EmailValidated=v);
          test.Check(users[i].LogonName=users[i].Phone1);
          test.Check(users[i].Name.First='First'+users[i].LogonName);
        end;
      end;
      test.Check(cmd.DeleteAll=cqrsSuccess,'delete all evFailed');
      test.check(cmd.Commit=cqrsSuccess);
      ObjArrayClear(users);
      test.Check(cmd.SelectAll=cqrsSuccess);
      test.Check(cmd.GetAll(users)=cqrsSuccess);
      test.Check(length(users)=usersCount-count[evFailed]);
      for i := 0 to high(users) do begin
        test.Check(users[i].LogonName=users[i].Phone1);
        test.Check(users[i].Name.First='First'+users[i].LogonName);
        test.Check(users[i].Address.Country.Iso=250);
      end;
    finally
      ObjArrayClear(users);
    end;
    test.Check(Rest.Services.Resolve(IDomUserCommand,cmd));
    for v := low(TDomUserEmailValidation) to high(TDomUserEmailValidation) do begin
      test.Check(cmd.SelectByEmailValidation(v)=cqrsSuccess);
      if v=evFailed then
        test.Check(cmd.GetCount=0) else
        test.Check(cmd.GetCount=count[v]);
      i := 0;
      while cmd.GetNext(user)=cqrsSuccess do begin
        test.Check(user.EmailValidated=v);
        test.Check(user.Name.First='First'+user.LogonName);
        test.Check(user.Address.Country.Iso=250);
        inc(i);
      end;
      test.Check(i=cmd.GetCount);
    end;
    test.Check(cmd.HowManyValidatedEmail=count[evValidated]);
    user.LogonName := '';
    test.check(cmd.Add(user)=cqrsDDDValidationFailed);
    test.check(cmd.GetLastError=cqrsDDDValidationFailed);
    msg := cmd.GetLastErrorInfo.msg;
    test.check(pos('TUser.LogonName',msg)>0,msg);
  finally
    user.Free;
  end;
end;

var RestServer: TSQLRestServerFullMemory;
    RestClient: TSQLRestClientURI;
begin
  RestServer := TSQLRestServerFullMemory.CreateWithOwnModel([TSQLRecordUser]);
  try // first try directly on server side
    RestServer.ServiceContainer.InjectResolver([TInfraRepoUserFactory.Create(RestServer)],true);
    TestOne(RestServer); // sub function will ensure that all I*Command are released
  finally
    RestServer.Free;
  end;
  RestServer := TSQLRestServerFullMemory.CreateWithOwnModel([TSQLRecordUser]);
  try // then try from a client-server process
    RestServer.ServiceContainer.InjectResolver([TInfraRepoUserFactory.Create(RestServer)],true);
    RestServer.ServiceDefine(TInfraRepoUser,[IDomUserCommand,IDomUserQuery],sicClientDriven);
    test.Check(RestServer.ExportServer);
    RestClient := TSQLRestClientURIDll.Create(TSQLModel.Create(RestServer.Model),@URIRequest);
    try
      RestClient.Model.Owner := RestClient;
      RestClient.ServiceDefine([IDomUserCommand],sicClientDriven);
      TestOne(RestServer);
      RestServer.DropDatabase;
      USEFASTMM4ALLOC := true; // for slightly faster process
      TestOne(RestClient);
    finally
      RestClient.Free;
    end;
  finally
    RestServer.Free;
  end;
end;



end.
