(Whoops, I guess because I marked the last one as 'solved' I can't add to it anymore?)
Alright, a bit more fiddling and I've replaced the WebCore side of the example with the following functions and everything seems to work just as I want now. So, to recap, I was looking to have a service endpoint from XData return a TStream that I could then use within WebCore as a dataset without having to know anything about the fields ahead of time. As I'm using FireDAC, the JSON coming through via the TStream already has everything needed, just a matter of parsing it.
My "Main Menu" example now has three functions in the client. These first two can be put somewhere like in a utility function unit or something that isn't specific to a form:
function GetQueryData(Endpoint: String; Dataset: TXDataWebDataSet):Integer;
var
Conn: TXDataWebConnection;
Client: TXDataWebClient;
Response: TXDataClientResponse;
JFDBS: TJSObject;
JManager: TJSObject;
JTableList: TJSObject;
JColumnList: TJSArray;
JRowList: TJSArray;
StringFields: Array of TStringField;
IntegerFields: Array of TIntegerField;
// Likely need a few more datatypes here
i: Integer;
begin
// Value to indicate the request was unsuccessful
Result := -1;
// Setup connection to XData Server
Conn := TXDataWebConnection.Create(nil);
Conn.URL := DM1.CarnivalCoreServer; // could also be a parameter
Conn.OnRequest := PopulateJWT; // See below
Client := TXDataWebClient.Create(nil);
Client.Connection := Conn;
await(Conn.OpenAsync);
// Make the Request
// Likely to have another version of function that includes more endpoint parameters
try
Response := await(Client.RawInvokeAsync(Endpoint, ['FireDAC']));
except
on Error: Exception do
begin
Client.Free;
Conn.Free;
console.log('...something is amiss...');
exit;
end;
end;
// Process the FireDAC-specific JSON that was returned
JFDBS := TJSObject(TJSObject(TJSJson.Parse(string(Response.Result)))['FDBS']);
JManager := TJSObject(JFDBS['Manager']);
JTableList := TJSObject(TJSArray(JManager['TableList'])[0]);
JColumnList := TJSArray(JTableList['ColumnList']);
JRowList := TJSArray(JTableList['RowList']);
// Don't really want 'Original' in field names, so let's remove it from JSON first
// Probably a better one-liner, but this seems to work
for i := 0 to JRowList.Length - 1 do
JRowList.Elements[i] := TJSObject(JRowList.Elements[i])['Original'];
// We're assuming Dataset parameter is newly created and empty.
// First, add all the fields from JSON
// NOTE: Very likely more datatypes need to be added here
for i := 0 to JColumnList.Length-1 do
begin
if (String(TJSObject(JColumnList.Elements[i])['DataType']) = 'AnsiString') then
begin
// NOTE: Different datatypes may need different values set (eg: Size for strings)
SetLength(StringFields, Length(StringFields) + 1);
StringFields[Length(StringFields)-1] := TStringField.Create(Dataset);
StringFields[Length(StringFields)-1].FieldName := String(TJSObject(JColumnList.Elements[i])['Name']);
StringFields[Length(StringFields)-1].Size := Integer(TJSObject(JColumnList.Elements[i])['Size']);
StringFields[Length(StringFields)-1].Dataset := Dataset;
end
else if (String(TJSObject(JColumnList.Elements[i])['DataType']) = 'Int32') then
begin
SetLength(IntegerFields, Length(IntegerFields) + 1);
IntegerFields[Length(IntegerFields)-1] := TIntegerField.Create(Dataset);
IntegerFields[Length(IntegerFields)-1].FieldName := String(TJSObject(JColumnList.Elements[i])['Name']);
IntegerFields[Length(IntegerFields)-1].Dataset := Dataset;
end
else
begin
console.log('ERROR: Field ['+String(TJSObject(JColumnList.Elements[i])['Name'])+'] has an unexpected datatype ['+String(TJSObject(JColumnList.Elements[i])['DataType'])+']');
end;
end;
// Add the data and return the dataset as opened
Dataset.SetJSONData(JRowList);
Dataset.Open;
// Just for fun
Result := Dataset.RecordCount;
// No dataset stuff to free as the dataset was created by the caller and
// all the fields created were created with that dataset as the parent
Client.Free;
Conn.Free;
end;
procedure PopulateJWT(Args: TXDataWebConnectionRequest);
begin
Args.Request.Headers.SetValue('Authorization', JWT-From-Somewhere);
end;
With those out of the way, the previous RefreshMainMenu function is then much simpler, primarily just processing the dataset and not having to think about how it was retrieved:
procedure RefreshMainMenu;
var
i: Integer;
MenuGroup: TTreeNode;
MenuGroupName: String;
MenuDataset: TXDataWebDataSet;
MenuRecords: Integer;
begin
MenuDataset := TXDataWebDataset.Create(nil);
MenuRecords := await(GetQueryData('ISomethingService.EndpointName', MenuDataset));
if (MenuRecords = -1) then
begin
console.log('...Menu refresh not working...');
exit;
end;
MenuDataset.First;
MenuGroupName := '';
MainMenuIndex.Text := '';
MainMenuSearch.Text := '';
treeMainMenu.BeginUpdate;
treeMainMenu.Items.Clear;
i := 0;
while not(MenuDataset.EOF) do
begin
if (MenuDataset.FieldByName('GRP').AsString <> MenuGroupName) then
begin
MenuGroupName := MenuDataset.FieldByName('GRP').AsString;
MenuGroup := treeMainMenu.Items.Add(MenuGroupName);
SetLength(MainMenuNodes, i+1);
MainMenuNodes[i] := MenuGroup;
i := i + 1;
end;
SetLength(MainMenuNodes, i+1);
MainMenuNodes[i] := treeMainMenu.Items.AddChild(MenuGroup,MenuDataset.FieldByName('NAM').AsString);
i := i + 1;
MainMenuIndex.Add(RightStr('00000'+MenuDataset.FieldByName('REF').AsString,5)+':'+MenuDataset.FieldByName('NAM').AsString);
MainMenusearch.Add(RightStr('00000'+MenuDataset.FieldByName('REF').AsString,5)+':'+MenuDataset.FieldByName('NAM').AsString+';'+MenuDataset.FieldByName('DSC').AsString);
MenuDataset.Next;
end;
treeMainMenu.EndUpdate;
MenuDataset.Close;
MenuDataset.Free;
end;
So thanks everyone for your help and ideas!