Master Detail data in RAD Server using TEMSDataSetResource

Master Detail data in RAD Server

The TEMSDataSetResource is a very powerful component that enables rapid development of full document REST API’s for TDataSet using RAD Server. Using TEMSDataSetResource, along with traditional master detail relationship configurations, it is possible to expose, and automatically document data APIs via REST with no code at all.

In this article, I will cover sharing master detail data with no code, but also how to roll your own REST endpoint to cover more advanced detail with detail embedded calls.

In my previous article, I updated advise on getting started with Swagger UI, using the new WebFiles feature of RAD Server (from 10.3.2) as a way to view your documentation as you build your backend services API. This article will build upon the sample application created in that post.

Master Details Data Basics

The basics of Master Detail in RAD Studio / Delphi / C++Builder have not change since Delphi 1.

  • You have two datasets, master and detail
  • A TDataSource is connected to the master
  • The detail uses the TDataSource to identify  where to read the current record from
  • The detail sets up the master field(s)

This setup ensures that as the master is navigated, the detail is filtered to the current records matching the key from the master.

(Need to see more… watch from 2:20 in the video)

Master Detail Data via REST

To expose the data as a REST API’s requires three to four steps.

  1. Connecting a TEMSDataSetResource – this converts the dataset into a REST consumable resource.

    TEMSDataSetResource linked to a Dataset with Allowed Actions defined for List and Get
  2. Definition of the endpoint to call to access the resource. – This is done via the ResourceSuffix Attribute. The below example shows three end points used in the video above that belong to the Resource exams.
    [ResourceName('exams')]
    TExamResource1 = class(TDataModule)[ResourceSuffix('./')]
    [ResourceSuffix('List', './')]
    [ResourceSuffix('Get', './{EXAM_ID}')]
    [ResourceSuffix('Put', './{EXAM_ID}')]
    [ResourceSuffix('Delete', './{EXAM_ID}')]
    dsrEXAM: TEMSDataSetResource;
    
    [ResourceSuffix('./{EXAM_ID}/questions')]
    [ResourceSuffix('List', './')]
    [ResourceSuffix('Get', './{QUESTION_ID}')]
    [ResourceSuffix('Put', './{QUESTION_ID}')]
    [ResourceSuffix('Delete', './{QUESTION_ID}')]
    dsrQuestions: TEMSDataSetResource;
    
    [ResourceSuffix('./{EXAM_ID}/questions/{QUESTION_ID}/answers')]
    [ResourceSuffix('List', './')]
    [ResourceSuffix('Get', './{ANSWER_ID}')]
    [ResourceSuffix('Put', './{ANSWER_ID}')]
    [ResourceSuffix('Delete', './{ANSWER_ID}')]
    dsrAnswers: TEMSDataSetResource;
    
    

    The first ResourceSuffix shows the endpoint to call. Named parameters are then showing what is appended to that call to make specific item requests. e.g. Assuming there is an exam 1 and question 2 – The following end points become available.

    /exams/ List of exams
    /exams/1/ Exam 1
    /exams/1/questions/ List of questions for Exam 1
    /exams/1/questions/2/ Question ID 2 (part of Exam 1)
    /exams/1/questions/2/answers/ Answers for Question 2
  3. Once the end points are defined, link the key fields to the endpoint to enable the parameters to apply at runtime.
  4. Optionally,  define the documentation – rapidly done via attributes. (covered previously).  – Adding attributes for each parameter listed in the ResourceSuffix. Anything in the base ResourceSuffix should be mark as required.

Rolling your own custom REST end point with detailed detail

