pas2js/packages/fcl-db/extjsdataset.pas
2019-07-07 18:40:35 +00:00

414 lines
11 KiB
ObjectPascal

{
This file is part of the Free Pascal run time library.
Copyright (c) 2019 by Michael Van Canneyt, member of the
Free Pascal development team
Simple EXTJS JSON dataset component.
See the file COPYING.FPC, included in this distribution,
for details about the copyright.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
**********************************************************************}
unit ExtJSDataset;
{$mode objfpc}
interface
uses
Classes, SysUtils, db, JS, jsondataset;
type
{ TExtJSJSONDataSet }
// Base for ExtJS datasets. It handles MetaData conversion.
TExtJSJSONDataSet = Class(TBaseJSONDataset)
Private
FFields : TJSArray;
FIDField: String;
FRoot: String;
Protected
// Data proxy support
Procedure InternalOpen; override;
function DoResolveRecordUpdate(anUpdate: TRecordUpdateDescriptor): Boolean; override;
Function DataPacketReceived(ARequest: TDataRequest) : Boolean; override;
Function GenerateMetaData : TJSObject;
function ConvertDateFormat(S: String): String; virtual;
Procedure MetaDataToFieldDefs; override;
procedure InitDateTimeFields; override;
function StringToFieldType(S: String): TFieldType;virtual;
function GetStringFieldLength(F: TJSObject; AName: String; AIndex: Integer): integer; virtual;
Public
Constructor Create(AOwner : TComponent); override;
// Can be set directly if the dataset is closed.
Property MetaData;
// Can be set directly if the dataset is closed. If metadata is set, it must match the data.
Property Rows;
// Root of data array in data packet
property Root : String Read FRoot Write FRoot;
// property IDField
property IDField : String Read FIDField Write FIDField;
published
Property FieldDefs;
Property Indexes;
Property ActiveIndex;
// redeclared data set properties
property Active;
property BeforeOpen;
property AfterOpen;
property BeforeClose;
property AfterClose;
property BeforeInsert;
property AfterInsert;
property BeforeEdit;
property AfterEdit;
property BeforePost;
property AfterPost;
property BeforeCancel;
property AfterCancel;
property BeforeDelete;
property AfterDelete;
property BeforeScroll;
property AfterScroll;
property OnCalcFields;
property OnDeleteError;
property OnEditError;
property OnFilterRecord;
property OnNewRecord;
property OnPostError;
Property OwnsData;
end;
{ TExtJSJSONObjectDataSet }
// Use this dataset for data where the data is an array of objects.
TExtJSJSONObjectDataSet = Class(TExtJSJSONDataSet)
Protected
Function CreateFieldMapper : TJSONFieldMapper; override;
end;
{ TExtJSJSONArrayDataSet }
// Use this dataset for data where the data is an array of arrays.
TExtJSJSONArrayDataSet = Class(TExtJSJSONDataSet)
Protected
Function CreateFieldMapper : TJSONFieldMapper; override;
end;
implementation
{ TExtJSJSONDataSet }
function TExtJSJSONDataSet.StringToFieldType(S: String): TFieldType;
begin
if (s='int') then
Result:=ftLargeInt
else if (s='float') then
Result:=ftFloat
else if (s='boolean') then
Result:=ftBoolean
else if (s='date') then
Result:=ftDateTime
else if (s='string') or (s='auto') or (s='') then
Result:=ftString
else
if MapUnknownToStringType then
Result:=ftString
else
Raise EJSONDataset.CreateFmt('Unknown JSON data type : %s',[s]);
end;
function TExtJSJSONDataSet.GetStringFieldLength(F: TJSObject; AName: String;
AIndex: Integer): integer;
Var
I,L : Integer;
D : JSValue;
begin
Result:=0;
D:=F.Properties['maxlen'];
if Not jsIsNan(toNumber(D)) then
begin
Result:=Trunc(toNumber(D));
if (Result<=0) then
Raise EJSONDataset.CreateFmt('Invalid maximum length specifier for field %s',[AName])
end
else
begin
For I:=0 to Rows.Length-1 do
begin
D:=FieldMapper.GetJSONDataForField(Aname,AIndex,Rows[i]);
if isString(D) then
begin
l:=Length(String(D));
if L>Result then
Result:=L;
end;
end;
end;
if (Result=0) then
Result:=20;
end;
constructor TExtJSJSONDataSet.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
UseDateTimeFormatFields:=True;
end;
procedure TExtJSJSONDataSet.MetaDataToFieldDefs;
Var
A : TJSArray;
F : TJSObject;
I,FS : Integer;
N: String;
ft: TFieldType;
D : JSValue;
begin
FieldDefs.Clear;
D:=Metadata.Properties['fields'];
if Not IsArray(D) then
Raise EJSONDataset.Create('Invalid metadata object');
A:=TJSArray(D);
For I:=0 to A.Length-1 do
begin
If Not isObject(A[i]) then
Raise EJSONDataset.CreateFmt('Field definition %d in metadata is not an object',[i]);
F:=TJSObject(A[i]);
D:=F.Properties['name'];
If Not isString(D) then
Raise EJSONDataset.CreateFmt('Field definition %d in has no or invalid name property',[i]);
N:=String(D);
D:=F.Properties['type'];
If IsNull(D) or isUndefined(D) then
ft:=ftstring
else If Not isString(D) then
begin
Raise EJSONDataset.CreateFmt('Field definition %d in has invalid type property',[i])
end
else
begin
ft:=StringToFieldType(String(D));
end;
if (ft=ftString) then
fs:=GetStringFieldLength(F,N,I)
else
fs:=0;
FieldDefs.Add(N,ft,fs);
end;
FFields:=A;
end;
procedure TExtJSJSONDataSet.InternalOpen;
Var
I : integer;
begin
inherited InternalOpen;
for I:=0 to Fields.Count-1 do
If SameText(Fields[i].FieldName,IDField) then
Fields[i].ProviderFlags:=Fields[i].ProviderFlags+[pfInKey];
end;
function TExtJSJSONDataSet.DoResolveRecordUpdate(anUpdate: TRecordUpdateDescriptor): Boolean;
Var
D : JSValue;
O : TJSObject;
A : TJSArray;
I,RecordIndex : Integer;
FN : String;
begin
Result:=True;
if anUpdate.OriginalStatus=usDeleted then
exit;
D:=anUpdate.ServerData;
If isNull(D) then
exit;
if not isNumber(AnUpdate.Bookmark.Data) then
exit(False);
RecordIndex:=Integer(AnUpdate.Bookmark.Data);
If isString(D) then
O:=TJSOBject(TJSJSON.Parse(String(D)))
else if isObject(D) then
O:=TJSOBject(D)
else
Exit(False);
if Not isArray(O[Root]) then
exit(False);
A:=TJSArray(O[Root]);
If A.Length=1 then
begin
O:=TJSObject(A[0]);
For I:=0 to Fields.Count-1 do
begin
FN:=Fields[i].FieldName;
if O.hasOwnProperty(FN) then
FieldMapper.SetJSONDataForField(Fields[i],Rows[RecordIndex],O[FN]);
end;
end;
end;
function TExtJSJSONDataSet.DataPacketReceived(ARequest: TDataRequest): Boolean;
Var
O : TJSObject;
A : TJSArray;
begin
Result:=False;
If isNull(aRequest.Data) then
exit;
If isString(aRequest.Data) then
O:=TJSOBject(TJSJSON.Parse(String(aRequest.Data)))
else if isObject(aRequest.Data) then
O:=TJSOBject(aRequest.Data)
else
DatabaseError('Cannot handle data packet');
if (Root='') then
root:='rows';
if (IDField='') then
idField:='id';
if O.hasOwnProperty('metaData') and isObject(o['metaData']) then
begin
if not Active then // Load fields from metadata
metaData:=TJSObject(o['metaData']);
// We must always check this one...
if metaData.hasOwnProperty('root') and isString(metaData['root']) then
Root:=string(metaData['root']);
if metaData.hasOwnProperty('idField') and isString(metaData['idField']) then
IDField:=string(metaData['idField']);
end;
if O.hasOwnProperty(Root) and isArray(o[Root]) then
begin
A:=TJSArray(o[Root]);
Result:=A.Length>0;
AddToRows(A);
end;
end;
function TExtJSJSONDataSet.GenerateMetaData: TJSObject;
Var
F : TJSArray;
O : TJSObject;
I,M : Integer;
T : STring;
begin
Result:=TJSObject.New;
F:=TJSArray.New;
Result.Properties['fields']:=F;
For I:=0 to FieldDefs.Count -1 do
begin
O:=New(['name',FieldDefs[i].name]);
F.push(O);
M:=0;
case FieldDefs[i].DataType of
ftfixedchar,
ftString:
begin
T:='string';
M:=FieldDefs[i].Size;
end;
ftBoolean: T:='boolean';
ftDate,
ftTime,
ftDateTime: T:='date';
ftFloat: t:='float';
ftInteger,
ftAutoInc,
ftLargeInt : t:='int';
else
Raise EJSONDataset.CreateFmt('Unsupported field type : %s',[Ord(FieldDefs[i].DataType)]);
end; // case
O.Properties['type']:=t;
if M<>0 then
O.Properties['maxlen']:=M;
end;
Result.Properties['root']:='rows';
end;
function TExtJSJSONDataSet.ConvertDateFormat(S: String): String;
{ Not handled: N S w z W t L o O P T Z c U MS }
begin
Result:=StringReplace(S,'y','yy',[rfReplaceall]);
Result:=StringReplace(Result,'Y','yyyy',[rfReplaceall]);
Result:=StringReplace(Result,'g','h',[rfReplaceall]);
Result:=StringReplace(Result,'G','hh',[rfReplaceall]);
Result:=StringReplace(Result,'F','mmmm',[rfReplaceall]);
Result:=StringReplace(Result,'M','mmm',[rfReplaceall]);
Result:=StringReplace(Result,'n','m',[rfReplaceall]);
Result:=StringReplace(Result,'D','ddd',[rfReplaceall]);
Result:=StringReplace(Result,'j','d',[rfReplaceall]);
Result:=StringReplace(Result,'l','dddd',[rfReplaceall]);
Result:=StringReplace(Result,'i','nn',[rfReplaceall]);
Result:=StringReplace(Result,'u','zzz',[rfReplaceall]);
Result:=StringReplace(Result,'a','am/pm',[rfReplaceall,rfIgnoreCase]);
Result:=LowerCase(Result);
end;
procedure TExtJSJSONDataSet.InitDateTimeFields;
Var
F : TJSObject;
FF : TField;
I: Integer;
Fmt : String;
D : JSValue;
begin
If (FFields=Nil) then
Exit;
For I:=0 to FFields.Length-1 do
begin
F:=TJSObject(FFields[i]);
D:=F.Properties['type'];
if isString(D) and (String(D)='date') then
begin
D:=F.Properties['dateFormat'];
if isString(D) then
begin
FMT:=ConvertDateFormat(String(D));
FF:=FindField(String(F.Properties['name']));
if (FF<>Nil) and (FF.DataType in [ftDate,ftTime,ftDateTime]) and (FF.FieldKind=fkData) then
begin
if FF is TJSONDateField then
TJSONDateField(FF).DateFormat:=Fmt
else if FF is TJSONTimeField then
TJSONTimeField(FF).TimeFormat:=Fmt
else if FF is TJSONDateTimeField then
TJSONDateTimeField(FF).DateTimeFormat:=Fmt;
end;
end;
end;
end;
end;
{ TExtJSJSONArrayDataSet }
function TExtJSJSONArrayDataSet.CreateFieldMapper: TJSONFieldMapper;
begin
Result:=TJSONArrayFieldMapper.Create;
end;
{ TExtJSJSONObjectDataSet }
function TExtJSJSONObjectDataSet.CreateFieldMapper: TJSONFieldMapper;
begin
Result:=TJSONObjectFieldMapper.Create;
end;
end.