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

Add InputBuilder validators & guards on fields #141

Open
karatakis opened this issue Aug 8, 2023 · 8 comments
Open

Add InputBuilder validators & guards on fields #141

karatakis opened this issue Aug 8, 2023 · 8 comments
Assignees
Labels
enhancement New feature or request good first issue Good for newcomers

Comments

@karatakis
Copy link
Collaborator

Motivation

When creating, updating or deleting and item you can access all fields and properties. There should be a way to limit this by applying policy rules.

Proposed Solutions

Extend the input module to allow the configuration of validators and guards to be associated with entity input fields.

Additional Information

See guards on entities output fields

@karatakis karatakis added enhancement New feature or request good first issue Good for newcomers labels Aug 8, 2023
@karatakis karatakis added this to the Good to have features milestone Aug 8, 2023
@ShivangRawat30
Copy link

Hey I am currently learning rust, as it is a good first issue can you please assign this to me.

@YiNNx
Copy link
Contributor

YiNNx commented Nov 26, 2023

Hi, I'm interested in the approach to adding the InputBuilder Validators. Are there more details about how to implement it?

@guidomb
Copy link

guidomb commented Nov 28, 2023

Hi, I'm also interested in this feature. My current use case is skipping fields in input object types for which their corresponding SeaORM entity has a default value. For example:

This is a migration that creates an author table where the columns id and created_at have default values and must not be null. The idea is that values will be provided by the database.

use entities::*;
use sea_orm_migration::{
    prelude::*,
    sea_orm::{EntityTrait, Set},
};

#[derive(DeriveMigrationName)]
pub struct Migration;

#[async_trait::async_trait]
impl MigrationTrait for Migration {
    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        manager
            .create_table(
                Table::create()
                    .table(Author)
                    .if_not_exists()
                    .col(
                        ColumnDef::new(author::Column::Id)
                            .uuid()
                            .extra("DEFAULT gen_random_uuid()")
                            .not_null()
                            .primary_key(),
                    )
                    .col(
                        ColumnDef::new(author::Column::Email)
                            .string_len(254)
                            .unique_key()
                            .not_null(),
                    )
                    .col(
                        ColumnDef::new(author::Column::FirstName)
                            .string_len(100)
                            .not_null(),
                    )
                    .col(
                        ColumnDef::new(author::Column::LastName)
                            .string_len(100)
                            .not_null(),
                    )
                    .col(
                        ColumnDef::new(author::Column::Username)
                            .string_len(50)
                            .unique_key()
                            .not_null(),
                    )
                    .col(
                        ColumnDef::new(post::Column::CreatedAt)
                            .timestamp_with_time_zone()
                            .extra("DEFAULT now()")
                            .not_null(),
                    )
                    .to_owned(),
            )
            .await?;

        Ok(())
    }

    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        manager
            .drop_table(Table::drop().table(Author).to_owned())
            .await?;

        Ok(())
    }
}

here is the corresponding entity definition

use sea_orm::entity::prelude::*;
use serde::Serialize;

use crate::AuthorRole;

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize)]
#[sea_orm(table_name = "author")]
pub struct Model {
    #[sea_orm(primary_key, auto_increment = false)]
    pub id: Uuid,
    #[sea_orm(column_type = "Text", unique)]
    pub email: String,
    #[sea_orm(column_type = "Text")]
    pub first_name: String,
    #[sea_orm(column_type = "Text")]
    pub last_name: String,
    pub created_at: DateTimeWithTimeZone,
    #[sea_orm(unique)]
    pub username: String,
    pub role: AuthorRole,
}

Currently on main (9c43f72017fb27bd5e2602c553ee59acc45ae26e) the generated GraphQL schema

type Author {
  id: String!
  email: String!
  firstName: String!
  lastName: String!
  createdAt: String!
  username: String!
  role: AuthorRoleEnum!
  post(
    filters: PostFilterInput
    orderBy: PostOrderInput
    pagination: PaginationInput
  ): PostConnection!
}

type AuthorBasic {
  id: String!
  email: String!
  firstName: String!
  lastName: String!
  createdAt: String!
  username: String!
  role: AuthorRoleEnum!
}

type AuthorConnection {
  pageInfo: PageInfo!
  paginationInfo: PaginationInfo
  nodes: [Author!]!
  edges: [AuthorEdge!]!
}

type AuthorEdge {
  cursor: String!
  node: Author!
}

input AuthorFilterInput {
  id: TextFilterInput
  email: StringFilterInput
  firstName: StringFilterInput
  lastName: StringFilterInput
  createdAt: TextFilterInput
  username: StringFilterInput
  role: AuthorRoleEnumFilterInput
  and: [AuthorFilterInput!]
  or: [AuthorFilterInput!]
}