While the components are great, there may be a time you want to embed detail inside the original API call. e.g. getting a customer with their active orders (rather than making two api calls.

For the example at hand, this is true of the exam Questions. Each question has 2 to 4 answers. As one exam has 90 questions, this is 90 API calls that can be removed by including the data in the initial call.

To do this, you can write a custom Get method. This follows the similar practices to before, with the same attributes being used.

[hint] To rapidly start this, it might be simplest to create a new Resource from File > New > Other, and then choosing RAD Server package from under RAD Server menu. Follow the wizard and choose the Sample End Points to create. You can then modify those.

Defining the REST API end point

[ResourceSuffix('./{EXAM_ID}/questionsfull/')]

[EndPointRequestSummary('Exams', 'Exam Questions and Answers', 'Retrieves list of Questions for an exam, with the multiple-choice answers.', 'application/json', '')]

[EndPointRequestParameter(TAPIDocParameter.TParameterIn.Path, 'EXAM_ID', 'EXAM_ID from the /exams/ end point', true, TAPIDoc.TPrimitiveType.spInteger,
TAPIDoc.TPrimitiveFormat.None, TAPIDoc.TPrimitiveType.spInteger, '', '')]

[EndPointResponseDetails(200, 'Ok', TAPIDoc.TPrimitiveType.spObject, TAPIDoc.TPrimitiveFormat.None, '', '')]

procedure Get(const AContext: TEndpointContext;
          const ARequest: TEndpointRequest; 
          const AResponse: TEndpointResponse);

The above example shows the end point /exams/1/questionsfull/. The resource name (‘exams’) from the datamodule is again used as the root, with the ResourceSuffix defining the end point.

Building the REST data packet manually

To export the data, the following steps need to be taken

  1. Get the Parameter being passed in
  2. Set the dataset parameter and open the dataset (trapping any errors from bad parameters)
  3. Build the response JSON

Get the Parameter being passed in

 LItem := ARequest.Params.Values['EXAM_ID'];

Getting the parameter is as easy as asking for it by Value. This returns a string value.

Set the query parameter and open the query.

qryQuestions.Close;
try
  qryQuestions.ParamByName('EXAM_ID').AsString := LItem;
  qryQuestions.Open();
  qryAnswers.Open();
except
  // This raises the default not found error
  AContext.Response.RaiseNotFound('','Invalid   EXAM_ID');
  Exit;
end;

It is important to trap for a bad parameter. The end user can push in anything, so if you are expecting an Integer, you could get a string!

Build the response JSON

Now, it’s time to build the output. This is simple thanks to the JSONWriter that is part of the AResponse.Body. e,g,

AResponse.Body.JSONWriter.WriteStartArray;
AResponse.Body.JSONWriter.WriteStartObject;
AResponse.Body.JSONWriter.WritePropertyName('foo');
AResponse.Body.JSONWriter.WriteValue('bar');
AResponse.Body.JSONWriter.WriteEndObject;
AResponse.Body.JSONWriter.WriteEndArray;

In the video,  I loop the fields from the dataset to build the output, before looping the current detail and adding it in as a named array called answers, while ignoring the master field from the detail (as its explicit already) Watch now (from 16:15)

AResponse.Body.JSONWriter.WriteStartArray;
// Sample code
qryQuestions.First;
while not qryQuestions.Eof do begin
  // Build Question
  AResponse.Body.JSONWriter.WriteStartObject;
  ConvertRecordToJSONObject(qryQuestions);

  // Answers for the question
  AResponse.Body.JSONWriter.
    WritePropertyName('ANSWERS');
  AResponse.Body.JSONWriter.WriteStartArray;
  qryAnswers.First;
  while not qryAnswers.Eof do begin
    AResponse.Body.JSONWriter.WriteStartObject;
    ConvertRecordToJSONObject(qryAnswers,     
       'QUESTION_ID');
    AResponse.Body.JSONWriter.WriteEndObject;
    qryAnswers.Next;
  end;
  AResponse.Body.JSONWriter.WriteEndArray;
  AResponse.Body.JSONWriter.WriteEndObject;
  qryQuestions.Next;
end;
AResponse.Body.JSONWriter.WriteEndArray;

The above code uses an inline procedure (declared after var, and before begin in the Get procedure. This loops the data set and put the field name and value into the current JSON object.

procedure TExamResource1.Get(
  const AContext: TEndpointContext;
  const ARequest: TEndpointRequest; 
  const AResponse: TEndpointResponse);
var
  LItem: string;

procedure ConvertRecordToJSONObject(
  DataSet : TDataSet; 
  MasterField : string = '');
var
  FieldIdx : Integer;
begin
  for FieldIdx := 0 to DataSet.FieldCount -1 do
begin
  // Skip master field
  if (MasterField > '') and   
    (Uppercase(MasterField) =
     Uppercase(DataSet.Fields[FieldIdx].
     FieldName)) then
    Continue;

  // Add name value pairs
  AResponse.Body.JSONWriter.
    WritePropertyName(DataSet.Fields[FieldIdx].
      FieldName);
  AResponse.Body.JSONWriter.
    WriteValue(DataSet.Fields[FieldIdx].
      AsString);
  end;
end;

begin 
<main method code from above>
end;

The resulting output, then covers master detail in the same response.

 

 

Leave a Reply

Your email address will not be published. Required fields are marked *