Zendro-dev / graphql-server-model-codegen

Command line utility to auto-generate the structure files for a graphql server
MIT License
1 stars 2 forks source link

Implement generic models and generic association support #120

Open asishallab opened 4 years ago

asishallab commented 4 years ago

We want the user / programmer be able to write their own code to resolve data models and their associations. The specification is to treat generic models and generic associations separately. The resolvers and schema should largely remain unchanged, i.e. the default case generated by Cenzontle. The respective resolver functions should invoke so called "Impl" functions in the data models.

For further details on the spec see our markdown specification.

Also see the summary shared in our Slack channel written to give more details on the above (older) specification. See screenshot below.

asishallab commented 4 years ago

Summary and update taken from Slack communication

Distinguish between generic models and generic associations. A generic model has standard resolvers and a "standard" data model layer, except all data model layer functions throw a 'not implemented error'. A generic model can have our standard associations to_one and to_many using foreign-keys. The respective field resolvers and model functions would still be generated. Those that end up in the generic data model layer will throw a not implemented error. Additionally add support for generic_to_one and generic_to_many associations. If these are specified, and note, that they should even be usable inside the other non-generic models, the field resolvers still get generated as normal without expecting foreign-keys. So the line this.employer_id = input.addEmployer would not appear. Also these field resolvers only perform the authorization, validation of input, and record-limit diminishing, but invoke a "Impl" function in the data model layer. This is exactly as the version of Cenzontle before our refactoring. Every field resolver invokes a namesake in the model that is called the "Impl".

Methodology: Generic models are a must. This would be the refactoring done for our previous web-service storage type. In this case no generic associations are supported. But the standard to_one and to_many should ideally still work. Think about how much more work generic associations would be. Could we even make them available in non-generic data models? How would that work? Try to summarize your ideas in a software design using our standard pseudo-code specifications. You can write a new document or use the existing spec.

asishallab commented 4 years ago

In all read resolvers, i.e. root and field (associations), Cenzontle by default generates security and efficiency checks. These are:

  1. Check authorization to the requested model. That includes associated models, in the context of field resolvers.
  2. Diminish record limits. Currently this is done "automatically" because the field (association) resolver "forward" to the associated data model's root resolver. In the context of generic association this has to be added manually and has to be done before invoking the so called "Impl" function in the data model.

See the template for e.g. "standard" SQL data models for details on Cenzontle's default behaviour.

In the case of write operations, especially on associations adhere to our specification. Make sure the respective create and update root mutations still perform the security and efficiency checks as defined in the spec before invoking implementations in the data model.

asishallab commented 4 years ago

Consider the following example from our specification:

// root mutation (resolver)
// e.g. 
addPerson( input, context ) {
  let inputSanitized = sanitizeAssociationArguments( input, /* assocArgNames */ )
  // Security check
  checkAuthorization(...) // standard create permission on "Person" model as before
  checkAuthorizationOnAssocArgs( inputSanitized, context, associationArgsDef )
  // check if association args don't exceed record limit
  checkAndAdjustRecordLimitForCreateUpdate( inputSanitized, context, associationArgsDef )
  // check if association args actually point to existing IDs
  // MAKE SURE THAT IN CASE OF A REMOTE DATA MODEL (DDM, cenzontle-web-server, generic web-service)
  // that the association related args in input are ignored or removed, i.e. not processed.
  // MAKE SURE THE 
  validateAssociationArgsExistence( inputSanitized, context, 
  // HANDLE PERSON ATTRIBUTES
  // models.Person.addOne uses named function arguments, to
  // only receive Person scalar attributes
  let theNewBaby = models.Person.addOne( sanitizedInput )
  // In Case of the DDM the above line passes the input the a responsible adapter!
  // HANDLE ASSOCIATIONS
  theNewBaby.handleAssociations()
}

Note that because we do not expect generic associations to use foreign keys, maybe the above validateAssociationArgsExistence cannot be invoked? Check that. If it has to be omitted make the code generator write a "warning" code comment at that line, so the user knows what to add.

asishallab commented 4 years ago

A note on generic associations in distributed data models

Problem

Associations of distributed data models (DDMs) can be distributed as well. In that case the security and efficiency checks are more elaborated, because responsible adapters have to be found for the associated records' identifiers (see adapterForIri). However, generic associations only know two things from their definition, not more. Firstly the type of the associated record and secondly that this associated data model has some identifier attribute. Because generic associations don't guarantee the presence of foreign keys the above security and efficiency checks cannot be done in the standard way, simply because Cenzontle does not know where to get the associated records' identifiers from - again: there is no guaranteed foreign key.

Conclusion

Thus generic associations cannot be distributed in the way Cenzontle handles standard to_one or to_many distributed associations! While for our standard to_one and to_many associations the targetStorageType and keyIn arguments imply that there are different sub-types of these standard associations, this is not the case for generic associations.

A generic association is a generic association is a generic association..

Solution

Now, that this has been understood, we can define how generic associations should be implemented for DDMs. Basically, they stay exactly the same as in any other model.

The only noteworthy detail here is, that no code will be generated in the adapters. Generic associations will be handled directly in the DDM model module itself.

framirez07 commented 4 years ago

Implemented and merged with main refactoring branch. Reviewed with @vsuaste . Commits: 605da8d a2a59dc 51b09b3

asishallab commented 4 years ago

Fix error handling

Adapt error handling as in issue #123

In detail this means that the data model layer functions should receive a new argument benignErrorReporter and generate the JSDoc @parameter tag accordingly. This tag explains the programmer that the benignErrorReporter can be used to generate the standard GraphQL output { error: ..., data: ...}. If the function reportError of the benignErrorReporter is invoked the server will include any so reported errors in the final response, i.e. the GraphQL response will have a non empty errors property.

Root readers and writers

Functions in the data model layer affected by this are all root-reader and root-writer functions:

Because our default to_one and to_many associations invoke the associated data model's root reader the above already takes care of passing on the benignErrorReporter. Association writing is handled by the so called "infix" functions, which are the add_<Assoc> and remove_<Assoc> resolvers and their respective implementations. As long as generic models make use of these, the beignErrorReporter is thus already passed on to the data model layer as well.

Special association cases

All generic associations invoke "Impl-Functions" in the respective data model modules. This goes for reading and writing generic associations. In these cases the resolver needs to initialize a beignErrorReporter and pass it as an argument to the respective data model layer function invoked. These functions are the "Not Implemented Error"-functions. Here the same as above has to be done:

  1. Add the new argument benignErrorReporter
  2. Add JSDoc @parameter description of it

Use a single template for resolvers

Generic data models and generic associations do not require a separate template for the generated resolvers. Merge into a single template