input AuthorInsertInput {
  id: String!
  email: String!
  firstName: String!
  lastName: String!
  createdAt: String!
  username: String!
  role: AuthorRoleEnum!
}

input AuthorOrderInput {
  id: OrderByEnum
  email: OrderByEnum
  firstName: OrderByEnum
  lastName: OrderByEnum
  createdAt: OrderByEnum
  username: OrderByEnum
  role: OrderByEnum
}

enum AuthorRoleEnum {
  WRITTER
  PUBLISHER
  EDITOR
}

input AuthorRoleEnumFilterInput {
  eq: AuthorRoleEnum
  ne: AuthorRoleEnum
  gt: AuthorRoleEnum
  gte: AuthorRoleEnum
  lt: AuthorRoleEnum
  lte: AuthorRoleEnum
  is_in: [AuthorRoleEnum!]
  is_not_in: [AuthorRoleEnum!]
  is_null: AuthorRoleEnum
  is_not_null: AuthorRoleEnum
}

input AuthorUpdateInput {
  id: String
  email: String
  firstName: String
  lastName: String
  createdAt: String
  username: String
  role: AuthorRoleEnum
}

input CursorInput {
  cursor: String
  limit: Int!
}

type Mutation {
  _ping: String
  authorCreateOne(data: AuthorInsertInput!): AuthorBasic!
  authorCreateBatch(data: [AuthorInsertInput!]!): [AuthorBasic!]!
  authorUpdate(
    data: AuthorUpdateInput!
    filter: AuthorFilterInput
  ): [AuthorBasic!]!
}

input OffsetInput {
  limit: Int!
  offset: Int!
}

enum OrderByEnum {
  ASC
  DESC
}

type PageInfo {
  hasPreviousPage: Boolean!
  hasNextPage: Boolean!
  startCursor: String
  endCursor: String
}

input PageInput {
  limit: Int!
  page: Int!
}

type PaginationInfo {
  pages: Int!
  current: Int!
  offset: Int!
  total: Int!
}

input PaginationInput {
  cursor: CursorInput
  page: PageInput
  offset: OffsetInput
}

type Query {
  author(
    filters: AuthorFilterInput
    orderBy: AuthorOrderInput
    pagination: PaginationInput
  ): AuthorConnection!
}

input StringFilterInput {
  eq: String
  ne: String
  gt: String
  gte: String
  lt: String
  lte: String
  is_in: [String!]
  is_not_in: [String!]
  is_null: String
  is_not_null: String
  contains: String
  starts_with: String
  ends_with: String
  like: String
  not_like: String
  between: [String!]
  not_between: [String!]
}

input TextFilterInput {
  eq: String
  ne: String
  gt: String
  gte: String
  lt: String
  lte: String
  is_in: [String!]
  is_not_in: [String!]
  is_null: String
  is_not_null: String
  between: [String!]
  not_between: [String!]
}

Ideally I think that the behavior should be that the fields that have default values don't need to be optionally in the Rust entity but should not be included in the UpdateInput and InsertInput. What do you think?

@guidomb
Copy link

guidomb commented Nov 28, 2023

I found that if I modify how the BuilderContext is initialized I can skip fields from the input object generation. For example:

lazy_static::lazy_static! {
    static ref CONTEXT : BuilderContext = {
        let mut context = BuilderContext::default();
        context.entity_input.insert_skips.push("Author.id".into());
        context.entity_input.insert_skips.push("Author.role".into());
        context.entity_input.insert_skips.push("Author.createdAt".into());
        context
    };
}

What would the proper way of initializing the BuilderContext without having to write this manually by inferring it from the SQL schema?

@guidomb
Copy link

guidomb commented Nov 29, 2023

The problem I see is that for some fields I would like to exclude them from the insert and update input like id or created_at but there are other fields like role that have a default value in postgres, should not be skipped in the insert or update input, should have a default value in the generated GraphQL schema but must not be optional in the Rust entity.

@karatakis
Copy link
Collaborator Author

Hello @YiNNx and thanks for your interest. So far the supported mutations are create and update and I hope to release the delete soon. If you read the comment I created a while ago it provides a file from where to start. The guards file currently has guards for query and query filter options. If you search those on code you can find more about how this feature works. We just need to further expand it for mutations.

I will look it up and add more info on this issue later.

@karatakis
Copy link
Collaborator Author

Thanks for your comments @guidomb. Could you open a separate issue so we can address this separately?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request good first issue Good for newcomers
Projects
None yet
Development

No branches or pull requests

4 participants