Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reuse @constraint directive for adding entity field validations? #1

Closed
kristianmandrup opened this issue Nov 20, 2018 · 10 comments
Closed

Comments

@kristianmandrup
Copy link

Great project!!

Curious if we could reuse the @constraint directive graphql-constraint-directive for adding validation constraints to the generated TypeORM entities as well. Could be awesome! Write once, "validate everywhere" ;)

What would it take?

Looking at:

export const getColumns = pipe(
  path(["fields"]),
  filter(
    cond([
      [fieldIsPrimary, T],
      [path(["directives", consts.COLUMN]), T],
      [F, F]
    ])
  )
);

I would assume it requires adding a similar function

export const getConstraints = pipe(
  path(["fields"]),
  filter(
    cond([
      [fieldIsPrimary, T],
      [path(["directives", consts.CONSTRAINTS]), T],
      [F, F]
    ])
  )
);

And then incorporating the constraints in the output (templates for query and mutation?)
No experience with ramda however, so a bit hard to follow that code...

Cheers

@jjwtay
Copy link
Owner

jjwtay commented Nov 20, 2018

Thx. So first off I'm not an expert on TypeORM at all so take this as you will. As far as I can tell TypeORM does not handle validation at all, they offload that to a separate library:

http://typeorm.io/#/validation

In that document it seems to show that they don't even require validation even if you choose to decorate your fields it appears you still have to manually opt in on all transactions (or more realistically create your own method to enforce that behavior).

So keeping that in mind it would probably be pretty simple to do exactly what you are asking if you are willing to accept the need for creating your own validation enforcing method which it seems was the direction you were suggesting. Without double checking to the library that builds out that schema object from your example I think your code would need a very slight modification. Your code as is just returns all fields that are either Primary OR have a directive called constraints applied. You probably want to just return the fields that have the constraint. I'm more than happy to post the code if you'd like assuming I'm not missunderstanding something.

To wire it all up should be pretty straight forward from there. The library they suggest in their docs is actually just a decorator wrapper around https://github.com/chriso/validator.js but to be honest at this point seeing how type orm doesn't really enforce this the exact location in the pipeline that you would want to enforce that is debatable. Personally since it seems Type ORM doesn't enforce those validations I'd probably go at the resolvers directly either with middleware or a wrapper around the auto generated resolvers either way would be simple to incorporate and achieve the same thing.

@jjwtay
Copy link
Owner

jjwtay commented Nov 20, 2018

I also just realizing that the library you posted probably does all that for you already. So since typeorm doesn't seem to handle stuff directly as near as I can tell and it seems I just suggested what you library already does I'm not sure exactly a best course of action for that.

@kristianmandrup
Copy link
Author

kristianmandrup commented Nov 20, 2018

Thanks. I'm just trying to understand your code and see if I can built on it.

I'd like to generate the following kind of output, however I can't seem to find any template to generate an Entity.

@Entity()
export class Post {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    @Length(10, 20)
    title: string;
    ///...
}

What I have found so far:

export const getEntitySchemas = schema =>
  Object.keys(getEntities(schema)).map(
    entityKey => getEntitySchema(entityKey, schema[entityKey]),
    []
  );

