stablekernel / aqueduct

Dart HTTP server framework for building REST APIs. Includes PostgreSQL ORM and OAuth2 provider.
https://aqueduct.io
BSD 2-Clause "Simplified" License
2.41k stars 279 forks source link

How I can document endpoint with nested ManagedObjects? #678

Open vladlaba opened 5 years ago

vladlaba commented 5 years ago

Hi Aqueduct team,

I have a User ManagedObject that relates to the RoleType one (belongs-to):

class User extends ManagedObject<_User>
    implements _User, ManagedAuthResourceOwner<_User> {
  @Serialize(input: true, output: false)
  String password;
}

@Table(name: 't_user')
class _User extends ResourceOwnerTableDefinition {
  @Column(nullable: true)
  String fullName;

  @Relate(#users, isRequired: true, onDelete: DeleteRule.setDefault)
  RoleType role;
}
class RoleType extends ManagedObject<_RoleType> implements _RoleType{}

@Table(name: 't_role_type')
class _RoleType extends TypeTableDefinition {
  ManagedSet<User> users;
}

And I have an endpoint that responses with User:

class UserController extends ResourceController {
  UserController(this.context, this.authServer);

  final ManagedContext context;
  final AuthServer authServer;

  @Operation.get()
  Future<Response> getUserInfo() async {
    final id = request.authorization.ownerID;

    final userQuery = Query<User>(context)
      ..where((user) => user.id).identifiedBy(id)
      ..join(object: (user) => user.role);

    final User user = await userQuery.fetchOne();

    if (user == null) {
      return Response.notFound();
    }

    return Response.ok(user);
  }

  @override
  Map<String, APIResponse> documentOperationResponses(
      APIDocumentContext context, Operation operation) {
    if (operation.method == "GET") {
      return {
        "200": APIResponse.schema('OK', context.schema.getObjectWithType(User))
      };
    }

    return null;
  }
}

Here is endpoint response where Role has all fields populated.

{  
   "id":3,
   "fullName":"FName SName",
   "username":"testmail@gmail.com",
   "role":{  
      "id":1,
      "name":"Supervisor"
   }
}

After running aqueduct document the schema for the User object does not contain reference to the RoleType one. Instead, the 'role' field contains the object with 'id' only:

"User": {
        "title": "User",
        "type": "object",
        "properties": {
          "password": {
            "title": "password",
            "type": "string",
            "nullable": false,
            "writeOnly": true
          },
          "fullName": {
            "title": "fullName",
            "type": "string",
            "nullable": true
          },
          "id": {
            "title": "id",
            "type": "integer",
            "description": "This is the primary identifier for this object.\n",
            "nullable": false
          },
          "username": {
            "title": "username",
            "type": "string",
            "description": "No two objects may have the same value for this field.\n",
            "nullable": false
          },
          "role": {
            "title": "role",
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              }
            }
          },
          "tokens": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagedAuthToken"
            },
            "nullable": true,
            "readOnly": true
          }
        },
        "description": ""
      },

I wonder is it possible to document the User in such way that the result of document generation will contain the User with 'role' field as a reference to the RoleType scheme?

There is a possible workaround for this situation - we can override documentComponents method for our application channel and register User with fields we want:

@override
  void documentComponents(APIDocumentContext registry) {

    registry.schema.register(
        "User",
        APISchemaObject.object({
          "id": APISchemaObject.integer(),
          "fullName": APISchemaObject.string(),
          "username": APISchemaObject.string(),
          "role": APISchemaObject.object({
            "id": APISchemaObject.integer(),
            "name": APISchemaObject.string(),
          }),
        }),
        representation: User);

    super.documentComponents(registry);
  }

But this approach is seemed to be complicated especially if we have a lot of endpoints that need to be handled in such way.

itsjoeconway commented 5 years ago

This discussed in #663 as well - clearly the framework needs to provide a solution. I am not sure what it is yet.

One option is to override your ManagedObject subclass' documentSchema method (this impacts every endpoint that exposes User).

class User extends ManagedObject<_User>
    implements _User, ManagedAuthResourceOwner<_User> {
  @override
  APISchemaObject documentSchema(APIDocumentContext context) {
    final object = super.documentSchema(context);
    object.properties["role"] = context.schema.getObjectWithType(RoleType)
    return object;
  }
}

The missing piece appears to be an easy-to-use, endpoint-specific adjustment of a schema object. There's two things to think about - how should this modified object appear in the OpenAPI doc and what is the Dart API for that modification?

For the OpenAPI doc, would it make sense for there to be a schema component for each permutation? That is, you will end up with a User schema component and a UserWithRole schema component. Is there a better way than this and/or will this generate decent client code?

For the Dart API in Aqueduct - sounds like having a cloning method for schema objects would be helpful? I've also wondered if the documentOperationResponses method is too cumbersome - and if there is a better way there?