Skip to content

Instantly share code, notes, and snippets.

@codingkarthik
Last active January 10, 2023 10:07
Show Gist options
  • Save codingkarthik/dda977081e1dcada89add9001dc5bcca to your computer and use it in GitHub Desktop.
Save codingkarthik/dda977081e1dcada89add9001dc5bcca to your computer and use it in GitHub Desktop.
Cloud team action use cases

Real world Actions use cases

1. Feature flag - check db latency

Input: project_id Output: db_latency_job_id

  1. Make a GraphQL call to the DB to get more details about the project associated with the given project_id. More details include

    a. name of the project b. plan - paid or free c. tenant - id, region, cloud, fqdn d. owner - id, email

  2. Validation: Check if the fetched project is a cloud one, if it's a self hosted project throw an error.

  3. Make a network call to LaunchDarkly to evaluate if the feature is enabled for the current project. LaunchDarkly returns a Boolean to indicate if the project can access the feature.

  4. If the feature is not enabled for the project, throw an error saying the same.

  5. If the feature is enabled, create a new record in the jobs table and return the id of the inserted row. This id is returned as the output of the action request.

2. Decline billing manager invite

Input: project_id Output:

type BillingManagerInvitation struct {
	ID                    *uuid.UUID `json:"id,omitempty"`
	ProjectID             *uuid.UUID `json:"project_id,omitempty"`
	ManagerEmail          string     `json:"manager_email,omitempty"`
	InvitedBy             *uuid.UUID `json:"invited_by,omitempty"`
	InvitedAt             string     `json:"invited_at,omitempty"`
	Key                   string     `json:"key,omitempty"`
	AcceptedAt            *string    `json:"accepted_at,omitempty"`
	DeclinedAt            *string    `json:"declined_at,omitempty"`
	ResendInvitationCount int        `json:"resend_invitation_count,omitempty"`

	// Object RelationShip
	InvitedByUser *User    `json:"invited_by_user,omitempty"`
	Project       *Project `json:"project,omitempty"`
}
  1. Update the project_billing_manager_invitations with the following set and where:
   set:
     declined_at: now()
   where:
     project_id:
       _eq: project_id
     accepted_at:
       _is_null: true
     declined_at:
       _is_null: true
     _or:
       - project:
           owner_id:
             _eq: user-id
       - project:
           billing_manager_id:
             _eq: user_id
       - manager_email:
           _eq: email
       - project:
           -- Complex boolean expression which checks if the given user id is an active collaborator or not

Idea: Response transform for regular mutations

  1. If the affected_rows of the above update is 0, then throw a custom error message.

  2. Otherwise return the id, project_id, manager_email, invited_by and invited_at.

Note: I'm not entirely sure why this is an action because it seems that this can be done using an update mutation itself. The only thing I can think of to model this as an action is to throw a custom error message.

3. Deploy latest github commit

Input: github_integration_config_id Output:

{
  "status": "success"
}
  1. Make the following graphql query:
query QueryGithubIntegration {

  github_integration_config(where: {id: {_eq: $github_integration_config_id}}) {
    project_id
    organisation
    repository
    branch
    directory
    mode
    email_status
  }
}

Throw error if the github integration config is not found.

  1. Using the data from the previous response make another request to see if there have been any new commits since the last deployment. This is done by making the following GraphQL request:
query QueryGithubPushEvent {
  github_push_event(where: $where, limit: 1, order_by: {commit_timestamp: desc}) {
    commit
    archive_url
    id
  }
}

variables:

{
  "where": {
    "push_metadata": {
      "_contains_": {
        "organisation": <configDetails.organisation>,
        "repository": <configDetails.repository>,
        "branch": <configDetails.branch>,
        "directory": <configDetails.directory>
      }
    },
    "github_integration_config_id": {
      "_eq": github_integration_config_id
    }
  }
}

Validation: If no rows are returned by the above query then throw error.

  1. Complex validation:

    Check if the user that requested to deploy the latest commit is either the owner of the project or is an collaborator with admin priviliges and only then proceed with the actual deployment of the latest commit.

    The above check is done by making a query to the users table.

  2. Create a job that contains the archive_url, branch, directory, project_id, email and insert it into the jobs table.

4. Move project region

input: project_id, cloud, region, tenantGroupId output:

{
  "project_id": project_id
}
  1. Get the role of the user who made the request.

  2. [static input validation] Either the tenantGroupID should be set or the (cloud, region) should be set.

  3. If the role is a non admin one, Check if the user who requested to move the project has access to move the project to another region. A bunch of validations are done here:

    • The plan_name should be cloud_payg i.e. paid project.
    • The user should either be the owner of the project or an active collaborator of the project with admin privileges.
    • Check if the project's tenant is not in Maintenance mode.
    • Check if the project's tenant is active.
  4. Business logic: Move the project to the requested region and get the tenant's FQDN where the project was moved.

5. Get invoice and receipt URLs (Stripe)

input: invoice_id output: invoice_url, receipt_url

  1. Make a GraphQL request to fetch the invoice (invoice_url, receipt_url, updated_at) from the invoice table where stripe_invoice_id = $.input.invoice_id.
  2. Throw error if no invoice found or the invoice doesn't contain an updated_at timestamp.
  3. Check if the invoice is older than 30 days by comparing the current timestamp to the invoice's updated_at field.
  4. If the difference calculated in step iii is greater than 30 days, then make an external call to strip using the invoice_id to fetch the new invoice.
  5. If the returned invoice contains a charge then make another external call to Stripe to fetch the corresponding receipt URL using the charge's id.
  6. Return the fetched invoice_url and the receipt_url (if available).

6. Pay invoice (Stripe)

input: invoice_id, payment_method_id output: status

  1. Validation: Check if x-hasura-user-email and x-hasura-user-id are not null.
  2. Fetch the id of the payment method and the invoice corresponding to the invoice_id with a GraphQL request.
  3. Validation: Check if the invoice's collection method is automatic, otherwise throw error.
  4. Make an external call to stripe to pay the invoice.

7. Neon create database

Input: project_id Output: isAuthenticated, email, databaseUrl, envVar, integrationId

  1. Validation: If the project_id is provided, check if the user who requested to create the neon database has admin priviliges or is the owner for the project corresponding to the project_id. If the project_id is null, then the above validation is not done.

  2. Create a neon HTTP client which will be used to interact with neon.

    Steps to create the neon HTTP client:

    • [EXTERNAL CALL] To interact with Neon, first the client needs to get authenticated with Neon and after a successful authentication, a Session is created and stored in Hasura's vault corresponding to the x-hasura-user-id of the user who made the request.

      A session is defined as follows:

        type Session struct {
             AccessToken  string `json:"access_token"`
             RefreshToken string `json:"refresh_token"`
             ExpiresAt    int    `json:"expires_at"`
             TokenType    string `json:"token_type"`
        }
    - [EXTERNAL CALL] Fetch the existing session from the Hasura vault corresponding to the `x-hasura-user-id`.
    
    - [EXTERNAL CALL] If the session has expired or the user's authenticity couldn't be verified with Neon then refresh the Neon session to get a new session.
    
         If error in refreshing the session,
           then - [EXTERNAL CALL] delete the stored session from the vault.
           else - [EXTERNAL CALL] update the stored session with the new session obtained in the vault.
    
  3. [External call] Get the user info associated with the current neon session. If the external call fails it is assumed that the user has not authenticated yet and the frontend would prompt a login automatically.

  4. Business logic: Make a POST request to Neon to create a project and passing the access token present in the session in the Authorization header.

  5. Read the status code and handle the >500, 401 and 400 status differently and then combine the status and the message in the body to form an informative error message.

    When the response status is 200, then metadata about the project created on Neon is expected in the response. A project is defined as below:

       type Project struct {
           Name      string            `json:"name"`
           ID        string            `json:"id"`
           Roles     []ProjectRole     `json:"roles"`
           Databases []ProjectDatabase `json:"databases"`
       }
    
       type ProjectRole struct {
           Dsn  string `json:"dsn"`
           Name string `json:"name"`
       }
    
       type ProjectDatabase struct {
           Id   int64  `json:"id"`
           Name string `json:"name"`
       }

    Validation: Check that the id or the name of the project created is not empty.

  6. Get the DB URL from the response returned. The DB URL is present in the ProjectRole type in the Dsn key and consider only a ProjectRole where Name != "web-access". Throw error if no DB URL was found.

  7. The DB URL is modified to add options like ssl=require and include the neon project id in the options key of the URL.

  8. A neon_db_integration table exists which contains the mappings of Hasura's project_id to neon's project_id. A new row is now inserted in the said table.

  9. Send the response to the client:

      { "isAuthenticated": true,
        "email": userInfo.email, // From step iii
        "databaseUrl": dbUrl // from step vii
      }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment