delphimvcframework/sources/MVCFramework.ActiveRecordController.pas

499 lines
16 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, MVCFramework.Swagger.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;
function GetURLSegment: String;
[MVCPath('/($entityname)')]
[MVCHTTPMethod([httpGET])]
[MVCSwagSummary(TSwaggerConst.USE_DEFAULT_SUMMARY_TAGS, 'Retrieve a list of {singularmodel}', 'Get{pluralmodel}')]
[MVCSwagResponses(HTTP_STATUS.OK, 'List of {singularmodel}', SWAGUseDefaultControllerModel, True)]
[MVCSwagResponses(HTTP_STATUS.BadRequest, '', TMVCErrorResponse)]
[MVCSwagParam(TMVCSwagParamLocation.plQuery, 'rql', 'RQL filter used to filter the list of {singularmodel}', SWAGUseDefaultControllerModel, TMVCSwagParamType.ptString, False)]
procedure GetEntities(const entityname: string); virtual;
2018-09-27 12:26:50 +02:00
[MVCPath('/($entityname)/searches')]
[MVCHTTPMethod([httpGET])]
[MVCSwagSummary(TSwaggerConst.USE_DEFAULT_SUMMARY_TAGS, 'Searches through {pluralmodel} and returns a list of {singularmodel}', 'Get{pluralmodel}BySearch')]
[MVCSwagResponses(HTTP_STATUS.OK, 'List of {singularmodel}', SWAGUseDefaultControllerModel, True)]
[MVCSwagResponses(HTTP_STATUS.BadRequest, '', TMVCErrorResponse)]
[MVCSwagParam(TMVCSwagParamLocation.plQuery, 'rql', 'RQL filter used to filter the list of {singularmodel}', SWAGUseDefaultControllerModel, TMVCSwagParamType.ptString, False)]
2018-09-27 12:26:50 +02:00
procedure GetEntitiesByRQL(const entityname: string); virtual;
[MVCPath('/($entityname)/searches')]
[MVCHTTPMethod([httpPOST])]
[MVCSwagSummary(TSwaggerConst.USE_DEFAULT_SUMMARY_TAGS, 'Searches through {pluralmodel} and returns a list of {singularmodel}', 'Get{pluralmodel}BySearchAsPOST')]
[MVCSwagResponses(HTTP_STATUS.OK, 'List of {singularmodel}', SWAGUseDefaultControllerModel, True)]
[MVCSwagResponses(HTTP_STATUS.BadRequest, '', TMVCErrorResponse)]
[MVCSwagParam(TMVCSwagParamLocation.plQuery, 'rql', 'RQL filter used to filter the list of {singularmodel}', SWAGUseDefaultControllerModel, TMVCSwagParamType.ptString, False)]
procedure GetEntitiesByRQLwithPOST(const entityname: string); virtual;
[MVCPath('/($entityname)/($id)')]
[MVCHTTPMethod([httpGET])]
[MVCSwagSummary(TSwaggerConst.USE_DEFAULT_SUMMARY_TAGS, 'Gets a {singularmodel} entity or 404 not found', 'Get{singularmodel}ByID')]
[MVCSwagResponses(HTTP_STATUS.OK, 'One {singularmodel}', SWAGUseDefaultControllerModel)]
[MVCSwagResponses(HTTP_STATUS.NotFound, 'Error', TMVCErrorResponse)]
[MVCSwagResponses(HTTP_STATUS.BadRequest, '', TMVCErrorResponse)]
procedure GetEntity(const entityname: string; const id: Integer); virtual;
[MVCPath('/($entityname)')]
[MVCHTTPMethod([httpPOST])]
[MVCSwagSummary(TSwaggerConst.USE_DEFAULT_SUMMARY_TAGS, 'Creates a {singularmodel} and returns a new id', 'Create{singularmodel}')]
[MVCSwagResponses(HTTP_STATUS.Created, 'One {singularmodel}', '')]
[MVCSwagResponses(HTTP_STATUS.NotFound, 'Error', TMVCErrorResponse)]
[MVCSwagResponses(HTTP_STATUS.BadRequest, '', TMVCErrorResponse)]
[MVCSwagParam(TMVCSwagParamLocation.plBody, '{singularmodel}', 'A single entity of type {singularmodel}', SWAGUseDefaultControllerModel, TMVCSwagParamType.ptString, True)]
procedure CreateEntity(const entityname: string); virtual;
[MVCPath('/($entityname)/($id)')]
[MVCHTTPMethod([httpPUT])]
[MVCSwagSummary(TSwaggerConst.USE_DEFAULT_SUMMARY_TAGS, 'Updates a {singularmodel} by id', 'Update{singularmodel}ByID')]
[MVCSwagResponses(HTTP_STATUS.OK, 'One {singularmodel}', SWAGUseDefaultControllerModel)]
[MVCSwagResponses(HTTP_STATUS.NotFound, 'Error', TMVCErrorResponse)]
[MVCSwagResponses(HTTP_STATUS.BadRequest, '', TMVCErrorResponse)]
[MVCSwagParam(TMVCSwagParamLocation.plBody, '{singularmodel}', 'A single entity of type {singularmodel}', SWAGUseDefaultControllerModel, TMVCSwagParamType.ptString, True)]
procedure UpdateEntity(const entityname: string; const id: Integer); virtual;
[MVCPath('/($entityname)/($id)')]
[MVCHTTPMethod([httpDELETE])]
[MVCSwagSummary(TSwaggerConst.USE_DEFAULT_SUMMARY_TAGS, 'Deletes a {singularmodel} by id', 'Delete{singularmodel}ByID')]
[MVCSwagResponses(HTTP_STATUS.NoContent, '')]
[MVCSwagResponses(HTTP_STATUS.NotFound, 'Error', TMVCErrorResponse)]
[MVCSwagResponses(HTTP_STATUS.BadRequest, '', TMVCErrorResponse)]
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;
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
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);
begin
GetEntities(entityname);
end;
procedure TMVCActiveRecordController.GetEntitiesByRQLwithPOST(const entityname: string);
2018-09-27 12:26:50 +02:00
var
lRQL: string;
lJSON: TJsonObject;
begin
lJSON := TJsonObject.Parse(Context.Request.Body) as TJsonObject;
try
if Assigned(lJSON) then
begin
lRQL := lJSON.s['rql'];
end
else
begin
lRQL := '';
2018-09-28 13:01:46 +02:00
end;
finally
lJSON.Free;
2018-09-27 12:26:50 +02:00
end;
Context.Request.QueryStringParams.Values['rql'] := lRQL;
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;
function TMVCActiveRecordController.GetURLSegment: String;
begin
Result := fURLSegment;
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.