[Updated] I desire mutant cells [Solved]

Hi Again,

I wonder if anyone could share a cell that mutates something in the database (and the parts of the api side also)

I’m having a hell of a time making this work…

thanks
Al;

ah, I’ve found useMutation – proceeding along those lines

now looking for custom sdl files that just write a value to the database

got it

users.sdl.ts

export const schema = gql`
  type User {
    id: String!
    businessPK: String
    customerPK: String
    hasPlanner: Boolean!
    inserted_at: DateTime!
    updated_at: DateTime!
  }

  type Query {
    user(id: String!): [User]!
    users: [User]!
  }

  type Mutation {
    setHasPlanner(id: String!, hasIt: Boolean!): [User]!
  }

`;

users.ts

import type { BeforeResolverSpecType } from "@redwoodjs/api";

const stringify = require('json-stringify-safe');
import { context } from '@redwoodjs/api'
import { logger } from 'src/lib/logger'

import { db } from "src/lib/db";
import { requireAuth } from "src/lib/auth";

import { thenDebug } from "src/lib/thenables"

// Used when the environment variable REDWOOD_SECURE_SERVICES=1
export const beforeResolver = (rules: BeforeResolverSpecType) => {
  rules.add(requireAuth);
};

export const users = () => {
  // requireAuth()
  return db.user.findMany()
  .then(thenDebug(`db.user.findMany`))
  .then(data => {
    // logger.debug(stringify(data,null,2));
    return data;
  })
};

export const user = ({ id = context.currentUser.sub }) => {
  // requireAuth()
  return db.user.findUnique({
    where: { id }
  })
  .then(thenDebug(`db.user.findUnique`))
  .then(data => {
    // logger.debug(stringify(data,null,2));
    return [data];
  })
};

export const setHasPlanner = ({ id = context.currentUser.sub, hasIt }) => {
  // requireAuth()
  return db.user.update({
    where: { id },
    data: {
      hasPlanner: hasIt
    }
  })
  .then(thenDebug(`db.user.update`))
  .then(data => {
    // logger.debug(stringify(data,null,2));
    return [data];
  })
};

src/lib/thenables.js

const stringify = require('json-stringify-safe');

import { logger } from './logger'

export const thenDebug = (label) => (data) => {
  logger.debug(`${label}: ${stringify(data, null, 2)}`)
  return data
}

export const thenError = (label) => (resJSON) => {
  if (resJSON.error && resJSON.error.statusCode > 300) {
    logger.error(`Error: in [${label}], statusCode: ${resJSON.error.statusCode}, ${stringify(resJSON.error, null, 2)}`)
    throw new Error(JSON.stringify(resJSON.error)) // whatever we throw will be .toString()'d -- it will arrive in error.message
  }
  return resJSON
}

called via GraphiQL

mutation {
  setHasPlanner( id: "3fa6dbaf-9e0a-458c-82c1-526a96b3e052", hasIt: true ) {
    id
    hasPlanner
  } 
}

returns

{
  "data": {
    "setHasPlanner": [
      {
        "id": "3fa6dbaf-9e0a-458c-82c1-526a96b3e052",
        "hasPlanner": true
      }
    ]
  }
}

Call it thusly:

  const MUTATION = gql`
    mutation setHasPlanner($id: String!, $has: Boolean!) {
      setHasPlanner(id: $id, has: $has) {
        id
        hasPlanner
      }
    }
  `
  const [setHasPlanner] = useMutation(MUTATION)

  setHasPlanner({ variables: { id, hasIt: purchased }})

Hello @ajoslin103!

Here are the docs about mutations, just for reference.

Are you looking for something along these lines?

Your GraphQL SDL would be something along:

export const schema = gql`
  type User {
    id: String!
    email: String!
    last_sign_in_at: DateTime
    created_at: DateTime!
    updated_at: DateTime!
  }

  input SignupUserInput {
    email: String!
  }

  type Mutation {
    signupUser(user: SignupUserInput!): User!
  }
`

And then the Cell making use of the mutation would be:

import { useCallback, useEffect, useState } from 'react'
import { useMutation } from '@apollo/client'

export const MUTATION = gql`
  mutation signupUser($user: SignupUserInput!) {
    user: signupUser(user: $user) {
      email
    }
  }
`

export const Loading = () => {
  return (
    <p>Loading...</p>
  )
}

export const Failure = ({ error }) => {
  return (
    <p>Oops: {error.message}</p>
  )
}

export const Success = ({ user }) => {
  return (
    <>
      <p>You've been signed up!</p>
      <p>Check your e-mail for further instructions: {user.email}</p>
    </>
  )
}

const SignupCell = ({ onComplete, onError, user }) => {
  // user is of type SignupUserInput; passed from the parent component using the Cell.
  // onComplete and onError are optional to a "working" mutation-cell, can be used to respond to the change from a parent component

  const [stage, setStage] = useState('loading')

  const [error, setError] = useState()
  const [result, setResult] = useState()

  const _onComplete = useCallback(
    (data) => {
      onComplete(data)
      setResult(data.result)
      setStage('success')
    },
    [onComplete, setResult, setStage]
  )

  const _onError = useCallback(
    (err) => {
      onError(err)
      setError(err)
      setStage('failure')
    },
    [onError, setError, setStage]
  )

  const [mutate, { called }] = useMutation(MUTATION, {
    onCompleted: _onComplete,
    onError: _onError,
  })

  useEffect(() => {
    if (!called) {
      mutate({ variables: { user } })
    }
  }, [called, mutate, user])

  return (
    <>
      {stage === 'loading' && <Loading />}
      {stage === 'failure' && <Failure {...error} />}
      {stage === 'success' && <Success {...result} />}
    </>
  )
}

export default SignupCell

Your service would receive its input the same as a query-powered service:

export const signupUser = ({ user }) => {
  // user is of type SignupUserInput
}

I ripped it out of my app, so let me know if you need any help fitting it into place.

2 Likes

Does calling it as:

 setHasPlanner({ variables: { id, hasIt: purchased } })

change?

I was writing as you replied so just hit post anyway; hopefully that’s all you need now.

1 Like

I really do appreciate all the time and attention you RedwoodJS Contributors give to me

I’m going to get this into the docs and do a PR

thanks

al;

More than happy to help!

Here is the API reference for useMutation as well. More straightforward documentation on Redwood’s part would certainly help, I’ve had to jump around Apollo’s docs myself. More-so lack of clarity I felt than it being a confusing idea.

My next thing to try was to turn this work into a cell – and you have just given me that

But I admit to some confusion – how the does normal Cell process know not to try to manage your mutating Cell – is it because there is no exported QUERY ?

Your Mutating Cell Wrapper looks very generic, will that be done for us in a future version of RW ?

Actually looking at this again with morning eyes – everything I need is there in a Cell – I should be able to export QUERY | MUTATION – I think a [Redwood] Cell should just know which process to perform

While it might seem like there must be lot of magic involved, a Cell is actually just a higher-order component that executes a GraphQL query and manages its lifecycle. All the logic’s actually in just one file: withCellHOC.tsx. The idea is that, by exporting named constants that match the parameters of withCell , Redwood can assemble this higher-order component out of these constants at build-time using a babel plugin!

The link for withCellHOC.tsx is obsoleted, where is the code that replaced it?

I should be able to do this…

By exporting a default from your cell you’re telling Redwood “hey, don’t make this a query-cell, I got it”

I believe withCell was replaced with createCell, however it still only applies to query-cells. createCell does something similar to our SignupCell I shared here, invoking the query and displaying the appropriate component (Success, Failure, Loading) based on the response/current state. It just invokes useQuery or similar instead of useMutation

Check out this section for another explanation.

Leaving QUERY out seems to be part of it as well, can’t recall I’ve ever left it in and had a default export, if that still brought about the same conclusion.

Your Mutating Cell Wrapper looks very generic, will that be done for us in a future version of RW ?

I assume so, at least in some capacity, I haven’t heard or seen anything personally.

It’d likey fall into the “contributions welcome” category if I ware a betting man.