/// Domain-Driven-Design toolbox for mORMot // - this unit is a part of the freeware Synopse mORMot framework, // licensed under a MPL/GPL/LGPL tri-license; version 1.18 unit mORMotDDD; { This file is part of Synopse mORMot framework. Synopse mORMot framework. Copyright (C) 2020 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) 2020 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 {$ifdef MSWINDOWS} Windows, {$else} {$ifdef FPC} SynFPCLinux, {$endif FPC} {$endif MSWINDOWS} SysUtils, Classes, Contnrs, Variants, SyncObjs, SynCrtSock, SynCommons, SynLog, SynCrypto, SynTable, // for TSynFilter and TSynValidate mORMot; { some mORMot conventions about DDD implementation: * most services methods should return an enumerate: - Exceptions should be raised only in case of a real failure (e.g. unexpected execution context, service shutdown...) but most of the time an enumerate will be used to manage errors - no textual error message should be sent by the application layer: it is up to the end-user application to react to a given unexpected result - within the DDD services, CQRS methods will use a TCQRSResult enumerate and advanced error process, with an optional error text for debugging - first value should mean success, so that TInterfaceStub would let the test pass by returning the default 0 ordinal * persistence ignorance is mostly implemented via CQRS repository services: - we implement the "Command Query Responsibility Segregation" pattern to potentially increase scaling ability of reading, and allow distributed transactions via a service-based two-phase commit, implementing the Unit Of Work pattern - no ID should be transmitted most of the time, but write commands have to follow a read query which would specify the corresponding item so that the ID would be stored during the process - I*Query interfaces should have some SelectBy*() methods to change the current selected aggregate, which could later on be retrieved by a Get(out aAggregate: T....) method - I*Command instances would be our way of implementing the Unit Of Work - I*Command interfaces should have standard Add/Update/Delete methods, expecting a previous SelectBy*() call for Update/Delete: those methods should prepare the corresponding data change - I*Command interfaces should have Commit to perform all the pending changes (which may be implemented by transactions or via TSQLRestBatch) - I*Command interfaces should abort the process if the instance is released without any prior call to Commit (e.g. rollback the transaction or free any pending TSQLRestBatch instance) } { *********** Some Domain-Driven-Design Common Definitions } type /// abstract ancestor for all Domain-Driven Design related Exceptions EDDDException = class(ESynException); /// Exception type linked to CQRS repository service methods ECQRSException = class(EDDDException); /// abstract ancestor for any Domain-Driven Design infrastructure Exceptions EDDDInfraException = class(EDDDException); { ----- Persistence / Repository Interfaces } type /// result enumerate for I*Query/I*Command CQRS repository service methods // - cqrsSuccess will map the default TInterfaceStub returned value // - cqrsSuccessWithMoreData would be used e.g. for versioned publish/ // subscribe to notify the caller that there are still data available, and // the call should be reiterated until cqrsSuccess is returned // - cqrsBadRequest would indicate that the method was not called in the // expected workflow sequence // - cqrsNotFound appear after a I*Query SelectBy*() method with no match // - cqrsNoMoreData indicates a GetNext*() method has no more matching data // - cqrsDataLayerError indicates a low-level error at database level // - cqrsInvalidCallback is returned if a callback is required for this method // - cqrsInternalError for an unexpected issue, like an Exception raised // - cqrsDDDValidationFailed will be trigerred when // - cqrsInvalidContent for any I*Command method with invalid aggregate input // value (e.g. a missing field) // - cqrsAlreadyExists for a I*Command.Add method with a primay key conflict // - cqrsNoPriorQuery for a I*Command.Update/Delete method with no prior // call to SelectBy*() // - cqrsNoPriorCommand for a I*Command.Commit with no prior Add/Update/Delete // - cqrsNoMatch will notify that a command did not have any match // - cqrsNotImplemented may be returned when there is no code yet for a method // - cqrsBusy is returned if the command could not be executed, since it is // currently processing a request // - cqrsTimeout indicates that the method didn't succeed in the expected time // - otherwise, cqrsUnspecifiedError will be used for any other kind of error TCQRSResult = (cqrsSuccess, cqrsSuccessWithMoreData, cqrsUnspecifiedError, cqrsBadRequest, cqrsNotFound, cqrsNoMoreData, cqrsDataLayerError, cqrsInvalidCallback, cqrsInternalError, cqrsDDDValidationFailed, cqrsInvalidContent, cqrsAlreadyExists, cqrsNoPriorQuery, cqrsNoPriorCommand, cqrsNoMatch, cqrsNotImplemented, cqrsBusy, cqrsTimeout); /// generic interface, to be used for CQRS I*Query and I*Command types definition // - TCQRSService class will allow to easily implement LastError* members // - all CQRS services, which may be executed remotely, would favor a function // result as TCQRSResult enumeration for error handling, rather than a local // Exception, which is not likely to be transferred easily on consummer side ICQRSService = interface(IInvokable) /// should return the last error as an enumerate // - when stubed or mocked via TInterfaceStub, any method interface would // return 0, i.e. cqrsSuccess by default, to let the test pass function GetLastError: TCQRSResult; /// should return addition information for the last error // - may be a plain string, or a JSON document stored as TDocVariant function GetLastErrorInfo: variant; end; /// returns the text equivalency of a CQRS result enumeration function ToText(res: TCQRSResult): PShortString; overload; const /// successfull result enumerates for I*Query/I*Command CQRS // - those items would generate no log entry // - i.e. any command not included in CQRSRESULT_SUCCESS nor CQRSRESULT_WARNING // would trigger a sllDDDError log entry CQRSRESULT_SUCCESS = [ cqrsSuccess, cqrsSuccessWithMoreData, cqrsNoMoreData, cqrsNotFound]; /// dubious result enumerates for I*Query/I*Command CQRS // - those items would generate a sllDDDInfo log entry // - i.e. any command not included in CQRSRESULT_SUCCESS nor CQRSRESULT_WARNING // would trigger a sllDDDError log entry CQRSRESULT_WARNING = [ cqrsNotFound, cqrsNoMatch]; { ----- Services / Daemon Interfaces } type /// generic interface, to be used so that you may retrieve a running state IMonitored = interface(IInvokable) ['{7F5E1569-E06B-48A0-954C-95784EC23363}'] /// retrieve the current status of the instance // - the status is returned as a schema-less value (typically a TDocVariant // document), which may contain statistics about the current processing // numbers, timing and throughput function RetrieveState(out Status: variant): TCQRSResult; end; /// generic interface, to be used so that you may manage a service/daemon instance IMonitoredDaemon = interface(IMonitored) ['{F5717AFC-5D0E-4E13-BD5B-25C08CB177A7}'] /// launch the service/daemon // - should first stop any previous running instance (so may be used to // restart a service on demand) function Start: TCQRSResult; /// abort the service/daemon, returning statistics about the whole execution function Stop(out Information: variant): TCQRSResult; end; /// generic interface, to manage a service/daemon instance from an executable // - in addition to Start/Stop methods, Halt would force the whole executable // to abort its execution, SubscribeLog allows log monitoring, and // DatabaseList/DatabaseExecute remote SQL/SOA execution on one or several // logicial REST servers // - those methods would allow a single administration daemon (installed e.g. // as a Windows Service) to be able to launch and monitor child processes as // individual executables, or via a custom DDD's ToolsAdmin tool // - since SubscribeLog() uses a callback, this REST server should be // published via supported transmission protocol, e.g. WebSockets IAdministratedDaemon = interface(IMonitoredDaemon) ['{BD02919E-56ED-4559-967A-EFBC0E85C031}'] /// will Stop the service/daemon process, then quit the executable // - the returned Information and TCQRSResult are passed directly from // the Stop() method function Halt(out Information: variant): TCQRSResult; /// returns a list of internal database names, exposed by this daemon // - in practice, each database name should identify a TSQLRest instance // - the database name should be supplied to DatabaseExecute() as target function DatabaseList: TRawUTF8DynArray; /// returns a list of tables, stored in the internal database names // - the database name should match one existing in the DatabaseList // - in practice, returns all TSQLRecord table of the TSQLRest instance function DatabaseTables(const DatabaseName: RawUTF8): TRawUTF8DynArray; /// execute a SQL query on an internal database // - the database name should match one existing in the DatabaseList // - the supplied SQL parameter may be #cmd internal commands: in this case, // the database name may not be mandatory // - will return JSON most of the time, but may return binary if needed function DatabaseExecute(const DatabaseName,SQL: RawUTF8): TServiceCustomAnswer; /// used to subscribe for real-time remote log monitoring // - allows to track the specified log events, with a callback // - you can specify a number of KB of existing log content to send to the // monitoring tool, before the actual real-time process: Callback.Log() // would be called first with Level=sllNone and all the existing text procedure SubscribeLog(const Level: TSynLogInfos; const Callback: ISynLogCallback; ReceiveExistingKB: cardinal); /// will be called when a callback is released on the client side // - this method matches IServiceWithCallbackReleased signature // - will be used to unsubscribe any previous ISynLogCallback notification procedure CallbackReleased(const callback: IInvokable; const interfaceName: RawUTF8); end; /// any service/daemon implementing this interface would be able to redirect // all the administration process to another service/daemon // - i.e. would work as a safe proxy service, over several networks IAdministratedDaemonAsProxy = interface(IAdministratedDaemon) ['{5B7A9086-3D96-48F2-8E27-C6624B2EB45A}'] /// allows to connect to another service/daemon IAdministratedDaemon // - detailed connection definition would be supplied as a TDocVariantData // object, serialized from dddInfraApp.pas TDDDRestClientSettings function StartProxy(const aDDDRestClientSettings: variant): TCQRSResult; end; /// allow persistence of any TObject settings storage IDDDSettingsStorable = interface ['{713A9C16-4BBD-4FB6-A7A6-566162767622}'] /// persist the settings if needed // - will call the virtual InternalPersist method procedure StoreIfUpdated; end; { *********** Cross-Cutting Layer Implementation} { ----- Persistence / Repository CQRS Implementation } type /// which kind of process is about to take place after an CqrsBeginMethod() TCQRSQueryAction = ( qaNone, qaSelect, qaGet, qaCommandDirect, qaCommandOnSelect, qaCommit); /// define one or several process to take place after an CqrsBeginMethod() TCQRSQueryActions = set of TCQRSQueryAction; /// the current step of a TCQRSQuery state machine // - basic state diagram is defined by the methods execution: // - qsNone refers to the default state, with no currently selected values, // nor any pending write request // - qsQuery corresponds to a successful I*Query.Select*(), expecting // either a I*Query.Get*(), or a I*Command.Add/Update/Delete // - qsCommand corresponds to a successful I*Command.Add/Update/Delete, // expected a I*Command.Commit TCQRSQueryState = (qsNone, qsQuery, qsCommand); /// to be inherited to implement CQRS I*Query or I*Command services extended // error process // - you should never assign directly a cqrs* value to a method result, but // rather use the CqrsBeginMethod/CqrsSetResult/CqrsSetResultMsg methods provided by this class: // ! function TMyService.MyMethod: TCQRSResult; // ! begin // ! CqrsBeginMethod(qsNone,result); // reset the error information to cqrsUnspecifiedError // ! ... // do some work // ! if error then // ! CqrsSetResultMsg(cqrsUnspecifiedError,'Oups! For "%"',[name],result) else // ! CqrsSetResult(cqrsSuccess,result); // instead of result := cqrsSuccess // ! end; // - the methods are implemented as a simple state machine, following // the TCQRSQueryAction and TCQRSQueryState definitions // - warning: by definition, fLastError* access is NOT thread-safe so the // CqrsBeginMethod/CqrsSetResult feature should be used in a single context TCQRSService = class(TInjectableObject, ICQRSService) protected fLastError: TCQRSResult; fLastErrorContext: variant; fAction: TCQRSQueryAction; fState: TCQRSQueryState; {$ifdef WITHLOG} fLog: TSynLogFamily; {$endif} fSafe: TSynLocker; // method to be called at first for LastError process function CqrsBeginMethod(aAction: TCQRSQueryAction; var aResult: TCQRSResult; aError: TCQRSResult=cqrsUnspecifiedError): boolean; virtual; function CqrsSetResultError(aError: TCQRSResult): TCQRSResult; virtual; // methods to be used to set the process end status procedure CqrsSetResult(Error: TCQRSResult; var Result: TCQRSResult); overload; procedure CqrsSetResult(E: Exception; var Result: TCQRSResult); overload; procedure CqrsSetResultSuccessIf(SuccessCondition: boolean; var Result: TCQRSResult; ErrorIfFalse: TCQRSResult=cqrsDataLayerError); procedure CqrsSetResultMsg(Error: TCQRSResult; const ErrorMessage: RawUTF8; var Result: TCQRSResult); overload; procedure CqrsSetResultMsg(Error: TCQRSResult; const ErrorMsgFmt: RawUTF8; const ErrorMsgArgs: array of const; var Result: TCQRSResult); overload; procedure CqrsSetResultString(Error: TCQRSResult; const ErrorMessage: string; var Result: TCQRSResult); procedure CqrsSetResultDoc(Error: TCQRSResult; const ErrorInfo: variant; var Result: TCQRSResult); procedure CqrsSetResultJSON(Error: TCQRSResult; const JSONFmt: RawUTF8; const Args,Params: array of const; var Result: TCQRSResult); function GetLastError: TCQRSResult; function GetLastErrorInfo: variant; virtual; procedure InternalCqrsSetResult(Error: TCQRSResult; var Result: TCQRSResult); virtual; procedure AfterInternalCqrsSetResult; virtual; public /// initialize the instance constructor Create; override; /// finalize the instance destructor Destroy; override; {$ifdef WITHLOG} /// where logging should take place property Log: TSynLogFamily read fLog write fLog; {$endif} published /// the last error, as an enumerate property LastError: TCQRSResult read GetLastError; /// the last error extended information, as a string or TDocVariant property LastErrorInfo: variant read GetLastErrorInfo; /// the action currently processing property Action: TCQRSQueryAction read fAction; /// current step of the TCQRSService state machine property State: TCQRSQueryState read fState; end; /// a CQRS Service, which maintains an internal list of "Subscribers" // - allow to notify in cascade when a callback is released TCQRSServiceSubscribe = class(TCQRSService) protected fSubscriber: array of IServiceWithCallbackReleased; // will call all fSubscriber[].CallbackReleased() methods procedure CallbackReleased(const callback: IInvokable; const interfaceName: RawUTF8); end; /// a CQRS Service, ready to implement a set of synchronous (blocking) commands // over an asynchronous (non-blocking) service // - you may use this class e.g. at API level, over a blocking REST server, // and communicate with the Domain event-driven services via asynchronous calls // - this class won't inherit from TCQRSService, since it would be called // from multiple threads at once, so all CQRSSetResult() methods would fail TCQRSServiceSynch = class(TInterfacedObject) protected fSharedCallbackRef: IUnknown; public constructor Create(const sharedcallback: IUnknown); reintroduce; end; /// used to acknowledge asynchronous CQRS Service calls // - e.g. to implement TCQRSServiceSynch TCQRSServiceAsynchAck = class(TInterfacedObject) protected fLog: TSynLogClass; fCalls: TBlockingProcessPool; public destructor Destroy; override; end; /// class-reference type (metaclass) of TCQRSService TCQRSServiceClass = class of TCQRSService; /// returns the text equivalency of a CQRS state enumeration function ToText(res: TCQRSQueryState): PShortString; overload; { ----- Persistence / Repository Implementation using mORMot's ORM } type TDDDRepositoryRestFactory = class; TDDDRepositoryRestQuery = class; /// class-reference type (metaclass) to implement I*Query or I*Command // interface definitions using our RESTful ORM TDDDRepositoryRestClass = class of TDDDRepositoryRestQuery; /// abstract ancestor for all persistence/repository related Exceptions EDDDRepository = class(ESynException) public /// constructor like FormatUTF8() which will also serialize the caller info constructor CreateUTF8(Caller: TDDDRepositoryRestFactory; const Format: RawUTF8; const Args: array of const); end; /// store reference of several factories, each with one mapping definition TDDDRepositoryRestFactoryObjArray = array of TDDDRepositoryRestFactory; /// home repository of several DDD Entity factories using REST storage // - this shared class will be can to manage a service-wide repositories, // e.g. manage actual I*Query/I*Command implementation classes accross a // set of TSQLRest instances // - is designed to optimize BATCH or transactional process TDDDRepositoryRestManager = class protected fFactory: TDDDRepositoryRestFactoryObjArray; public /// finalize all factories destructor Destroy; override; /// register one DDD Entity repository over an ORM's TSQLRecord // - will raise an exception if the aggregate has already been defined function AddFactory( const aInterface: TGUID; aImplementation: TDDDRepositoryRestClass; aAggregate: TClass; aRest: TSQLRest; aTable: TSQLRecordClass; const TableAggregatePairs: array of RawUTF8): TDDDRepositoryRestFactory; /// retrieve the registered definition of a given DDD Entity in Factory[] // - returns -1 if the TPersistence class is unknown function GetFactoryIndex(const aInterface: TGUID): integer; /// retrieve the registered Factory definition of a given DDD Entity // - raise an EDDDRepository exception if the TPersistence class is unknown function GetFactory(const aInterface: TGUID): TDDDRepositoryRestFactory; /// read-only access to all defined DDD Entity factories property Factory: TDDDRepositoryRestFactoryObjArray read fFactory; end; /// implement a DDD Entity factory over one ORM's TSQLRecord // - it will centralize some helper classes and optimized class mapping // - the Entity class may be defined as any TPersistent or TSynPersistent, with // an obvious preference for TSynPersistent and TSynAutoCreateFields classes TDDDRepositoryRestFactory = class(TInterfaceResolverForSingleInterface) protected fOwner: TDDDRepositoryRestManager; fInterface: TInterfaceFactory; fRest: TSQLRest; fTable: TSQLRecordClass; fAggregate: TClassInstance; fAggregateRTTI: TSQLPropInfoList; // stored in fGarbageCollector, following fAggregateProp[] fGarbageCollector: TObjectDynArray; fFilter: array of array of TSynFilter; fValidate: array of array of TSynValidate; // TSQLPropInfoList correspondance, as filled by ComputeMapping: fAggregateToTable: TSQLPropInfoObjArray; fAggregateProp: TSQLPropInfoRTTIObjArray; fAggregateID: TSQLPropInfoRTTI; // store custom field mapping between TSQLRecord and Aggregate fPropsMapping: TSQLRecordPropertiesMapping; fPropsMappingVersion: cardinal; procedure ComputeMapping; function GetAggregateName: string; function GetTableName: string; // override those methods to customize the data marshalling procedure AggregatePropToTable( aAggregate: TObject; aAggregateProp: TSQLPropInfo; aRecord: TSQLRecord; aRecordProp: TSQLPropInfo); virtual; procedure TablePropToAggregate( aRecord: TSQLRecord; aRecordProp: TSQLPropInfo; aAggregate: TObject; aAggregateProp: TSQLPropInfo); virtual; function GetAggregateRTTIOptions: TSQLPropInfoListOptions; virtual; // main IoC/DI method, returning a TDDDRepositoryRest instance function CreateInstance: TInterfacedObject; override; public /// will compute the ORM TSQLRecord* source code type definitions // corresponding to DDD aggregate objects into a a supplied file name // - will generate one TSQLRecord* per aggregate class level, following the // inheritance hierarchy // - dedicated DDD types will be translated into native ORM types (e.g. RawUTF8) // - if no file name is supplied, it will generate a dddsqlrecord.inc file // in the executable folder // - could be used as such: // ! TDDDRepositoryRestFactory.ComputeSQLRecord([TPersonContactable,TAuthInfo]); // - once created, you may refine the ORM definition, by adding // ! ... read f.... write f... stored AS_UNIQUE; // for fields which should be unique, and/or // ! ... read f... write f... index #; // to specify an optional textual field width (VARCHAR n) for SQL storage // - most advanced ORM-level filters/validators, or low-level implementation // details (like the Sqlite3 collation) may be added by overriding this method: // !protected // ! class procedure InternalDefineModel(Props: TSQLRecordProperties); override; // ! ... // !class procedure TSQLRecordMyAggregate.InternalDefineModel( // ! Props: TSQLRecordProperties); // !begin // ! AddFilterNotVoidText(['HashedPassword']); // ! Props.SetCustomCollation('Field','BINARY'); // ! Props.AddFilterOrValidate('Email',TSynValidateEmail.Create); // !end; class procedure ComputeSQLRecord(const aAggregate: array of TClass; DestinationSourceCodeFile: TFileName=''); /// initialize the DDD Aggregate factory using a mORMot ORM class // - by default, field names should match on both sides - but you can // specify a custom field mapping as TSQLRecord,Aggregate pairs // - any missing or unexpected field on any side will just be ignored constructor Create( const aInterface: TGUID; aImplementation: TDDDRepositoryRestClass; aAggregate: TClass; aRest: TSQLRest; aTable: TSQLRecordClass; const TableAggregatePairs: array of RawUTF8; aOwner: TDDDRepositoryRestManager=nil); reintroduce; overload; /// initialize the DDD Aggregate factory using a mORMot ORM class // - this overloaded constructor does not expect any custom fields // - any missing or unexpected field on any side will just be ignored constructor Create( const aInterface: TGUID; aImplementation: TDDDRepositoryRestClass; aAggregate: TClass; aRest: TSQLRest; aTable: TSQLRecordClass; aOwner: TDDDRepositoryRestManager=nil); reintroduce; overload; /// finalize the factory destructor Destroy; override; /// register a custom filter or validator to some Aggregate's fields // - once added, the TSynFilterOrValidate instance will be owned to // this factory, until it is released // - the field names should be named from their full path (e.g. 'Email' or // 'Address.Country.Iso') unless aFieldNameFlattened is TRUE, which will // expect ORM-like naming (e.g. 'Address_Country') // - if '*' is specified as field name, it will be applied to all text // fields, so the following will ensure that all text fields will be // trimmed for spaces: // ! AddFilterOrValidate(['*'],TSynFilterTrim.Create); // - filters and validators will be applied to a specified aggregate using // AggregateFilterAndValidate() method // - the same filtering classes as with the ORM can be applied to DDD's // aggregates, e.g. TSynFilterUpperCase, TSynFilterLowerCase or // TSynFilterTrim // - the same validation classes as with the ORM can be applied to DDD's // aggregates, e.g. TSynValidateText.Create for a void field, // TSynValidateText.Create('{MinLength:5}') for a more complex test // (including custom password strength validation if TSynValidatePassWord // is not enough), TSynValidateIPAddress.Create or TSynValidateEmail.Create // for some network settings, or TSynValidatePattern.Create() // - you should not define TSynValidateUniqueField here, which could't be // checked at DDD level, but rather set a "stored AS_UNIQUE" attribute // in the corresponding property of the TSQLRecord type definition procedure AddFilterOrValidate(const aFieldNames: array of RawUTF8; aFilterOrValidate: TSynFilterOrValidate; aFieldNameFlattened: boolean=false); virtual; /// clear all properties of a given DDD Aggregate procedure AggregateClear(aAggregate: TObject); /// create a new DDD Aggregate instance function AggregateCreate: TObject; {$ifdef HASINLINE}inline;{$endif} /// perform filtering and validation on a supplied DDD Aggregate // - all logic defined by AddFilterOrValidate() will be processed function AggregateFilterAndValidate(aAggregate: TObject; aInvalidFieldIndex: PInteger=nil; aValidator: PSynValidate=nil): RawUTF8; virtual; /// serialize a DDD Aggregate as JSON // - you can optionaly force the generated JSON to match the mapped // TSQLRecord fields, so that it would be compatible with ORM's JSON procedure AggregateToJSON(aAggregate: TObject; W: TJSONSerializer; ORMMappedFields: boolean; aID: TID); overload; /// serialize a DDD Aggregate as JSON RawUTF8 function AggregateToJSON(aAggregate: TObject; ORMMappedFields: boolean; aID: TID): RawUTF8; overload; /// convert a DDD Aggregate into an ORM TSQLRecord instance procedure AggregateToTable(aAggregate: TObject; aID: TID; aDest: TSQLRecord); /// convert a ORM TSQLRecord instance into a DDD Aggregate procedure AggregateFromTable(aSource: TSQLRecord; aAggregate: TObject); /// convert ORM TSQLRecord.FillPrepare instances into a DDD Aggregate ObjArray procedure AggregatesFromTableFill(aSource: TSQLRecord; var aAggregateObjArray); /// the home repository owning this factory property Owner: TDDDRepositoryRestManager read fOwner; /// the DDD's Entity class handled by this factory // - may be any TPersistent, but very likely a TSynAutoCreateFields class property Aggregate: TClass read fAggregate.ItemClass; /// the ORM's TSQLRecord used for actual storage property Table: TSQLRecordClass read fTable; /// the mapped DDD's Entity class published properties RTTI property Props: TSQLPropInfoList read fAggregateRTTI; /// access to the Aggregate / ORM field mapping property FieldMapping: TSQLRecordPropertiesMapping read fPropsMapping; published /// the associated I*Query / I*Command repository interface property Repository: TInterfaceFactory read fInterface; /// the associated TSQLRest instance property Rest: TSQLRest read fRest; /// the DDD's Entity class name handled by this factory property AggregateClass: string read GetAggregateName; /// the ORM's TSQLRecord class name used for actual storage property TableClass: string read GetTableName; end; /// abstract repository class to implement I*Query interface using RESTful ORM // - actual repository implementation will just call the ORM*() protected // method from the published Aggregate-oriented CQRS service interface TDDDRepositoryRestQuery = class(TCQRSService) protected fFactory: TDDDRepositoryRestFactory; fCurrentORMInstance: TSQLRecord; function CqrsBeginMethod(aAction: TCQRSQueryAction; var aResult: TCQRSResult; aError: TCQRSResult=cqrsUnspecifiedError): boolean; override; // one-by-one retrieval in local ORM: TSQLRecord function ORMSelectOne(const ORMWhereClauseFmt: RawUTF8; const Bounds: array of const; ForcedBadRequest: boolean=false): TCQRSResult; function ORMSelectID(const ID: TID; RetrieveRecord: boolean=true; ForcedBadRequest: boolean=false): TCQRSResult; overload; function ORMSelectID(const ID: RawUTF8; RetrieveRecord: boolean=true; ForcedBadRequest: boolean=false): TCQRSResult; overload; function ORMGetAggregate(aAggregate: TObject): TCQRSResult; // list retrieval - using cursor-like access via ORM.FillOne function ORMSelectAll(const ORMWhereClauseFmt: RawUTF8; const Bounds: array of const; ForcedBadRequest: boolean=false): TCQRSResult; function ORMGetNextAggregate(aAggregate: TObject; aRewind: boolean=false): TCQRSResult; function ORMGetAllAggregates(var aAggregateObjArray): TCQRSResult; function ORMSelectCount(const ORMWhereClauseFmt: RawUTF8; const Args,Bounds: array of const; out aResultCount: integer; ForcedBadRequest: boolean=false): TCQRSResult; public /// you should not have to use this constructor, since the instances would // be injected by TDDDRepositoryRestFactory.TryResolve() constructor Create(aFactory: TDDDRepositoryRestFactory); reintroduce; virtual; /// finalize the used memory destructor Destroy; override; /// return the number all currently selected aggregates // - returns 0 if no select was available, 1 if it was a ORMGetSelectOne(), // or the number of items after a ORMGetSelectAll() // - this is a generic operation which would work for any class // - if you do not need this method, just do not declare it in I*Command function GetCount: integer; virtual; /// returns the associated TSQLRest instance used in the associated factory // - this method is able to extract it from a I*Query/I*Command instance, // if it is implemented by a TDDDRepositoryRestQuery class // - returns nil if the supplied Service is not recognized class function GetRest(const Service: ICQRSService): TSQLRest; published /// access to the associated factory property Factory: TDDDRepositoryRestFactory read fFactory; /// access to the current state of the underlying mapped TSQLRecord // - is nil if no query was run yet // - contains the queried object after a successful Select*() method // - is either a single object, or a list of objects, via its internal // CurrentORMInstance.FillTable cursor property CurrentORMInstance: TSQLRecord read fCurrentORMInstance; end; /// abstract class to implement I*Command interface using ORM's TSQLRecord // - it will use an internal TSQLRestBatch for dual-phase commit, therefore // implementing a generic Unit Of Work / Transaction pattern TDDDRepositoryRestCommand = class(TDDDRepositoryRestQuery) protected fBatch: TSQLRestBatch; fBatchAutomaticTransactionPerRow: cardinal; fBatchOptions: TSQLRestBatchOptions; fBatchResults: TIDDynArray; procedure ORMEnsureBatchExists; virtual; // this default implementation will check the status vs command, // call DDD's + ORM's FilterAndValidate, then add to the internal BATCH // - you should override it, if you need a specific behavior // - if aAggregate is nil, fCurrentORMInstance field values would be used // - if aAggregate is set, its fields would be set to fCurrentORMInstance procedure ORMPrepareForCommit(aCommand: TSQLOccasion; aAggregate: TObject; var Result: TCQRSResult; aAllFields: boolean=false); virtual; /// minimal implementation using AggregateToTable() conversion function ORMAdd(aAggregate: TObject; aAllFields: boolean=false): TCQRSResult; virtual; function ORMUpdate(aAggregate: TObject; aAllFields: boolean=false): TCQRSResult; virtual; /// this default implementation will send the internal BATCH // - you should override it, if you need a specific behavior procedure InternalCommit(var Result: TCQRSResult); virtual; /// on rollback, delete the internal BATCH - called by Destroy procedure InternalRollback; virtual; public /// this constructor will set default fBatch options constructor Create(aFactory: TDDDRepositoryRestFactory); override; /// finalize the Unit Of Work context // - any uncommited change will be lost destructor Destroy; override; /// perform a deletion on the currently selected aggregate // - this is a generic operation which would work for any class // - if you do not need this method, just do not declare it in I*Command function Delete: TCQRSResult; virtual; /// perform a deletion on all currently selected aggregates // - this is a generic operation which would work for any class // - if you do not need this method, just do not declare it in I*Command function DeleteAll: TCQRSResult; virtual; /// write all pending changes prepared by Add/Update/Delete methods // - this is the only mandatory method, to be declared in your I*Command // - in practice, will send the current internal BATCH to the REST instance function Commit: TCQRSResult; virtual; /// flush any pending changes prepared by Add/Update/Delete methods // - if you do not need this method, just do not publish it in I*Command // - the easiest to perform a roll-back would be to release the I*Command // instance - but you may explictly reset the pending changes by calling // this method // - in practice, will release the internal BATCH instance function Rollback: TCQRSResult; virtual; /// access to the low-level BATCH instance, used for dual-phase commit // - you should not need to access it directly, but rely on Commit and // Rollback methods to property Batch: TSQLRestBatch read fBatch; end; /// abstract CQRS class tied to a TSQLRest instance for low-level persistence // - not used directly by the DDD repositories (since they will rely on // a TDDDRepositoryRestFactory for the actual ORM process), but may be the // root class for any Rest-based infrastructure cross-cutting features TCQRSQueryObjectRest = class(TCQRSService) protected fRest: TSQLRest; public /// this constructor would identify a TServiceContainer SOA resolver // and set the Rest property // - when called e.g. by TServiceFactoryServer.CreateInstance() constructor CreateWithResolver(aResolver: TInterfaceResolver; aRaiseEServiceExceptionIfNotFound: boolean); overload; override; /// reintroduced constructor, allowing to specify the associated REST instance constructor Create(aRest: TSQLRest); reintroduce; virtual; /// reintroduced constructor, associating a REST instance with the supplied // IoC resolvers constructor CreateWithResolver(aRest: TSQLRest; aResolver: TInterfaceResolver; aRaiseEServiceExceptionIfNotFound: boolean=true); reintroduce; overload; /// reintroduced constructor, associating a REST instance with the supplied // IoC resolvers (may be stubs/mocks, resolver classes or single instances) constructor CreateInjected(aRest: TSQLRest; const aStubsByGUID: array of TGUID; const aOtherResolvers: array of TInterfaceResolver; const aDependencies: array of TInterfacedObject); reintroduce; /// access to the associated REST instance property Rest: TSQLRest read FRest; end; { ----- Services / Daemon Implementation } type TDDDMonitoredDaemon = class; /// the current state of a process thread TDDDMonitoredDaemonProcessState = ( dpsPending, dpsProcessing, dpsProcessed, dpsFailed); /// abstract process thread class with monitoring abilities TDDDMonitoredDaemonProcess = class(TThread) protected fDaemon: TDDDMonitoredDaemon; fIndex: integer; fProcessIdleDelay: cardinal; fMonitoring: TSynMonitorWithSize; /// the main thread loop, which will call the protected Execute* methods procedure Execute; override; protected /// check for any pending task, and mark it as started // - will be executed within fDaemon.fProcessLock so that it would be // atomic among all fDaemon.fProcess[] threads - overriden implementation // should therefore ensure that this method executes as fast as possible // to minimize contention // - returns FALSE if there is no pending task // - returns TRUE if there is a pending task, and ExecuteProcessAndSetResult // should be called by Execute outside the fDaemon.fProcessLock function ExecuteRetrievePendingAndSetProcessing: boolean; virtual; abstract; /// execute the task, and set its resulting state // - resulting state may be e.g. "processed" or "failed" // - should return the number of bytes processed, for fMonitoring update // - will be executed outside fDaemon.fProcessLock so that overriden // implementation may take as much time as necessary for its process function ExecuteProcessAndSetResult: QWord; virtual; abstract; /// finalize the pending task // - will always be called, even if ExecuteRetrievePendingAndSetProcessing // returned FALSE procedure ExecuteProcessFinalize; virtual; abstract; /// is called when there is no more pending task // - may be used e.g. to release a connection or some resource (e.g. an // ORM TSQLRestBatch instance) procedure ExecuteIdle; virtual; abstract; /// will be called on any exception in Execute // - this default implementation will call fMonitoring.ProcessError() procedure ExecuteOnException(E: Exception); virtual; public /// initialize the process thread for a given Service/Daemon instance constructor Create(aDaemon: TDDDMonitoredDaemon; aIndexInDaemon: integer); virtual; /// finalize the process thread destructor Destroy; override; /// milliseconds delay defined before getting the next pending tasks // - equals TDDDMonitoredDaemon.ProcessIdleDelay, unless a fatal exception // occurred during TDDDMonitoredDaemonProcess.ExecuteIdle method: in this // case, the delay would been increased to 500 ms property IdleDelay: cardinal read fProcessIdleDelay; end; /// abstract process thread class with monitoring abilities, using the ORM // for pending tasks persistence // - a protected TSQLRecord instance will be maintained to store the // processing task and its current state TDDDMonitoredDaemonProcessRest = class(TDDDMonitoredDaemonProcess) protected /// the internal ORM instance used to maintain the current task // - it should contain the data to be processed, the processing state // (e.g. at least "processing", "processed" and "failed"), and optionally // the resulting content (if any) // - overriden ExecuteRetrievePendingAndSetProcessing method should create // fPendingTask then save fPendingTask.State to "processing" // - overriden ExecuteProcessAndSetResult method should perform the task, // then save fPendingTask.State to "processed" or "failed" fPendingTask: TSQLRecord; /// finalize the pending task: will free fPendingTask and set it to nil procedure ExecuteProcessFinalize; override; end; /// class-reference type (metaclass) to determine which actual thread class // will implement the monitored process TDDDMonitoredDaemonProcessClass = class of TDDDMonitoredDaemonProcess; /// abstract class using several process threads and with monitoring abilities // - able to implement any DDD Daemon/Service, with proper statistics gathering // - each TDDDMonitoredDaemon will own its TDDDMonitoredDaemonProcess TDDDMonitoredDaemon = class(TCQRSQueryObjectRest,IMonitoredDaemon) protected fProcess: array of TDDDMonitoredDaemonProcess; fProcessClass: TDDDMonitoredDaemonProcessClass; fProcessMonitoringClass: TSynMonitorClass; fProcessLock: IAutoLocker; fProcessTimer: TPrecisionTimer; fProcessThreadCount: integer; fProcessIdleDelay: integer; fMonitoringClass: TSynMonitorClass; function GetStatus: variant; virtual; public /// abstract constructor, which should not be called by itself constructor Create(aRest: TSQLRest); overload; override; /// you should override this constructor to set the actual process // - i.e. define the fProcessClass protected property constructor Create(aRest: TSQLRest; aProcessThreadCount: integer); reintroduce; overload; /// finalize the Daemon destructor Destroy; override; /// monitor the Daemon/Service by returning some information as a TDocVariant // - its Status.stats sub object will contain global processing statistics, // and Status.threadstats similar information, detailled by running thread function RetrieveState(out Status: variant): TCQRSResult; /// launch all processing threads // - any previous running threads are first stopped function Start: TCQRSResult; virtual; /// finalize all processing threads // - and returns updated statistics as a TDocVariant function Stop(out Information: variant): TCQRSResult; virtual; published /// how many process threads should be created by this Daemon/Service property ProcessThreadCount: integer read fProcessThreadCount; /// how many milliseconds each process thread should wait before checking // for pending tasks // - default value is 50 ms, which seems good enough in practice property ProcessIdleDelay: integer read fProcessIdleDelay write fProcessIdleDelay; end; /// current status of an administrable service/daemon TDDDAdministratedDaemonStatus = ( dsUnknown, dsCreated, dsStarted, dsStopped, dsHalted); /// abstract class to implement an administrable service/daemon // - a single administration daemon (running e.g. as a Windows Service) would // be able to launch and administrate such process, via a remote REST link // - inherited class should override the Internal* virtual abstract protected // methods to supply the actual process (e.g. set a background thread) TDDDAdministratedDaemon = class(TCQRSService, IAdministratedDaemon) protected {$ifdef WITHLOG} fLog: TSynLogFamily; {$endif} fStatus: TDDDAdministratedDaemonStatus; fFinished: TEvent; fRemoteLog: TSynLogCallbacks; fInternalDatabases: TSQLRestDynArray; fInternalSettings: TObject; fInternalSettingsFolder: TFileName; fAdministrationServer: TSQLRestServer; fAdministrationServerOwned: boolean; fAdministrationHTTPServer: TObject; // return TRUE e.g. if TDDDAdministratedThreadDaemon.fThread<>nil function InternalIsRunning: boolean; virtual; abstract; // should start the daemon: e.g. set TDDDAdministratedThreadDaemon.fThread procedure InternalStart; virtual; abstract; // return TRUE and set Status (e.g. from monitoring info) on success // - this default implement returns the system memory info as current state function InternalRetrieveState(var Status: variant): boolean; virtual; // should end the daemon: e.g. TDDDAdministratedThreadDaemon.fThread := nil procedure InternalStop; virtual; // set the list of published TSQLRestInstances - InternalStop would release it procedure PublishORMTables(const Rest: array of TSQLRest); virtual; function PublishedORM(const DatabaseName: RawUTF8): TSQLRest; procedure SetInternalSettings(Settings: TObject); virtual; public /// initialize the administrable service/daemon // - aAdministrationServer.ServiceDefine(IAdministratedDaemon) will be // called to publish the needed methods over it, to allow remote // administration from a single administration daemon (installed e.g. as a // Windows Service) // - this constructor won't start the associated process, which would be // idle until the Start method is called constructor Create(aAdministrationServer: TSQLRestServer); reintroduce; overload; virtual; /// initialize the administrable service/daemon with its own TSQLRestServer // - will initialize and own its dedicated TSQLRestServerFullMemory // - if aUserName is specified, authentication will be enabled, and a // single TSQLAuthUser will be created, with the supplied credentials // (the password matching TSQLAuthUser.PasswordHashHexa expectations) // - under Windows, you can export the administration server as named pipe, // if the optional aServerNamedPipe parameter is set constructor Create(const aUserName,aHashedPassword: RawUTF8; const aRoot: RawUTF8='admin'; const aServerNamedPipe: TFileName=''); reintroduce; overload; /// finalize the service/daemon // - will call Halt() if the associated process is still running destructor Destroy; override; /// monitor the Daemon/Service by returning some information as a TDocVariant function RetrieveState(out Status: variant): TCQRSResult; virtual; /// IAdministratedDaemon command to launch the associated process // - if the process was already running, returns cqrsAlreadyExists function Start: TCQRSResult; virtual; /// IAdministratedDaemon command to finalize the associated process // - and returns updated statistics as a TDocVariant function Stop(out Information: variant): TCQRSResult; virtual; /// IAdministratedDaemon command to Stop the associated process, then // quit the executable // - returning the same output information than Stop() function Halt(out Information: variant): TCQRSResult; virtual; /// IAdministratedDaemon command to retrieve all internal databases names // - will return fInternalDatabases[].Model.Root values function DatabaseList: TRawUTF8DynArray; virtual; /// IAdministratedDaemon command to return the table names of an internal database function DatabaseTables(const DatabaseName: RawUTF8): TRawUTF8DynArray; virtual; /// IAdministratedDaemon command to execute a SQL query on an internal database // - you may override this method to implement addition "pseudo-SQL" commands function DatabaseExecute(const DatabaseName,SQL: RawUTF8): TServiceCustomAnswer; virtual; /// IAdministratedDaemon command to subscribe to a set of events for // real-time remote monitoring of the specified log events procedure SubscribeLog(const Levels: TSynLogInfos; const Callback: ISynLogCallback; ReceiveExistingKB: cardinal); virtual; /// IAdministratedDaemon command called when a callback is released on the client side procedure CallbackReleased(const callback: IInvokable; const interfaceName: RawUTF8); /// run the daemon, until it is halted // - if RemotelyAdministrated is FALSE, it will Start the process, then // wait until the [Enter] key is pressed (to be used in pure console mode) // - if RemotelyAdministrated is TRUE, it will follow remote activation // from its administration server // - both modes will log some minimal message on the console (if any) procedure Execute(RemotelyAdministrated: boolean); /// this method will wait until Halt() is executed // - i.e. protected fFinished TEvent is notified procedure WaitUntilHalted; virtual; /// returns the daemon name // - e.g. TMyOwnDaemon would return 'MyOwn' text function DaemonName: RawUTF8; virtual; {$ifdef WITHLOG} /// access to the associated loging class property Log: TSynLogFamily read fLog; {$endif} /// access to the associated internal settings // - is defined as an opaque TObject instance, to avoid unneeded dependencies property InternalSettings: TObject read fInternalSettings write SetInternalSettings; /// access to the associated internal settings storage folder property InternalSettingsFolder: TFileName read fInternalSettingsFolder write fInternalSettingsFolder; /// reference to the WebSockets/HTTP server publishing AdministrationServer // - is defined as an opaque TObject instance, to avoid unneeded dependencies property AdministrationHTTPServer: TObject read fAdministrationHTTPServer write fAdministrationHTTPServer; published /// the current status of the service/daemon property Status: TDDDAdministratedDaemonStatus read fStatus; /// reference to the REST server publishing IAdministratedDaemon service // - e.g. from named pipe local communication on Windows property AdministrationServer: TSQLRestServer read fAdministrationServer; end; /// type used to define a class kind of TDDDAdministratedDaemon TDDDAdministratedDaemonClass = class of TDDDAdministratedDaemon; /// abstract class to implement an TThread-based administrable service/daemon // - inherited class should override InternalStart and InternalRetrieveState // abstract methods, and set the protected fThread with the processing thread TDDDAdministratedThreadDaemon = class(TDDDAdministratedDaemon) protected fThread: TThread; // return TRUE if fThread<>nil (i.e. has been set by InternalStart) function InternalIsRunning: boolean; override; // end the daemon, i.e fThread := nil procedure InternalStop; override; end; /// abstract class to implement a TSQLRest-based administrable service/daemon // - inherited class should override InternalStart and InternalRetrieveState // abstract methods, and set the protected fRest with the processing TSQLRest TDDDAdministratedRestDaemon = class(TDDDAdministratedDaemon) protected fRest: TSQLRestServer; // return TRUE if fRest<>nil (i.e. has been set by InternalStart) function InternalIsRunning: boolean; override; // end the daemon, i.e. FreeAndNil(fRest) procedure InternalStop; override; public /// read-only access to the associated REST instance // - is assigned only between daemon Start/Stop property Rest: TSQLRestServer read fRest; end; /// abstract class to monitor an administrable service/daemon // - including Input/Output statistics and connected Clients count // - including OS Memory information TDDDAdministratedDaemonMonitor = class(TSynAutoCreateFields) protected fServer: TSynMonitorServer; function GetMemory: variant; public procedure ProcessException(E: Exception); virtual; published /// information about the REST server process property Server: TSynMonitorServer read fServer; /// information about the main System memory, as returned by the OS property SystemMemory: variant read GetMemory; end; { *********** Application Layer Implementation } type /// abstract class for implementing an Application Layer service // - is defined as an TInjectableAutoCreateFields, so that any published // properties defined as interfaces would be resolved at creation, and // published properties defined as TPersistent/TSynPersistent will be // managed by this instance, i.e. created and released with it TDDDApplication = class(TInjectableAutoCreateFields) protected public end; implementation { *********** Persistence / Repository Interfaces } var TCQRSResultText: array[TCQRSResult] of PShortString; function ToText(res: TCQRSResult): PShortString; begin result := TCQRSResultText[res]; end; function ToText(res: TCQRSQueryState): PShortString; overload; begin result := GetEnumName(TypeInfo(TCQRSQueryState),ord(res)); end; { TCQRSService } constructor TCQRSService.Create; begin inherited Create; fSafe.Init; {$ifdef WITHLOG} fLog := SQLite3Log.Family; // may be overriden {$endif} end; function TCQRSService.GetLastError: TCQRSResult; begin result := fLastError; end; function TCQRSService.GetLastErrorInfo: Variant; begin result := fLastErrorContext; end; const NEEDS_QUERY = [qaGet, qaCommandOnSelect]; NEEDS_COMMAND = [qaCommit]; ACTION_TO_STATE: array[TCQRSQueryAction] of TCQRSQueryState = ( // qsNone = no state change after this action qsNone, qsQuery, qsNone, qsCommand, qsCommand, qsNone); function TCQRSService.CqrsBeginMethod(aAction: TCQRSQueryAction; var aResult: TCQRSResult; aError: TCQRSResult): boolean; begin aResult := aError; VarClear(fLastErrorContext); if (aAction in NEEDS_QUERY) and (fStatecqrsSuccess then fLastErrorContext := ObjectToVariantDebug(self,'%',[NowToString]) else if ACTION_TO_STATE[fAction]<>qsNone then fState := ACTION_TO_STATE[fAction]; fAction := qaNone; end; procedure TCQRSService.AfterInternalCqrsSetResult; {$ifdef WITHLOG} var level: TSynLogInfo; {$endif} begin {$ifdef WITHLOG} if fLastError in CQRSRESULT_SUCCESS then exit; if fLastError in CQRSRESULT_WARNING then level := sllDDDInfo else level := sllDDDError; if level in fLog.Level then fLog.SynLog.Log(level, 'CqrsSetResult(%) state=% %', [ToText(fLastError)^, ToText(fState)^, fLastErrorContext], self); {$endif} end; procedure TCQRSService.CqrsSetResultSuccessIf(SuccessCondition: boolean; var Result: TCQRSResult; ErrorIfFalse: TCQRSResult); begin if SuccessCondition then CqrsSetResult(cqrsSuccess,Result) else CqrsSetResult(ErrorIfFalse,Result); end; procedure TCQRSService.CqrsSetResultDoc(Error: TCQRSResult; const ErrorInfo: variant; var Result: TCQRSResult); begin InternalCqrsSetResult(Error,Result); _ObjAddProps(['ErrorInfo',ErrorInfo],fLastErrorContext); AfterInternalCqrsSetResult; end; procedure TCQRSService.CqrsSetResultJSON(Error: TCQRSResult; const JSONFmt: RawUTF8; const Args,Params: array of const; var Result: TCQRSResult); begin CqrsSetResultDoc(Error,_JsonFastFmt(JSONFmt,Args,Params),Result); end; procedure TCQRSService.CqrsSetResultMsg(Error: TCQRSResult; const ErrorMessage: RawUTF8; var Result: TCQRSResult); begin InternalCqrsSetResult(Error,Result); _ObjAddProps(['Msg',ErrorMessage],fLastErrorContext); AfterInternalCqrsSetResult; end; procedure TCQRSService.CqrsSetResultString(Error: TCQRSResult; const ErrorMessage: string; var Result: TCQRSResult); begin InternalCqrsSetResult(Error,Result); _ObjAddProps(['Msg',ErrorMessage],fLastErrorContext); AfterInternalCqrsSetResult; end; procedure TCQRSService.CqrsSetResultMsg(Error: TCQRSResult; const ErrorMsgFmt: RawUTF8; const ErrorMsgArgs: array of const; var Result: TCQRSResult); begin CqrsSetResultMsg(Error,FormatUTF8(ErrorMsgFmt,ErrorMsgArgs),Result); end; destructor TCQRSService.Destroy; begin inherited Destroy; fSafe.Done; end; { TCQRSServiceSubscribe } procedure TCQRSServiceSubscribe.CallbackReleased(const callback: IInvokable; const interfaceName: RawUTF8); var i: integer; begin fSafe.Lock; try {$ifdef WITHLOG} fLog.SynLog.Log(sllTrace,'CallbackReleased(%,"%") callback=%', [callback,interfaceName,ObjectFromInterface(callback)],Self); {$endif} for i := 0 to high(fSubscriber) do // try to release on ALL subscribers fSubscriber[i].CallbackReleased(callback, interfaceName); finally fSafe.UnLock; end; end; { TCQRSServiceSynch } constructor TCQRSServiceSynch.Create(const sharedcallback: IInterface); begin inherited Create; if sharedcallback = nil then raise EDDDException.CreateUTF8('%.Create(nil)', [self]); fSharedCallbackRef := sharedcallback; end; { TCQRSServiceAsynchAck } destructor TCQRSServiceAsynchAck.Destroy; begin fCalls.Free; // would force evTimeOut if some WaitFor are still pending inherited Destroy; end; { ----- Persistence / Repository Implementation using mORMot's ORM } { EDDDRepository } constructor EDDDRepository.CreateUTF8(Caller: TDDDRepositoryRestFactory; const Format: RawUTF8; const Args: array of const); begin if Caller=nil then inherited CreateUTF8(Format,Args) else inherited CreateUTF8('% - %',[FormatUTF8(Format,Args),ObjectToJSONDebug(Caller)]); end; { TDDDRepositoryRestFactory } constructor TDDDRepositoryRestFactory.Create( const aInterface: TGUID; aImplementation: TDDDRepositoryRestClass; aAggregate: TClass; aRest: TSQLRest; aTable: TSQLRecordClass; const TableAggregatePairs: array of RawUTF8; aOwner: TDDDRepositoryRestManager); begin fInterface := TInterfaceFactory.Get(aInterface); if fInterface=nil then raise EDDDRepository.CreateUTF8(self, '%.Create(%): Interface not registered - you could use TInterfaceFactory.'+ 'RegisterInterfaces()',[self,GUIDToShort(aInterface)]); inherited Create(fInterface.InterfaceTypeInfo,aImplementation); fOwner := aOwner; fRest := aRest; fTable := aTable; if (aAggregate=nil) or (fRest=nil) or (fTable=nil) then raise EDDDRepository.CreateUTF8(self,'Invalid %.Create(nil)',[self]); fAggregate.Init(aAggregate); fPropsMapping.Init(aTable,RawUTF8(aAggregate.ClassName),aRest,false,[]); fPropsMapping.MapFields(['ID','####']); // no ID/RowID for our aggregates fPropsMapping.MapFields(TableAggregatePairs); fAggregateRTTI := TSQLPropInfoList.Create(aAggregate, GetAggregateRTTIOptions); SetLength(fAggregateToTable,fAggregateRTTI.Count); SetLength(fAggregateProp,fAggregateRTTI.Count); ComputeMapping; {$ifdef WITHLOG} Rest.LogClass.Add.Log(sllDDDInfo,'Started % implementing % for % over %', [self,fInterface.InterfaceName,aAggregate,fTable],self); {$endif} end; constructor TDDDRepositoryRestFactory.Create(const aInterface: TGUID; aImplementation: TDDDRepositoryRestClass; aAggregate: TClass; aRest: TSQLRest; aTable: TSQLRecordClass; aOwner: TDDDRepositoryRestManager); begin Create(aInterface,aImplementation,aAggregate,aRest,aTable,[],aOwner); end; function TDDDRepositoryRestFactory.GetAggregateRTTIOptions: TSQLPropInfoListOptions; begin Result := [pilAllowIDFields,pilSubClassesFlattening,pilIgnoreIfGetter]; end; destructor TDDDRepositoryRestFactory.Destroy; begin {$ifdef WITHLOG} Rest.LogClass.Add.Log(sllDDDInfo,'Destroying %',[self],self); {$endif} fAggregateRTTI.Free; ObjArrayClear(fGarbageCollector); inherited; end; class procedure TDDDRepositoryRestFactory.ComputeSQLRecord( const aAggregate: array of TClass; DestinationSourceCodeFile: TFileName); const RAW_TYPE: array[TSQLFieldType] of RawUTF8 = ( // values left to '' will use the RTTI type '', // sftUnknown 'RawUTF8', // sftAnsiText 'RawUTF8', // sftUTF8Text '', // sftEnumerate '', // sftSet '', // sftInteger '', // sftID = TSQLRecord(aID) 'TRecordReference',// sftRecord = TRecordReference 'boolean', // sftBoolean 'double', // sftFloat 'TDateTime', // sftDateTime 'TTimeLog', // sftTimeLog 'currency', // sftCurrency '', // sftObject 'variant', // sftVariant '', // sftNullable 'TSQLRawBlob', // sftBlob 'variant', // sftBlobDynArray T*ObjArray=JSON=variant (RawUTF8?) '', // sftBlobCustom 'variant', // sftUTF8Custom '', // sftMany 'TModTime', // sftModTime 'TCreateTime', // sftCreateTime '', // sftTID 'TRecordVersion', // sftRecordVersion = TRecordVersion 'TSessionUserID', // sftSessionUserID '', // sftDateTimeMS '', // sftUnixTime ''); // sftUnixMSTime var hier: TClassDynArray; a,i,f: integer; code,aggname,recname,parentrecname,typ: RawUTF8; map: TSQLPropInfoList; rectypes: TRawUTF8DynArray; begin {$ifdef KYLIX3} hier := nil; {$endif to make compiler happy} if DestinationSourceCodeFile='' then DestinationSourceCodeFile := ExeVersion.ProgramFilePath+'ddsqlrecord.inc'; for a := 0 to high(aAggregate) do begin hier := ClassHierarchyWithField(aAggregate[a]); code := code+#13#10'type'; parentrecname := 'TSQLRecord'; for i := 0 to high(hier) do begin aggname := RawUTF8(hier[i].ClassName); recname := 'TSQLRecord'+copy(aggname,2,100); map := TSQLPropInfoList.Create(hier[i], [pilSingleHierarchyLevel,pilAllowIDFields, pilSubClassesFlattening,pilIgnoreIfGetter]); try code := FormatUTF8('%'#13#10+ ' /// ORM class corresponding to % DDD aggregate'#13#10+ ' % = class(%)'#13#10' protected'#13#10, [code,aggname,recname,parentrecname]); SetLength(rectypes,map.count); for f := 0 to map.Count-1 do with map.List[f] do begin rectypes[f] := RAW_TYPE[SQLFieldType]; if rectypes[f]='' then if SQLFieldType=sftInteger then begin rectypes[f] := 'Int64'; if InheritsFrom(TSQLPropInfo) then with TSQLPropInfoRTTI(map.List[f]).PropType^ do if (Kind=tkInteger) and (OrdType<>otULong) then rectypes[f] := 'integer'; // cardinal -> Int64 end else rectypes[f] := SQLFieldRTTITypeName; code := FormatUTF8('% f%: %; // %'#13#10, [code,Name,rectypes[f],SQLFieldRTTITypeName]); end; code := code+' published'#13#10; for f := 0 to map.Count-1 do with map.List[f] do begin typ := SQLFieldRTTITypeName; if IdemPropNameU(typ, rectypes[f]) then typ := '' else typ := ' ('+typ+')'; code := FormatUTF8('% /// maps %.%%'#13#10+ ' property %: % read f% write f%;'#13#10, [code,aggname, NameUnflattened,typ,Name,rectypes[f],Name,Name]); end; code := code+' end;'#13#10; finally map.Free; end; parentrecname := recname; end; end; FileFromString(code,DestinationSourceCodeFile); end; procedure TDDDRepositoryRestFactory.ComputeMapping; procedure EnsureCompatible(agg,rec: TSQLPropInfo); { note about dynamic arrays (e.g. TRawUTF8DynArray or T*ObjArray) published fields: TOrder = class(TSynAutoCreateFields) published property Lines: TOrderLineObjArray In all cases, T*ObjArray should be accessible directly, using ObjArray*() wrapper functions, and other dynamic arrays too. Storage at TSQLRecord level would use JSON format, i.e. a variant in the current implementation - you may use a plain RawUTF8 field if the on-the-fly conversion to/from TDocVariant appears to be a bottleneck. } begin if agg.SQLDBFieldType=rec.SQLDBFieldType then exit; // very same type at DB level -> OK if (agg.SQLFieldType=sftBlobDynArray) and (rec.SQLFieldType in [sftVariant,sftUTF8Text]) then exit; // allow array <-> JSON/TEXT <-> variant/RawUTF8 marshalling raise EDDDRepository.CreateUTF8(self, '% types do not match at DB level: %.%:%=% and %.%:%=%',[self, Aggregate,agg.Name,agg.SQLFieldRTTITypeName,agg.SQLDBFieldTypeName^, fTable,rec.Name,rec.SQLFieldRTTITypeName,rec.SQLDBFieldTypeName^]); end; var i,ndx: integer; ORMProps: TSQLPropInfoObjArray; agg: TSQLPropInfoRTTI; begin fAggregateID := nil; ORMProps := fTable.RecordProps.Fields.List; for i := 0 to fAggregateRTTI.Count-1 do begin agg := fAggregateRTTI.List[i] as TSQLPropInfoRTTI; fAggregateProp[i] := agg; ndx := fPropsMapping.ExternalToInternalIndex(agg.Name); if ndx=-1 then // ID/RowID mapped with an existing String/Hexa field if agg.SQLDBFieldType in [ftInt64,ftUTF8] then begin fAggregateID := agg; fAggregateToTable[i] := nil; end else raise EDDDRepository.CreateUTF8(self,'% types error: %.%:%=% and %.RowID', [self,Aggregate,agg.Name,agg.SQLFieldRTTITypeName,agg.SQLDBFieldTypeName^,fTable]) else if ndx<0 then // e.g. TSynPersistent property flattened in TSQLRecord fAggregateToTable[i] := nil else begin fAggregateToTable[i] := ORMProps[ndx]; EnsureCompatible(agg,fAggregateToTable[i]); end; end; fPropsMappingVersion := fPropsMapping.MappingVersion; end; procedure TDDDRepositoryRestFactory.AggregatePropToTable( aAggregate: TObject; aAggregateProp: TSQLPropInfo; aRecord: TSQLRecord; aRecordProp: TSQLPropInfo); procedure ProcessID; var v: RawUTF8; id: TID; begin fAggregateID.GetValueVar(aAggregate,false,v,nil); case fAggregateID.SQLDBFieldType of ftInt64: SetID(pointer(v),id); ftUTF8: if not HexDisplayToBin(pointer(v),@id,sizeof(id)) then id := 0; end; aRecord.IDValue := id; end; begin if fAggregateID=aAggregateProp then ProcessID else if aRecordProp<>nil then aAggregateProp.CopyProp(aAggregate,aRecordProp,aRecord); end; procedure TDDDRepositoryRestFactory.TablePropToAggregate( aRecord: TSQLRecord; aRecordProp: TSQLPropInfo; aAggregate: TObject; aAggregateProp: TSQLPropInfo); procedure ProcessID; var v: RawUTF8; begin case fAggregateID.SQLDBFieldType of ftInt64: begin Int64ToUtf8(aRecord.IDValue,v); fAggregateID.SetValue(aAggregate,pointer(v),false); end; ftUTF8: begin Int64ToHex(aRecord.IDValue,v); fAggregateID.SetValue(aAggregate,pointer(v),true); end; end; end; begin if fAggregateID=aAggregateProp then ProcessID else if aRecordProp=nil then aAggregateProp.SetValue(aAggregate,nil,false) else aRecordProp.CopyProp(aRecord,aAggregateProp,aAggregate); end; function TDDDRepositoryRestFactory.CreateInstance: TInterfacedObject; begin result := TDDDRepositoryRestClass(fImplementation.ItemClass).Create(self); end; procedure TDDDRepositoryRestFactory.AggregateClear(aAggregate: TObject); var i: integer; begin if aAggregate<>nil then for i := 0 to high(fAggregateProp) do with fAggregateProp[i] do SetValue(Flattened(aAggregate),nil,false); end; function TDDDRepositoryRestFactory.AggregateCreate: TObject; begin result := fAggregate.CreateNew; end; procedure TDDDRepositoryRestFactory.AggregateToJSON(aAggregate: TObject; W: TJSONSerializer; ORMMappedFields: boolean; aID: TID); var i: integer; begin if fPropsMapping.MappingVersion<>fPropsMappingVersion then ComputeMapping; if aAggregate=nil then begin W.AddShort('null'); exit; end; W.Add('{'); if aID<>0 then begin W.AddShort('"RowID":'); W.Add(aID); W.Add(','); end; for i := 0 to high(fAggregateProp) do begin if ORMMappedFields then if fAggregateToTable[i]=nil then continue else W.AddFieldName(fAggregateToTable[i].Name) else W.AddFieldName(fAggregateProp[i].Name); with fAggregateProp[i] do GetJSONValues(Flattened(aAggregate),W); W.Add(','); end; W.CancelLastComma; W.Add('}'); end; function TDDDRepositoryRestFactory.AggregateToJSON( aAggregate: TObject; ORMMappedFields: boolean; aID: TID): RawUTF8; var W: TJSONSerializer; begin if aAggregate=nil then begin result := 'null'; exit; end; W := TJSONSerializer.CreateOwnedStream; try AggregateToJSON(aAggregate,W,ORMMappedFields,aID); W.SetText(result); finally W.Free; end; end; procedure TDDDRepositoryRestFactory.AggregateToTable( aAggregate: TObject; aID: TID; aDest: TSQLRecord); var i: integer; begin if fPropsMapping.MappingVersion<>fPropsMappingVersion then ComputeMapping; if aDest=nil then raise EDDDRepository.CreateUTF8(self,'%.AggregateToTable(%,%,%=nil)', [self,aAggregate,aID,fTable]); aDest.ClearProperties; aDest.IDValue := aID; if aAggregate<>nil then for i := 0 to length(fAggregateProp)-1 do AggregatePropToTable(aAggregate,fAggregateProp[i],aDest,fAggregateToTable[i]); end; procedure TDDDRepositoryRestFactory.AggregateFromTable( aSource: TSQLRecord; aAggregate: TObject); var i: integer; begin if fPropsMapping.MappingVersion<>fPropsMappingVersion then ComputeMapping; if aAggregate=nil then raise EDDDRepository.CreateUTF8(self,'%.AggregateFromTable(%=nil)',[self,Aggregate]); if aSource=nil then AggregateClear(aAggregate) else for i := 0 to length(fAggregateProp)-1 do TablePropToAggregate(aSource,fAggregateToTable[i],aAggregate,fAggregateProp[i]); end; procedure TDDDRepositoryRestFactory.AggregatesFromTableFill( aSource: TSQLRecord; var aAggregateObjArray); var res: TObjectDynArray absolute aAggregateObjArray; i: integer; begin SetLength(res, aSource.FillTable.RowCount); i := 0; if aSource.FillRewind then while aSource.FillOne do begin res[i] := fAggregate.CreateNew; AggregateFromTable(aSource,res[i]); inc(i); end; if i <> length(res) then ObjArrayClear(res); end; function TDDDRepositoryRestFactory.GetAggregateName: string; begin if (self=nil) or (Aggregate=nil) then result := '' else result := string(Aggregate.ClassName); end; function TDDDRepositoryRestFactory.GetTableName: string; begin if (self=nil) or (fTable=nil) then result := '' else result := fTable.ClassName; end; procedure TDDDRepositoryRestFactory.AddFilterOrValidate( const aFieldNames: array of RawUTF8; aFilterOrValidate: TSynFilterOrValidate; aFieldNameFlattened: boolean); var f,ndx: integer; arr: ^TPointerDynArray; begin if aFilterOrValidate=nil then exit; ObjArrayAdd(fGarbageCollector,aFilterOrValidate); for f := 0 to high(aFieldNames) do begin if aFilterOrValidate.InheritsFrom(TSynValidate) then arr := @fValidate else arr := @fFilter; if arr^=nil then SetLength(arr^,fAggregateRTTI.Count); if aFieldNames[f]='*' then begin // apply to all text fields for ndx := 0 to high(fAggregateProp) do if fAggregateProp[ndx].SQLFieldType in RAWTEXT_FIELDS then aFilterOrValidate.AddOnce(TSynFilterOrValidateObjArray(arr^[ndx]),false); end else begin if aFieldNameFlattened then ndx := fAggregateRTTI.IndexByNameUnflattenedOrExcept(aFieldNames[f]) else ndx := fAggregateRTTI.IndexByNameOrExcept(aFieldNames[f]); aFilterOrValidate.AddOnce(TSynFilterOrValidateObjArray(arr^[ndx]),false); end; end; end; function TDDDRepositoryRestFactory.AggregateFilterAndValidate( aAggregate: TObject; aInvalidFieldIndex: PInteger; aValidator: PSynValidate): RawUTF8; var f,i: integer; Value: TRawUTF8DynArray; // avoid twice retrieval Old: RawUTF8; msg: string; str: boolean; begin if (aAggregate=nil) or not aAggregate.ClassType.InheritsFrom(Aggregate) then raise EDDDRepository.CreateUTF8(self,'%.AggregateFilterAndValidate(%) '+ 'expects a % instance',[self,aAggregate,Aggregate]); // first process all filters SetLength(Value,fAggregateRTTI.Count); for f := 0 to high(fFilter) do if fFilter[f]<>nil then begin with fAggregateProp[f] do GetValueVar(Flattened(aAggregate),false,Value[f],@str); Old := Value[f]; for i := 0 to high(fFilter[f]) do fFilter[f,i].Process(f,Value[f]); if Old<>Value[f] then with fAggregateProp[f] do SetValueVar(Flattened(aAggregate),Value[f],str); end; // then validate the content for f := 0 to high(fValidate) do if fValidate[f]<>nil then begin if Value[f]='' then // if not already retrieved with fAggregateProp[f] do GetValueVar(Flattened(aAggregate),false,Value[f],nil); for i := 0 to high(fValidate[f]) do if not fValidate[f,i].Process(f,Value[f],msg) then begin if aInvalidFieldIndex<>nil then aInvalidFieldIndex^ := f; if aValidator<>nil then aValidator^ := fValidate[f,i]; if msg='' then // no custom message -> show a default message msg := format(sValidationFailed,[GetCaptionFromClass(fValidate[f,i].ClassType)]); result := FormatUTF8('%.%: %',[Aggregate,fAggregateProp[f].NameUnflattened,msg]); exit; end; end; result := ''; // if we reached here, there was no error end; { TDDDRepositoryRestManager } function TDDDRepositoryRestManager.AddFactory( const aInterface: TGUID; aImplementation: TDDDRepositoryRestClass; aAggregate: TClass; aRest: TSQLRest; aTable: TSQLRecordClass; const TableAggregatePairs: array of RawUTF8): TDDDRepositoryRestFactory; begin if GetFactoryIndex(aInterface)>=0 then raise EDDDRepository.CreateUTF8(nil,'Duplicated GUID for %.AddFactory(%,%,%)', [self,GUIDToShort(aInterface),aImplementation,aAggregate]); result := TDDDRepositoryRestFactory.Create( aInterface,aImplementation,aAggregate,aRest,aTable,TableAggregatePairs,self); ObjArrayAdd(fFactory,result); {$ifdef WITHLOG} aRest.LogClass.Add.Log(sllDDDInfo,'Added factory % to %',[result,self],self); {$endif} end; destructor TDDDRepositoryRestManager.Destroy; begin ObjArrayClear(fFactory); inherited; end; function TDDDRepositoryRestManager.GetFactory( const aInterface: TGUID): TDDDRepositoryRestFactory; var i: integer; begin i := GetFactoryIndex(aInterface); if i<0 then raise EDDDRepository.CreateUTF8(nil,'%.GetFactory(%)=nil', [self,GUIDToShort(aInterface)]); result := fFactory[i]; end; function TDDDRepositoryRestManager.GetFactoryIndex( const aInterface: TGUID): integer; begin for result := 0 to length(fFactory)-1 do if IsEqualGUID(@fFactory[result].fInterface.InterfaceIID,@aInterface) then exit; result := -1; end; { TDDDRepositoryRestQuery } constructor TDDDRepositoryRestQuery.Create( aFactory: TDDDRepositoryRestFactory); begin fFactory := aFactory; fCurrentORMInstance := fFactory.Table.Create; {$ifdef WITHLOG} fLog := fFactory.Rest.LogFamily; {$endif} end; destructor TDDDRepositoryRestQuery.Destroy; begin fCurrentORMInstance.Free; inherited; end; class function TDDDRepositoryRestQuery.GetRest(const Service: ICQRSService): TSQLRest; var instance: TObject; begin instance := ObjectFromInterface(Service); if (instance=nil) or not instance.InheritsFrom(TDDDRepositoryRestQuery) then result := nil else result := TDDDRepositoryRestQuery(instance).fFactory.fRest; end; function TDDDRepositoryRestQuery.CqrsBeginMethod(aAction: TCQRSQueryAction; var aResult: TCQRSResult; aError: TCQRSResult): boolean; begin result := inherited CqrsBeginMethod(aAction,aResult,aError); if aAction=qaSelect then fCurrentORMInstance.ClearProperties; // reset internal instance end; function TDDDRepositoryRestQuery.ORMSelectOne(const ORMWhereClauseFmt: RawUTF8; const Bounds: array of const; ForcedBadRequest: boolean): TCQRSResult; begin CqrsBeginMethod(qaSelect,result); if ForcedBadRequest then CqrsSetResult(cqrsBadRequest,result) else CqrsSetResultSuccessIf(Factory.Rest.Retrieve(ORMWhereClauseFmt,[],Bounds, fCurrentORMInstance),result,cqrsNotFound); end; function TDDDRepositoryRestQuery.ORMSelectID(const ID: TID; RetrieveRecord, ForcedBadRequest: boolean): TCQRSResult; begin CqrsBeginMethod(qaSelect,result); if ForcedBadRequest or (ID=0) then CqrsSetResult(cqrsBadRequest,result) else if RetrieveRecord then CqrsSetResultSuccessIf(Factory.Rest.Retrieve(ID,fCurrentORMInstance),result,cqrsNotFound) else begin fCurrentORMInstance.IDValue := ID; CqrsSetResult(cqrsSuccess,result); end end; function TDDDRepositoryRestQuery.ORMSelectID(const ID: RawUTF8; RetrieveRecord, ForcedBadRequest: boolean): TCQRSResult; begin result := ORMSelectID(HexDisplayToInt64(ID),RetrieveRecord,ForcedBadRequest); end; function TDDDRepositoryRestQuery.ORMSelectAll( const ORMWhereClauseFmt: RawUTF8; const Bounds: array of const; ForcedBadRequest: boolean): TCQRSResult; begin CqrsBeginMethod(qaSelect,result); if ForcedBadRequest then CqrsSetResult(cqrsBadRequest,result) else CqrsSetResultSuccessIf(fCurrentORMInstance.FillPrepare( Factory.Rest,ORMWhereClauseFmt,[],Bounds),result,cqrsNotFound); end; function TDDDRepositoryRestQuery.ORMSelectCount( const ORMWhereClauseFmt: RawUTF8; const Args,Bounds: array of const; out aResultCount: integer; ForcedBadRequest: boolean): TCQRSResult; var tmp: Int64; begin CqrsBeginMethod(qaNone,result); // qaNone and not qaSelect which would fill ORM if ForcedBadRequest then CqrsSetResult(cqrsBadRequest,result) else if Factory.Rest.OneFieldValue( Factory.Table,'count(*)',ORMWhereClauseFmt,Args,Bounds,tmp) then begin aResultCount := tmp; CqrsSetResult(cqrsSuccess,result) end else CqrsSetResult(cqrsNotFound,result); end; function TDDDRepositoryRestQuery.GetCount: integer; var dummy: TCQRSResult; begin if not CqrsBeginMethod(qaGet,dummy) then result := 0 else if fCurrentORMInstance.FillTable<>nil then result := fCurrentORMInstance.FillTable.RowCount else if fCurrentORMInstance.IDValue=0 then result := 0 else result := 1; end; function TDDDRepositoryRestQuery.ORMGetAggregate( aAggregate: TObject): TCQRSResult; begin if CqrsBeginMethod(qaGet,result) then begin Factory.AggregateFromTable(fCurrentORMInstance,aAggregate); CqrsSetResult(cqrsSuccess,result); end; end; function TDDDRepositoryRestQuery.ORMGetNextAggregate( aAggregate: TObject; aRewind: boolean): TCQRSResult; begin if CqrsBeginMethod(qaGet,result) then if (aRewind and fCurrentORMInstance.FillRewind) or (not aRewind and fCurrentORMInstance.FillOne) then begin Factory.AggregateFromTable(fCurrentORMInstance,aAggregate); CqrsSetResult(cqrsSuccess,result); end else CqrsSetResult(cqrsNoMoreData,result); end; function TDDDRepositoryRestQuery.ORMGetAllAggregates( var aAggregateObjArray): TCQRSResult; begin if CqrsBeginMethod(qaGet,result) then if (fCurrentORMInstance.FillTable=nil) or (fCurrentORMInstance.FillTable.RowCount=0) then CqrsSetResult(cqrsSuccess,result) else begin Factory.AggregatesFromTableFill(fCurrentORMInstance,aAggregateObjArray); if Pointer(aAggregateObjArray)=nil then CqrsSetResult(cqrsNoMoreData,result) else CqrsSetResult(cqrsSuccess,result); end; end; { TDDDRepositoryRestCommand } constructor TDDDRepositoryRestCommand.Create( aFactory: TDDDRepositoryRestFactory); begin inherited Create(aFactory); fBatchAutomaticTransactionPerRow := 1000; // for better performance fBatchOptions := [boExtendedJSON]; end; destructor TDDDRepositoryRestCommand.Destroy; begin InternalRollback; inherited Destroy; end; function TDDDRepositoryRestCommand.Delete: TCQRSResult; begin if CqrsBeginMethod(qaCommandOnSelect,result) then ORMPrepareForCommit(soDelete,nil,result); end; function TDDDRepositoryRestCommand.DeleteAll: TCQRSResult; var i: integer; begin if CqrsBeginMethod(qaCommandOnSelect,result) then if fCurrentORMInstance.FillTable=nil then ORMPrepareForCommit(soDelete,nil,result) else if fStatenil) and (validator.ClassType=TSynValidateUniqueField) then CqrsSetResultMsg(cqrsAlreadyExists,msg,Result) else CqrsSetResultMsg(default,msg,Result); end; begin case aCommand of soSelect: begin CqrsSetResult(cqrsBadRequest,Result); exit; end; soUpdate,soDelete: if (fStatenil then begin msg := Factory.AggregateFilterAndValidate(aAggregate,nil,@validator); if msg<>'' then begin SetValidationError(cqrsDDDValidationFailed); exit; end; Factory.AggregateToTable(aAggregate,fCurrentORMInstance.IDValue,fCurrentORMInstance); end; msg := fCurrentORMInstance.FilterAndValidate( Factory.Rest,[0..MAX_SQLFIELDS-1],@validator); if msg<>'' then begin SetValidationError(cqrsDataLayerError); exit; end; end; ORMEnsureBatchExists; ndx := -1; if aAllFields then fields := ALL_FIELDS else fields := []; case aCommand of soInsert: ndx := fBatch.Add(fCurrentORMInstance,true,fFactory.fAggregateID<>nil,fields); soUpdate: ndx := fBatch.Update(fCurrentORMInstance,fields); soDelete: ndx := fBatch.Delete(fCurrentORMInstance.IDValue); end; CqrsSetResultSuccessIf(ndx>=0,Result); end; procedure TDDDRepositoryRestCommand.InternalCommit(var Result: TCQRSResult); begin if fBatch.Count=0 then CqrsSetResult(cqrsBadRequest,Result) else begin CqrsSetResultSuccessIf(Factory.Rest.BatchSend(fBatch,fBatchResults)=HTTP_SUCCESS,Result); FreeAndNil(fBatch); end; end; procedure TDDDRepositoryRestCommand.InternalRollback; begin FreeAndNil(fBatch); fBatchResults := nil; end; function TDDDRepositoryRestCommand.Commit: TCQRSResult; begin if CqrsBeginMethod(qaCommit,result) then InternalCommit(result); end; function TDDDRepositoryRestCommand.Rollback: TCQRSResult; begin CqrsBeginMethod(qaNone,result,cqrsSuccess); if fBatch.Count=0 then CqrsSetResult(cqrsNoPriorCommand,result) else InternalRollback; end; { TCQRSQueryObjectRest } constructor TCQRSQueryObjectRest.Create(aRest: TSQLRest); begin fRest := aRest; if (fResolver<>nil) or (aRest=nil) or (aRest.Services=nil) then inherited Create else inherited CreateWithResolver(aRest.Services); end; constructor TCQRSQueryObjectRest.CreateInjected(aRest: TSQLRest; const aStubsByGUID: array of TGUID; const aOtherResolvers: array of TInterfaceResolver; const aDependencies: array of TInterfacedObject); begin if not Assigned(aRest) then raise ECQRSException.CreateUTF8('%.CreateInjected(Rest=nil)',[self]); Create(aRest); inherited CreateInjected(aStubsByGUID,aOtherResolvers, aDependencies,true); end; constructor TCQRSQueryObjectRest.CreateWithResolver(aRest: TSQLRest; aResolver: TInterfaceResolver; aRaiseEServiceExceptionIfNotFound: boolean); begin if not Assigned(aRest) then raise ECQRSException.CreateUTF8('%.CreateWithResolver(Rest=nil)',[self]); fRest := aRest; inherited CreateWithResolver(aResolver,aRaiseEServiceExceptionIfNotFound); end; constructor TCQRSQueryObjectRest.CreateWithResolver( aResolver: TInterfaceResolver; aRaiseEServiceExceptionIfNotFound: boolean); begin if (aResolver<>nil) and aResolver.InheritsFrom(TServiceContainer) then fRest := TServiceContainer(aResolver).Rest; inherited CreateWithResolver(aResolver,aRaiseEServiceExceptionIfNotFound); end; { TDDDMonitoredDaemonProcess } constructor TDDDMonitoredDaemonProcess.Create(aDaemon: TDDDMonitoredDaemon; aIndexInDaemon: integer); begin fDaemon := aDaemon; if fDaemon.fProcessMonitoringClass=nil then fMonitoring := TSynMonitorWithSize.Create(aDaemon.Rest.Model.Root) else fMonitoring := fDaemon.fProcessMonitoringClass.Create(aDaemon.Rest.Model.Root) as TSynMonitorWithSize; fProcessIdleDelay := fDaemon.ProcessIdleDelay; fIndex := aIndexInDaemon; inherited Create(False); end; destructor TDDDMonitoredDaemonProcess.Destroy; begin fMonitoring.Free; inherited; end; procedure TDDDMonitoredDaemonProcess.Execute; begin fDaemon.Rest.BeginCurrentThread(self); try repeat SleepHiRes(fProcessIdleDelay); try try repeat if Terminated then exit; try try fDaemon.fProcessLock.Enter; // atomic unqueue via pending.Status try fMonitoring.ProcessStart; if not ExecuteRetrievePendingAndSetProcessing then break; // no more pending tasks fMonitoring.ProcessDoTask; finally fDaemon.fProcessLock.Leave; end; // always set, even if Terminated fMonitoring.AddSize(ExecuteProcessAndSetResult); finally fMonitoring.ProcessEnd; ExecuteProcessFinalize; end; except on E: Exception do begin ExecuteOnException(E); break; // will call ExecuteIdle then go to idle state end; end; until false; finally ExecuteIdle; end; except on E: Exception do begin ExecuteOnException(E); // exception during ExecuteIdle should not happen if fProcessIdleDelay<500 then fProcessIdleDelay := 500; // avoid CPU+resource burning end; end; until false; finally fDaemon.Rest.EndCurrentThread(self); end; end; procedure TDDDMonitoredDaemonProcess.ExecuteOnException(E: Exception); begin fMonitoring.ProcessError(ObjectToVariantDebug( E,'{threadindex:?,daemon:?}',[fIndex,fDaemon.GetStatus])); end; { TDDDMonitoredDaemonProcessRest } procedure TDDDMonitoredDaemonProcessRest.ExecuteProcessFinalize; begin FreeAndNil(fPendingTask); end; { TDDDMonitoredDaemon } constructor TDDDMonitoredDaemon.Create(aRest: TSQLRest); begin fProcessIdleDelay := 50; fProcessLock := TAutoLocker.Create; if fProcessThreadCount<1 then fProcessThreadCount := 1 else if fProcessThreadCount>20 then fProcessThreadCount := 20; inherited Create(aRest); end; constructor TDDDMonitoredDaemon.Create(aRest: TSQLRest; aProcessThreadCount: integer); begin fProcessThreadCount := aProcessThreadCount; Create(aRest); end; destructor TDDDMonitoredDaemon.Destroy; var dummy: variant; begin Stop(dummy); inherited Destroy; end; function TDDDMonitoredDaemon.GetStatus: variant; var i,working: integer; stats: TSynMonitor; pool: TDocVariantData; begin {$ifdef WITHLOG} Rest.LogClass.Enter('GetStatus',[],self); {$endif} VarClear(result); fProcessLock.Enter; try try working := 0; if fMonitoringClass=nil then if fProcessMonitoringClass=nil then stats := TSynMonitorWithSize.Create else stats := fProcessMonitoringClass.Create else stats := fMonitoringClass.Create; try pool.InitArray([],JSON_OPTIONS[true]); for i := 0 to High(fProcess) do with fProcess[i] do begin if fMonitoring.Processing then inc(working); pool.AddItem(fMonitoring.ComputeDetails); stats.Sum(fMonitoring); end; result := ObjectToVariantDebug(self); _ObjAddProps(['working',working, 'stats',stats.ComputeDetails, 'threadstats',variant(pool)],result); finally stats.Free; end; except on E: Exception do result := ObjectToVariantDebug(E); end; finally fProcessLock.Leave; end; end; function TDDDMonitoredDaemon.RetrieveState( out Status: variant): TCQRSResult; begin CqrsBeginMethod(qaNone,result,cqrsSuccess); Status := GetStatus; end; function TDDDMonitoredDaemon.Start: TCQRSResult; var i: integer; {$ifdef WITHLOG} Log: ISynLog; {$endif} dummy: variant; begin {$ifdef WITHLOG} Log := Rest.LogClass.Enter('Start %',[fProcessClass],self); {$endif} if fProcessClass=nil then raise EDDDException.CreateUTF8('%.Start with no fProcessClass',[self]); Stop(dummy); // ignore any error when stopping fProcessTimer.Resume; {$ifdef WITHLOG} if Log<>nil then Log.Log(sllTrace,'Start %',[self],self); {$endif} CqrsBeginMethod(qaNone,result,cqrsSuccess); SetLength(fProcess,fProcessThreadCount); for i := 0 to fProcessThreadCount-1 do fProcess[i] := fProcessClass.Create(self,i); SleepHiRes(1); // some time to actually start the threads end; function TDDDMonitoredDaemon.Stop(out Information: variant): TCQRSResult; var i: integer; allfinished: boolean; begin CqrsBeginMethod(qaNone,result); try if fProcess<>nil then begin fProcessTimer.Pause; Information := GetStatus; {$ifdef WITHLOG} Rest.LogClass.Enter('Stop % process %',[fProcessClass,Information],self); {$endif} fProcessLock.Enter; try for i := 0 to high(fProcess) do fProcess[i].Terminate; finally fProcessLock.Leave; end; repeat SleepHiRes(5); allfinished := true; fProcessLock.Enter; try for i := 0 to high(fProcess) do if fProcess[i].fMonitoring.Processing then begin allfinished := false; break; end; finally fProcessLock.Leave; end; until allfinished; fProcessLock.Enter; try ObjArrayClear(fProcess); finally fProcessLock.Leave; end; end; CqrsSetResult(cqrsSuccess,result); except on E: Exception do CqrsSetResult(E,result); end; end; { TDDDAdministratedDaemon } constructor TDDDAdministratedDaemon.Create( aAdministrationServer: TSQLRestServer); begin if aAdministrationServer=nil then raise ECQRSException.CreateUTF8('%.Create(aAdministrationServer=nil)',[self]); fAdministrationServer := aAdministrationServer; {$ifdef WITHLOG} fLog := fAdministrationServer.LogFamily; {$endif} CreateWithResolver(fAdministrationServer.ServiceContainer); fAdministrationServer.ServiceDefine(self,[IAdministratedDaemon]); fFinished := TEvent.Create(nil,false,false,''); fStatus := dsCreated; end; constructor TDDDAdministratedDaemon.Create( const aUserName, aHashedPassword, aRoot: RawUTF8; const aServerNamedPipe: TFileName); var server: TSQLRestServer; begin server := TSQLRestServerFullMemory.CreateWithOwnedAuthenticatedModel([], aUserName,aHashedPassword,aRoot); server.Options := server.Options+[rsoSecureConnectionRequired]; Create(server); if FRefCount=2 then dec(FRefCount); // circumvent a Delphi compiler bug fAdministrationServerOwned := true; if aServerNamedPipe<>'' then {$ifdef MSWINDOWS} fAdministrationServer.ExportServerNamedPipe(aServerNamedPipe); {$else} {$ifdef WITHLOG} fLog.SynLog.Log(sllTrace,'Ignored AuthNamedPipeName=% under Linux', [aServerNamedPipe],self); {$endif} {$endif} end; destructor TDDDAdministratedDaemon.Destroy; var dummy: variant; {$ifdef WITHLOG} log: ISynLog; begin log := fLog.SynLog.Enter(self, 'Destroy'); {$else} begin {$endif} if InternalIsRunning then Halt(dummy); try FreeAndNil(fAdministrationHTTPServer); inherited Destroy; fFinished.Free; FreeAndNil(fRemoteLog); finally if fAdministrationServerOwned then FreeAndNil(fAdministrationServer); end; end; function TDDDAdministratedDaemon.Start: TCQRSResult; {$ifdef WITHLOG} var log: ISynLog; begin log := fLog.SynLog.Enter(self, 'Start'); {$else} begin {$endif} CqrsBeginMethod(qaNone,result); if not (fStatus in [dsCreated,dsStopped]) then CqrsSetResultError(cqrsBadRequest) else if InternalIsRunning then CqrsSetResult(cqrsAlreadyExists,result) else try {$ifdef WITHLOG} if log<>nil then log.Log(sllDDDInfo,'Starting',self); {$endif} InternalStart; fStatus := dsStarted; CqrsSetResult(cqrsSuccess,result); except on E: Exception do try CqrsSetResult(E,result); InternalStop; // automatically release resources on starting error except end; end; end; function TDDDAdministratedDaemon.RetrieveState(out Status: variant): TCQRSResult; begin CqrsBeginMethod(qaNone,result); try if not InternalIsRunning then CqrsSetResult(cqrsBadRequest,result) else if InternalRetrieveState(Status) then CqrsSetResult(cqrsSuccess,result) else CqrsSetResult(cqrsInternalError,result); except on E: Exception do CqrsSetResult(E,result); end; end; function TDDDAdministratedDaemon.Stop(out Information: variant): TCQRSResult; {$ifdef WITHLOG} var log: ISynLog; begin log := fLog.SynLog.Enter(self, 'Stop'); {$else} begin {$endif} CqrsBeginMethod(qaNone,result); if fStatus<>dsStarted then CqrsSetResultError(cqrsBadRequest) else begin if InternalRetrieveState(Information) then try {$ifdef WITHLOG} if log<>nil then log.Log(sllDDDInfo,'Stopping %',[Information],self); {$endif} InternalStop; // always stop fStatus := dsStopped; {$ifdef WITHLOG} if log<>nil then log.Log(sllDDDInfo,'Stopped: %',[Information],self); {$endif} CqrsSetResult(cqrsSuccess,result); except on E: Exception do CqrsSetResult(E,result); end else CqrsSetResult(cqrsInternalError,result); end; end; function TDDDAdministratedDaemon.Halt(out Information: variant): TCQRSResult; {$ifdef WITHLOG} var log: ISynLog; begin log := fLog.SynLog.Enter(self, 'Halt'); {$else} begin {$endif} CqrsBeginMethod(qaNone,result); if InternalIsRunning then try {$ifdef WITHLOG} if log<>nil then log.Log(sllDDDInfo,'Halting',self); {$endif} CqrsSetResult(Stop(Information),result); except on E: Exception do CqrsSetResult(E,result); end else CqrsSetResult(cqrsSuccess,result); fStatus := dsHalted; fFinished.SetEvent; end; procedure TDDDAdministratedDaemon.WaitUntilHalted; begin if fStatus in [dsUnknown] then exit; {$ifdef WITHLOG} with fLog.SynLog.Enter(self, 'WaitUntilHalted') do {$endif} FixedWaitForever(fFinished); end; procedure TDDDAdministratedDaemon.Execute(RemotelyAdministrated: boolean); var name: string; msg: RawUTF8; {$ifdef WITHLOG} log: ISynLog; begin log := fLog.SynLog.Enter(self, 'Execute'); {$else} begin {$endif} name := ClassName; {$I-} if RemotelyAdministrated then begin writeln(name,' expecting commands'); WaitUntilHalted; end else begin if Start=cqrsSuccess then writeln(name,' is running') else begin writeln(#10,name,' failed to Start'); if _Safe(LastErrorInfo)^.GetAsRawUTF8('Exception',msg) then begin TextColor(ccLightMagenta); writeln(msg); TextColor(ccLightGray); end; end; writeln('Press [Enter] to quit'); ioresult; readln; {$ifdef LINUX} if ioresult<>0 then // e.g. when redirected from "nohup daemon &" command WaitUntilHalted; {$endif} end; writeln('Shutting down server'); ioresult; {$I+} end; procedure TDDDAdministratedDaemon.CallbackReleased( const callback: IInvokable; const interfaceName: RawUTF8); begin if IdemPropNameU(interfaceName,'ISynLogCallback') and (fRemoteLog<>nil) then fRemoteLog.Unsubscribe(ISynLogCallback(callback)); end; procedure TDDDAdministratedDaemon.SubscribeLog(const Levels: TSynLogInfos; const Callback: ISynLogCallback; ReceiveExistingKB: cardinal); {$ifdef WITHLOG} var previousContentSize: integer; begin if fRemoteLog=nil then fRemoteLog := TSynLogCallbacks.Create(fLog); previousContentSize := fRemoteLog.Subscribe(Levels,Callback,ReceiveExistingKB); fLog.SynLog.Log(sllTrace,'SubscribeLog sent % bytes as previous content', [previousContentSize],self); {$else} begin {$endif} end; function TDDDAdministratedDaemon.PublishedORM(const DatabaseName: RawUTF8): TSQLRest; var i: integer; begin for i := 0 to high(fInternalDatabases) do begin result := fInternalDatabases[i]; if IdemPropNameU(result.Model.Root,DatabaseName) then exit; end; result := nil; end; function TDDDAdministratedDaemon.DatabaseExecute(const DatabaseName,SQL: RawUTF8): TServiceCustomAnswer; var rest: TSQLRest; name,value: RawUTF8; doc: TDocVariantData; valid: Boolean; status: variant; res: TCQRSResult; cmd: integer; store: IDDDSettingsStorable; begin result.Header := JSON_CONTENT_TYPE_HEADER_VAR; result.Status := HTTP_SUCCESS; if SQL='' then exit; if SQL[1]='#' then begin cmd := IdemPCharArray(@SQL[2],['STATE','SETTINGS','VERSION','COMPUTER','LOG', 'CHAT','STARTDAEMON','STOPDAEMON','RESTARTDAEMON','HELP','INFO']); case cmd of 0: begin if InternalRetrieveState(status) then result.Content := VariantSaveJSON(status) else result.Content := '"Daemon seems stopped"'; exit; end; 1: if fInternalSettings<>nil then begin if SQL[10]=' ' then begin name := copy(SQL,11,maxInt); if PosExChar('=',name)>0 then begin Split(name,'=',name,value); if (name<>'') and (value<>'') then begin VariantLoadJSON(status,pointer(value)); doc.InitObjectFromPath(name,status); JsonToObject(fInternalSettings,pointer(doc.ToJSON),valid); end; end else if IdemPropNameU(name,'save') then begin if fInternalSettings.GetInterface(IDDDSettingsStorable,store) then begin store.StoreIfUpdated; result.Content := FormatUTF8('"% saved"',[fInternalSettings.ClassType]); end else result.Content := FormatUTF8('"% does not implement IDDDSettingsStorable"', [fInternalSettings.ClassType]); exit; end else if fInternalSettingsFolder<>'' then begin AdministrationExecuteGetFiles(fInternalSettingsFolder, '*.config;*.settings',name,result); exit; end; end; result.Content := ObjectToJSON(fInternalSettings,[woEnumSetsAsText]); exit; end; 2: result.Content := JSONEncode(['daemon',DaemonName, 'exe',ExeVersion.ProgramFileName,'version',ExeVersion.Version.Detailed, 'buildTime',DateTimeToIso8601(ExeVersion.Version.BuildDateTime,true), 'framework',SYNOPSE_FRAMEWORK_FULLVERSION,'compiler',GetDelphiCompilerVersion]); // no exit: #version handled e.g. in TSQLRestServerDB.AdministrationExecute 3: begin {$ifdef MSWINDOWS_INCLUDESENV} P := pointer(GetEnvironmentStringsA); while P^<>#0 do begin L := StrLen(P); if (L>0) and (P^<>'=') then begin if value<>'' then value := value+#10; SetString(name,PAnsiChar(P),L); value := value+name; end; inc(P,table+1); end; {$endif} result.Content := SystemInfoJson; {$ifdef MSWINDOWS} result.Content[length(result.Content)] := ','; result.Content := result.Content+'"ip":"'+GetIPAddressesText+'"}'; {$endif} exit; end; {$ifdef WITHLOG} 4: begin split(SQL,' ',name,value); if value='' then result.Content := ObjectToJSON(fLog.SynLog,[woEnumSetsAsText]) else AdministrationExecuteGetFiles(fLog.DestinationPath,'*.log;*.synlz',value,result); exit; end; 5: begin fLog.SynLog.Log(sllMonitoring,'[CHAT] % %', [ServiceContext.Request.InHeader['remoteip'],copy(SQL,7,maxInt)]); exit; end; {$endif} 6,7,8: begin // 6=start/7=stop/8=restart if cmd=6 then res := cqrsSuccess else res := Stop(status); if res=cqrsSuccess then begin if cmd=8 then SleepHiRes(200); // leave some time between stop and start if cmd<>7 then res := Start; if res=cqrsSuccess then result.Content := VariantSaveJSON(status) else result.Content := JSONEncode(['errorStart',ToText(res)^, 'stopStatus',status]); end else result.Content := JSONEncode(['errorStop',ToText(res)^]); exit; end; 9: result.Content := '"Enter either a SQL request, or one of the following commands:|'+ '|#state|#settings [full.path=value/*/save/filename]|#version|#computer|#log [*/filename]|'+ '#startdaemon|#stopdaemon|#restartdaemon|#help"'; 10: begin result.Content := JSONEncode(['daemon',DaemonName]); if (DatabaseName='') and (fInternalDatabases<>nil) then begin fInternalDatabases[0].AdministrationExecute('',SQL,result); exit; end; end; end; end; rest := PublishedORM(DatabaseName); if rest<>nil then rest.AdministrationExecute(DatabaseName,SQL,result); end; function TDDDAdministratedDaemon.DatabaseList: TRawUTF8DynArray; var i,n: integer; begin n := length(fInternalDatabases); SetLength(result,n); for i := 0 to n-1 do result[i] := fInternalDatabases[i].Model.Root; end; function TDDDAdministratedDaemon.DatabaseTables(const DatabaseName: RawUTF8): TRawUTF8DynArray; var rest: TSQLRest; i: integer; begin rest := PublishedORM(DatabaseName); if rest=nil then result := nil else with rest.Model do begin SetLength(result,TablesMax+1); for i := 0 to TablesMax do result[i] := TableProps[i].Props.SQLTableName; end; end; procedure TDDDAdministratedDaemon.PublishORMTables(const Rest: array of TSQLRest); var i,n: integer; begin SetLength(fInternalDatabases,length(Rest)); n := 0; for i := 0 to high(Rest) do if Rest[i]<>nil then begin fInternalDatabases[n] := Rest[i]; inc(n); end; SetLength(fInternalDatabases,n); end; procedure TDDDAdministratedDaemon.InternalStop; begin fInternalDatabases := nil; end; function TDDDAdministratedDaemon.InternalRetrieveState( var Status: variant): boolean; begin Status := _ObjFast(['SystemMemory',TSynMonitorMemory.ToVariant]); result := true; end; procedure TDDDAdministratedDaemon.SetInternalSettings(Settings: TObject); begin fInternalSettings := Settings; end; function TDDDAdministratedDaemon.DaemonName: RawUTF8; var L: integer; begin result := RawUTF8(ClassName); if result[1]='T' then delete(result,1,1); L := length(result); if copy(result,L-5,6)='Daemon' then SetLength(result,L-6); end; { TDDDAdministratedThreadDaemon } function TDDDAdministratedThreadDaemon.InternalIsRunning: boolean; begin result := fThread<>nil; end; procedure TDDDAdministratedThreadDaemon.InternalStop; begin inherited InternalStop; // fInternalDatabases := [] FreeAndNil(fThread); // should terminate and wait for the thread to finish end; { TDDDAdministratedRestDaemon } function TDDDAdministratedRestDaemon.InternalIsRunning: boolean; begin result := fRest<>nil; end; procedure TDDDAdministratedRestDaemon.InternalStop; begin inherited InternalStop; // fInternalDatabases := [] FreeAndNil(fRest); end; { TDDDAdministratedDaemonMonitor } function TDDDAdministratedDaemonMonitor.GetMemory: variant; begin result := TSynMonitorMemory.ToVariant; end; procedure TDDDAdministratedDaemonMonitor.ProcessException(E: Exception); begin Server.ProcessError(ObjectToVariantDebug(E)); end; initialization {$ifndef ISDELPHI2010} {$ifndef HASINTERFACERTTI} // circumvent a old FPC bug TTextWriter.RegisterCustomJSONSerializerFromTextSimpleType([ TypeInfo(TCQRSResult), TypeInfo(TCQRSQueryAction), TypeInfo(TCQRSQueryState), TypeInfo(TDDDAdministratedDaemonStatus)]); {$endif} {$endif} GetEnumNames(TypeInfo(TCQRSResult), @TCQRSResultText); TInterfaceFactory.RegisterInterfaces([ TypeInfo(IMonitored),TypeInfo(IMonitoredDaemon), TypeInfo(IAdministratedDaemon),TypeInfo(IAdministratedDaemonAsProxy)]); end.