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

Examples for Adding Validation to #719

Open
FroeMic opened this issue Oct 18, 2024 · 2 comments
Open

Examples for Adding Validation to #719

FroeMic opened this issue Oct 18, 2024 · 2 comments
Labels
bug Something isn't working enhancement New feature or request

Comments

@FroeMic
Copy link

FroeMic commented Oct 18, 2024

Describe the improvement

When trying to follow the docs on how to create a custom mutation with the typescript connector I stumbled across several problems, when trying to implement validation:

It would be great to have a reference case how to implement validation. For example,

1. How to validate the input for a mutation (types etc)

Zod-Validation

I didn't manage to get Zod validation working in function.ts.

Importing the zod schema in function.ts creates to a faulty generation when running "ddn connector introspect my_ts_connector"

import { eq } from "drizzle-orm";
import { db } from "../../lib/db";
import { studentTable } from "../../lib/db/schema";

// Note: Importing the zod schema creates to a faulty generation when running "ddn connector introspect my_ts_connector"
// import { createStudentSchema } from "./dto/create-student-input";

// Define the User type using TypeScript
export interface Student {
  id: string;
  workspaceId: string;
  firstname: string;
  lastname: string;
  email: string;
  phone: string | null | undefined;
  profilePicture: string | null | undefined;
  createdAt: Date;
  updatedAt: Date;
}

//  * Inserts a student into the database and returns the inserted student.
//  *
//  * @param input The student to insert into the database
//  * @returns The student that was inserted into the database.
//  */
export async function createStudent(input: {
  workspaceId: string;
  firstname: string;
  lastname: string;
  email: string;
  phone: string | null | undefined;
  profilePicture: string | null | undefined;
}): Promise<Student> {
  const student = await db
    .insert(studentTable)
    .values({
      workspaceId: input.workspaceId,
      firstname: input.firstname,
      lastname: input.lastname,
      email: input.email,
      phone: input.phone,
      profilePicture: input.profilePicture,
    })
    .returning();

  return student[0];
}

2. How to check permissions the permission – i.e, who can execute a specific mutation

For example, similar to the ModelPermissions, I would like to check whether a user can execute a specific mutation based on their x-hasura-user-id.

For example, a user should only be able to add an object (with a foreign reference workspace_id) to a workspace, if they are also an active member of said workspace.

How can I make that check and where should I do it?

Location of the improvement

https://hasura.io/docs/3.0/getting-started/build/mutate-data?db=PostgreSQL

Additional context (optional)

I hope that's the correct place to post that!

I feel like creating the GraphQL Query API was all really smooth. Creating mutations has been much more difficult.

@FroeMic FroeMic added bug Something isn't working enhancement New feature or request labels Oct 18, 2024
@robertjdominguez
Copy link
Collaborator

Thanks for dropping in @FroeMic!

I can help you rubberduck this a bit. And, if needed, we can incorporate some of the connector authors to help get you where you need to be 🤙

I would like to check whether a user can execute a specific mutation based on their x-hasura-user-id.

In the vernacular of DDN, Mutations are exposed as Commands. You can utilize CommandPermissions to restrict which users can utilize which mutations. As an example, you can set a permission using field comparisons like this:

role: user
allowExecution: true
fieldComparison:
  field: id
  operator: _eq
  value:
    sessionVariable: x-hasura-user-id

This is a great addition that we'll surface in some of the guides! Knowing this, do you still need zod for validation or even need a custom mutation via the TypeScript connector? If so, we can tackle that next.

@FroeMic
Copy link
Author

FroeMic commented Oct 20, 2024

@robertjdominguez – thanks for the fast answer ⚡⚡⚡

I feel like I have two issues. I don't want to mix them up – so let's go step by step if that is ok for you!

Context

In plain-english, what I want to achieve is the following: "If a user has an active membership in a workspace they should be allowed to create a new entry.

Entity Model (simplified): A user can be part of one or many workspaces. The m-n relationship is stored in a table WorkspaceMembership and can be either "ACTIVE" or "REMOVED". Users should be able to CRUD entries. Each entry belongs to exactly one workspace.

Unbenannt-2024-10-20-1458

Read Permissions (via ModelPermissions)

In the ModelPermissions I would have achieved the equivalent read permissions check with the following select filter:

kind: ModelPermissions
version: v1
definition:
  modelName: Entry
  permissions:
    - role: admin
      select:
        filter: null
    - role: user
      select:
        filter:
          # only allow access to entries of the user's workspace
          relationship:
            name: workspace
            predicate: 
              relationship:
                name: workspaceMembershipList
                predicate: 
                  and: 
                    - fieldComparison:
                        field: userId
                        operator: _eq
                        value: 
                          sessionVariable: x-hasura-user-id
                    - fieldComparison:
                        field: status
                        operator: _eq
                        value: 
                          literal: "ACTIVE"

Write Permissions

To achieve a similar thing via the CommandPermissions function, to check whether the user is part of the workspace they would like to create an entry in, I would need to access the workspaceId .

Option 1: Storing workspaceId in JWT

My intuition is that writing it to the JWT, isn't quite the right place to do this. With a growing model with more relations, I would need to add an increasing number of variables to the JWT and store more and more state at the session level.

Option 2: Accessing Payload in CommandPermission

One alternative – I couldn't figure out whether this is possible – would be to access the Input data of the command.

Here is parts of the generate hml file for the command.

---
kind: ObjectType
version: v1
definition:
  name: CreateEntryInput
  fields:
    - name: email
      type: String!
    - name: workspaceId
      type: String!
  graphql:
    typeName: CreateEntryInput
    inputTypeName: CreateEntryInputInput
  dataConnectorTypeMapping:
    - dataConnectorName: my_ts_connector
      dataConnectorObjectType: createEntry_input



// skipped


---
kind: Command
version: v1
definition:
  name: CreateEntry
  outputType: Entry!
  arguments:
    - name: input
      type: CreateEntryInput!
  source:
    dataConnectorName: my_ts_connector
    dataConnectorCommand:
      procedure: createEntry
  graphql:
    rootFieldName: createEntry
    rootFieldKind: Mutation

Is this possible?

Option 3: Validating in Lambda

The naive alternative would be to move this permissions-check into the actual Lambda. My attempt doing this with zod lead to ddn connector introspect my_ts_connector generating empty Command.hml files (no error message in the console). At this point I am not sure why exactly and how would I best debug this.

I think that defining permission-checks at the Metadata-Layer feels more elegant for me. However, I see how certain mutations might need more complex input validation checks, which would like happen in the lambda.

What I didn't understand from the documentation how one is supposed to best deal with errors in the lambda. I.e., if validation fails should I throw an error? How would this be exposed to the client calling the command? Or do I have to write a ReponseType abstraction that instead of returning Entry would return Entry | Error.

Generalized Questions:

I understand that the scenario above is specific to my situation. However, I think there are a few generalized questions that follow from it that might be useful to a broader audience:

  1. What is the intended "ddn way" to write mutations with input validation
  2. What is the intended way to deal with errors within lambdas
  3. How to debug lambda's if the introspect command fails silently

I appreciate your help!

My initial experience with ddn was quite smooth for generating the read API and I think it's quite cool stuff!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants