xtool/contrib/mORMot/CrossPlatform/SynCrossPlatformREST.pas

3807 lines
129 KiB
ObjectPascal

/// minimum stand-alone cross-platform REST process for mORMot client
// - this unit is a part of the freeware Synopse mORMot framework,
// licensed under a MPL/GPL/LGPL tri-license; version 1.18
unit SynCrossPlatformREST;
{
This file is part of Synopse mORMot framework.
Synopse mORMot framework. Copyright (C) 2022 Arnaud Bouchez
Synopse Informatique - https://synopse.info
*** BEGIN LICENSE BLOCK *****
Version: MPL 1.1/GPL 2.0/LGPL 2.1
The contents of this file are subject to the Mozilla Public License Version
1.1 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL
Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the License.
The Original Code is Synopse mORMot framework.
The Initial Developer of the Original Code is Arnaud Bouchez.
Portions created by the Initial Developer are Copyright (C) 2022
the Initial Developer. All Rights Reserved.
Contributor(s):
- hanspi
- warleyalex
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 *****
Should compile with Delphi for any platform (including NextGen for mobiles),
with FPC 2.7 or Kylix, and with SmartMobileStudio 2.1.1
}
{$ifdef DWSCRIPT} // always defined since SMS 1.1.2
{$define ISDWS} // e.g. for SmartMobileStudio or Delphi Web Script
{$define ISSMS} // for SmartMobileStudio
{$else}
{$i SynCrossPlatform.inc} // define e.g. HASINLINE
{$endif}
interface
{$ifdef ISDWS}
uses
SmartCL.System,
System.Types,
ECMA.Json,
{$else}
uses
SysUtils,
Classes,
TypInfo,
{$ifdef ISDELPHI2010}
System.Generics.Collections,
{$endif}
{$ifndef NEXTGEN}
Contnrs,
{$endif}
Variants,
SynCrossPlatformJSON,
{$endif ISDWS}
SynCrossPlatformSpecific,
SynCrossPlatformCrypto;
const
/// maximum number of fields in a database Table
MAX_SQLFIELDS = 256;
/// used as "stored AS_UNIQUE" published property definition in TSQLRecord
AS_UNIQUE = false;
type
/// alias to share the same string type between client and server
RawUTF8 = string;
TSQLRest = class;
TSQLRecord = class;
TSQLModel = class;
TSQLRecordClass = class of TSQLRecord;
TSQLRecordClassDynArray = array of TSQLRecordClass;
{$ifdef ISDWS}
// UTILS functions
function window: variant; external 'window' property;
function document: variant; external 'document' property;
// URI functions
function EncodeURIComponent(str: String): String; external 'encodeURIComponent';
function DecodeURIComponent(str: String): String; external 'decodeURIComponent';
function EncodeURI(str: String): String; external 'encodeURI';
function DecodeURI(str: String): String; external 'decodeURI';
// Variant management
function VarIsValidRef(const aRef: Variant): Boolean;
type
// circumvent limited DWS / SMS syntax
TPersistent = TObject;
TObjectList = array of TObject;
// stored as binary, transmitted as Base64 (VariantToBlob=atob and BlobToVariant=btoa)
TSQLRawBlob = variant;
// TTimeLogBits.Value has a 38-bit precision, so features exact representation
// as JavaScript numbers (stored in a 52-bit mantissa)
TTimeLog = Int53;
TModTime = TTimeLog;
TCreateTime = TTimeLog;
TGUID = string;
TSQLFieldBit = enum (Low = 0, High = MAX_SQLFIELDS-1);
ERestException = class(EW3Exception);
/// handle a JSON result table, as returned by mORMot's REST server ORM
// - we define a dedicated class to by-pass SynCrossPlatformJSON unit
TSQLTableJSON = class
protected
fInternalState: cardinal;
fFieldCount, fRowCount, fCurrentRow: integer;
fFieldNames: TStrArray;
fValues: TVariantDynArray;
public
/// parse the supplied JSON content
constructor Create(const aJSON: string);
/// to be called in a loop to iterate through all data rows
// - if returned true, Object published properties will contain this row
function FillOne(Value: TSQLRecord; SeekFirst: boolean=false): boolean;
end;
{$else}
/// Exception type raised when working with REST access
ERestException = class(Exception);
/// alias to share the same blob type between client and server
TSQLRawBlob = TByteDynArray;
/// fast bit-encoded date and time value
TTimeLog = type Int64;
/// used to define a field which shall be set at each modification
TModTime = type TTimeLog;
/// used to define a field which shall be set at record creation
TCreateTime = type TTimeLog;
/// used to identify the a field in a Table as in TSQLFieldBits
TSQLFieldBit = 0..MAX_SQLFIELDS-1;
/// handle a JSON result table, as returned by mORMot's REST server ORM
// - this class is expected to work with TSQLRecord instances only
// - it will let any "RowID" JSON key match TSQLRecord.ID property
TSQLTableJSON = class(TJSONTableObject)
protected
fInternalState: cardinal;
/// allow to let "RowID" JSON key match TSQLRecord.ID
function GetPropInfo(aTypeInfo: TRTTITypeInfo; const PropName: string): TRTTIPropInfo; override;
public
/// to be called in a loop to iterate through all data rows
// - if returned true, Object published properties will contain this row
function FillOne(aValue: TSQLRecord; aSeekFirst: boolean=false): boolean;
end;
{$endif ISDWS}
/// Exception type raised when working with interface-based service process
EServiceException = class(ERestException);
/// the available logging events, as handled by our Cross-Platform units
// - defined with the same values in SynCommons.pas
// - sllInfo will log general information events
// - sllDebug will log detailed debugging information
// - sllTrace will log low-level step by step debugging information
// - sllWarning will log unexpected values (not an error)
// - sllError will log errors
// - sllEnter will log every method start
// - sllLeave will log every method exit
// - sllLastError will log the GetLastError OS message
// - sllException will log all exception raised - available since Windows XP
// - sllExceptionOS will log all OS low-level exceptions (EDivByZero,
// ERangeError, EAccessViolation...)
// - sllMemory will log memory statistics
// - sllStackTrace will log caller's stack trace (it's by default part of
// TSynLogFamily.LevelStackTrace like sllError, sllException, sllExceptionOS,
// sllLastError and sllFail)
// - sllFail was defined for TSynTestsLogged.Failed method, and can be used
// to log some customer-side assertions (may be notifications, not errors)
// - sllSQL is dedicated to trace the SQL statements
// - sllCache should be used to trace the internal caching mechanism
// - sllResult could trace the SQL results, JSON encoded
// - sllDB is dedicated to trace low-level database engine features
// - sllHTTP could be used to trace HTTP process
// - sllClient/sllServer could be used to trace some Client or Server process
// - sllServiceCall/sllServiceReturn to trace some remote service or library
// - sllUserAuth to trace user authentication (e.g. for individual requests)
// - sllCustom* items can be used for any purpose
// - sllNewRun will be written when a process opens a rotated log
// - sllDDDError will log any DDD-related low-level error information
// - sllDDDInfo will log any DDD-related low-level debugging information
TSynLogInfo = (
sllNone, sllInfo, sllDebug, sllTrace, sllWarning, sllError,
sllEnter, sllLeave,
sllLastError, sllException, sllExceptionOS, sllMemory, sllStackTrace,
sllFail, sllSQL, sllCache, sllResult, sllDB, sllHTTP, sllClient, sllServer,
sllServiceCall, sllServiceReturn, sllUserAuth,
sllCustom1, sllCustom2, sllCustom3, sllCustom4,
sllNewRun, sllDDDError, sllDDDInfo);
/// used to define a set of logging level abilities
// - i.e. a combination of none or several logging event
// - e.g. use LOG_VERBOSE constant to log all events, or LOG_STACKTRACE
// to log all errors and exceptions
TSynLogInfos = set of TSynLogInfo;
/// callback event used to write some text to a logging system
// - could be a local file (not for SMS apps), or a remote log server
// - the Text is already in the same format than the one generated by TSynLog
TOnSQLRestLog = procedure(const Text: string) of object;
/// used to store bit set for all available fields in a Table
// - in this unit, field at index [0] indicates TSQLRecord.ID
TSQLFieldBits = set of TSQLFieldBit;
/// a published property kind
// - does not match mORMot.pas TSQLFieldType: here we recognize only types
// which may expect a special behavior in this unit
TSQLFieldKind = (
sftUnspecified, sftDateTime, sftTimeLog, sftBlob, sftModTime, sftCreateTime,
sftRecord, sftVariant);
/// a set of published property Kind
TSQLFieldKinds = set of TSQLFieldKind;
{ Should TID be a string since number is limited to 53-bit in JavaScript?
-> or define and use an explicit Int52 type for SMS? }
/// the TSQLRecord primary key is a 64 bit integer
TID = {$ifndef ISDWS}type{$endif} Int64;
/// a dynamic array of TSQLRecord primary keys
// - used e.g. for BATCH process
TIDDynArray = array of TID;
/// store information of one TSQLRecord published property
TSQLModelInfoPropInfo = class
public
/// the name of the published property
// - e.g. 'FirstName'
Name: string;
/// the property field type
Kind: TSQLFieldKind;
{$ifdef ISDWS}
/// index of the published property in the associated Prop[]
FieldIndex: TSQLFieldBit;
{$else}
/// the property type name, as retrieved from RTTI
TypeName: string;
/// RTTI information about the published property
RTTI: TRTTIPropInfo;
/// initialize the instance
constructor CreateFrom(aRTTI: TRTTIPropInfo);
{$endif}
end;
/// store information of all TSQLRecord published properties
TSQLModelInfoPropInfoDynArray = array of TSQLModelInfoPropInfo;
/// store information of one TSQLRecord class
TSQLModelInfo = class
public
/// the TSQLRecord class type itself
Table: TSQLRecordClass;
/// the short name of the class
// - i.e. 'People' for TSQLRecordPeople
Name: string;
/// information about every published property
// - first is always the ID field
Prop: TSQLModelInfoPropInfoDynArray;
/// specifies the "simple" fields, i.e. all non BLOB fields
SimpleFields: TSQLFieldBits;
/// specifies the BLOB fields
BlobFields: TSQLFieldBits;
/// specifies all fields, including simple and BLOB fields
AllFields: TSQLFieldBits;
/// specifies the TModTime fields
ModTimeFields: TSQLFieldBits;
/// specifies the TCreateTime fields
CreateTimeFields: TSQLFieldBits;
/// specifies the TModTime and TCreateTime fields
ModAndCreateTimeFields: TSQLFieldBits;
/// specifies the Record fields
RecordFields: TSQLFieldBits;
/// specifies the Variant fields
VariantFields: TSQLFieldBits;
/// contains all published properties kind
HasKind: TSQLFieldKinds;
/// TRUE if has TModTime or TCreateTime fields
HasTimeFields: boolean;
{$ifdef ISSMS}
/// allow fast by-name access to Prop[]
PropCache: variant;
{$else}
/// finalize the memory used
destructor Destroy; override;
{$endif}
/// initialize the class member for the supplied TSQLRecord
constructor CreateFromRTTI(aTable: TSQLRecordClass);
/// FieldNames='' to retrieve simple fields, '*' all fields, or as specified
function FieldNamesToFieldBits(const FieldNames: string;
IncludeModTimeFields: boolean): TSQLFieldBits;
/// return the corresponding field names
function FieldBitsToFieldNames(const FieldBits: TSQLFieldBits): string;
/// set TModTime and TCreateFields
procedure ComputeFieldsBeforeWrite(aClient: TSQLRest;
Value: TSQLRecord; AndCreate: Boolean);
/// compute the 'SELECT ... FROM ...' corresponding to the supplied fields
function SQLSelect(const FieldNames: string): string;
/// save the specified record as JSON for record adding
function ToJSONAdd(Client: TSQLRest; Value: TSQLRecord; ForceID: boolean;
const FieldNames: string): string;
/// save the specified record as JSON for record update
function ToJSONUpdate(Client: TSQLRest; Value: TSQLRecord;
const FieldNames: string; ForceID: boolean): string;
/// save the specified record as JSON
function ToJSON(Value: TSQLRecord; const Fields: TSQLFieldBits): string; overload;
end;
/// store information of several TSQLRecord class
TSQLModelInfoDynArray = array of TSQLModelInfo;
/// store the database model
TSQLModel = class
protected
fRoot: string;
fInfo: TSQLModelInfoDynArray;
public
/// initialize the Database Model
// - set the Tables to be associated with this Model, as TSQLRecord classes
// - set the optional Root URI path of this Model - default is 'root'
constructor Create(const Tables: array of TSQLRecordClass;
const aRoot: string {$ifndef ISDWS}='root'{$endif});
/// register a new Table class to this Model
procedure Add(Table: TSQLRecordClass);
{$ifndef ISSMS}
/// finalize the memory used
destructor Destroy; override;
{$endif}
/// get index of aTable in Tables[], returns -1 if not found
function GetTableIndex(aTable: TSQLRecordClass): integer; overload;
/// get index of aTable in Tables[], returns -1 if not found
function GetTableIndex(const aTableName: string): integer; overload;
/// get index of aTable in Tables[], raise an ERestException if not found
function GetTableIndexExisting(aTable: TSQLRecordClass): integer;
/// get the RTTI information for the specified class or raise an ERestException
function InfoExisting(aTable: TSQLRecordClass): TSQLModelInfo;
/// the RTTI information for each class
property Info: TSQLModelInfoDynArray read fInfo;
/// the Root URI path of this Database Model
property Root: string read fRoot;
end;
{$ifdef ISSMS}
/// low-level structure used for server-side generated pseudo RTTI
TRTTIPropInfos = class
public
Props: TSQLModelInfoPropInfoDynArray;
PropCache: variant;
/// define the published properties
// - optional PropKinds[] can override default sftUnspecified type
constructor Create(const PropNames: array of string;
const PropKinds: array of TSQLFieldKind);
end;
{$endif}
/// abstract ORM class to access remote tables
// - in comparison to mORMot.pas TSQLRecord published fields, dynamic arrays
// shall be defined as variant (since SynCrossPlatformJSON do not serialize)
// - inherit from TPersistent to have RTTI for its published properties
// (SmartMobileStudio does not allow {$M+} in the source)
TSQLRecord = class(TPersistent)
protected
fID: TID;
fInternalState: cardinal;
fFill: TSQLTableJSON;
{$ifdef ISSMS}
class function GetRTTI: TRTTIPropInfos;
/// you should override these methods
class function ComputeRTTI: TRTTIPropInfos; virtual;
procedure SetProperty(FieldIndex: integer; const Value: variant); virtual;
function GetProperty(FieldIndex: integer): variant; virtual;
{$endif}
public
/// this constructor initializes the record
constructor Create; overload; virtual;
/// this constructor loads a record from a REST instance from its ID
constructor Create(aClient: TSQLRest; aID: TID;
ForUpdate: boolean=false); overload;
/// this constructor loads a record from a REST instance
// - you can bind parameters by using ? in the SQLWhere clause
// - use DateTimeToSQL() for date/time database fields
// - FieldNames='' retrieve simple fields, '*' all fields, or as specified
constructor Create(aClient: TSQLRest; const FieldNames, SQLWhere: string;
const BoundsSQLWhere: array of const); overload;
/// this constructor ask the server for a list of matching records
// - you can bind parameters by using ? in the SQLWhere clause
// - use DateTimeToSQL() for date/time database fields
// - FieldNames='' retrieve simple fields, '*' all fields, or as specified
// - then you can also loop through all rows with
// ! while Rec.FillOne do
// ! dosomethingwith(Rec);
constructor CreateAndFillPrepare(aClient: TSQLRest; const FieldNames,
SQLWhere: string; const BoundsSQLWhere: array of const);
/// this constructor will loads a record from its variant representation
// - will call internaly the FromJSON() method
constructor CreateFromVariant(const aValue: variant);
/// finalize the record memory
destructor Destroy; override;
/// fill the specified record from the supplied JSON
function FromJSON(const aJSON: string): boolean;
/// fill the specified record from its variant representation
function FromVariant(const aValue: variant): boolean;
{$ifdef ISSMS}
/// fill the specified record from Names/Values pairs
function FromNamesValues(const Names: TStrArray; const Values: TVariantDynArray;
ValuesStartIndex: integer): boolean;
{$endif}
/// fill all published properties of this object with the next available
// row of data, as returned by CreateAndFillPrepare() constructor
function FillOne: boolean;
/// go to the first data row, as returned by CreateAndFillPrepare(),
// then fill all published properties of this object
// - you can use it e.g. as:
// ! while Rec.FillOne do
// ! dosomethingwith(Rec);
// ! if Rec.FillRewind then
// ! repeat
// ! dosomeotherthingwith(Rec);
// ! until not Rec.FillOne;
function FillRewind: boolean;
/// get the object properties as JSON
// - FieldNames='' to retrieve simple fields, '*' all fields, or as specified
function ToJSON(aModel: TSQLModel; aFieldNames: string=''): string;
/// get the object properties as a TJSONVariant document
function ToVariant: variant;
/// return the class type of this TSQLRecord
function RecordClass: TSQLRecordClass;
{$ifdef HASINLINE}inline;{$endif}
/// contains the TSQLTableJSON instance after CreateAndFillPrepare()
property FillTable: TSQLTableJSON read fFill;
/// internal state counter of the mORMot server at last access time
// - can be used to check if retrieved data may be out of date
property InternalState: cardinal read fInternalState;
published
/// stores the record's primary key
property ID: TID read fID write fID;
end;
/// table containing the available user access rights for authentication
// - is added here since should be part of the model
// - no wrapper is available to handle AccessRights, since for security
// reasons it is not available remotely from client side
TSQLAuthGroup = class(TSQLRecord)
protected
fIdent: string;
fAccessRights: string;
fSessionTimeOut: integer;
{$ifdef ISSMS}
class function ComputeRTTI: TRTTIPropInfos; override;
procedure SetProperty(FieldIndex: integer; const Value: variant); override;
function GetProperty(FieldIndex: integer): variant; override;
{$endif}
published
/// the access right identifier, ready to be displayed
// - the same identifier can be used only once (this column is marked as
// unique via a "stored AS_UNIQUE" (i.e. "stored false") attribute)
property Ident: string read fIdent write fIdent
{$ifndef ISDWS}stored AS_UNIQUE{$endif};
/// the number of minutes a session is kept alive
property SessionTimeout: integer read fSessionTimeOut write fSessionTimeOut;
/// a textual representation of a TSQLAccessRights buffer
property AccessRights: string read fAccessRights write fAccessRights;
end;
/// class of the table containing the available user access rights for authentication
TSQLAuthGroupClass = class of TSQLAuthGroup;
/// table containing the Users registered for authentication
TSQLAuthUser = class(TSQLRecord)
protected
fLogonName: string;
fPasswordHashHexa: string;
fDisplayName: string;
fData: TSQLRawBlob;
fGroup: TID;
{$ifdef ISSMS}
class function ComputeRTTI: TRTTIPropInfos; override;
procedure SetProperty(FieldIndex: integer; const Value: variant); override;
function GetProperty(FieldIndex: integer): variant; override;
{$endif}
procedure SetPasswordPlain(const Value: string);
public
/// able to set the PasswordHashHexa field from a plain password content
// - in fact, PasswordHashHexa := SHA256('salt'+PasswordPlain) in UTF-8
property PasswordPlain: string write SetPasswordPlain;
published
/// the User identification Name, as entered at log-in
// - the same identifier can be used only once (this column is marked as
// unique via a "stored AS_UNIQUE" - i.e. "stored false" - attribute), and
// therefore indexed in the database (e.g. hashed in TSQLRestStorageInMemory)
property LogonName: string read fLogonName write fLogonName
{$ifndef ISDWS}stored AS_UNIQUE{$endif};
/// the User Name, as may be displayed or printed
property DisplayName: string read fDisplayName write fDisplayName;
/// the hexa encoded associated SHA-256 hash of the password
property PasswordHashHexa: string read fPasswordHashHexa write fPasswordHashHexa;
/// the associated access rights of this user in TSQLAuthGroup
// - access rights are managed by group
// - note that 'Group' field name is not allowed by SQLite
property GroupRights: TID read fGroup write fGroup;
/// some custom data, associated to the User
// - Server application may store here custom data
// - its content is not used by the framework but 'may' be used by your
// application
property Data: TSQLRawBlob read fData write fData;
end;
TSQLRestServerAuthentication = class;
/// class used for client authentication
TSQLRestServerAuthenticationClass = class of TSQLRestServerAuthentication;
/// the possible Server-side instance implementation patterns for
// interface-based services
// - each interface-based service will be implemented by a corresponding
// class instance on the server: this parameter is used to define how
// class instances are created and managed
// - on the Client-side, each instance will be handled depending on the
// server side implementation (i.e. with sicClientDriven behavior if necessary)
TServiceInstanceImplementation = (
sicSingle, sicShared, sicClientDriven, sicPerSession, sicPerUser, sicPerGroup,
sicPerThread);
TSQLRestClientURI = class;
/// abstract ancestor to all client-side interface-based services
// - any overriden class will in fact call the server to execute its methods
// - inherited classes are in fact the main entry point for all interface-based
// services, without any interface use:
// ! aCalculator := TServiceCalculator.Create(aClient);
// ! try
// ! aIntegerResult := aCalculator.Add(10,20);
// ! finally
// ! aCalculator.Free;
// ! end;
// - under SmartMobileStudio, calling Free is mandatory only for
// sicClientDriven mode (to release the server-side associated session),
// so e.g. for a sicShared instance, you can safely write:
// ! aIntegerResult := TServiceCalculator.Create(aClient).Add(10,20);
// - as you already noted, server-side interface-based services are in fact
// consummed without any interface in this cross-platform unit!
TServiceClientAbstract = class{$ifndef ISDWS}(TInterfacedObject){$endif}
protected
fClient: TSQLRestClientURI;
fServiceName: string;
fServiceURI: string;
fInstanceImplementation: TServiceInstanceImplementation;
fContractExpected: string;
function GetClient: TSQLRestClientURI;
function GetContractExpected: string;
function GetInstanceImplementation: TServiceInstanceImplementation;
function GetRunningInstance: TServiceClientAbstract;
function GetServiceName: string;
function GetServiceURI: string;
public
/// initialize the fake instance
// - this method will synchronously (i.e. blocking) check the server
// contract according to the one expected by the client
// - overriden constructors will set the parameters expected by the server
constructor Create(aClient: TSQLRestClientURI); virtual;
/// the associated TSQLRestClientURI instance
property Client: TSQLRestClientURI read GetClient;
/// the unmangdled remote service name
property ServiceName: string read GetServiceName;
/// the URI to access to the remote service
property ServiceURI: string read GetServiceURI;
/// how this instance lifetime is expected to be handled
property InstanceImplementation: TServiceInstanceImplementation read GetInstanceImplementation;
/// the published service contract, as expected by both client and server
property ContractExpected: string read GetContractExpected;
end;
{$ifndef ISDWS}
/// all generated client interfaces will inherit from this abstract parent
IServiceAbstract = interface
['{06F02DCC-0DD1-4961-A5F4-C11AE375F03B}']
function GetClient: TSQLRestClientURI;
function GetContractExpected: string;
function GetInstanceImplementation: TServiceInstanceImplementation;
function GetRunningInstance: TServiceClientAbstract;
function GetServiceName: string;
function GetServiceURI: string;
/// the associated TSQLRestClientURI instance
property Client: TSQLRestClientURI read GetClient;
/// the unmangdled remote service name
property ServiceName: string read GetServiceName;
/// the URI to access to the remote service
property ServiceURI: string read GetServiceURI;
/// how this instance lifetime is expected to be handled
property InstanceImplementation: TServiceInstanceImplementation read GetInstanceImplementation;
/// the published service contract, as expected by both client and server
property ContractExpected: string read GetContractExpected;
/// the client class instance currently implementing this interface
property RunningInstance: TServiceClientAbstract read GetRunningInstance;
end;
{$endif}
/// abstract ancestor to all sicClientDriven interface-based services
// - since server-side life-time is driven by the client, this kind of class
// expects an explicit call to aService.Free (even on SmartMobileStudio)
TServiceClientAbstractClientDriven = class(TServiceClientAbstract)
protected
fClientID: string;
public
/// initialize the fake instance and create the remote per-client session
// - raise an EServiceException if a per-client session was already started
// for the specified TSQLRestClientURI
// - overriden constructors will set the parameters expected by the server
constructor Create(aClient: TSQLRestClientURI); override;
/// this overriden method (called at aService.Free) will notify the server
destructor Destroy; override;
/// the currently running instance ID on the server side
// - only one instance is allowed per TSQLRestClientURI process
property ClientID: string read fClientID;
end;
/// class type used to identify an interface-based service
// - we do not rely on interfaces here, but simply on abstract classes
TServiceClientAbstractClass = class of TServiceClientAbstract;
/// class used to determine the protocol of interface-based services
// - see TSQLRestRoutingREST and TSQLRestRoutingJSON_RPC
// for overridden methods - NEVER set this abstract TSQLRestRoutingAbstract
// class on TSQLRest.ServicesRouting property !
TSQLRestRoutingAbstract = class
public
/// at Client Side, compute URI and BODY according to the routing scheme
// - abstract implementation which is to be overridden
// - as input, "method" should be the method name to be executed for "uri",
// "params" should contain the incoming parameters as JSON array (with []),
// and "clientDriven" ID should contain the optional Client ID value
// - at output, should update the HTTP "uri" corresponding to the proper
// routing, and should return the corresponding HTTP body within "sent"
class procedure ClientSideInvoke(var uri: string;
const method, params, clientDrivenID: string; var sent: string); virtual; abstract;
end;
/// used to define the protocol of interface-based services
TSQLRestRoutingAbstractClass = class of TSQLRestRoutingAbstract;
/// default simple REST protocol for interface-based services
// - this is the default protocol used by TSQLRest
TSQLRestRoutingREST = class(TSQLRestRoutingAbstract)
public
/// at Client Side, compute URI and BODY according to RESTful routing scheme
// - e.g. on input uri='root/Calculator', method='Add', params='[1,2]' and
// clientDrivenID='1234' -> on output uri='root/Calculator.Add/1234' and
// sent='[1,2]'
class procedure ClientSideInvoke(var uri: string;
const method, params, clientDrivenID: string; var sent: string); override;
end;
/// JSON/RPC protocol for interface-based services
// - alternative to the TSQLRestRoutingREST default protocol set by TSQLRest
TSQLRestRoutingJSON_RPC = class(TSQLRestRoutingAbstract)
public
/// at Client Side, compute URI and BODY according to JSON/RPC routing scheme
// - e.g. on input uri='root/Calculator', method='Add', params='[1,2]' and
// clientDrivenID='1234' -> on output uri='root/Calculator' and
// sent={"method":"Add","params":[1,2],"id":1234}
class procedure ClientSideInvoke(var uri: string;
const method, params, clientDrivenID: string; var sent: string); override;
end;
/// the available options for TSQLRest.BatchStart() process
// - boInsertOrIgnore will create 'INSERT OR IGNORE' statements instead of
// plain 'INSERT' - by now, only direct SQLite3 engine supports it on server
TSQLRestBatchOption = (
boInsertOrIgnore);
/// a set of options for TSQLRest.BatchStart() process
TSQLRestBatchOptions = set of TSQLRestBatchOption;
{$ifdef ISSMS}
/// callback used e.g. by TSQLRestClientURI.Connect() overloaded method
TSQLRestEvent = procedure(Client: TSQLRestClientURI);
/// callback which should return TRUE on process success, or FALSE on error
TSQLRestEventProcess = function: boolean;
{$else}
TSQLRestLogClientThread = class;
{$endif ISSMS}
/// abstract REST access class
TSQLRest = class
protected
fModel: TSQLModel;
fServerTimeStampOffset: TDateTime;
fBatch: string;
fBatchTable: TSQLRecordClass;
fBatchCount: integer;
fServicesRouting: TSQLRestRoutingAbstractClass;
fInternalState: cardinal;
fOwnModel: boolean;
fLogLevel: TSynLogInfos;
fOnLog: TOnSQLRestLog;
{$ifdef ISSMS}
fLogClient: TSQLRestClientURI;
procedure LogToRemoteServerText(const Text: string);
{$else}
fLogClient: TSQLRestLogClientThread;
fLogFileBuffer: array of byte;
fLogFile: system.text;
procedure LogToFileText(const Text: string);
{$endif}
procedure LogClose;
function GetServerTimeStamp: TTimeLog;
function SetServerTimeStamp(const ServerResponse: string): boolean;
function InternalBatch(Table: TSQLRecordClass; const CMD: string; var Info: TSQLModelInfo): Integer;
function ExecuteAdd(tableIndex: integer; const json: string): TID; virtual; abstract;
function ExecuteUpdate(tableIndex: integer; ID: TID; const json: string): boolean; virtual; abstract;
function ExecuteBatchSend(Table: TSQLRecordClass; const Data: string;
var Results: TIDDynArray): integer; virtual; abstract;
public
/// initialize the class, and associate it to a specified database Model
// - if aOwnModel is TRUE, this class destructor will free aModel instance
constructor Create(aModel: TSQLModel; aOwnModel: boolean=false); virtual;
/// will release the associated Model, if aOwnModel was TRUE at Create()
destructor Destroy; override;
/// get a member from its ID
// - return true on success, and fill all simple fields
function Retrieve(aID: TID; Value: TSQLRecord;
ForUpdate: boolean=false): boolean; overload; virtual; abstract;
/// get a member from a where clause
// - you can bind parameters by using ? in the SQLWhere clause
// - use DateTimeToSQL() for date/time database fields
// - FieldNames='' retrieve simple fields, '*' all fields, or as specified
function Retrieve(const FieldNames, SQLWhere: string;
const BoundsSQLWhere: array of const; Value: TSQLRecord): boolean; overload;
{$ifndef ISSMS}
/// get a blob field content from its record ID and supplied blob field name
// - returns true on success, and the blob binary data
function RetrieveBlob(Table: TSQLRecordClass; aID: TID;
const BlobFieldName: string; out BlobData: TSQLRawBlob): boolean; virtual; abstract;
{$endif}
/// execute directly a SQL statement, expecting a list of results
// - return a result table on success, nil on failure
// - you can bind parameters by using ? in the SQLWhere clause
// - use DateTimeToSQL() for date/time database fields
// - FieldNames='' retrieve simple fields, '*' all fields, or as specified
function MultiFieldValues(Table: TSQLRecordClass; const FieldNames,
SQLWhere: string; const BoundsSQLWhere: array of const;
LimitFirstRow: Boolean=false): TSQLTableJSON; overload;
/// execute directly a SQL statement, expecting a list of results
// - return a result table on success, nil on failure
// - FieldNames='' retrieve simple fields, '*' all fields, or as specified
function MultiFieldValues(Table: TSQLRecordClass; const FieldNames,
SQLWhere: string): TSQLTableJSON; overload;
/// execute directly a SQL statement, returning a list of TSQLRecord
// - you can bind parameters by using ? in the SQLWhere clause
// - use DateTimeToSQL() for date/time database fields
// - FieldNames='' retrieve simple fields, '*' all fields, or as specified
function RetrieveList(Table: TSQLRecordClass; const FieldNames,
SQLWhere: string; const BoundsSQLWhere: array of const): TObjectList; overload;
/// execute directly a SQL statement, returning a list of data rows or nil
function ExecuteList(const SQL: string): TSQLTableJSON; virtual; abstract;
{$ifdef ISDELPHI2010} // Delphi 2009 generics support is buggy :(
/// execute directly a SQL statement, returning a generic list of TSQLRecord
// - you can bind parameters by using ? in the SQLWhere clause
// - use DateTimeToSQL() for date/time database fields
// - FieldNames='' retrieve simple fields, '*' all fields, or as specified
function RetrieveList<T: TSQLRecord>(const FieldNames, SQLWhere: string; const BoundsSQLWhere: array of const): TObjectList<T>; overload;
{$endif}
/// create a new member, returning the newly created ID, or 0 on error
// - if SendData is true, content of Value is sent to the server as JSON
// - if ForceID is true, client sends the Value.ID field to use this ID for
// adding the record (instead of a database-generated ID)
// - by default, only simple fields are pushed to the server, but you may
// specify a CSV list of field values to be transmitted - including blobs,
// which will be sent as base-64 encoded JSON
function Add(Value: TSQLRecord; SendData: boolean; ForceID: boolean=false;
FieldNames: string=''): TID; virtual;
/// delete a member
function Delete(Table: TSQLRecordClass; ID: TID): boolean; virtual; abstract;
/// update a member
// - you can let default FieldNames='' to update simple fields, '*' to
// update all fields (including BLOBs), or specify a CSV list of updated fields
function Update(Value: TSQLRecord; FieldNames: string=''): boolean; virtual;
/// begin a BATCH sequence to speed up huge database change
// - then call BatchAdd(), BatchUpdate() or BatchDelete() methods with the
// proper class or instance of the
// - at BatchSend call, all the sequence transactions will be sent at once
// - at BatchAbort call, all operations will be aborted
// - expect one TSQLRecordClass as parameter, which will be used for the whole
// sequence (in this case, you can't mix classes in the same BATCH sequence)
// - if no TSQLRecordClass is supplied, the BATCH sequence will allow any
// kind of individual record in BatchAdd/BatchUpdate/BatchDelete
// - return TRUE on success, FALSE if aTable is incorrect or a previous BATCH
// sequence was already initiated
// - this method includes a AutomaticTransactionPerRow parameter, which will
// let all BATCH process be executed on the server side within an unique
// transaction grouped by the given number of rows
function BatchStart(aTable: TSQLRecordClass;
AutomaticTransactionPerRow: cardinal=10000;
BatchOptions: TSQLRestBatchOptions=[]): boolean; virtual;
/// create a new member in current BATCH sequence
// - similar to Add(), but in BATCH mode: nothing is sent until BatchSend()
// - returns the corresponding index in the current BATCH sequence, -1 on error
// - you can set FieldNames='' to sent simple fields, '*' to add all fields
// (including BLOBs), or specify a CSV list of added fields
// - this method will always compute and send TCreateTime/TModTime fields
function BatchAdd(Value: TSQLRecord; SendData: boolean; ForceID: boolean=false;
FieldNames: string=''): integer;
/// update a member in current BATCH sequence
// - similar to Update(), but in BATCH mode: nothing is sent until BatchSend()
// - returns the corresponding index in the current BATCH sequence, -1 on error
// - you can set FieldNames='' to sent simple fields, '*' to add all fields
// (including BLOBs), or specify a CSV list of added fields
// - this method will always compute and send any TModTime fields
function BatchUpdate(Value: TSQLRecord; FieldNames: string=''): integer;
/// delete a member in current BATCH sequence
// - similar to Delete(), but in BATCH mode: nothing is sent until BatchSend()
// - returns the corresponding index in the current BATCH sequence, -1 on error
// - deleted record class is the TSQLRecordClass used at BatchStart()
// call: it will fail if no class was specified for this BATCH sequence
function BatchDelete(ID: TID): integer; overload;
/// delete a member in current BATCH sequence
// - similar to Delete(), but in BATCH mode: nothing is sent until BatchSend()
// - returns the corresponding index in the current BATCH sequence, -1 on error
// - with this overloaded method, the deleted record class is specified:
// no class shall have been set at BatchStart() call, or should be the same
function BatchDelete(Table: TSQLRecordClass; ID: TID): integer; overload;
/// delete a member in current BATCH sequence
// - similar to Delete(), but in BATCH mode: nothing is sent until BatchSend()
// - returns the corresponding index in the current BATCH sequence, -1 on error
function BatchDelete(Value: TSQLRecord): integer; overload;
/// retrieve the current number of pending transactions in the BATCH sequence
// - every call to BatchAdd/Update/Delete methods increases this count
function BatchCount: integer;
/// execute a BATCH sequence started by BatchStart() method
// - send all pending BatchAdd/Update/Delete statements to the remote server
// - will return the URI Status value, i.e. 200/HTTP_SUCCESS OK on success
// - a dynamic array of 64 bit integers will be created in Results,
// containing all ROWDID created for each BatchAdd call, or 200
// (=HTTP_SUCCESS) for all successfull BatchUpdate/BatchDelete, or 0 on error
// - any error during server-side process MUST be checked against Results[]
// (the main URI Status is 200 if about communication success, and won't
// imply that all statements in the BATCH sequence were successfull
function BatchSend(var Results: TIDDynArray): integer;
/// abort a BATCH sequence started by BatchStart() method
// - in short, nothing is sent to the remote server, and sequence is voided
procedure BatchAbort;
/// call this method to add some information to the log at a specified level
// - the supplied log level will be checked against TSQLRest.LogLevel
// - if Instance is set, it will log the corresponding class name and address
// - will compute the text line in the very same format as TSynLog class
// - use LogToFile() or LogToRemoteServer() to set the OnLog callback
procedure Log(Level: TSynLogInfo; const Text: string; Instance: TObject=nil); overload;
/// call this method to add some information to the log at a specified level
// - overloaded method which will call Format() to render the text
// - here the Fmt layout is e.g. '%s %d %g', as standard Format(), and not
// the same as with SynCommons' FormatUTF8()
// - the supplied log level will be checked against TSQLRest.LogLevel
// - if Instance is set, it will log the corresponding class name and address
// - use LogToFile() or LogToRemoteServer() to set the OnLog callback
procedure Log(Level: TSynLogInfo; const Fmt: string; const Args: array of const;
Instance: TObject=nil); overload;
/// call this method to add some information to the log at a specified level
// - overloaded method which will log the corresponding class name and address
// - the supplied log level will be checked against TSQLRest.LogLevel
// - use LogToFile() or LogToRemoteServer() to set the OnLog callback
procedure Log(Level: TSynLogInfo; Instance: TObject); overload;
/// call this method to add some information to the log for an Exception
// - will log the Exception class name and message, if sllExecption is set
procedure Log(E: Exception); overload;
{$ifdef ISSMS}
/// start the logging process into a remote log server
// - the server could be for instance a LogView tool running in server mode
procedure LogToRemoteServer(LogLevel: TSynLogInfos;
const aServer: string; aPort: integer=8091; aRoot: string='LogService');
{$else}
/// start the logging process into a file
// - if no directory is specified, will use the current one
// - if no file name is supplied, will compute a new one with the current
// time stamp, in the specified directory
procedure LogToFile(LogLevel: TSynLogInfos;
const aFolderName: TFileName=''; const aFileName: TFileName='');
/// start the logging process into a remote log server
// - the server could be for instance a LogView tool running in server mode
procedure LogToRemoteServer(LogLevel: TSynLogInfos;
const aServer: string; aPort: integer=8091; const aRoot: string='LogService');
{$endif}
/// the associated data model
property Model: TSQLModel read fModel;
/// the set of log events which will be logged by Log() overloaded methods
// - set to [] by default, meaning that log is disabled
property LogLevel: TSynLogInfos read fLogLevel write fLogLevel;
/// the callback to be executed by Log() overloaded methods
// - if none is set, the instance won't log anything
property OnLog: TOnSQLRestLog read fOnLog write fOnLog;
/// the current Date and Time, as retrieved from the server at connection
property ServerTimeStamp: TTimeLog read GetServerTimeStamp;
/// internal state counter of the mORMot server at last access time
// - can be used to check if retrieved data may be out of date
property InternalState: cardinal read fInternalState;
/// the access protocol to be used for interface-based services
// - is set to TSQLRestRoutingREST by default
// - you can set TSQLRestRoutingJSON_RPC if the server expects this protocol
property ServicesRouting: TSQLRestRoutingAbstractClass read fServicesRouting;
end;
/// REST client access class
TSQLRestClientURI = class(TSQLRest)
protected
fAuthentication: TSQLRestServerAuthentication;
fOnlyJSONRequests: boolean;
fRunningClientDriven: TStringList;
{$ifdef ISSMS}
fAsynchCount: integer;
fAsynchPendingText: array of string;
procedure SetAsynchText(const Text: string);
procedure CallAsynchText;
/// connect to the REST server, and retrieve its time stamp offset
// - under SMS, you SHOULD use this asynchronous method, which won't block
// the browser, e.g. if the network is offline
procedure SetAsynch(var Call: TSQLRestURIParams; onSuccess, onError: TSQLRestEvent;
onBeforeSuccess: TSQLRestEventProcess);
{$endif}
function getURI(aTable: TSQLRecordClass): string;
function getURIID(aTableExistingIndex: integer; aID: TID): string;
function getURICallBack(const aMethodName: string; aTable: TSQLRecordClass; aID: TID): string;
function ExecuteAdd(tableIndex: integer; const json: string): TID; override;
function ExecuteUpdate(tableIndex: integer; ID: TID; const json: string): boolean; override;
function ExecuteBatchSend(Table: TSQLRecordClass; const Data: string;
var Results: TIDDynArray): integer; override;
procedure InternalURI(var Call: TSQLRestURIParams); virtual; abstract;
procedure InternalStateUpdate(const Call: TSQLRestURIParams);
procedure CallRemoteServiceInternal(var Call: TSQLRestURIParams;
aCaller: TServiceClientAbstract; const aMethod, aParams: string);
procedure InternalServiceCheck(const aMethodName: string;
const Call: TSQLRestURIParams);
public
{$ifndef ISSMS}
/// initialize the class, and associate it to a specified database Model
// - if aOwnModel is TRUE, this class destructor will free aModel instance
constructor Create(aModel: TSQLModel; aOwnModel: boolean=false); override;
{$endif}
/// will call SessionClose
destructor Destroy; override;
{$ifdef ISSMS}
/// connect to the REST server, and retrieve its time stamp offset
// - under SMS, only this asynchronous method is available, which won't
// block the browser, e.g. if the network is offline
// - code sample using two lambda functions may be:
// ! client := TSQLRestClientHTTP.Create(ServerAddress.Text,888,model,false);
// ! client.Connect(
// ! lambda
// ! if client.ServerTimeStamp=0 then
// ! ShowMessage('Impossible to retrieve server time stamp') else
// ! writeln('ServerTimeStamp='+IntToStr(client.ServerTimeStamp));
// ! if not client.SetUser(TSQLRestServerAuthenticationDefault,LogonName.Text,LogonPassWord.Text) then
// ! ShowMessage('Authentication Error');
// ! writeln('Safely connected with SessionID='+IntToStr(client.Authentication.SessionID));
// ! people := TSQLRecordPeople.Create(client,1); // blocking request
// ! assert(people.ID=1);
// ! writeln('Disconnect from server');
// ! client.Free;
// ! end,
// ! lambda
// ! ShowMessage('Impossible to connect to the server');
// ! end);
procedure Connect(onSuccess, onError: TSQLRestEvent);
{$else}
/// connect to the REST server, and retrieve its time stamp offset
// - under SMS, you should not use this blocking version, but
// the overloaded asynchronous method
function Connect: boolean;
{$endif ISSMS}
/// method calling the remote Server via a RESTful command
// - calls the InternalURI abstract method
// - this method will sign the url, if authentication is enabled
procedure URI(var Call: TSQLRestURIParams); virtual;
/// get a member from its ID using URI()
function Retrieve(aID: TID; Value: TSQLRecord;
ForUpdate: boolean=false): boolean; overload; override;
{$ifndef ISSMS}
/// get a blob field content from its record ID and supplied blob field name
// - returns true on success, and the blob binary data, as direclty
// retrieved from the server via a dedicated HTTP GET request
function RetrieveBlob(Table: TSQLRecordClass; aID: TID;
const BlobFieldName: string; out BlobData: TSQLRawBlob): boolean; override;
{$endif}
/// execute directly a SQL statement, returning a list of rows or nil
// - we expect reUrlEncodedSQL to be defined in AllowRemoteExecute on
// server side, since we will encode the SQL at URL level, so that all
// HTTP client libraires will accept this layout (e.g. Indy or AJAX)
function ExecuteList(const SQL: string): TSQLTableJSON; override;
/// delete a member
function Delete(Table: TSQLRecordClass; ID: TID): boolean; override;
/// wrapper to the protected URI method to call a method on the server
// - perform a ModelRoot/[TableName/[ID/]]MethodName RESTful GET request
// - if no Table is expected, set aTable=nil (we do not define nil as
// default parameter, since the SMS compiler is sometimes confused)
procedure CallBackGet(const aMethodName: string;
const aNameValueParameters: array of const; var Call: TSQLRestURIParams;
aTable: TSQLRecordClass; aID: TID=0);
/// decode "result":... content as returned by CallBackGet()
// - if no Table is expected, set aTable=nil (we do not define nil as
// default parameter, since the SMS compiler is sometimes confused)
function CallBackGetResult(const aMethodName: string;
const aNameValueParameters: array of const;
aTable: TSQLRecordClass; aID: TID=0): string;
/// authenticate an User to the current connected Server
// - using TSQLRestServerAuthenticationDefault or TSQLRestServerAuthenticationNone
// - will set Authentication property on success
function SetUser(aAuthenticationClass: TSQLRestServerAuthenticationClass;
const aUserName, aPassword: string; aHashedPassword: Boolean=False): boolean;
/// close the session initiated with SetUser()
// - will reset Authentication property to nil
procedure SessionClose;
{$ifdef ISSMS}
/// asynchronous execution a specified interface-based service method on the server
// - under SMS, this asynchronous method won't block the browser, e.g. if
// the network is offline
// - you should not call it, but directly TServiceClient* methods
procedure CallRemoteServiceAsynch(aCaller: TServiceClientAbstract;
const aMethodName: string; aExpectedOutputParamsCount: integer;
const aInputParams: array of variant;
onSuccess: procedure(res: array of Variant); onError: TSQLRestEvent;
aReturnsCustomAnswer: boolean=false);
/// synchronous execution a specified interface-based service method on the server
// - under SMS, this synchronous method would block the browser, e.g. if
// the network is offline, or the server is late to answer
// - but synchronous code is somewhat easier to follow than asynchronous
// - you should not call it, but directly TServiceClient* methods
function CallRemoteServiceSynch(aCaller: TServiceClientAbstract;
const aMethodName: string; aExpectedOutputParamsCount: integer;
const aInputParams: array of variant; aReturnsCustomAnswer: boolean=false): TVariantDynArray;
{$else}
/// execute a specified interface-based service method on the server
// - this blocking method would raise an EServiceException on error
// - you should not call it, but directly TServiceClient* methods
procedure CallRemoteService(aCaller: TServiceClientAbstract;
const aMethodName: string; aExpectedOutputParamsCount: integer;
const aInputParams: array of variant; out res: TVariantDynArray;
aReturnsCustomAnswer: boolean=false);
{$endif ISSMS}
/// set this property to TRUE if the server expects only APPLICATION/JSON
// - applies only for AJAX clients (i.e. SmartMobileStudio platform)
// - true will let any remote call be identified as "preflighted requests",
// so will send an OPTIONS method prior to any request: may be twice slower
// - the default is false, as in TSQLHttpServer.OnlyJSONRequests
property OnlyJSONRequests: boolean read fOnlyJSONRequests write fOnlyJSONRequests;
/// if not nil, point to the current authentication session running
property Authentication: TSQLRestServerAuthentication read fAuthentication;
end;
{$ifndef ISSMS}
TSQLRestClientHTTP = class;
/// thread used to asynchronously log to a remote client
TSQLRestLogClientThread = class(TThread)
protected
fOwner: TSQLRest;
fOnLog: TOnSQLRestLog;
fClient: TSQLRestClientHTTP;
fLock: TMutex;
fPending: string;
procedure Execute; override;
public
/// initialize the thread
constructor Create(Owner: TSQLRest;
const aServer: string; aPort: integer; const aRoot: string);
/// log one line of text
procedure LogToRemoteServerText(const Text: string);
/// finalize the thread
destructor Destroy; override;
end;
{$endif ISSMS}
/// abstract class used for client authentication
TSQLRestServerAuthentication = class
protected
fUser: TSQLAuthUser;
fSessionID: cardinal;
fSessionIDHexa8: string;
procedure SetSessionID(Value: Cardinal);
// override this method to return the session key
function ClientComputeSessionKey(Sender: TSQLRestClientURI): string;
virtual; abstract;
function ClientSessionComputeSignature(Sender: TSQLRestClientURI;
const url: string): string; virtual; abstract;
public
/// initialize client authentication instance, i.e. the User associated instance
constructor Create(const aUserName, aPassword: string;
aHashedPassword: Boolean=false);
/// finalize the instance
destructor Destroy; override;
/// read-only access to the logged user information
// - only LogonName and PasswordHashHexa are set here
property User: TSQLAuthUser read fUser;
/// contains the session ID used for the authentication
property SessionID: cardinal read fSessionID;
end;
/// mORMot secure RESTful authentication scheme
TSQLRestServerAuthenticationDefault = class(TSQLRestServerAuthentication)
protected
fSessionPrivateKey: hash32;
function ClientComputeSessionKey(Sender: TSQLRestClientURI): string; override;
function ClientSessionComputeSignature(Sender: TSQLRestClientURI;
const url: string): string; override;
end;
/// mORMot weak RESTful authentication scheme
TSQLRestServerAuthenticationNone = class(TSQLRestServerAuthentication)
protected
function ClientComputeSessionKey(Sender: TSQLRestClientURI): string; override;
function ClientSessionComputeSignature(Sender: TSQLRestClientURI;
const url: string): string; override;
end;
/// REST client via HTTP
// - note that this implementation is not thread-safe yet
TSQLRestClientHTTP = class(TSQLRestClientURI)
protected
fConnection: TAbstractHttpConnection;
fParameters: TSQLRestConnectionParams;
fKeepAlive: Integer;
fCustomHttpHeader: RawUTF8; // e.g. for SetHttpBasicAuthHeaders()
fForceTerminate: Boolean;
procedure InternalURI(var Call: TSQLRestURIParams); override;
public
/// access to a mORMot server via HTTP
constructor Create(const aServer: string; aPort: integer; aModel: TSQLModel;
aOwnModel: boolean=false; aHttps: boolean=false
{$ifndef ISSMS}; const aProxyName: string='';
const aProxyByPass: string=''; aSendTimeout: Cardinal=30000;
aReceiveTimeout: Cardinal=30000; aConnectionTimeOut: cardinal=30000{$endif});
reintroduce; virtual;
/// finalize the connection
destructor Destroy; override;
/// force the HTTP headers of any request to contain some HTTP BASIC
// authentication, without creating any remote session
// - here the password should be given as clear content
// - potential use case is to use a mORMot client through a HTTPS proxy
// - then you can use SetUser(TSQLRestServerAuthenticationDefault,...) to
// define any another "mORMot only" authentication
procedure SetHttpBasicAuthHeaders(const aUserName, aPasswordClear: RawUTF8);
/// the associated connection, if active
property Connection: TAbstractHttpConnection read fConnection;
/// the connection parameters
property Parameters: TSQLRestConnectionParams read fParameters;
{$ifndef ISSMS}
/// the keep-alive timeout, in ms (20000 by default)
property KeepAlive: Integer read fKeepAlive write fKeepAlive;
{$endif ISSMS}
end;
const
/// \uFFF1 special code to mark ISO-8601 SQLDATE in JSON
// - e.g. '"\uFFF12012-05-04"' pattern
// - Unicode special char U+FFF1 is UTF-8 encoded as EF BF B1 bytes
// - as generated by DateTimeToSQL/TimeLogToSQL functions, and expected by
// our mORMot server
// - should be used with BoundsSQLWhere parameters, e.g. with FormatBind()
{$ifdef UNICODE}
JSON_SQLDATE_MAGIC = #$fff1;
{$else}
{$ifdef ISSMS}
JSON_SQLDATE_MAGIC = #$fff1;
{$else}
JSON_SQLDATE_MAGIC = #$ef#$bf#$b1;
{$endif}
{$endif}
var
/// can be set to TSQLRest.LogLevel in order to log all available events
LOG_VERBOSE: TSynLogInfos;
/// contains the logging levels for which stack trace should be dumped
// - which are mainly exceptions or application errors
LOG_STACKTRACE: TSynLogInfos;
/// the text equivalency of each logging level, as written in the log content
// - and expected by TSynLog and our LogView tool
LOG_LEVEL_TEXT: array[TSynLogInfo] of string = (
' ', ' info ', ' debug ', ' trace ', ' warn ', ' ERROR ',
' + ', ' - ',
' OSERR ', ' EXC ', ' EXCOS ', ' mem ', ' stack ', ' fail ',
' SQL ', ' cache ', ' res ', ' DB ', ' http ', ' clnt ', ' srvr ',
' call ', ' ret ', ' auth ',
' cust1 ', ' cust2 ', ' cust3 ', ' cust4 ', ' rotat ', ' dddER ', ' dddIN ');
/// true if PropName is either 'ID' or 'RowID'
function IsRowID(const PropName: string): boolean;
{$ifndef FPC}{$ifdef HASINLINE}inline;{$endif}{$endif}
/// can be used to create a statement with inlined parameters
// - use DateTimeToSQL() for date/time database fields
function FormatBind(const SQLWhere: string;
const BoundsSQLWhere: array of const): string;
/// compute a TTimeLog value from Delphi date/time type
function DateTimeToTTimeLog(Value: TDateTime): TTimeLog;
/// convert a TTimeLog value into the Delphi date/time type
function TTimeLogToDateTime(Value: TTimeLog): TDateTime;
/// convert a TTimeLog value into an ISO-8601 encoded date/time text
function TTimeLogToIso8601(Value: TTimeLog): string;
/// returns a string with 2 digits
// - the supplied value should be in 0..99 range
function ToDigit2(value: integer): string;
/// returns a string with 4 digits
// - the supplied value should be in 0..9999 range
function ToDigit4(value: integer): string;
/// convert a date/time to a ISO-8601 string format for SQL '?' inlined parameters
// - if DT=0, returns ''
// - if DT contains only a date, returns the date encoded as '\uFFF1YYYY-MM-DD'
// - if DT contains only a time, returns the time encoded as '\uFFF1Thh:mm:ss'
// - otherwise, returns the ISO-8601 date and time encoded as '\uFFF1YYYY-MM-DDThh:mm:ss'
// - to be used e.g. as in:
// ! aRec.CreateAndFillPrepare(Client,'Datum<=?',[DateTimeToSQL(Now)]);
// - see TimeLogToSQL() if you are using TTimeLog/TModTime/TCreateTime values
function DateTimeToSQL(DT: TDateTime): string;
/// convert a TTimeLog value into a ISO-8601 string format for SQL '?' inlined
// parameters
// - follows the same pattern as DateToSQL or DateTimeToSQL functions, i.e.
// will return the date or time encoded as '\uFFF1YYYY-MM-DDThh:mm:ss'
function TimeLogToSQL(const TimeStamp: TTimeLog): string;
/// convert a base-64 encoded blob into its binary representation
function VariantToBlob(const Value: variant): TSQLRawBlob;
/// convert a binary blob into its base-64 representation
function BlobToVariant(const Blob: TSQLRawBlob): variant;
/// convert a string value into a TGUID instance
function VariantToGUID(const value: variant): TGUID;
/// convert a TGUID instance into a string value
function GUIDToVariant(const GUID: TGUID): variant;
/// convert a variant value into a THttpBody binary
// - will use a variant of type string as mean of proprietary raw binary storage:
// format is limited to HttpBodyToVariant() conversion
function VariantToHttpBody(const value: variant): THttpBody;
/// convert a THttpBody binary content into a variant value
// - will use a variant of type string as mean of proprietary raw binary storage:
// you need to use VariantToHttpBody() to get the value back from the variant
function HttpBodyToVariant(const HttpBody: THttpBody): variant;
/// convert a text or integer enumeration representation into its ordinal value
function VariantToEnum(const Value: variant; const TextValues: array of string): integer;
/// convert any TSQLRecord class instance into a TJSONVariant type
function ObjectToVariant(value: TSQLRecord): variant;
/// hash the supplied text values after UTF-8 encoding
// - as expected by the framework server
function SHA256Compute(const Values: array of string): string;
/// encode a text as defined by RFC 3986
function UrlEncode(const aValue: string): string; overload;
/// encode name=value pairs as defined by RFC 3986
function UrlEncode(const aNameValueParameters: array of const): string; overload;
/// decode a text as defined by RFC 3986
function UrlDecode(const aValue: string): string;
/// retrieve one header from a low-level HTTP response
// - use e.g. location := GetOutHeader(Call,'location');
function GetOutHeader(const Call: TSQLRestURIParams; const Name: string): string;
const
/// the first field in TSQLFieldBits is always ID/RowID
ID_SQLFIELD: TSQLFieldBit = TSQLFieldBit(0);
var
/// contains no field bit set
NO_SQLFIELDBITS: TSQLFieldBits;
implementation
{$ifdef ISDWS}
function VarIsValidRef(const aRef: Variant): Boolean;
begin
asm
@Result = !((@aRef == null) || (@aRef == undefined));
end;
end;
{$endif}
function IsRowID(const PropName: string): boolean;
begin
result := IdemPropName(PropName,'ID') or
IdemPropName(PropName,'RowID');
end;
function FormatBind(const SQLWhere: string;
const BoundsSQLWhere: array of const): string;
var tmpIsString: Boolean;
tmp: string;
i,deb,arg,maxArgs,SQLWhereLen: integer;
{$ifdef ISSMS}
args: variant; // open parameters are not a true array in JavaScript
begin
asm
@args=@BoundsSQLWhere;
end;
maxArgs := args.length-1;
{$else}
begin
maxArgs := high(BoundsSQLWhere);
{$endif}
result := '';
arg := 0;
deb := 1;
i := 1; // we need i after then main loop -> do not use for i := 1 to ...
SQLWhereLen := length(SQLWhere);
while i<=SQLWhereLen do
if SQLWhere[i]='?' then begin
result := result+copy(SQLWhere,deb,i-deb)+':(';
if arg>maxArgs then
tmp := 'null' else begin
tmp := VarRecToValue(
{$ifdef ISSMS}args{$else}BoundsSQLWhere{$endif}[arg],tmpIsString);
if tmpIsString then
DoubleQuoteStr(tmp);
inc(arg);
end;
result := result+tmp+'):';
inc(i);
deb := i;
end else
inc(i);
result := result+copy(SQLWhere,deb,i-deb);
end;
function DateTimeToTTimeLog(Value: TDateTime): TTimeLog;
var HH,MM,SS,MS,Y,M,D: word;
{$ifndef ISSMS}
V: Int64;
{$endif}
begin
DecodeTime(Value,HH,MM,SS,MS);
DecodeDate(Value,Y,M,D);
{$ifdef ISSMS} // JavaScript truncates to 32 bit binary
result := SS+MM*$40+(HH+D*$20+M*$400+Y*$4000-$420)*$1000;
{$else}
V := HH+D shl 5+M shl 10+Y shl 14-(1 shl 5+1 shl 10);
result := SS+MM shl 6+V shl 12;
{$endif}
end;
function TTimeLogToDateTime(Value: TTimeLog): TDateTime;
var Y: cardinal;
Time: TDateTime;
begin
{$ifdef ISSMS} // JavaScript truncates to 32 bit binary
Y := (Value div $4000000) and 4095;
{$else}
Y := (Value shr (6+6+5+5+4)) and 4095;
{$endif}
if (Y=0) or not TryEncodeDate(Y,1+(Value shr (6+6+5+5)) and 15,
1+(Value shr (6+6+5)) and 31{$ifdef ISSMS},DateTimeZone.UTC{$endif},result) then
result := 0;
if (Value and (1 shl (6+6+5)-1)<>0) and
TryEncodeTime((Value shr (6+6)) and 31,
(Value shr 6) and 63,Value and 63, 0, Time) then
result := result+Time;
end;
function TTimeLogToIso8601(Value: TTimeLog): string;
begin
result := DateTimeToIso8601(TTimeLogToDateTime(Value));
end;
function DateTimeToSQL(DT: TDateTime): string;
begin
result := JSON_SQLDATE_MAGIC+DateTimeToIso8601(DT);
end;
function TimeLogToSQL(const TimeStamp: TTimeLog): string;
begin
result := JSON_SQLDATE_MAGIC+TTimeLogToIso8601(TimeStamp);
end;
function ToDigit2(value: integer): string;
begin
if value<=0 then
result := '00' else
if value>99 then
result := '99' else
result := chr(48+value div 10)+chr(48+value mod 10);
end;
function ToDigit4(value: integer): string;
begin
if value<=0 then
result := '0000' else
if value>9999 then
result := '9999' else
result := ToDigit2(value div 100)+ToDigit2(value mod 100);
end;
function UrlEncode(const aValue: string): string; overload;
{$ifdef ISSMS} inline;
begin // see http://www.w3schools.com/jsref/jsref_encodeuricomponent.asp
result := encodeURIComponent(aValue);
end;
{$else}
const
HexChars: array[0..15] of string = (
'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F');
var i,c: integer;
utf8: TUTF8Buffer;
begin
result := '';
{$ifdef NEXTGEN}
utf8 := TEncoding.UTF8.GetBytes(aValue);
for i := 0 to high(utf8) do begin
{$else}
utf8 := UTF8Encode(aValue);
for i := 1 to length(utf8) do begin
{$endif}
c := ord(utf8[i]);
case c of
ord('0')..ord('9'),ord('a')..ord('z'),ord('A')..ord('Z'),
ord('_'),ord('-'),ord('.'),ord('~'):
AppendChar(result,char(c));
ord(' '): AppendChar(result,'+');
else result := result+'%'+HexChars[c shr 4]+HexChars[c and $F];
end; // see rfc3986 2.3. Unreserved Characters
end;
end;
{$endif}
function UrlEncode(const aNameValueParameters: array of const): string; overload;
var n,a: integer;
name,value: string;
{$ifdef ISSMS}
temp: variant;
{$else}
wasString: Boolean;
i: integer;
{$endif}
begin
result := '';
{$ifdef ISSMS} // open parameters are not a true array in JavaScript
asm
@temp=@aNameValueParameters;
end;
n := temp.length;
if n>1 then begin
for a := 0 to (n-1)shr 1 do begin
name := temp[a*2];
value := temp[a*2+1];
{$else}
n := high(aNameValueParameters);
if n>0 then begin
for a := 0 to n div 2 do begin
name := VarRecToValue(aNameValueParameters[a*2],wasString);
for i := 1 to length(name) do
if not (ord(name[i]) in [ord('a')..ord('z'),ord('A')..ord('Z')]) then
raise ERestException.CreateFmt(
'UrlEncode() expect alphabetic names, not "%s"',[name]);
value := VarRecToValue(aNameValueParameters[a*2+1],wasString);
{$endif}
result := result+'&'+name+'='+UrlEncode(value);
end;
end;
if result<>'' then
result[1] := '?';
end;
function HexDecode(a,b: char): integer;
var ai,bi: integer;
begin
ai := Pos(upcase(a),'0123456789ABCDEF')-1;
bi := Pos(upcase(b),'0123456789ABCDEF')-1;
if (ai<0) or (bi<0) then
result := ord('?') else
result := ai shl 4+bi;
end;
function UrlDecode(const aValue: string): string;
{$ifdef ISSMS}
begin
result := decodeURIComponent(aValue);
end;
{$else}
var i,c,n,len: integer;
utf8: TUTF8Buffer;
begin
i := 1;
len := length(aValue);
n := 0;
SetLength(utf8,len);
while i<=length(aValue) do begin
{$ifndef NEXTGEN} // TUTF8Buffer = UTF8String is [1-based]
inc(n);
{$endif}
c := ord(aValue[i]);
case c of
ord('+'):
utf8[n] := AnsiChar(' ');
ord('%'): begin
if i+2<=len then
utf8[n] := AnsiChar(HexDecode(aValue[i+1],aValue[i+2])) else
utf8[n] := AnsiChar('?');
inc(i,2);
end;
else if c>127 then
utf8[n] := AnsiChar('?') else
utf8[n] := AnsiChar(c);
end;
inc(i);
{$ifdef NEXTGEN} // TUTF8Buffer = TBytes is [0-based]
inc(n);
{$endif}
end;
SetLength(utf8,n);
{$ifdef NEXTGEN}
result := TEncoding.UTF8.GetString(utf8);
{$else}
{$ifdef UNICODE}
result := UTF8ToString(utf8);
{$else}
result := Utf8Decode(utf8);
{$endif}
{$endif}
end;
{$endif ISSMS}
{ TSQLRecord }
{$ifdef ISSMS}
constructor TRTTIPropInfos.Create(const PropNames: array of string;
const PropKinds: array of TSQLFieldKind);
var name: string;
p: integer;
prop: TSQLModelInfoPropInfo;
begin
prop := new TSQLModelInfoPropInfo;
prop.Name := 'RowID'; // first Field is RowID
Props.Add(prop);
for name in PropNames do begin
prop := new TSQLModelInfoPropInfo;
prop.Name := name;
Props.Add(prop);
end;
PropCache := new JObject;
for p := 0 to high(Props) do begin
prop := Props[p];
prop.FieldIndex := TSQLFieldBit(p);
if (p>0) and (p<=length(PropKinds)) then
prop.Kind := PropKinds[p-1] else
prop.Kind := sftUnspecified;
PropCache[uppercase(prop.Name)] := prop;
end;
end;
function Find(PropCache: variant; Name: string; var Info: TSQLModelInfoPropInfo): boolean; inline;
begin
Name := UpperCase(Name);
if Name='ID' then
Name := 'ROWID';
var nfo: TSQLModelInfoPropInfo;
asm
@nfo=@PropCache[@Name];
end;
result := VarIsValidRef(nfo);
Info := nfo;
end;
var
RTTI_Cache: variant = new JObject;
{$HINTS OFF}
class function TSQLRecord.GetRTTI: TRTTIPropInfos;
begin // use RTTI_Cache as global dictionary of all TSQLRecord's RTTI
var res = RTTI_Cache[ClassName];
if VarIsValidRef(res) then asm
@result=@res;
end else begin
result := ComputeRTTI;
RTTI_Cache[ClassName] := result;
end;
end;
{$HINTS ON}
class function TSQLRecord.ComputeRTTI: TRTTIPropInfos;
begin
result := TRTTIPropInfos.Create([],[]);
end;
procedure TSQLRecord.SetProperty(FieldIndex: integer; const Value: variant);
begin
case FieldIndex of
0: fID := Value;
end;
end;
function TSQLRecord.GetProperty(FieldIndex: integer): variant;
begin
case FieldIndex of
0: result := fID;
end;
end;
{$endif ISSMS}
constructor TSQLRecord.Create;
begin
// do nothing by now: inherited classes may set some properties
end;
constructor TSQLRecord.Create(aClient: TSQLRest; aID: TID;
ForUpdate: boolean=false);
begin
Create;
if aClient<>nil then
aClient.Retrieve(aID,self,ForUpdate);
end;
constructor TSQLRecord.Create(aClient: TSQLRest;
const FieldNames, SQLWhere: string; const BoundsSQLWhere: array of const);
begin
Create;
if aClient<>nil then
aClient.Retrieve(FieldNames,SQLWhere,BoundsSQLWhere,self);
end;
constructor TSQLRecord.CreateAndFillPrepare(aClient: TSQLRest;
const FieldNames, SQLWhere: string;
const BoundsSQLWhere: array of const);
begin
Create;
fFill := aClient.MultiFieldValues(RecordClass,FieldNames,SQLWhere,BoundsSQLWhere);
end;
destructor TSQLRecord.Destroy;
begin
fFill.Free; // may help even with SMS (marking objects as Garbage Collect)
inherited;
end;
function TSQLRecord.RecordClass: TSQLRecordClass;
begin
if self=nil then
result := nil else
result := TSQLRecordClass(ClassType);
end;
function TSQLRecord.FillOne: boolean;
begin
if (self=nil) or (fFill=nil) then
result := false else
result := fFill.FillOne(self);
end;
function TSQLRecord.FillRewind: boolean;
begin
if (self=nil) or (fFill=nil) then
result := false else
result := fFill.FillOne(self,true);
end;
{$ifdef ISSMS}
function TSQLRecord.FromNamesValues(const Names: TStrArray;
const Values: TVariantDynArray; ValuesStartIndex: integer): boolean;
var i: integer;
info: TSQLModelInfoPropInfo;
rtti: TRTTIPropInfos;
begin
result := false;
if ValuesStartIndex+length(Names)>length(Values) then
exit;
rtti := GetRTTI;
for i := 0 to high(Names) do
if Find(rtti.PropCache,Names[i],info) then
SetProperty(info.FieldIndex,Values[i+ValuesStartIndex]) else
exit;
result := true;
end;
{$endif}
function TSQLRecord.FromJSON(const aJSON: string): boolean;
var doc: TJSONVariantData;
table: TSQLTableJSON;
{$ifndef ISSMS}
i: Integer;
{$endif}
begin
if (self=nil) or (aJSON='') then
result := false else
if StartWithPropName(aJSON,'{"fieldCount":') then begin
table := TSQLTableJSON.Create(aJSON); // non expanded format
try
result := table.FillOne(self);
finally
table.Free;
end;
end else begin // expanded format
{$ifdef ISSMS}
doc := TJSONVariantData.Create(aJSON);
result := FromNamesValues(doc.Names,doc.Values,0);
{$else}
doc.Init(aJSON);
for i := 0 to doc.Count-1 do
if IsRowID(doc.Names[i]) then
doc.Names[i] := 'ID';
result := doc.ToObject(self);
{$endif}
end;
end;
constructor TSQLRecord.CreateFromVariant(const aValue: variant);
begin
Create;
FromVariant(aValue);
end;
function TSQLRecord.FromVariant(const aValue: variant): boolean;
begin
result := FromJSON(ValueToJSON(aValue));
end;
function TSQLRecord.ToJSON(aModel: TSQLModel; aFieldNames: string=''): String;
var nfo: TSQLModelInfo;
begin
if self=nil then
result := 'null' else begin
nfo := aModel.InfoExisting(RecordClass);
result := nfo.ToJSON(self,nfo.FieldNamesToFieldBits(aFieldNames,false));
end;
end;
function TSQLRecord.ToVariant: variant;
begin
if self=nil then
result := null else begin
{$ifdef ISSMS}
result := new JObject;
var rtti := GetRTTI;
for var f := 0 to high(rtti.Props) do
result[rtti.Props[f].Name] := GetProperty(f);
{$else}
result := JSONVariant(ObjectToJSON(self));
{$endif}
end;
end;
{ TSQLTableJSON }
{$ifdef ISDWS} // circumvent weird DWS / SMS syntax
constructor TSQLTableJSON.Create(const aJSON: string);
begin
var dat = JSON.Parse(aJSON);
case VariantType(dat) of
jvObject: begin
// non expanded format: {"fieldCount":2,"values":["ID","Int",1,0,2,0,3,...]
fFieldCount := dat.fieldCount;
var values := dat.values;
if VariantType(values)<>jvArray then
exit;
asm
@fValues=@values;
end;
var n = fValues.Count;
if (n<fFieldCount) or (n mod fFieldCount<>0) then
exit;
for var i := 0 to fFieldCount-1 do
fFieldNames.Add(string(fValues[i]));
fRowCount := (n div fFieldCount)-1;
end;
jvArray: begin
// expanded format: [{"ID":1,"Int":0},{"ID":2,"Int":0},{"ID":3,...]
asm
@fValues=@dat;
end;
fRowCount := fValues.Count;
end;
end;
if fRowCount>0 then
fCurrentRow := 1;
end;
function TSQLTableJSON.FillOne(Value: TSQLRecord; SeekFirst: boolean=false): boolean;
begin
result := false;
if (Value=nil) or (fRowCount=0) then
exit;
if SeekFirst then
fCurrentRow := 1 else
if fCurrentRow>fRowCount then
exit;
if fFieldNames.Count>0 then begin
// non expanded format
result := Value.FromNamesValues(fFieldNames,fValues,fCurrentRow*fFieldCount);
end else begin
// expanded format
var doc := TJSONVariantData.CreateFrom(fValues[fCurrentRow-1]);
result := Value.FromNamesValues(doc.Names,doc.Values,0);
end;
inc(fCurrentRow);
if result then
Value.fInternalState := fInternalState;
end;
{$else}
function TSQLTableJSON.FillOne(aValue: TSQLRecord; aSeekFirst: boolean): boolean;
begin
result := StepObject(aValue,aSeekFirst);
if result then
aValue.fInternalState := fInternalState;
end;
function TSQLTableJSON.GetPropInfo(aTypeInfo: TRTTITypeInfo;
const PropName: string): TRTTIPropInfo;
begin
result := inherited GetPropInfo(aTypeInfo,PropName);
if (result=nil) and IdemPropName(PropName,'RowID') then
result := inherited GetPropInfo(aTypeInfo,'ID');
end;
{ TSQLModelInfoPropInfo }
constructor TSQLModelInfoPropInfo.CreateFrom(aRTTI: TRTTIPropInfo);
begin
RTTI := aRTTI;
TypeName := RTTIPropInfoTypeName(RTTI);
case RTTI^.PropType^.Kind of
tkRecord: Kind := sftRecord;
tkVariant: Kind := sftVariant;
else
if TypeName='TByteDynArray' then
Kind := sftBlob else
if TypeName='TDateTime' then
Kind := sftDateTime else
if TypeName='TCreateTime' then
Kind := sftCreateTime else
if TypeName='TModTime' then
Kind := sftModTime;
end;
end;
{$endif ISDWS}
{ TSQLModelInfo }
procedure TSQLModelInfo.ComputeFieldsBeforeWrite(aClient: TSQLRest;
Value: TSQLRecord; AndCreate: Boolean);
var f: TSQLFieldBit;
fields: TSQLFieldBits;
TimeStamp: Int64;
begin
if (Value=nil) or not HasTimeFields then
exit;
if AndCreate then
fields := ModAndCreateTimeFields else
fields := ModTimeFields;
TimeStamp := aClient.ServerTimeStamp;
for f := 0 to length(Prop)-1 do
if f in fields then
{$ifdef ISSMS}
Value.SetProperty(ord(f),TimeStamp);
{$else}
SetInstanceProp(Value,Prop[f].RTTI,TimeStamp);
{$endif}
end;
function GetDisplayNameFromClass(C: TClass): string;
begin
if C=nil then
result := '' else begin
result := C.ClassName;
if IdemPropName(copy(result,1,4),'TSQL') then
if IdemPropName(copy(result,5,6),'Record') then
delete(result,1,10) else
delete(result,1,4) else
if result[1]='T' then
delete(result,1,1);
end;
end;
constructor TSQLModelInfo.CreateFromRTTI(aTable: TSQLRecordClass);
var f: TSQLFieldBit;
Kind: TSQLFieldKind;
{$ifdef ISDWS}
rtti: TRTTIPropInfos;
{$else}
List: TRTTIPropInfoDynArray;
Names: TStringDynArray;
{$endif}
begin
Table := aTable;
Name := GetDisplayNameFromClass(Table);
{$ifdef ISDWS}
rtti := aTable.GetRTTI;
Prop := rtti.Props;
PropCache := rtti.PropCache;
{$else}
GetPropsInfo(Table.ClassInfo,Names,List);
SetLength(Prop,length(List));
for f := 0 to high(List) do begin
Prop[f] := TSQLModelInfoPropInfo.CreateFrom(List[f]);
if f=0 then
Prop[f].Name := 'RowID' else
Prop[f].Name := Names[f];
end;
{$endif}
for f := 0 to TSQLFieldBit(high(Prop)) do begin
include(AllFields,f);
Kind := Prop[ord(f)].Kind;
include(HasKind,Kind);
if Kind=sftBlob then
Include(BlobFields,f) else
Include(SimpleFields,f);
case Kind of
sftModTime: begin
include(ModTimeFields,f);
include(ModAndCreateTimeFields,f);
HasTimeFields := true;
end;
sftCreateTime: begin
include(CreateTimeFields,f);
include(ModAndCreateTimeFields,f);
HasTimeFields := true;
end;
sftRecord:
include(RecordFields,f);
sftVariant:
include(VariantFields,f);
end;
end;
end;
{$ifndef ISSMS}
destructor TSQLModelInfo.Destroy;
var i: integer;
begin
inherited;
for i := 0 to Length(Prop)-1 do
Prop[i].Free;
end;
{$endif}
function TSQLModelInfo.FieldBitsToFieldNames(
const FieldBits: TSQLFieldBits): string;
var f: TSQLFieldBit;
begin
result := '';
for f := 0 to length(Prop)-1 do
if f in FieldBits then
result := result+Prop[ord(f)].Name+',';
if result<>'' then
SetLength(result,length(result)-1);
end;
function TSQLModelInfo.FieldNamesToFieldBits(const FieldNames: string;
IncludeModTimeFields: boolean): TSQLFieldBits;
var i: integer;
f: TSQLFieldBit;
field: string;
begin
if FieldNames='' then
result := SimpleFields else
if FieldNames='*' then
result := AllFields else begin
result := NO_SQLFIELDBITS;
i := 1;
while GetNextCSV(FieldNames,i,field,',',true) do begin
{$ifdef ISSMS}
var Info: TSQLModelInfoPropInfo;
if Find(PropCache,field,info) then
include(result,info.FieldIndex);
{$else}
if IsRowID(field) then
Include(result,ID_SQLFIELD) else
for f := 1 to length(Prop)-1 do
if IdemPropName(field,Prop[ord(f)].Name) then begin
include(result,f);
break;
end;
{$endif}
end;
{$ifdef ISSMS}
if IncludeModTimeFields and (sftModTime in HasKind) then
for f := 1 to length(Prop)-1 do
if f in ModTimeFields then
include(result,f);
{$else}
if IncludeModTimeFields then
result := result+ModTimeFields;
{$endif}
end;
end;
function TSQLModelInfo.SQLSelect(const FieldNames: string): string;
begin
result := 'select '+FieldBitsToFieldNames(FieldNamesToFieldBits(
FieldNames,false))+' from '+Name;
end;
function TSQLModelInfo.ToJSON(Value: TSQLRecord; const Fields: TSQLFieldBits): string;
var f: TSQLFieldBit;
begin
{$ifdef ISSMS}
if Value=nil then
exit('null');
var doc: variant := new JObject;
for f := 0 to length(Prop)-1 do
if f in Fields then
doc[Prop[ord(f)].Name] := Value.GetProperty(f);
result := JSON.Stringify(doc); // rely on JavaScript serialization
{$else}
result := '{';
for f := 0 to length(Prop)-1 do
if f in Fields then
result := result+'"'+Prop[ord(f)].Name+'":'+
ValueToJSON(GetInstanceProp(Value,Prop[f].RTTI))+',';
if result='{' then
result := 'null' else
result[Length(Result)] := '}';
{$endif}
end;
function TSQLModelInfo.ToJSONAdd(Client: TSQLRest;
Value: TSQLRecord; ForceID: boolean; const FieldNames: string): string;
var Fields: TSQLFieldBits;
begin
ComputeFieldsBeforeWrite(Client,Value,true);
fields := FieldNamesToFieldBits(FieldNames,true);
if not ForceID then
exclude(fields,ID_SQLFIELD);
result := ToJSON(Value,fields);
end;
function TSQLModelInfo.ToJSONUpdate(Client: TSQLRest; Value: TSQLRecord;
const FieldNames: string; ForceID: boolean): string;
var Fields: TSQLFieldBits;
begin
fields := FieldNamesToFieldBits(FieldNames,true);
if ForceID then
include(fields,ID_SQLFIELD) else
exclude(fields,ID_SQLFIELD);
ComputeFieldsBeforeWrite(Client,Value,false);
result := ToJSON(Value,fields);
end;
{ TSQLModel }
procedure TSQLModel.Add(Table: TSQLRecordClass);
var n,i: integer;
nfo: TSQLModelInfo;
begin
n := length(fInfo);
for i := 0 to n-1 do
if fInfo[i].Table=Table then
raise ERESTException.CreateFmt('%s registered twice',[Table.ClassName]);
nfo := TSQLModelInfo.CreateFromRTTI(Table);
{$ifdef ISSMS}
fInfo.Add(nfo);
{$else}
SetLength(fInfo,n+1);
fInfo[n] := nfo;
{$endif}
end;
constructor TSQLModel.Create(const Tables: array of TSQLRecordClass;
const aRoot: string);
var t: integer;
begin
{$ifdef ISSMS}
for t := 0 to high(Tables) do
fInfo.Add(TSQLModelInfo.CreateFromRTTI(Tables[t]));
{$else}
SetLength(fInfo,length(Tables));
for t := 0 to high(fInfo) do
fInfo[t] := TSQLModelInfo.CreateFromRTTI(Tables[t]);
{$endif}
if aRoot<>'' then
if aRoot[length(aRoot)]='/' then
fRoot := copy(aRoot,1,Length(aRoot)-1) else
fRoot := aRoot;
end;
function TSQLModel.GetTableIndex(aTable: TSQLRecordClass): integer;
begin
if self<>nil then
for result := 0 to High(fInfo) do
if fInfo[result].Table=aTable then
exit;
result := -1;
end;
{$ifndef ISSMS}
destructor TSQLModel.Destroy;
var i: integer;
begin
inherited;
for i := 0 to high(fInfo) do
fInfo[i].Free;
end;
{$endif}
function TSQLModel.InfoExisting(aTable: TSQLRecordClass): TSQLModelInfo;
begin
result := Info[GetTableIndexExisting(aTable)];
end;
function TSQLModel.GetTableIndex(const aTableName: string): integer;
begin
if self<>nil then
for result := 0 to High(fInfo) do
if IdemPropName(fInfo[result].Name,aTableName) then
exit;
result := -1;
end;
function TSQLModel.GetTableIndexExisting(aTable: TSQLRecordClass): integer;
begin
if self=nil then
result := -1 else
result := GetTableIndex(aTable);
if result<0 then
raise ERestException.CreateFmt('%s should be part of the Model',
[aTable.ClassName]);
end;
{ TSQLRest }
constructor TSQLRest.Create(aModel: TSQLModel; aOwnModel: boolean);
begin
inherited Create;
fModel := aModel;
fOwnModel := aOwnModel;
fServicesRouting := TSQLRestRoutingREST;
end;
destructor TSQLRest.Destroy;
begin
Log(sllInfo,'Destroy',self);
inherited;
if fOwnModel then
fModel.Free;
LogClose;
end;
function TSQLRest.MultiFieldValues(Table: TSQLRecordClass;
const FieldNames, SQLWhere: string; const BoundsSQLWhere: array of const;
LimitFirstRow: Boolean): TSQLTableJSON;
var where: string;
begin
where := FormatBind(SQLWhere,BoundsSQLWhere);
if LimitFirstRow then
where := where+' limit 1';
result := MultiFieldValues(Table,FieldNames,where);
end;
function TSQLRest.GetServerTimeStamp: TTimeLog;
begin
if fServerTimeStampOffset=0 then
result := 0 else
result := DateTimeToTTimeLog(Now+fServerTimeStampOffset);
end;
function TSQLRest.SetServerTimeStamp(const ServerResponse: string): boolean;
var TimeStamp: Int64;
begin
if not TryStrToInt64(ServerResponse,TimeStamp) then
result := false else begin
fServerTimeStampOffset := TTimeLogToDateTime(TimeStamp)-Now;
if fServerTimeStampOffset=0 then
fServerTimeStampOffset := 0.000001; // ensure <> 0 (indicates error)
result := true;
end;
end;
function TSQLRest.MultiFieldValues(Table: TSQLRecordClass;
const FieldNames, SQLWhere: string): TSQLTableJSON;
var sql: string;
begin
sql := Model.InfoExisting(Table).SQLSelect(FieldNames);
if SQLWhere<>'' then
sql := sql+' where '+SQLWhere;
result := ExecuteList(sql);
end;
function TSQLRest.Retrieve(const FieldNames,SQLWhere: string;
const BoundsSQLWhere: array of const; Value: TSQLRecord): boolean;
var table: TSQLTableJSON;
begin
table := MultiFieldValues(Value.RecordClass,FieldNames,
SQLWhere,BoundsSQLWhere,true);
if table=nil then
result := false else
try
result := table.FillOne(Value);
finally
table.Free;
end;
end;
function TSQLRest.RetrieveList(Table: TSQLRecordClass; const FieldNames,
SQLWhere: string; const BoundsSQLWhere: array of const): TObjectList;
var rows: TSQLTableJSON;
rec: TSQLRecord;
begin
{$ifndef ISSMS} // result is already created as "array of TObject"
result := TObjectList.Create;
{$endif}
rows := MultiFieldValues(Table,FieldNames,SQLWhere,BoundsSQLWhere);
if rows<>nil then
try
repeat
rec := Table.Create;
if not rows.FillOne(rec) then begin
rec.Free;
break;
end;
result.Add(rec);
until false;
finally
rows.Free;
end;
end;
{$ifdef ISDELPHI2010}
function TSQLRest.RetrieveList<T>(const FieldNames, SQLWhere: string;
const BoundsSQLWhere: array of const): TObjectList<T>;
var rows: TSQLTableJSON;
rec: TSQLRecord;
begin
result := TObjectList<T>.Create; // TObjectList<T> will free each T instance
rows := MultiFieldValues(TSQLRecordClass(T),FieldNames,SQLWhere,BoundsSQLWhere);
if rows<>nil then
try
repeat
rec := TSQLRecordClass(T).Create;
if not rows.FillOne(rec) then begin
rec.Free;
break;
end;
result.Add(rec);
until false;
finally
rows.Free;
end;
end;
{$endif}
function TSQLRest.Add(Value: TSQLRecord; SendData, ForceID: boolean;
FieldNames: string): TID;
var tableIndex: Integer;
json: string;
begin
tableIndex := Model.GetTableIndexExisting(Value.RecordClass);
if SendData then
json := Model.Info[tableIndex].ToJSONAdd(self,Value,ForceID,FieldNames);
result := ExecuteAdd(tableIndex,json);
if result>0 then
Value.fInternalState := InternalState;
end;
function TSQLRest.Update(Value: TSQLRecord; FieldNames: string): boolean;
var tableIndex: Integer;
json: string;
begin
if (Value=nil) or (Value.ID<=0) then begin
result := false;
exit;
end;
tableIndex := Model.GetTableIndexExisting(Value.RecordClass);
json := Model.Info[tableIndex].ToJSONUpdate(self,Value,FieldNames,false);
result := ExecuteUpdate(tableIndex,Value.ID,json);
if result then
Value.fInternalState := InternalState;
end;
function TSQLRest.BatchStart(aTable: TSQLRecordClass;
AutomaticTransactionPerRow: cardinal; BatchOptions: TSQLRestBatchOptions): boolean;
begin
if (fBatchCount<>0) or (fBatch<>'') or (AutomaticTransactionPerRow<=0) then begin
result := false; // already opened BATCH sequence
exit;
end;
if aTable<>nil then // sent as '{"Table":["cmd",values,...]}'
fBatch := '{"'+Model.InfoExisting(aTable).Name+'":';
fBatch := Format('%s["automaticTransactionPerRow",%d,"options",%d,',
[fBatch,AutomaticTransactionPerRow,byte(BatchOptions)]);
fBatchTable := aTable;
result := true;
end;
function TSQLRest.InternalBatch(Table: TSQLRecordClass; const CMD: string;
var Info: TSQLModelInfo): Integer;
begin
result := -1;
if (self=nil) or (Table=nil) or (fBatch='') then
exit; // invalid parameters, or not opened BATCH sequence
Info := Model.InfoExisting(Table);
if fBatchTable<>nil then
if fBatchTable<>Table then
exit else
fBatch := fBatch+CMD+'",' else
fBatch := fBatch+CMD+'@'+Info.Name+'",';
result := fBatchCount;
inc(fBatchCount);
end;
function TSQLRest.BatchAdd(Value: TSQLRecord; SendData: boolean; ForceID: boolean;
FieldNames: string): integer;
var info: TSQLModelInfo;
begin
result := InternalBatch(Value.RecordClass,'"POST',info);
if result>=0 then
if not SendData then
fBatch := fBatch+'{},' else
fBatch := fBatch+info.ToJSONAdd(self,Value,ForceID,FieldNames)+',';
end;
function TSQLRest.BatchUpdate(Value: TSQLRecord; FieldNames: string): integer;
var info: TSQLModelInfo;
begin
if (Value=nil) or (Value.ID<=0) then
result := -1 else begin
result := InternalBatch(Value.RecordClass,'"PUT',info);
if result>=0 then
fBatch := fBatch+info.ToJSONUpdate(self,Value,FieldNames,true)+',';
end;
end;
function TSQLRest.BatchDelete(Table: TSQLRecordClass; ID: TID): integer;
var info: TSQLModelInfo;
begin
if ID<=0 then
result := -1 else begin
result := InternalBatch(Table,'"DELETE',info);
if result>=0 then
fBatch := fBatch+IntToStr(ID)+',';
end;
end;
function TSQLRest.BatchDelete(ID: TID): integer;
begin
result := BatchDelete(fBatchTable,ID);
end;
function TSQLRest.BatchDelete(Value: TSQLRecord): integer;
begin
result := BatchDelete(Value.RecordClass,Value.ID);
end;
function TSQLRest.BatchCount: integer;
begin
if self=nil then
result := 0 else
result := fBatchCount;
end;
function TSQLRest.BatchSend(var Results: TIDDynArray): integer;
begin
if (self=nil) or (fBatch='') then
result := HTTP_BADREQUEST else
try
if BatchCount>0 then begin
fBatch[length(fBatch)] := ']';
if fBatchTable<>nil then
fBatch := fBatch+'}';
result := ExecuteBatchSend(fBatchTable,fBatch,Results);
end else
result := HTTP_SUCCESS; // nothing to send
finally
BatchAbort;
end;
end;
procedure TSQLRest.BatchAbort;
begin
if self=nil then
exit;
fBatchCount := 0;
fBatchTable := nil;
fBatch := '';
end;
procedure TSQLRest.Log(Level: TSynLogInfo; const Text: string; Instance: TObject);
procedure DoLog;
var line: string;
Value: TDateTime;
HH,MM,SS,MS,Y,M,D: word;
{$ifndef ISSMS}
i: integer;
{$endif}
begin
// compute the line as expected by TSynLog / LogView
Value := Now;
DecodeTime(Value,HH,MM,SS,MS);
DecodeDate(Value,Y,M,D);
line := ToDigit4(Y)+ToDigit2(M)+ToDigit2(D)+' '+ToDigit2(HH)+ToDigit2(MM)+
ToDigit2(SS)+ToDigit2(MS shr 4)+LOG_LEVEL_TEXT[Level];
if Assigned(Instance) then
line := line+Instance.ClassName+
{$ifdef ISSMS}' ';{$else}'('+IntToHex(
{$ifdef CPU64}Int64(Instance),16{$else}cardinal(Instance),8{$endif})+') ';
{$endif}
line := line+Text;
// ensure no CR/LF in the output row
{$ifdef ISSMS}
line := line.Replace(#10,' ').Replace(#13,' ');
{$else}
for i := 1 to length(line) do
if ord(line[i])<32 then
line[i] := ' ';
{$endif}
// line output
fOnLog(line);
end;
begin
if Assigned(self) and Assigned(fOnLog) and (Level in fLogLevel) then
DoLog;
end;
procedure TSQLRest.Log(Level: TSynLogInfo; const Fmt: string; const Args: array of const;
Instance: TObject);
begin
if Assigned(self) and Assigned(fOnLog) and (Level in fLogLevel) then
Log(Level,Format(Fmt,Args),Instance);
end;
procedure TSQLRest.Log(Level: TSynLogInfo; Instance: TObject);
begin
if Assigned(self) and Assigned(fOnLog) and (Level in fLogLevel) then
Log(Level,'',Instance);
end;
procedure TSQLRest.Log(E: Exception);
begin
if Assigned(self) and Assigned(fOnLog) and (sllException in fLogLevel) then begin
{$ifdef ISSMS}
var msg: string;
asm @msg = new Error().stack; end;
Log(sllException,'%s raised with message "%s" %s',[E.ClassName,E.Message,msg]);
{$else}
Log(sllException,'%s raised with message "%s"',[E.ClassName,E.Message]);
{$endif}
end;
end;
{$ifdef ISSMS}
procedure TSQLRest.LogToRemoteServer(LogLevel: TSynLogInfos;
const aServer: string; aPort: integer; aRoot: string);
var Call: TSQLRestURIParams;
userAgent: string;
begin
LogClose;
fLogClient := TSQLRestClientHTTP.Create(aServer,aPort,TSQLModel.Create([],aRoot),true);
fLogClient.CallBackGet('TimeStamp',[],Call,nil); // synchronous connection
if Call.OutStatus=HTTP_SUCCESS then begin
fLogLevel := LogLevel;
OnLog := LogToRemoteServerText;
asm @userAgent = navigator.userAgent; end;
Log(sllClient,'Remote Cross-Platform Client Connected from AJAX app '+userAgent);
end else
LogClose;
end;
procedure TSQLRest.LogToRemoteServerText(const Text: string);
begin
if fLogClient<>nil then
fLogClient.SetAsynchText(Text);
end;
{$else}
constructor TSQLRestLogClientThread.Create(Owner: TSQLRest;
const aServer: string; aPort: integer; const aRoot: string);
begin
fLock := TMutex.Create;
fOwner := Owner;
fClient := TSQLRestClientHTTP.Create(aServer,aPort,
TSQLModel.Create([],aRoot),true,false,'','',10000,10000,500);
fOwner.OnLog := {$ifdef FPC}@{$endif}LogToRemoteServerText;
inherited Create(false);
end;
destructor TSQLRestLogClientThread.Destroy;
begin
if fOwner.fLogClient=Self then begin
fOwner.fLogClient := nil;
fOwner.fOnlog := nil;
end;
fClient.fForceTerminate := true;
inherited Destroy;
fClient.Free;
fLock.Free;
end;
procedure TSQLRestLogClientThread.LogToRemoteServerText(const Text: string);
begin
if self=nil then
exit; // avoid GPF
fLock.Enter;
if fPending='' then
fPending := Text else
fPending := fPending+#13#10+Text;
fLock.Leave;
end;
procedure TSQLRestLogClientThread.Execute;
var exeName, data: string;
Call: TSQLRestURIParams;
begin
if not fClient.Connect then
exit;
fOwner.OnLog := {$ifdef FPC}@{$endif}LogToRemoteServerText;
exeName := paramstr(0);
if exeName='' then
exeName := 'non Windows platform';
fOwner.Log(sllClient,'Remote Cross-Platform Client %s Connected from %s',
[ClassName,exeName]);
while not Terminated do begin
sleep(10);
if Terminated then
break;
fLock.Enter;
data := fPending;
fPending := '';
fLock.Leave;
if data='' then
continue;
Call.Init(fClient.getURICallBack('RemoteLog',nil,0),'PUT',data);
fClient.URI(Call);
end;
end;
procedure TSQLRest.LogToRemoteServer(LogLevel: TSynLogInfos;
const aServer: string; aPort: integer; const aRoot: string);
begin
LogClose;
fLogLevel := LogLevel;
fLogClient := TSQLRestLogClientThread.Create(self,aServer,aPort,aRoot);
end;
procedure TSQLRest.LogToFile(LogLevel: TSynLogInfos; const aFolderName,aFileName: TFileName);
var FN: TFileName;
begin
LogClose;
if aFolderName<>'' then
FN := IncludeTrailingPathDelimiter(aFolderName);
if aFileName<>'' then
if ExtractFileExt(aFileName)='' then
FN := FN+aFileName+'.log' else
FN := FN+aFileName else
FN := FN+FormatDateTime('yyyymmddhhnnss',Now)+'.log';
try
AssignFile(fLogFile,FN);
SetLength(fLogFileBuffer,4096);
system.SetTextBuf(fLogFile,fLogFileBuffer[0],4096);
Rewrite(fLogFile);
Writeln(fLogFile,paramstr(0),' 0.0.0.0 (',DateTimeToIso8601(Date),')');
Writeln(fLogFile,'Host=Unknown User=Unknown CPU=Unknown OS=0.0=0.0.0 Wow64=0 Freq=1');
Writeln(fLogFile,'TSQLRest 1.18 CrossPlatform ',NowToIso8601,#13#10);
fLogLevel := LogLevel;
OnLog := {$ifdef FPC}@{$endif}LogToFileText;
except
on E: Exception do
Finalize(fLogFileBuffer);
end;
end;
procedure TSQLRest.LogToFileText(const Text: string);
begin
if fLogFileBuffer<>nil then
writeln(fLogFile,Text);
end;
{$endif ISSMS}
procedure TSQLRest.LogClose;
begin
fLogLevel := [];
fOnLog := nil;
{$ifdef ISSMS}
if fLogClient<>nil then begin
fLogClient.CallAsynchText; // send NOW any pending log
fLogClient.Free;
fLogClient := nil;
end;
{$else}
if fLogFileBuffer<>nil then
try
system.Close(fLogFile);
finally
Finalize(fLogFileBuffer);
end;
FreeAndNil(fLogClient);
{$endif}
end;
{ TSQLRestClientURI }
const
LOGLEVELDB: array[boolean] of TSynLogInfo = (sllError,sllDB);
function TSQLRestClientURI.getURI(aTable: TSQLRecordClass): string;
begin
result := Model.Root;
if (aTable<>nil) and (aTable<>TSQLRecord) then // SMS converts nil->TSQLRecord
result := result+'/'+Model.InfoExisting(aTable).Name;
end;
function TSQLRestClientURI.getURICallBack(const aMethodName: string;
aTable: TSQLRecordClass; aID: TID): string;
begin
result := getURI(aTable);
if aID>0 then
result := result+'/'+IntToStr(aID);
result := result+'/'+aMethodName;
end;
function TSQLRestClientURI.getURIID(aTableExistingIndex: integer; aID: TID): string;
begin
result := Model.Root+'/'+Model.Info[aTableExistingIndex].Name;
if aID>0 then
result := result+'/'+IntToStr(aID);
end;
function TSQLRestClientURI.ExecuteList(const SQL: string): TSQLTableJSON;
var Call: TSQLRestURIParams;
json: string;
begin
result := nil;
if self=nil then
exit;
Log(sllSQL,SQL);
// strict HTTP does not allow any body content -> encode SQL at URL
// so we expect reUrlEncodedSQL to be defined in AllowRemoteExecute
Call.Init(Model.Root+UrlEncode(['sql',sql]),'GET','');
URI(Call);
if Call.OutStatus=HTTP_SUCCESS then begin
json := Call.OutBodyUtf8;
result := TSQLTableJSON.Create(json);
result.fInternalState := fInternalState;
end else
Log(sllError,'ExecuteList failed');
end;
function TSQLRestClientURI.Retrieve(aID: TID; Value: TSQLRecord;
ForUpdate: boolean): boolean;
var tableIndex: Integer;
Call: TSQLRestURIParams;
json: string;
begin
tableIndex := Model.GetTableIndexExisting(Value.RecordClass);
Call.Url := getURIID(tableIndex,aID);
if ForUpdate then
Call.Verb := 'LOCK' else
Call.Verb := 'GET';
URI(Call);
result := Call.OutStatus=HTTP_SUCCESS;
if result then begin
json := Call.OutBodyUtf8;
Value.FromJSON(json);
Value.fInternalState := fInternalState;
end;
Log(LOGLEVELDB[result],'%s.Retrieve(ID=%d) %s',[Model.Info[tableIndex].Name,aID,json]);
end;
{$ifndef ISSMS}
function TSQLRestClientURI.RetrieveBlob(Table: TSQLRecordClass; aID: TID;
const BlobFieldName: string; out BlobData: TSQLRawBlob): boolean;
var tableIndex: Integer;
Call: TSQLRestURIParams;
begin
tableIndex := Model.GetTableIndexExisting(Table);
Call.Init(getURIID(tableIndex,aID)+'/'+BlobFieldName,'GET','');
URI(Call);
result := Call.OutStatus=HTTP_SUCCESS;
if result then
BlobData := TSQLRawBlob(Call.OutBody);
Log(LOGLEVELDB[result],'%s.RetrieveBlob(ID=%d,"%s") len=%d',
[Model.Info[tableIndex].Name,aID,BlobFieldName,length(BlobData)]);
end;
{$endif}
function FindHeader(const Headers, Name: string): string;
{$ifdef ISSMS} // dedicated function using faster JavaScript library
var search,nameValue: string;
searchLen: integer;
begin
if Headers='' then
exit '';
search := UpperCase(Name);
searchLen := Length(search);
for nameValue in Headers.Split(#13#10) do
if uppercase(copy(nameValue,1,searchLen))=search then
exit copy(nameValue,searchLen+1,length(nameValue));
end;
{$else}
var i: integer;
line: string;
begin
result := '';
i := 1;
while GetNextCSV(Headers,i,line,#10) do
if StartWithPropName(line,Name) then begin
delete(line,1,length(Name));
result := trim(line); // will work if EOL is CRLF or LF only
exit;
end;
end;
{$endif}
function GetOutHeader(const Call: TSQLRestURIParams; const Name: string): string;
begin
{$ifdef ISSMS_XHRISBUGGY} // retrieval from Call.XHR is buggy on some browers :(
// see https://synopse.info/forum/viewtopic.php?pid=11730#p11730
if VarIsValidRef(Call.XHR) then
result := Call.XHR.getResponseHeader(Name);
{$else}
result := FindHeader(Call.OutHead,Name+': ');
{$endif}
end;
procedure TSQLRestClientURI.InternalStateUpdate(const Call: TSQLRestURIParams);
var receivedState: cardinal;
begin
if Call.OutHead='' then
exit; // nothing to update from (e.g. asynchronous call)
receivedState := StrToIntDef(GetOutHeader(Call,'Server-InternalState'),0);
if receivedState>fInternalState then
fInternalState := receivedState;
if sllTrace in fLogLevel then
Log(sllTrace,'%s %s status=%d state=%d in=%d out=%d',
[Call.Verb,Call.UrlWithoutSignature,Call.OutStatus,fInternalState,
length(Call.InBody),length(Call.OutBody)]);
end;
procedure TSQLRestClientURI.URI(var Call: TSQLRestURIParams);
var sign: string;
begin
Call.OutStatus := HTTP_UNAVAILABLE;
if self=nil then
exit;
Call.UrlWithoutSignature := Call.Url;
if (fAuthentication<>nil) and (fAuthentication.SessionID<>0) then begin
if Pos('?',Call.Url)=0 then
sign := '?session_signature=' else
sign := '&session_signature=';
Call.Url := Call.Url+sign+
fAuthentication.ClientSessionComputeSignature(self,Call.Url);
end;
InternalURI(Call);
InternalStateUpdate(Call);
end;
procedure TSQLRestClientURI.InternalServiceCheck(const aMethodName: string;
const Call: TSQLRestURIParams);
begin
{$ifdef ISSMS}
if Assigned(Call.OnSuccess) then
exit; // asynchronous call do not have a result yet
{$endif}
if Call.OutStatus<>HTTP_SUCCESS then
Log(sllError,'Service %s returned %s',[aMethodName,Call.OutBodyUtf8]) else
Log(sllServiceReturn,'%s success',[aMethodName]);
end;
procedure TSQLRestClientURI.CallBackGet(const aMethodName: string;
const aNameValueParameters: array of const; var Call: TSQLRestURIParams;
aTable: TSQLRecordClass; aID: TID);
begin
Log(sllServiceCall,'Method-based service %s',[aMethodName]);
Call.Url := getURICallBack(aMethodName,aTable,aID)+UrlEncode(aNameValueParameters);
Call.Verb := 'GET';
URI(Call);
InternalServiceCheck(aMethodName,Call);
end;
function TSQLRestClientURI.ExecuteBatchSend(Table: TSQLRecordClass; const Data: string;
var Results: TIDDynArray): integer;
var {$ifdef ISSMS}
doc: variant;
{$else}
doc: TJSONVariantData;
jsonres: string;
{$endif}
Call: TSQLRestURIParams;
start: TDateTime;
i: integer;
begin
start := Now;
Log(sllServiceCall,'BATCH with %d rows',[fBatchCount]);
Call.Init(getURICallBack('Batch',Table,0),'POST',Data);
URI(Call);
result := Call.OutStatus;
if result<>HTTP_SUCCESS then begin
Log(sllError,'BATCH error');
exit; // transmission or internal server error
end;
Log(sllServiceReturn,'BATCH success in %s',[FormatDateTime('nn:ss:zzz',Now-start)]);
{$ifdef ISSMS}
Results.Clear;
if Call.OutBody='["OK"]' then begin
for i := 0 to fBatchCount-1 do
Results.Add(HTTP_SUCCESS);
end else begin
doc := JSON.Parse(Call.OutBody);
if (VariantType(doc)=jvArray) and (doc.length=fBatchCount) then
for i := 0 to fBatchCount-1 do
Results.Add(integer(doc[i]));
end;
{$else}
SetLength(Results,fBatchCount);
HttpBodyToText(Call.OutBody,jsonres);
if jsonres='["OK"]' then begin
for i := 0 to fBatchCount-1 do
Results[i] := HTTP_SUCCESS;
end else begin
doc.Init(jsonres);
if (doc.Kind=jvArray) and (doc.Count=fBatchCount) then
for i := 0 to fBatchCount-1 do
Results[i] := {$ifdef FPC}Int64{$endif}(doc.Values[i]);
end;
{$endif}
end;
/// marshall {result:...,id:...} and {result:...} body answers
function CallGetResult(const aCall: TSQLRestURIParams; var outID: TID): variant;
{$ifndef ISSMS}
var doc: TJSONVariantData;
jsonres: string;
{$endif}
begin
VarClear(result);
outID := 0;
if aCall.OutStatus<>HTTP_SUCCESS then
exit;
{$ifdef ISSMS}
var doc := JSON.Parse(aCall.OutBody);
if VarIsValidRef(doc.result) then
result := doc.result;
if VarIsValidRef(doc.id) then
outID := doc.id;
{$else}
HttpBodyToText(aCall.OutBody,jsonres);
doc.Init(jsonres);
result := doc.ValueCopy['result']; // Value[] -> varByRef
outID := doc.Value['id'];
{$endif}
end;
function TSQLRestClientURI.CallBackGetResult(const aMethodName: string;
const aNameValueParameters: array of const; aTable: TSQLRecordClass; aID: TID): string;
var Call: TSQLRestURIParams;
dummyID: TID;
begin
CallBackGet(aMethodName,aNameValueParameters,Call,aTable,aID);
result := CallGetResult(Call,dummyID);
end;
procedure TSQLRestClientURI.CallRemoteServiceInternal(var Call: TSQLRestURIParams;
aCaller: TServiceClientAbstract; const aMethod, aParams: string);
var url, clientDrivenID, sent, methName: string;
begin
methName:= aCaller.fServiceURI+'.'+aMethod;
Log(sllServiceCall,'Interface-based service '+methName);
url := Model.Root+'/'+aCaller.fServiceURI;
if aCaller.fInstanceImplementation=sicClientDriven then
clientDrivenID := (aCaller as TServiceClientAbstractClientDriven).ClientID;
ServicesRouting.ClientSideInvoke(url,aMethod,aParams,clientDrivenID,sent);
Call.Init(url,'POST',sent);
URI(Call); // asynchronous or synchronous call
InternalServiceCheck(methName,Call); // will log only for synchronous call
end;
{ Some definitions copied from mORMot.pas unit }
type
TServiceInternalMethod = (imFree, imContract, imSignature);
const
SERVICE_PSEUDO_METHOD: array[TServiceInternalMethod] of string = (
'_free_','_contract_','_signature_');
{$ifdef ISSMS}
procedure TSQLRestClientURI.SetAsynch(var Call: TSQLRestURIParams;
onSuccess, onError: TSQLRestEvent; onBeforeSuccess: TSQLRestEventProcess);
begin
if not Assigned(onSuccess) then
raise ERestException.Create('SetAsynch expects onSuccess');
inc(fAsynchCount);
Call.OnSuccess :=
lambda
if Call.XHR.readyState=rrsDone then begin
InternalStateUpdate(Call);
if not assigned(onBeforeSuccess) then
onSuccess(self) else
if onBeforeSuccess then
onSuccess(self) else
if assigned(onError) then
onError(self);
if fAsynchCount>0 then
dec(fAsynchCount);
if fAsynchCount=0 then
CallAsynchText; // send any pending asynchronous task
end;
end;
Call.OnError :=
lambda
if Assigned(onError) then
onError(Self);
if fAsynchCount>0 then
dec(fAsynchCount);
if fAsynchCount=0 then
CallAsynchText; // send any pending asynchronous task, even on error
end;
end;
procedure TSQLRestClientURI.SetAsynchText(const Text: string);
begin
fAsynchPendingText.Add(Text);
if fAsynchCount=0 then
CallAsynchText; // send it now if no pending asynchronous task
end;
procedure TSQLRestClientURI.CallAsynchText;
var Call: TSQLRestURIParams;
begin
if length(fAsynchPendingText)=0 then
exit; // nothing to send
Call.Init(getURICallBack('RemoteLog',nil,0),'PUT',
fAsynchPendingText.Join(#13#10)); // all rows sent at once
fAsynchPendingText.Clear;
SetAsynch(Call,lambda end,nil,nil); // asynchronous call without error check
URI(Call);
end;
procedure TSQLRestClientURI.Connect(onSuccess, onError: TSQLRestEvent);
var Call: TSQLRestURIParams;
begin
SetAsynch(Call,onSuccess,onError,
lambda
result := (Call.OutStatus=HTTP_SUCCESS) and SetServerTimeStamp(Call.OutBody);
end);
CallBackGet('TimeStamp',[],Call,nil); // asynchronous call
end;
procedure TSQLRestClientURI.CallRemoteServiceASynch(aCaller: TServiceClientAbstract;
const aMethodName: string; aExpectedOutputParamsCount: integer;
const aInputParams: array of variant;
onSuccess: procedure(res: array of Variant); onError: TSQLRestEvent;
aReturnsCustomAnswer: boolean);
var Call: TSQLRestURIParams;
begin
// ForceServiceResultAsJSONObject not implemented yet
SetAsynch(Call,
lambda
if not assigned(onSuccess) then
exit; // no result to handle
if aReturnsCustomAnswer then begin
if Call.OutStatus=HTTP_SUCCESS then begin
var result: TVariantDynArray;
result.Add(Call.OutBody);
onSuccess(result);
end else
if Assigned(onError) then
onError(self);
exit;
end;
var outID: TID;
var result := CallGetResult(Call,outID); // from {result:...,id:...}
if VarIsValidRef(result) then begin
if (aCaller.fInstanceImplementation=sicClientDriven) and (outID<>0) then
(aCaller as TServiceClientAbstractClientDriven).fClientID := IntToStr(outID);
if aExpectedOutputParamsCount=0 then
onSuccess([]) else begin
var res := TJSONVariantData.CreateFrom(result);
if (res.Kind=jvArray) and (res.Count=aExpectedOutputParamsCount) then
onSuccess(res.Values) else
if Assigned(onError) then
onError(self);
end;
end else
if Assigned(onError) then
onError(self);
end,
onError,
lambda
result := (Call.OutStatus=HTTP_SUCCESS) and (Call.OutBody<>'');
end);
CallRemoteServiceInternal(Call,aCaller,aMethodName,JSON.Stringify(variant(aInputParams)));
end;
function TSQLRestClientURI.CallRemoteServiceSynch(aCaller: TServiceClientAbstract;
const aMethodName: string; aExpectedOutputParamsCount: integer;
const aInputParams: array of variant; aReturnsCustomAnswer: boolean): TVariantDynArray;
var Call: TSQLRestURIParams;
outResult: variant;
outID: TID;
procedure RaiseError;
begin
raise EServiceException.CreateFmt('Error calling %s.%s - returned status %d',
[aCaller.fServiceName,aMethodName,Call.OutStatus]);
end;
begin
// ForceServiceResultAsJSONObject not implemented yet
CallRemoteServiceInternal(Call,aCaller,aMethodName,JSON.Stringify(variant(aInputParams)));
if aReturnsCustomAnswer then begin
if Call.OutStatus<>HTTP_SUCCESS then
RaiseError;
result.Add(Call.OutBody);
exit;
end;
outResult := CallGetResult(Call,outID); // from {result:...,id:...}
if not VarIsValidRef(outResult) then
RaiseError;
if (aCaller.fInstanceImplementation=sicClientDriven) and (outID<>0) then
(aCaller as TServiceClientAbstractClientDriven).fClientID := IntToStr(outID);
if aExpectedOutputParamsCount=0 then
exit; // returns default []
var res := TJSONVariantData.CreateFrom(outResult);
if (res.Kind=jvArray) and (res.Count=aExpectedOutputParamsCount) then
result := res.Values else
raise EServiceException.CreateFmt('Error calling %s.%s - '+
'received %d parameters (expected %d)',
[aCaller.fServiceName,aMethodName,res.Count,aExpectedOutputParamsCount]);
end;
{$else}
function TSQLRestClientURI.Connect: boolean;
var Call: TSQLRestURIParams;
begin
Log(sllInfo,'Connect',self);
CallBackGet('TimeStamp',[],Call,nil);
result := Call.OutStatus=HTTP_SUCCESS;
if not result then
exit;
result := SetServerTimeStamp(Call.OutBodyUtf8);
end;
procedure TSQLRestClientURI.CallRemoteService(aCaller: TServiceClientAbstract;
const aMethodName: string; aExpectedOutputParamsCount: integer;
const aInputParams: array of variant; out res: TVariantDynArray;
aReturnsCustomAnswer: boolean);
var Call: TSQLRestURIParams;
params: TJSONVariantData;
result: variant;
bodyerror: string;
arr: PJSONVariantData;
i: integer;
outID: TID;
begin
params.Init;
for i := 0 to high(aInputParams) do
params.AddValue(aInputParams[i]);
CallRemoteServiceInternal(Call,aCaller,aMethodName,params.ToJSON);
if Call.OutStatus<>HTTP_SUCCESS then begin
HttpBodyToText(Call.OutBody,bodyerror);
raise EServiceException.CreateFmt('Error calling %s.%s - returned status %d'#13#10'%s',
[aCaller.fServiceName,aMethodName,Call.OutStatus,bodyerror]);
end;
if aReturnsCustomAnswer then begin
SetLength(res,1);
res[0] := HttpBodyToVariant(Call.OutBody);
exit;
end;
result := CallGetResult(Call,outID);
if (aCaller.fInstanceImplementation=sicClientDriven) and (outID<>0) then
(aCaller as TServiceClientAbstractClientDriven).fClientID := IntToStr(outID);
if aExpectedOutputParamsCount=0 then
exit;
arr := JSONVariantDataSafe(result,jvArray); // Count=0 if not jvArray
if arr^.Count<>aExpectedOutputParamsCount then
raise EServiceException.CreateFmt('Error calling %s.%s - '+
'received %d parameters (expected %d)',
[aCaller.fServiceName,aMethodName,arr^.Count,aExpectedOutputParamsCount]);
res := arr^.Values;
end;
{$endif ISSMS}
function TSQLRestClientURI.ExecuteAdd(tableIndex: integer;
const json: string): TID;
var Call: TSQLRestURIParams;
location: string;
i: integer;
begin
result := 0;
Call.Init(getURIID(tableIndex,0),'POST',json);
URI(Call);
if Call.OutStatus<>HTTP_CREATED then begin
Log(sllError,'Error creating %s with %s',[Model.Info[tableIndex].Name,json]);
exit;
end;
location := GetOutHeader(Call,'location');
for i := length(location) downto 1 do
if not (ord(location[i]) in [ord('0')..ord('9')]) then begin
result := StrToInt64Def(Copy(location,i+1,length(location)),0);
break; // 'Location: root/People/11012' e.g.
end;
Log(sllDB,'%s.ID=%d created from %s',[Model.Info[tableIndex].Name,result,json]);
end;
function TSQLRestClientURI.Delete(Table: TSQLRecordClass;
ID: TID): boolean;
var Call: TSQLRestURIParams;
tableIndex: integer;
begin
result := false;
if ID<=0 then
exit;
tableIndex := Model.GetTableIndexExisting(Table);
Call.Init(getURIID(tableIndex,ID),'DELETE','');
URI(Call);
if Call.OutStatus=HTTP_SUCCESS then
result := true;
Log(LOGLEVELDB[result],'Delete %s.ID=%d',[Model.Info[tableIndex].Name,ID]);
end;
function TSQLRestClientURI.ExecuteUpdate(tableIndex: integer; ID: TID;
const json: string): boolean;
var Call: TSQLRestURIParams;
begin
Call.Init(getURIID(tableIndex,ID),'PUT',json);
URI(Call);
result := Call.OutStatus=HTTP_SUCCESS;
Log(LOGLEVELDB[result],'Update %s.ID=%d with %s',[Model.Info[tableIndex].Name,ID,json]);
end;
function TSQLRestClientURI.SetUser(aAuthenticationClass: TSQLRestServerAuthenticationClass;
const aUserName, aPassword: string; aHashedPassword: Boolean): boolean;
var aKey, aSessionID: string;
i: integer;
begin
result := false;
if fAuthentication<>nil then
SessionClose;
if aAuthenticationClass=nil then
exit;
fAuthentication := aAuthenticationClass.Create(aUserName,aPassword,aHashedPassword);
try
aKey := fAuthentication.ClientComputeSessionKey(self);
i := 1;
GetNextCSV(aKey,i,aSessionID,'+');
if TryStrToInt(aSessionID,i) and (i>0) then begin
fAuthentication.SetSessionID(i);
Log(sllUserAuth,'Session %d created for "%s" with %s',
[i,aUserName,fAuthentication.ClassName]);
result := true;
end else begin
fAuthentication.Free;
fAuthentication := nil;
end;
except
fAuthentication.Free;
fAuthentication := nil;
end;
if fAuthentication=nil then
Log(sllError,'Session not created for "%s"',[aUserName]);
end;
procedure TSQLRestClientURI.SessionClose;
var Call: TSQLRestURIParams;
begin
if (self<>nil) and (fAuthentication<>nil) then
try // notify Server to end of session
CallBackGet('Auth',['UserName',fAuthentication.User.LogonName,
'Session',fAuthentication.SessionID],Call,nil);
finally
fAuthentication.Free;
fAuthentication := nil;
end;
end;
{$ifndef ISSMS}
constructor TSQLRestClientURI.Create(aModel: TSQLModel; aOwnModel: boolean);
begin
fRunningClientDriven := TStringList.Create;
inherited Create(aModel,aOwnModel);
end;
{$endif}
destructor TSQLRestClientURI.Destroy;
begin
{$ifndef ISSMS}
fRunningClientDriven.Free;
{$endif}
SessionClose;
inherited Destroy;
end;
{ TSQLRestClientHTTP }
constructor TSQLRestClientHTTP.Create(const aServer: string;
aPort: integer; aModel: TSQLModel; aOwnModel, aHttps: boolean
{$ifndef ISSMS}; const aProxyName, aProxyByPass: string;
aSendTimeout, aReceiveTimeout, aConnectionTimeOut: Cardinal{$endif});
begin
inherited Create(aModel,aOwnModel);
fParameters.Server := aServer;
fParameters.Port := aPort;
fParameters.Https := aHttps;
{$ifndef ISSMS}
fParameters.ProxyName := aProxyName;
fParameters.ProxyByPass := aProxyByPass;
fParameters.ConnectionTimeOut := aConnectionTimeOut;
fParameters.SendTimeout := aSendTimeout;
fParameters.ReceiveTimeout := aReceiveTimeout;
{$endif}
fKeepAlive := 20000;
end;
destructor TSQLRestClientHTTP.Destroy;
begin
inherited;
fAuthentication.Free;
fConnection.Free;
end;
procedure TSQLRestClientHTTP.InternalURI(var Call: TSQLRestURIParams);
var inType: string;
retry: integer;
begin
inType := FindHeader(Call.InHead,'content-type: ');
if inType='' then begin
if OnlyJSONRequests then
inType := JSON_CONTENT_TYPE else
inType := 'text/plain'; // avoid slow CORS preflighted requests
Call.InHead := trim(Call.InHead+#13#10'content-type: '+inType);
end;
if fCustomHttpHeader<>'' then
Call.InHead := trim(Call.InHead+fCustomHttpHeader);
for retry := 0 to 1 do begin
if fConnection=nil then
try
fConnection := HttpConnectionClass.Create(fParameters);
// TODO: handle SynLZ compression and SHA/AES encryption?
except
on E: Exception do begin
Log(E);
fConnection.Free;
fConnection := nil;
end;
end;
if fConnection=nil then begin
Call.OutStatus := HTTP_NOTIMPLEMENTED;
break;
end;
try
fConnection.URI(Call,inType,fKeepAlive);
break; // do not retry on transmission success, or asynchronous request
except
on E: Exception do begin
Log(E);
fConnection.Free;
fConnection := nil;
Call.OutStatus := HTTP_NOTIMPLEMENTED;
if fForceTerminate then
break;
end; // will retry once (e.g. if connection broken)
end;
end;
end;
procedure TSQLRestClientHTTP.SetHttpBasicAuthHeaders(const aUserName, aPasswordClear: RawUTF8);
var base64: RawUTF8;
begin
base64 := aUsername+':'+aPasswordClear;
{$ifdef ISSMS}
base64 := window.btoa(base64);
{$else}
base64 := BytesToBase64JSONString(TByteDynArray(TextToHttpBody(base64)),false);
{$endif}
fCustomHttpHeader := #13#10'Authorization: Basic '+base64;
end;
{ TSQLAuthUser }
{$ifdef ISSMS} // manual RTTI for SMS
class function TSQLAuthUser.ComputeRTTI: TRTTIPropInfos;
begin
result := TRTTIPropInfos.Create(
['Data','Group','LogonName','DisplayName','PasswordHashHexa'],
[sftBlob]);
end;
procedure TSQLAuthUser.SetProperty(FieldIndex: integer; const Value: variant);
begin
case FieldIndex of
0: fID := Value;
1: fData := Value;
2: fGroup := Value;
3: fLogonName := Value;
4: fDisplayName := Value;
5: fPasswordHashHexa := Value;
end;
end;
function TSQLAuthUser.GetProperty(FieldIndex: integer): variant;
begin
case FieldIndex of
0: result := fID;
1: result := fData;
2: result := fGroup;
3: result := fLogonName;
4: result := fDisplayName;
5: result := fPasswordHashHexa;
end;
end;
{$endif}
function SHA256Compute(const Values: array of string): string;
var buf: THttpBody;
a: integer;
sha: TSHA256;
begin
sha := TSHA256.Create;
try
for a := 0 to high(Values) do begin
buf := TextToHttpBody(Values[a]);
sha.Update(buf);
end;
result := sha.Finalize;
finally
sha.Free;
end;
end;
procedure TSQLAuthUser.SetPasswordPlain(const Value: string);
begin
PasswordHashHexa := SHA256Compute(['salt',Value]);
end;
{ TSQLRestServerAuthentication }
constructor TSQLRestServerAuthentication.Create(const aUserName, aPassword: string;
aHashedPassword: Boolean);
begin
fUser := TSQLAuthUser.Create;
fUser.LogonName := aUserName;
if aHashedPassword then
fUser.PasswordHashHexa := aPassword else
fUser.PasswordPlain := aPassword;
end;
destructor TSQLRestServerAuthentication.Destroy;
begin
fUser.Free;
inherited;
end;
procedure TSQLRestServerAuthentication.SetSessionID(Value: Cardinal);
begin
fSessionID := Value;
fSessionIDHexa8 := LowerCase(IntToHex(Value,8));
end;
{ TSQLRestServerAuthenticationDefault }
function TSQLRestServerAuthenticationDefault.ClientComputeSessionKey(
Sender: TSQLRestClientURI): string;
var aServerNonce, aClientNonce, aPassHash: string;
begin
if fUser.LogonName='' then
exit;
aServerNonce := Sender.CallBackGetResult('Auth',['UserName',User.LogonName],nil);
if aServerNonce='' then
exit;
aClientNonce := SHA256Compute([Copy(NowToIso8601,1,16)]);
aPassHash := Sha256Compute([Sender.Model.Root,aServerNonce,aClientNonce,
User.LogonName,User.PasswordHashHexa]);
result := Sender.CallBackGetResult('Auth',['UserName',User.LogonName,
'Password',aPassHash,'ClientNonce',aClientNonce],nil);
fSessionPrivateKey := crc32ascii(crc32ascii(0,result),fUser.fPasswordHashHexa);
end;
function TSQLRestServerAuthenticationDefault.ClientSessionComputeSignature(
Sender: TSQLRestClientURI; const url: string): string;
var nonce: string;
begin
nonce := LowerCase(IntToHex(trunc(Now*(24*60*60)),8));
result := fSessionIDHexa8+nonce+LowerCase(IntToHex(
crc32ascii(crc32ascii(fSessionPrivateKey,nonce),url),8));
end;
{ TSQLRestServerAuthenticationNone }
function TSQLRestServerAuthenticationNone.ClientComputeSessionKey(
Sender: TSQLRestClientURI): string;
begin
result := Sender.CallBackGetResult('Auth',['UserName',User.LogonName],nil);
end;
function TSQLRestServerAuthenticationNone.ClientSessionComputeSignature(
Sender: TSQLRestClientURI; const url: string): string;
begin
result := fSessionIDHexa8;
end;
{$ifdef ISSMS}
{ TSQLAuthGroup } // manual RTTI for SMS
class function TSQLAuthGroup.ComputeRTTI: TRTTIPropInfos;
begin
result := TRTTIPropInfos.Create(
['Ident','SessionTimeOut','AccessRights'],[]);
end;
procedure TSQLAuthGroup.SetProperty(FieldIndex: integer; const Value: variant);
begin
case FieldIndex of
0: fID := Value;
1: fIdent := Value;
2: fSessionTimeOut := Value;
3: fAccessRights := Value;
end;
end;
function TSQLAuthGroup.GetProperty(FieldIndex: integer): variant;
begin
case FieldIndex of
0: result := fID;
1: result := fIdent;
2: result := fSessionTimeOut;
3: result := fAccessRights;
end;
end;
function VariantToBlob(const Value: variant): TSQLRawBlob;
begin
if TVariant.IsString(Value) then begin
var s: string := Value;
if s='' then
result := null else
result := BrowserAPI.Window.atob(s);
end else
result := null;
end;
function BlobToVariant(const Blob: TSQLRawBlob): variant;
begin
if TVariant.IsString(Blob) then
result := BrowserAPI.Window.btoa(Blob) else
result := null;
end;
function VariantToGUID(const value: variant): TGUID; inline;
begin
result := value; // no-op since TGUID=string
end;
function GUIDToVariant(const GUID: TGUID): variant; inline;
begin
result := GUID; // no-op since TGUID=string
end;
function VariantToHttpBody(const value: variant): THttpBody;
begin
result := value; // no-op since THttpBody=string
end;
function HttpBodyToVariant(const HttpBody: THttpBody): variant;
begin
result := HttpBody; // no-op since THttpBody=string
end;
{$else}
{$ifdef FPC} // original VarIsStr() does not handle varByRef as expected :(
function VarIsStr(const Value: variant): boolean; inline;
begin
result := Variants.VarIsStr(PVariant(FindVarData(Value))^);
end;
{$endif}
function VariantToBlob(const Value: variant): TSQLRawBlob;
begin
if VarIsStr(Value) then // avoid conversion error from null to string
Base64JSONStringToBytes(Value,result) else
Finalize(result);
end;
function BlobToVariant(const Blob: TSQLRawBlob): variant;
begin
if Blob=nil then
result := null else
result := BytesToBase64JSONString(Blob);
end;
function VariantToGUID(const value: variant): TGUID;
var S: string;
begin
FillChar(result,SizeOf(result),0);
if not VarIsStr(value) then
exit;
S := string(Value);
if S<>'' then
try
result := SysUtils.StringToGUID('{'+s+'}');
except
; // ignore any conversion error and return void TGUID
end;
end;
function GUIDToVariant(const GUID: TGUID): variant;
begin
try
result := Copy(SysUtils.GUIDToString(GUID),2,36);
except
result := ''; // should not happen
end;
end;
const
varHttpBody = {$ifdef UNICODE}varUString{$else}varString{$endif};
function VariantToHttpBody(const value: variant): THttpBody;
var P: PCardinal;
Len: cardinal;
begin
result := nil;
with TVarData(value) do begin
if (VType<>varHttpBody) or (VAny=nil) then
exit;
P := VAny;
{$ifdef UNICODE}
Len := P^;
if Len>=cardinal(length(UnicodeString(VAny))*2) then
exit; // input does not come from HttpBodyToVariant() -> avoid GPF
inc(P);
{$else}
Len := length(RawByteString(VAny));
{$endif}
SetLength(result,Len);
move(P^,pointer(result)^,len);
end;
end;
function HttpBodyToVariant(const HttpBody: THttpBody): variant;
var P: PCardinal;
Len: cardinal;
begin
VarClear(result);
Len := length(HttpBody);
if Len>0 then
with TVarData(result) do begin
VType := varHttpBody;
VAny := nil;
{$ifdef UNICODE}
SetLength(UnicodeString(VAny),Len shr 1+2);
P := VAny;
P^ := Len;
inc(P);
{$else}
SetLength(RawByteString(VAny),Len);
P := VAny;
{$endif}
move(pointer(HttpBody)^,P^,Len);
end;
end;
{$endif ISSMS}
function VariantToEnum(const Value: variant; const TextValues: array of string): integer;
{$ifdef ISSMS}
begin
if TVariant.IsNumber(Value) then
result := Value else begin
result := TextValues.IndexOf(string(Value));
if result>=0 then
exit;
{$else}
var str: string;
begin
if VarIsOrdinal(Value) then
result := Value else begin
str := Value;
if str<>'' then
for result := 0 to high(TextValues) do
if str=TextValues[result] then
exit;
{$endif}
result := 0; // return first item by default
end;
end;
function ObjectToVariant(value: TSQLRecord): variant;
begin
result := value.ToVariant;
end;
{ TServiceClientAbstract }
constructor TServiceClientAbstract.Create(aClient: TSQLRestClientURI);
var Call: TSQLRestURIParams; // manual synchronous call
dummyID: TID;
result: variant;
contract: string;
begin
if (fServiceName='') or (fServiceURI='') then
raise EServiceException.CreateFmt(
'Overriden %s.Create should have set properties',[ClassName]);
if aClient=nil then
raise EServiceException.CreateFmt('%s.Create(nil)',[ClassName]);
fClient := aClient;
fClient.CallRemoteServiceInternal(Call,self,SERVICE_PSEUDO_METHOD[imContract],'[]');
result := CallGetResult(Call,dummyID);
{$ifdef ISSMS}
if VariantType(result)=jvArray then
contract := result[0] else
contract := result.contract; // if ResultAsJSONObject=true
{$else}
with JSONVariantDataSafe(result,jvArray)^ do // Count=0 if not jvArray
if Count=1 then
contract := Values[0] else
contract := Value['contract']; // if ResultAsJSONObject=true
{$endif}
if contract<>fContractExpected then
raise EServiceException.CreateFmt('Invalid contract "%s" for %s: expected "%s"',
[contract,ClassName,fContractExpected]);
end;
function TServiceClientAbstract.GetClient: TSQLRestClientURI;
begin
result := fClient;
end;
function TServiceClientAbstract.GetContractExpected: string;
begin
result := fContractExpected;
end;
function TServiceClientAbstract.GetInstanceImplementation: TServiceInstanceImplementation;
begin
result := fInstanceImplementation;
end;
function TServiceClientAbstract.GetRunningInstance: TServiceClientAbstract;
begin
result := self;
end;
function TServiceClientAbstract.GetServiceName: string;
begin
result := fServiceName;
end;
function TServiceClientAbstract.GetServiceURI: string;
begin
result := fServiceURI;
end;
{ TServiceClientAbstractClientDriven }
constructor TServiceClientAbstractClientDriven.Create(aClient: TSQLRestClientURI);
begin
if fInstanceImplementation<>sicClientDriven then
raise EServiceException.CreateFmt(
'Overriden %s.Create should have set sicClientDriven',[ClassName]);
if aClient.fRunningClientDriven.IndexOf(fServiceName)>=0 then
raise EServiceException.CreateFmt('Only ONE instance of %s is allowed at once',
[ClassName]);
inherited Create(aClient); // will synchronously check the contract from server
aClient.fRunningClientDriven.Add(fServiceName); // mark as opened
end;
destructor TServiceClientAbstractClientDriven.Destroy;
var ndx: integer;
{$ifndef ISSMS}
res: TVariantDynArray;
{$endif}
begin
if fClient<>nil then
try
if fClientID<>'' then
{$ifdef ISSMS}
fClient.CallRemoteServiceAsynch(self,SERVICE_PSEUDO_METHOD[imFree],0,[],nil,nil);
{$else}
try // synchronous blocking call
fClient.CallRemoteService(self,SERVICE_PSEUDO_METHOD[imFree],0,[],res);
except
; // ignore, since the connection may be broken (will timeout on server)
end;
{$endif}
finally
ndx := fClient.fRunningClientDriven.IndexOf(ServiceName);
if ndx>=0 then
fClient.fRunningClientDriven.Delete(ndx); // mark as closed
end;
inherited;
end;
{ TSQLRestRoutingREST }
class procedure TSQLRestRoutingREST.ClientSideInvoke(var uri: String;
const method: String; const params: String; const clientDrivenID: String;
var sent: String);
begin
if clientDrivenID<>'' then
uri := uri+'.'+method+'/'+clientDrivenID else
uri := uri+'.'+method;
sent := params; // we may also encode them within the URI
end;
{ TSQLRestRoutingJSON_RPC }
class procedure TSQLRestRoutingJSON_RPC.ClientSideInvoke(var uri: String;
const method: String; const params: String; const clientDrivenID: String;
var sent: String);
begin
sent := '{"method":"'+method+'","params":'+params;
if clientDrivenID='' then
sent := sent+'}' else
sent := sent+',"id":'+clientDrivenID+'}';
end;
/// SmartMobileStudio does not like constant sets with the Delphi syntax
var nfo: TSynLogInfo;
initialization
include(LOG_STACKTRACE,sllLastError);
include(LOG_STACKTRACE,sllError);
include(LOG_STACKTRACE,sllException);
include(LOG_STACKTRACE,sllExceptionOS);
for nfo := succ(sllNone) to high(nfo) do
include(LOG_VERBOSE,nfo);
end.