Which is used in createConnection for the entities key, according to the example in the Readme

    const connection = createConnection({
        // ...
        entities: getEntitySchemas(jsSchema).map(schema => new EntitySchema(schema)),

Looking at entities option for connection here

entities - Entities to be loaded and used for this connection. Accepts both entity classes and directories paths to load from. Directories support glob patterns. Example: entities: [Post, Category, "entity/*.js", "modules/**/entity/*.js"]. Learn more about Entities.

So I don't quite see how that works with the nested map you are returning and how to incorporate the validation decorators?

To get the directive values, I was thinking something like this:

/** @type {function(Field) => { Directive } */
export const getFieldConstraints = path(["directives", consts.CONSTRAINT]);

Then in schema:

  const columns = getColumns(type);
  return {
    name: type.directives.Entity.name || name,
    columns: Object.keys(columns).reduce((columns, fieldName) => {
      const field = type.fields[fieldName];
      const constraints = getFieldConstraints(field);
      return {
        ...columns,
        [fieldName]: {
          constraints,
          ...field.directives,
          primary: isPrimary(fieldName, type),

But now I see you are already spreading field.directives, so no need for the special one for @contraints?

Hmm... would be nice to add the class validator using these decorators on update and create mutations. Something like the following (optional or configurable, ideally)

create: async (input: any) => {
    // lookup class name
    const Entity = entities[type]
   // create instance of class from input object
    const entityInstance = new Entity(input)
   // validate instance
    const errors = await validate(entityInstance)
   // save instance if no errors, otherwise signal validation error (both pluggable handlers)
    !errors ? save(post) : validationError(Entity, input, errors)
}

@kristianmandrup
Copy link
Author

Ah, looks like some docs in this issue

// Post.json
{
    "entity": "./Domain/Post.js"
    "name": "Post",
    "table": {
      "name": "post"
    },
    "columns": {
      "id": {
        "type": "int",
        "primary": true,
        "generated": true
      },
      "title": {
        "type": "varchar"
      },
      "text": {
        "type": "varchar"
      }
    }
}

Can be used like this:

 entitySchemas: [
        Object.assign({ target: Post }, require(__dirname + "/Schemas/Post.json"))
    ]

Or with entities as you have done. Interesting! But still not entirely clear if this supports the validation decorators as well.

@jjwtay
Copy link
Owner

jjwtay commented Nov 20, 2018

Originally when I wrote this I was building templates for the Models but then I found they support schema objects instead which opened it up to both ts and js and honestly made the code a ton simpler since I wasn't having to template at all I was just building an object. Since your original post I checked if they support validation at all in their schemas directly and it doesn't seem so which makes a ton of sense since they don't even directly support it in their own Model classes they just point out that you can use it side by side and direct call it before an insertion if you wish.

The way the supporting library for this works is it absorbs any and all directives you write automatically hanging them in field.directives so if you add @constraint to your schema it will be auto picked up in the schema object that is built you can just read it out of the field however you wish. If that library you pointed out already checks all those validations before hitting mutations (queries too?) then that would be equivalent to their decorators since they require you to call validate(obj) manually if you want validation then if that directive library works as i assume it does you'd be doing the exact same thing just automatically.

@kristianmandrup
Copy link
Author

kristianmandrup commented Nov 20, 2018

Thanks, but "The way the supporting library for this works..." what supporting library exactly?
Ah, I guess graphGenTypeorm is the supporting library. I thought you meant a dependency of typeorm

I first assumed it was come generic decorator builder, but then generator: true is not exactly the same as @PrimaryGeneratedColumn() so there must be some transformation, at least for some of the TypeORM specific ones. Now I can see it uses reflect-metadata to add the metadata dynamically, but can't see where the translation/transformation is done.

I would have to do some transformation into the schema object as well, so that:

name: String! @constraint(minLength: 4, maxLength: 20) becomes:

   length: [10, 20]

Ideally automatically transformed to

  @Length(4, 20)

Your thoughts?

@jjwtay
Copy link
Owner

jjwtay commented Nov 20, 2018

Sorry I ment to post this library https://github.com/jjwtay/graphSchemaToJson

This libary currently uses that to read an graphql executable schema and convert it into plain old javascript object purely of data that includes ALL the directives and such. In the readme there are these lines for the example usage:

import { schemaToJS } from 'graphschematojson'

const jsSchema = schemaToJS(schema)

Which is what I was referencing to and that object will have your field.directives.constraint whenever u apply it.

Your comment about generated not exactly mapping 1-1 with PrimaryGeneratedColumn is correct and in src/entity.js the field generated is set but also the field primary.

@kristianmandrup
Copy link
Author

OK, I also found a few related issues for typeorm

How to add custom decorators

"Look at existing decorator - it just stores some metadata which is used later in ORM logic. You can use e.g. custom subscriber to act on yours metadata."

export function UpdateDateColumn(options?: ColumnOptions): Function { 
     return function (object: Object, propertyName: string) { 
  
         getMetadataArgsStorage().columns.push({ 
             target: object.constructor, 
             propertyName: propertyName, 
             mode: "updateDate", 
             options: options ? options : {} 
         } as ColumnMetadataArgs); 
     }; 
 } 

The MetadataArgsStorage class

export class MetadataArgsStorage {

    // -------------------------------------------------------------------------
    // Properties
    // -------------------------------------------------------------------------

    readonly tables: TableMetadataArgs[] = [];
    readonly trees: TreeMetadataArgs[] = [];
    readonly entityRepositories: EntityRepositoryMetadataArgs[] = [];
    readonly transactionEntityManagers: TransactionEntityMetadataArgs[] = [];
    readonly transactionRepositories: TransactionRepositoryMetadataArgs[] = [];
    readonly namingStrategies: NamingStrategyMetadataArgs[] = [];
    readonly entitySubscribers: EntitySubscriberMetadataArgs[] = [];
    readonly indices: IndexMetadataArgs[] = [];
    readonly uniques: UniqueMetadataArgs[] = [];
    readonly checks: CheckMetadataArgs[] = [];
    readonly columns: ColumnMetadataArgs[] = [];
    readonly generations: GeneratedMetadataArgs[] = [];
    readonly relations: RelationMetadataArgs[] = [];
    readonly joinColumns: JoinColumnMetadataArgs[] = [];
    readonly joinTables: JoinTableMetadataArgs[] = [];
    readonly entityListeners: EntityListenerMetadataArgs[] = [];
    readonly relationCounts: RelationCountMetadataArgs[] = [];
    readonly relationIds: RelationIdMetadataArgs[] = [];
    readonly embeddeds: EmbeddedMetadataArgs[] = [];
    readonly inheritances: InheritanceMetadataArgs[] = [];
    readonly discriminatorValues: DiscriminatorValueMetadataArgs[] = [];

   //...
}

The mechanics are starting to make sense now. I understand how to go from decorator to metadata, such as PrimaryGeneratedColumn to:

{
  // ...
  generated: true,
  primary: true
}

However not clear how it transforms/loads and parses this metadata to create the decorators or Reflect.metadata. Thanks for all your assistance so far :)

@kristianmandrup
Copy link
Author

I now found the EntityMetadataBuilder which seems to hold the key.

export class EntityMetadataBuilder {
    /**
     * Builds a complete entity metadatas for the given entity classes.
     */
    build(entityClasses?: Function[]): EntityMetadata[] {

@kristianmandrup
Copy link
Author

Hmm, I looked into class-validator and it works as follows:

    const args: ValidationMetadataArgs = {
      type: ValidationTypes.WHITELIST,
      target: object.constructor,
      propertyName: propertyName,
      validationOptions: validationOptions
    };
    getFromContainer(MetadataStorage).addValidationMetadata(new ValidationMetadata(args));

So if I cold just populate validationOptions with the appropriate @constraints directive options, I could add validations to the Entities as well as to the Mutation resolver using a single source of truth.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants