Closed dobry-den closed 10 months ago
I'm going to turn this question into tests and documentation.
Implemented with example in https://github.com/graphile/crystal/pull/1917
I took the liberty of reworking your plugin and schema somewhat; but here's a plugin similar to yours that works with the latest crystal (not yet released):
import { withPgClient } from "@dataplan/pg";
import {
access,
constant,
ExecutableStep,
list,
object,
ObjectStep,
polymorphicBranch,
} from "grafast";
import { DatabaseError } from "pg";
import { gql, makeExtendSchemaPlugin } from "postgraphile/utils";
export const RegisterUserPlugin = makeExtendSchemaPlugin((build) => {
const { users } = build.input.pgRegistry.pgResources;
const { executor } = users;
return {
typeDefs: gql`
extend type Mutation {
registerUser(input: RegisterUserInput!): RegisterUserPayload
}
input RegisterUserInput {
username: String!
email: String!
}
type RegisterUserPayload {
result: RegisterUserResult
query: Query
}
union RegisterUserResult = User | UsernameConflict | EmailAddressConflict
type UsernameConflict {
message: String!
username: String!
}
type EmailAddressConflict {
message: String!
email: String!
}
`,
plans: {
Mutation: {
registerUser(_, { $input: { $username, $email } }) {
const $result = withPgClient(
executor,
list([$username, $email]),
async (pgClient, [username, email]) => {
try {
return await pgClient.withTransaction(async (pgClient) => {
const {
rows: [user],
} = await pgClient.query<{
id: string;
username: string;
}>({
text: `
insert into app_public.users (username)
values ($1)
returning *`,
values: [username],
});
await pgClient.query({
text: `
insert into app_public.user_emails(user_id, email)
values ($1, $2)`,
values: [user.id, email],
});
await sendEmail(email as string, "Welcome!");
return { id: user.id };
});
} catch (e) {
if (e instanceof DatabaseError && e.code === "23505") {
if (e.constraint === "unique_user_username") {
return {
__typename: "UsernameConflict",
message: `The username '${username}' is already in use`,
username,
};
} else if (e.constraint === "unique_user_email") {
return {
__typename: "EmailAddressConflict",
message: `The email address '${email}' is already in use`,
email,
};
}
}
throw e;
}
},
);
return object({ result: $result });
},
},
RegisterUserPayload: {
__assertStep: ObjectStep,
result($data: ObjectStep) {
const $result = $data.get("result");
return polymorphicBranch($result, {
UsernameConflict: { /* defaults */ },
EmailAddressConflict: { /* defaults */ },
User: {
match(obj) {
return obj.id != null;
},
plan($obj) {
const $id = access($obj, "id");
return users.get({ id: $id });
},
},
});
},
query() {
// The `Query` type just needs any truthy value.
return constant(true);
},
},
UsernameConflict: {
// Since User expects a step, our types must also expect a step. We
// don't care what the step is though.
__assertStep: ExecutableStep,
},
EmailAddressConflict: {
__assertStep: ExecutableStep,
},
},
};
});
async function sendEmail(_email: string, _message: string) {
/*
Write your email-sending logic here. Note that we recommend you enqueue a
job to send the email rather than sending it directly; if you don't already
have a job queue then check out https://worker.graphile.org
*/
}
Note: This is a question for grafast / postgraphile v5.
There are no google results for the error message I'm getting (shared later on), so hopefully this issue could at least answer some questions.
Summary
I want a graphql function to return a union of a table row and other data objects. Though this question is general, my concrete use-case is that I want my
registerUser()
function to returnUser | UnameTaken | EmailTaken
.I should also note that I have a
app_public.user
table and postgraphile has already created aUser
type for it that my code already works with. I'm just trying to add some localUnameTaken
andEmailTaken
types here for the registration result.(Traditionally, you would represent these as error codes for the client to check
err.code == "UNAME_TAKEN"
. However, these data objects let us pass along additional, arbitrary information to the client.)So my graphql declaration looks like this.
When I try to execute
registerUser()
, I get this error:I've tried various things like register UnameTaken under the plugin's
plans
property, but I haven't had any luck making progress here.Here is the full RegisterUserPlugin that sets all of this up:
I know it's not fully correct. It doesn't rollback the transaction (I don't even think you can do that inside the withTransaction helper). But it's a minimal code example to give us something concrete to work with.
Thanks for any help.