You are not logged in.
Pages: 1
I found another problem with a different OpenAPI specification that uploads and downloads files.
<script src="https://gist.github.com/georgkeller/11a5730d89d69c4815745b81ac6cb908.js"></script>
In this spec, the parameters are submitted in the request body via multipart/form-data
"/api/Conversion/upload": {
"post": {
"tags": [
"Conversion"
],
"summary": "Uploads document to a conversion service.",
"description": "Note: conversion times might not be constant. Returns ID that you will need to download the document again.",
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"required": [
"Document",
"SourceFormat",
"TargetFormat"
],
"type": "object",
"properties": {
"Document": {
"type": "string",
"description": "Uploaded document to be parsed/converted",
"format": "binary"
},
"SourceFormat": {
"$ref": "#/components/schemas/DocumentFormatEnumDTO"
},
"TargetFormat": {
"$ref": "#/components/schemas/DocumentFormatEnumDTO"
}
}
},
"encoding": {
"Document": {
"style": "form"
},
"SourceFormat": {
"style": "form"
},
"TargetFormat": {
"style": "form"
}
}
}
}
},
But in the pascal code, this parameter is missing:
// [post] /api/Conversion/upload
//
// Summary: Uploads document to a conversion service.
// Description:
// Note: conversion times might not be constant. Returns ID that you will need to download the document again.
//
// Responses:
// - 200 (main): Document uploaded successfully
// - 400: Invalid request DTO
// - 500: Internal server error
function PostApiConversionUpload(): RawUtf8;
It seems to me that the OpenAPI conversion does not work for this kind of content.
I checked the response when calling the endpoint with Postman, I get this:
"owner": {
"objectType": "Organization",
"id": "test-id",
"created": "2023-03-27T07:45:50.393Z",
"authorId": "author-id",
"name": "xy GmbH",
"type": "company",
"description": ""
},
This corresponds to the TOrganization type that was already created in the conversion:
TOrganization = packed record
// The unique ID of the object. If an ID is not specified when the object is created,
// the system generates an ID.
Id: TGuid;
// The date and time (ISO 8601) when the object was created.
// - Example: "1999-01-01T00:01:02.345Z"
Created: RawUtf8;
// The ID of the user who created the object.
AuthorId: TGuid;
// The date and time (ISO 8601) when the object was last modified.
// - Example: "2019-02-02T01:02:03.456Z"
Modified: RawUtf8;
// The ID of the user who last modified the object.
EditorId: TGuid;
// The type of the license owner.
ObjectType: RawUtf8;
// The name of the organization.
Name: RawUtf8;
// The type of the organization.
_Type: RawUtf8;
// The description of the organization.
Description: RawUtf8;
end;
POrganization = ^TOrganization;
The TUserProfile record is also created in the conversion, but both TOrganization and TUserProfile are not used in the client code because the conversion of "oneOf" is not working.
For comparison, I tried the conversion in Rust and in C++. In Rust, the structure is preserved and an enum with two elements (UserProfile and Organization) is created:
pub enum Owner {
#[serde(rename="User")]
User(Box<models::UserProfile>),
#[serde(rename="Organization")]
Organization(Box<models::Organization>),
}
In C++, the structure is flattened and an Owner class is created that has all the properties of UserProfile and of Organization (which is clunky but workable):
class LicenseOwner{
public:
...
private:
std::string objectType{};
std::string id{};
std::string created{};
std::string authorId{};
std::string modified{};
std::string editorId{};
std::string displayName{};
std::string email{};
std::string firstName{};
std::string lastName{};
std::string validFrom{};
std::string validUntil{};
std::string lastSignOn{};
std::string nickname{};
std::string picture{};
std::string phoneNumber{};
std::string professionalTitle{};
std::string name{};
std::string type{};
std::string description{};
};
I checked the spec file, the Owner structure is exactly as it is defined in the spec. I validated the spec file with the opentapi-generator-cli tool and there were no errors, so such a construction seems to be valid.
But I will check with the creator of the spec file if this section is defined as intended.
I found another problem with the OpenAPI conversion.
In the spec file, there is the following section:
"Owner" : {
"type" : "object",
"description" : "The owner; an organization or a user.",
"readOnly" : true,
"required" : [ "name", "objectType" ],
"properties" : {
"objectType" : {
"type" : "string",
"description" : "The type of the owner."
}
},
"oneOf" : [ {
"$ref" : "#/components/schemas/UserProfile"
}, {
"$ref" : "#/components/schemas/Organization"
} ],
"discriminator" : {
"propertyName" : "objectType",
"mapping" : {
"User" : "#/components/schemas/UserProfile",
"Organization" : "#/components/schemas/Organization"
}
}
},
This converts to the following type in the dto file:
// from #/components/schemas/Owner
TOwner = packed record
// The type of the owner.
ObjectType: RawUtf8;
end;
POwner = ^TOwner;
The part after the "oneOf" definition in the specification is missing in the conversion.
Thanks, the change for the Guid already helps a lot.
The rest is nice to have, but not essential.
Hello,
I found another caveat with the OpenAPI conversion.
In the following specification for an endpoint, there is an optional query parameter and some header parameters with a default value:
"/users/{userId}/organization-groups": {
"get": {
"tags": [
"user"
],
"summary": "List a user's user groups",
"description": "Retrieve the user groups that a user belongs to. You can also limit the query to a specified organization's user groups.",
"operationId": "listOrganizationGroupsOfUser",
"parameters": [
{
"name": "offset",
"in": "header",
"description": "The index of the first user group to include in the results. Indexing is done in the sorting order.",
"schema": {
"type": "integer",
"format": "int64",
"default": 0
}
},
{
"name": "limit",
"in": "header",
"description": "The maximum number of user groups to be returned.",
"schema": {
"type": "integer",
"format": "int32",
"default": 25
}
},
{
"name": "order",
"in": "header",
"description": "The name of the field based on which results are sorted.",
"schema": {
"type": "string",
"default": "name"
}
},
{
"name": "asc",
"in": "header",
"description": "The sorting order of the results: \"true\" = ascending, \"false\" = descending.",
"schema": {
"type": "boolean",
"default": true
}
},
{
"name": "userId",
"in": "path",
"description": "The ID of the user.",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "organizationId",
"in": "query",
"description": "The organization whose user groups to retrieve.",
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/OrganizationGroupOfUser"
}
}
}
}
},
"400": {
"description": "Bad request",
"content": {}
},
"401": {
"description": "Unauthorized",
"content": {}
},
"403": {
"description": "Forbidden",
"content": {}
},
"404": {
"description": "Resource not found",
"content": {}
},
"500": {
"description": "Internal server error",
"content": {}
}
}
},
The resulting pascal code is:
function TAPIClient.ListOrganizationGroupsOfUser(const UserId: TGuid;
const OrganizationId: TGuid; Offset: Int64; Limit: integer; const Order: RawUtf8;
Asc: boolean): TOrganizationGroupOfUserDynArray;
begin
fClient.Request('GET', '/users/%/organization-groups', [ToUtf8(UserId)], [
'organizationId', ToUtf8(OrganizationId)], [
'offset', Offset,
'limit', Limit,
'order', Order,
'asc', Asc],
result, TypeInfo(TOrganizationGroupOfUserDynArray));
end;
In the specification, only the UserId is required, all other parameters are optional or have a default value.
One problem is now that all parameters have to be provided (even if there is a default value).
An even bigger problem is that you have to provide an OrganizationId, there is no possibility to use the request without providing this parameter.
Without the OrganizationId, the call returns all the organization groups of all organizations of the user.
To sum it up, the request should also work like this (without the optional parameters):
fClient.Request('GET', '/users/%/organization-groups', [ToUtf8(UserId)], [], [], result, TypeInfo(TOrganizationGroupOfUserDynArray));
Hello,
it works now, thanks for your quick corrections .
Hello,
I have two small problems with the OpenAPI conversion.
1. For the following spec file https://gist.github.com/georgkeller/325 … 53dd3dc2fd,
the conversion creates the function:
function TTenDukeIDPClient.GetUser(const UserId: TGuid): TUser;
begin
fClient.Request('GET', '/users/%', [ToUtf8(UserId)], [], [],
result, TypeInfo(TUser));
end;
The endpoint for the request begins with a slash (/user).
In the constructor of the TJsonClient, the base uri gets a trailing slash:
fBaseUri := IncludeTrailingUriDelimiter(aBaseUri);
This results in a double slash in the uri and in a http 404 error.
2. In another spec file (which I cannot post unfortunately), the conversion results in the following function definition:
function ListClients(Offset: Int64 = 0; Limit: integer = 0; const Order: RawUtf8 = '';
Asc: boolean = false): variantDynArray;
The return type variantDynArray is unknown, I think it should be TvariantDynArray instead.
The generation of the client code now works and I was able to use the client code to interact with the api.
Thanks for your quick corrections and for your OpenAPI converter, great work .
Thanks for your analysis.
The API definition was generated by a webservice written in Rust (using the poem_openapi crate).
The order of the types in the spec is not the same as the order of their definitions in the source code, so I would assume that the order is decided in the poem_openapi crate.
The usage of "charset=utf-8" in the spec seems to originate in the poem_openapi library, I have not yet found a way to change that.
I checked my API with two validation tools, it is a valid OpenAPI specification.
Hello,
first of all, thanks for the new OpenAPI generator. It's really great that there is a fully open source OpenAPI generator for Pascal/Delphi now.
I have a small project with an OpenAPI specification that unfortunately causes a stack overflow in the mORMot OpenAPI generator, the spec file is here:
https://gist.github.com/georgkeller/ad9 … d913081301
I used the following call (the exe was compiled with Delphi 12.2 for Win64):
.\mopenapi.exe .\testapi.json Test
The code generation for other languages with the opencollective openapi_generator works (tested with go, rust, javascript).
Thank you very much for your hints, that helped a lot :-).
Hello,
I built an interfaced based service based on the example 14 with a windows client and everything works fine.
Now I'm trying to create an android client for this service. I exchanged the mormot.pas with the corresponding crossplatform units, but I'm stuck on how to proceed.
In the class TSQLRestClientHTTP in the crossplatform units, methods like:
FClient.ServiceDefine([IServerMethods], sicPerUser, SERVER_CONTRACT);// sicShared);
FClient.Services['ServerMethods'].Get(ServerMethods);
are not defined.
So how do I get the interface methods on the client side? Is it necessary to change/amend the server side or is it sufficient to make changes on the client side? I searched the forum and the documentation and checked out the example 27 on crossplatform clients, but it didn't really clarify things for me.
Any help would be appreciated.
Thanks in advance,
Georg
Thank you very much for your help and the hint with InitializeTable.
I didn't think that such a cast would be allowed.
Best regards
Thanks for your answer, unfortunately, this does not work (it results in a compiler error "incompatible types" in Delphi).
AuthUser.GroupRights is of the type TSQLAuthGroup, not TID.
Best regards
Hello,
I used the sample 14 (interfaced based services, Project14ServerExternal) and tried to add a new user at the startup of the server like this:
function CreateUser(const ServerL: TSQLRestServer; const strLogon, strDisplay, strPassword: String): Boolean;
var AuthUser: TSQLAuthUser;
AuthGroup: TSQLAuthGroup;
begin
AuthGroup := TSQLAuthGroup.Create(ServerL, 'Ident=''User''');
AuthUser := TSQLAuthUser.Create(ServerL, 'LogonName=''' + StringToUTF8(strLogon) + '''');
try
if Assigned(AuthUser) and (AuthUser.ID > 0) then
Result := False
else
begin
if not Assigned(AuthUser) then
AuthUser := TSQLAuthUser.Create;
AuthUser.LogonName := StringToUTF8(strLogon);
AuthUser.DisplayName := StringToUTF8(strDisplay);
AuthUser.PasswordPlain := StringToUTF8(strPassword);
AuthUser.GroupRights := AuthGroup;
ServerL.Add(AuthUser, True);
Result := True;
end;
finally
AuthUser.Free;
AuthGroup.Free;
end;
end;
I put the call to this function in the server programm directly after CreateMissingTables.
What I expected was that this would create a new user that is part of the group "User".
When I check in the sqlite database, the new user has GroupRights=49224560 in the AuthUser table, but I would have expected GroupRights=3 (since the new user should be in the "User" group with Id=3).
This new user is not able to call the interface methods, only when I change the GroupRights to 3 manually does the user account work as expected.
Thanks in advance for your help.
Thanks for your answer. Unfortunately, I don't really know how to proceed.
You can make custom download - using TServiceCustomAnswer custom record type.
If you supply STATICFILE_CONTENT_TYPE constant as output header, you can let http.sys download the file in chunks.
Can you elaborate on that? Oder maybe point me to an example or the part of the documentation where this is described?
Does this work with the TSQLHttpServer that I use in my interface-based approach?
Hello,
I'm new to mORMot (btw, thanks for the nice talks about mORMot at EKON20 :-)) and I'm now trying to convert a datasnap based client server application to mORMot.
For client server services, I use an interface (as in sample 14 with a TSQLHttpServer).
So far this worked well. But now, I need to upload and download big files (up to 1 GB) between server and client.
I checked out the other threads concerning this topic, but I'm still not sure how to do it.
In the datasnap code, I had a download and upload method like this (FileName is the name on the server, ServerType is for getting the right path on the server):
procedure UploadFile(str: TStream; ServerType: string; FileName: string);
function DownloadFile(ServerType: string; FileName: string): TStream;
The data transfer was then done via the TStream object (with progress indicator in the client).
This approach is not possible out of the box in mORMot, I get an error because TStream cannot automatically be serialized.
I tried to do the transfer via a RawByteString object (as described in another thread here in the forum), but due to the size of the files, the memory consumption is to high.
I also read about doing this via a method based service, but I'm not sure if this can be mixed with my interface based service (I would rather like to stay with the interface based approach if possible).
The client is a delphi client, but later on it may also be a web (AJAX) client, so the solution should work in both cases.
What would be the best way to implement this upload and download with mORMmot?
Thanks in advance,
Georg
Pages: 1