delphimvcframework/sources/MVCFramework.ActiveRecordController.pas

463 lines
13 KiB
ObjectPascal
Raw Normal View History

// *************************************************************************** }
//
// Delphi MVC Framework
//
2024-01-02 17:04:27 +01:00
// Copyright (c) 2010-2024 Daniele Teti and the DMVCFramework Team
//
// https://github.com/danieleteti/delphimvcframework
//
// ***************************************************************************
//
// Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// ***************************************************************************
unit MVCFramework.ActiveRecordController;
interface
uses
System.SysUtils,
MVCFramework,
MVCFramework.Commons,
MVCFramework.ActiveRecord,
FireDAC.Stan.Def,
FireDAC.Stan.Pool,
FireDAC.Stan.Async,
2018-09-28 13:01:46 +02:00
FireDAC.Comp.Client,
MVCFramework.RQL.Parser,
System.Generics.Collections,
MVCFramework.Serializer.Commons;
type
{$SCOPEDENUMS ON}
TMVCActiveRecordAction = (Create, Retrieve, Update, Delete);
TMVCActiveRecordAuthFunc = TFunc<TWebContext, TMVCActiveRecordClass, TMVCActiveRecordAction, Boolean>;
TMVCActiveRecordController = class(TMVCController)
private
fAuthorization: TMVCActiveRecordAuthFunc;
fURLSegment: string;
protected
function GetMaxRecordCount: Integer;
function CheckAuthorization(aClass: TMVCActiveRecordClass; aAction: TMVCActiveRecordAction): Boolean; virtual;
public
constructor Create(
const aAuthorization: TMVCActiveRecordAuthFunc = nil;
const aURLSegment: String = ''); reintroduce; overload;
destructor Destroy; override;
[MVCPath('/($entityname)')]
[MVCHTTPMethod([httpGET])]
procedure GetEntities(const entityname: string); virtual;
2018-09-27 12:26:50 +02:00
[MVCPath('/($entityname)/searches')]
2018-09-28 13:01:46 +02:00
[MVCHTTPMethod([httpGET, httpPOST])]
2018-09-27 12:26:50 +02:00
procedure GetEntitiesByRQL(const entityname: string); virtual;
[MVCPath('/($entityname)/($id)')]
[MVCHTTPMethod([httpGET])]
procedure GetEntity(const entityname: string; const id: Integer); virtual;
[MVCPath('/($entityname)')]
[MVCHTTPMethod([httpPOST])]
procedure CreateEntity(const entityname: string); virtual;
[MVCPath('/($entityname)/($id)')]
[MVCHTTPMethod([httpPUT])]
procedure UpdateEntity(const entityname: string; const id: Integer); virtual;
[MVCPath('/($entityname)/($id)')]
[MVCHTTPMethod([httpDELETE])]
procedure DeleteEntity(const entityname: string; const id: Integer); virtual;
end;
[MVCNameCase(ncLowerCase)]
TMVCActiveRecordListResponse = class
private
FList: TMVCActiveRecordList;
FMetadata: TMVCStringDictionary;
FOwns: Boolean;
public
constructor Create(AList: TMVCActiveRecordList; AOwns: Boolean = True); virtual;
destructor Destroy; override;
[MVCListOf(TMVCActiveRecord)]
2020-06-18 14:49:06 +02:00
[MVCNameAs('data')]
property Items: TMVCActiveRecordList read FList;
[MVCNameAs('meta')]
property Metadata: TMVCStringDictionary read FMetadata;
end;
implementation
uses
2018-09-27 12:26:50 +02:00
MVCFramework.Logger,
JsonDataObjects,
Data.DB;
procedure TMVCActiveRecordController.GetEntities(const entityname: string);
var
lARClassRef: TMVCActiveRecordClass;
2018-09-28 13:01:46 +02:00
lRQL: string;
lInstance: TMVCActiveRecord;
lMapping: TMVCFieldsMapping;
lConnection: TFDConnection;
lRQLBackend: string;
lProcessor: IMVCEntityProcessor;
lHandled: Boolean;
lARResp: TMVCActiveRecordList;
lStrDict : TMVCStringDictionary;
begin
lProcessor := nil;
if ActiveRecordMappingRegistry.FindProcessorByURLSegment(entityname, lProcessor) then
begin
lHandled := False;
lProcessor.GetEntities(Context, self, entityname, lHandled);
if lHandled then
begin
Exit;
end;
end;
if not ActiveRecordMappingRegistry.FindEntityClassByURLSegment(entityname, lARClassRef) then
begin
raise EMVCException.CreateFmt('Cannot find entity nor processor for entity "%s"', [entityname]);
end;
if not CheckAuthorization(lARClassRef, TMVCActiveRecordAction.Retrieve) then
begin
Render(TMVCErrorResponse.Create(HTTP_STATUS.Forbidden, 'Cannot read ' + entityname));
Exit;
end;
2018-09-28 13:01:46 +02:00
lRQL := Context.Request.QueryStringParam('rql');
try
// if lRQL.IsEmpty then
// begin
// lRQL := Format('limit(0,%d)', [GetMaxRecordCount]);
// end;
2018-09-28 13:01:46 +02:00
lConnection := ActiveRecordConnectionsRegistry.GetCurrent;
lRQLBackend := GetBackEndByConnection(lConnection);
LogD('[RQL PARSE]: ' + lRQL);
lInstance := lARClassRef.Create(True);
try
lMapping := lInstance.GetMapping;
finally
lInstance.Free;
end;
lARResp := TMVCActiveRecord.SelectRQL(lARClassRef, lRQL, GetMaxRecordCount);
try
lStrDict := StrDict(['page_size'],[lARResp.Count.ToString]);
try
if Context.Request.QueryStringParam('count').ToLower = 'true' then
begin
lStrDict.Add('count', TMVCActiveRecord.Count(lARClassRef, lRQL).ToString);
end;
Render(ObjectDict(False)
.Add('data', lARResp,
procedure(const AObject: TObject; const Links: IMVCLinks)
begin
//Links.AddRefLink.Add(HATEOAS.HREF, fURLSegment + '/' + )
case TMVCActiveRecord(AObject).GetPrimaryKeyFieldType of
ftInteger:
Links.AddRefLink.Add(HATEOAS.HREF, fURLSegment + '/' + TMVCActiveRecord(AObject).GetPK.AsInt64.ToString)
end;
end)
.Add('meta', lStrDict));
finally
lStrDict.Free;
end;
finally
lARResp.Free;
end;
2018-09-28 13:01:46 +02:00
except
on E: ERQLCompilerNotFound do
begin
LogE('RQL Compiler not found. Did you included MVCFramework.RQL.AST2<yourdatabase>.pas?');
raise;
end;
end;
end;
2018-09-27 12:26:50 +02:00
procedure TMVCActiveRecordController.GetEntitiesByRQL(const entityname: string);
var
lRQL: string;
lJSON: TJsonObject;
begin
2018-09-28 13:01:46 +02:00
if Context.Request.HTTPMethod = httpPOST then
2018-09-27 12:26:50 +02:00
begin
2018-09-28 13:01:46 +02:00
lJSON := TJsonObject.Parse(Context.Request.Body) as TJsonObject;
try
2020-06-18 14:49:06 +02:00
if Assigned(lJSON) then
begin
lRQL := lJSON.s['rql'];
end
else
begin
lRQL := '';
end;
2018-09-28 13:01:46 +02:00
finally
lJSON.Free;
end;
Context.Request.QueryStringParams.Values['rql'] := lRQL;
2018-09-27 12:26:50 +02:00
end;
2018-09-28 13:01:46 +02:00
GetEntities(entityname);
2018-09-27 12:26:50 +02:00
end;
procedure TMVCActiveRecordController.GetEntity(const entityname: string; const id: Integer);
var
lAR: TMVCActiveRecord;
lARClass: TMVCActiveRecordClass;
lProcessor: IMVCEntityProcessor;
lHandled: Boolean;
lResponse: IMVCResponse;
begin
lProcessor := nil;
if ActiveRecordMappingRegistry.FindProcessorByURLSegment(entityname, lProcessor) then
begin
lHandled := False;
lProcessor.GetEntity(Context, self, entityname, id, lHandled);
if lHandled then
begin
Exit;
end;
end;
if not ActiveRecordMappingRegistry.FindEntityClassByURLSegment(entityname, lARClass) then
begin
raise EMVCException.CreateFmt(HTTP_STATUS.NotFound, 'Cannot find entity %s', [entityname]);
end;
lAR := lARClass.Create;
try
if not CheckAuthorization(TMVCActiveRecordClass(lAR.ClassType), TMVCActiveRecordAction.Retrieve) then
begin
Render(TMVCErrorResponse.Create(HTTP_STATUS.Forbidden, 'Cannot read ' + entityname));
Exit;
end;
if lAR.LoadByPK(id) then
begin
lResponse := MVCResponseBuilder
.StatusCode(HTTP_STATUS.OK)
.Body(ObjectDict(false).Add('data', lAR))
.Build;
end
else
begin
lResponse := MVCResponseBuilder
.StatusCode(HTTP_STATUS.NotFound)
.Body(entityname.ToLower + ' not found')
.Build;
end;
TMVCRenderer.InternalRenderMVCResponse(Self, TMVCResponse(lResponse));
finally
lAR.Free;
end;
end;
function TMVCActiveRecordController.GetMaxRecordCount: Integer;
begin
Result := StrToIntDef(Config[TMVCConfigKey.MaxEntitiesRecordCount], 20);
end;
2020-06-18 14:49:06 +02:00
function TMVCActiveRecordController.CheckAuthorization(aClass: TMVCActiveRecordClass;
aAction: TMVCActiveRecordAction): Boolean;
begin
if Assigned(fAuthorization) then
begin
Result := fAuthorization(Context, aClass, aAction);
end
else
begin
Result := True;
end;
end;
constructor TMVCActiveRecordController.Create(
const aAuthorization: TMVCActiveRecordAuthFunc;
const aURLSegment: String);
begin
inherited Create;
fURLSegment := aURLSegment;
fAuthorization := aAuthorization;
end;
procedure TMVCActiveRecordController.CreateEntity(const entityname: string);
var
lAR: TMVCActiveRecord;
lARClass: TMVCActiveRecordClass;
lProcessor: IMVCEntityProcessor;
lHandled: Boolean;
begin
lProcessor := nil;
if ActiveRecordMappingRegistry.FindProcessorByURLSegment(entityname, lProcessor) then
begin
lHandled := False;
lProcessor.CreateEntity(Context, self, entityname, lHandled);
if lHandled then
begin
Exit;
end;
end;
if not ActiveRecordMappingRegistry.FindEntityClassByURLSegment(entityname, lARClass) then
begin
raise EMVCException.CreateFmt(HTTP_STATUS.NotFound, 'Cannot find entity %s', [entityname]);
end;
lAR := lARClass.Create;
try
if not CheckAuthorization(TMVCActiveRecordClass(lAR.ClassType), TMVCActiveRecordAction.Create) then
begin
Render(TMVCErrorResponse.Create(HTTP_STATUS.Forbidden, 'Cannot create ' + entityname));
Exit;
end;
Context.Request.BodyFor<TMVCActiveRecord>(lAR);
lAR.Insert;
2020-06-18 14:49:06 +02:00
Context.Response.CustomHeaders.Values['X-REF'] := Context.Request.PathInfo + '/' + lAR.GetPK.AsInt64.ToString;
if Context.Request.QueryStringParam('refresh').ToLower = 'true' then
begin
RenderStatusMessage(HTTP_STATUS.Created, entityname.ToLower + ' created', '', lAR, False);
end
else
begin
RenderStatusMessage(HTTP_STATUS.Created, entityname.ToLower + ' created');
end;
finally
lAR.Free;
end;
end;
procedure TMVCActiveRecordController.UpdateEntity(const entityname: string; const id: Integer);
var
lAR: TMVCActiveRecord;
lARClass: TMVCActiveRecordClass;
lProcessor: IMVCEntityProcessor;
lHandled: Boolean;
begin
lProcessor := nil;
if ActiveRecordMappingRegistry.FindProcessorByURLSegment(entityname, lProcessor) then
begin
lHandled := False;
2020-06-18 14:49:06 +02:00
lProcessor.UpdateEntity(Context, self, entityname, id, lHandled);
if lHandled then
begin
Exit;
end;
end;
2020-06-18 14:49:06 +02:00
if not ActiveRecordMappingRegistry.FindEntityClassByURLSegment(entityname, lARClass) then
begin
raise EMVCException.CreateFmt(HTTP_STATUS.NotFound, 'Cannot find class for entity %s', [entityname]);
end;
lAR := lARClass.Create;
try
if not CheckAuthorization(TMVCActiveRecordClass(lAR.ClassType), TMVCActiveRecordAction.Update) then
begin
Render(TMVCErrorResponse.Create(HTTP_STATUS.Forbidden, 'Cannot update ' + entityname));
Exit;
end;
lAR.CheckAction(TMVCEntityAction.eaUpdate);
if not lAR.LoadByPK(id) then
raise EMVCException.CreateFmt(HTTP_STATUS.NotFound, 'Cannot find entity %s', [entityname]);
Context.Request.BodyFor<TMVCActiveRecord>(lAR);
lAR.SetPK(id);
lAR.Update;
2020-06-18 14:49:06 +02:00
Context.Response.CustomHeaders.Values['X-REF'] := Context.Request.PathInfo;
if Context.Request.QueryStringParam('refresh').ToLower = 'true' then
begin
RenderStatusMessage(HTTP_STATUS.OK, entityname.ToLower + ' updated', '', lAR, False);
end
else
begin
RenderStatusMessage(HTTP_STATUS.OK, entityname.ToLower + ' updated');
end;
finally
lAR.Free;
end;
end;
procedure TMVCActiveRecordController.DeleteEntity(const entityname: string; const id: Integer);
var
lAR: TMVCActiveRecord;
lARClass: TMVCActiveRecordClass;
2021-02-04 14:11:33 +01:00
lProcessor: IMVCEntityProcessor;
lHandled: Boolean;
begin
2021-02-04 14:11:33 +01:00
lProcessor := nil;
if ActiveRecordMappingRegistry.FindProcessorByURLSegment(entityname, lProcessor) then
begin
lHandled := False;
lProcessor.DeleteEntity(Context, self, entityname, id, lHandled);
if lHandled then
begin
Exit;
end;
end;
if not ActiveRecordMappingRegistry.FindEntityClassByURLSegment(entityname, lARClass) then
begin
raise EMVCException.CreateFmt(HTTP_STATUS.NotFound, 'Cannot find class for entity %s', [entityname]);
end;
lAR := lARClass.Create;
try
2023-09-05 12:29:38 +02:00
if not CheckAuthorization(TMVCActiveRecordClass(lAR.ClassType),
2020-06-18 14:49:06 +02:00
TMVCActiveRecordAction.Delete) then
begin
Render(TMVCErrorResponse.Create(HTTP_STATUS.Forbidden, 'Cannot delete ' + entityname));
Exit;
end;
2020-06-18 14:49:06 +02:00
{
HTTP DELETE is an idempotent operation. Invoking it multiple times consecutively must result in
the same behavior as the first. Meaning: you shouldn't return HTTP 404.
}
if lAR.LoadByPK(id) then
begin
lAR.SetPK(id);
lAR.Delete;
end;
Render(HTTP_STATUS.OK, entityname.ToLower + ' deleted');
finally
lAR.Free;
end;
end;
destructor TMVCActiveRecordController.Destroy;
begin
inherited;
end;
{ TObjectListSetHolder }
constructor TMVCActiveRecordListResponse.Create(AList: TMVCActiveRecordList; AOwns: Boolean = True);
begin
inherited Create;
FOwns := AOwns;
FList := AList;
FMetadata := TMVCStringDictionary.Create;
end;
destructor TMVCActiveRecordListResponse.Destroy;
begin
if FOwns then
begin
FList.Free
end;
FMetadata.Free;
inherited;
end;
end.