Skip to content

Commit

Permalink
Add GraphQL mutation example (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
koistya authored May 25, 2021
1 parent c6741d2 commit e075f3f
Show file tree
Hide file tree
Showing 16 changed files with 1,018 additions and 29 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"esnext",
"idps",
"gamecenter",
"GraphQLID",
"gsutil",
"jsonb",
"knexfile",
Expand Down
1 change: 1 addition & 0 deletions api/mutations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
/* SPDX-License-Identifier: MIT */

export * from "./auth";
export * from "./updateUser";
119 changes: 119 additions & 0 deletions api/mutations/updateUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/* SPDX-FileCopyrightText: 2016-present Kriasoft <[email protected]> */
/* SPDX-License-Identifier: MIT */

import {
GraphQLBoolean,
GraphQLFieldConfig,
GraphQLID,
GraphQLInputObjectType,
GraphQLNonNull,
GraphQLObjectType,
GraphQLString,
} from "graphql";
import db, { User } from "../../db";
import { Context } from "../context";
import { UserType } from "../types";
import { fromGlobalId, validate, ValidationError } from "../utils";

type UpdateUserInput = {
id: string;
username?: string | null;
email?: string | null;
name?: string | null;
picture?: string | null;
timeZone?: string | null;
locale?: string | null;
};

/**
* @example
* mutation M {
* updateUser(input: { id: "xxx", email: "new@email.com" }, dryRun: false) {
* user {
* id
* email
* }
* }
* }
*/
export const updateUser: GraphQLFieldConfig<unknown, Context> = {
description: "Updates the user account.",

type: new GraphQLObjectType({
name: "UpdateUserPayload",
fields: {
user: { type: UserType },
},
}),

args: {
input: {
type: new GraphQLInputObjectType({
name: "UpdateUserInput",
fields: {
id: { type: new GraphQLNonNull(GraphQLID) },
username: { type: GraphQLString },
email: { type: GraphQLString },
name: { type: GraphQLString },
picture: { type: GraphQLString },
timeZone: { type: GraphQLString },
locale: { type: GraphQLString },
},
}),
},
dryRun: { type: new GraphQLNonNull(GraphQLBoolean), defaultValue: false },
},

async resolve(self, args, ctx) {
const input = args.input as UpdateUserInput;
const dryRun = args.dryRun as boolean;
const id = fromGlobalId(input.id, "User");

// Check permissions
ctx.ensureAuthorized((user) => user.id === id || user.admin);

// Validate and sanitize user input
const [data, errors] = validate(input, (value) => ({
username: value("username").isUsername(),
email: value("email").isLength({ max: 100 }).isEmail(),
name: value("name").isLength({ min: 2, max: 100 }),
picture: value("picture").isLength({ max: 100 }),
time_zone: value("timeZone").isLength({ max: 50 }),
locale: value("locale").isLength({ max: 10 }),
}));

// Once a new username is provided and it passes the initial
// validation, check if it's not used by any other user.
if (input.username && !("username" in errors)) {
const exists = await db
.table<User>("user")
.where({ username: input.username })
.whereNot({ id })
.first("id")
.then((x) => Boolean(x));
if (exists) {
errors.username = ["Username is not available."];
}
}

if (Object.keys(errors).length > 0) {
throw new ValidationError(errors);
}

if (Object.keys(data).length === 0) {
throw new ValidationError({ _: ["The input values cannot be empty."] });
}

if (dryRun) {
return { user: await ctx.userById.load(id) };
}

const [user] = await db
.table("user")
.where({ id })
.update({ ...data, updated_at: db.fn.now() })
.returning("*");

return { user };
},
};
17 changes: 13 additions & 4 deletions api/queries/fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,19 @@ import { GraphQLFieldConfig, GraphQLInt, GraphQLNonNull } from "graphql";
import type { Knex } from "knex";
import { Context } from "../context";

export const countField: GraphQLFieldConfig<
{ query: Knex.QueryBuilder },
Context
> = {
type Source = { query: Knex.QueryBuilder };

/**
* The total count field definition.
*
* @example
* query {
* users {
* totalCount
* }
* }
*/
export const countField: GraphQLFieldConfig<Source, Context> = {
type: new GraphQLNonNull(GraphQLInt),

async resolve(self) {
Expand Down
9 changes: 9 additions & 0 deletions api/queries/me.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ import type { User } from "../../db";
import type { Context } from "../context";
import { UserType } from "../types";

/**
* @example
* query {
* me {
* id
* email
* }
* }
*/
export const me: GraphQLFieldConfig<User, Context> = {
description: "The authenticated user.",
type: UserType,
Expand Down
9 changes: 9 additions & 0 deletions api/queries/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ import db from "../../db";
import type { Context } from "../context";
import { UserType } from "../types";

/**
* @example
* query {
* user(username: "john") {
* id
* email
* }
* }
*/
export const user: GraphQLFieldConfig<User, Context> = {
description: "Find user by username or email.",
type: UserType,
Expand Down
13 changes: 13 additions & 0 deletions api/queries/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@ import { Context } from "../context";
import { UserType } from "../types";
import { countField } from "./fields";

/**
* @example
* query {
* users(first: 10) {
* edges {
* user: node {
* id
* email
* }
* }
* }
* }
*/
export const users: GraphQLFieldConfig<unknown, Context> = {
type: connectionDefinitions({
name: "User",
Expand Down
1 change: 1 addition & 0 deletions api/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
/* SPDX-License-Identifier: MIT */

export * from "./map";
export * from "./relay";
export * from "./validator";
104 changes: 104 additions & 0 deletions api/utils/map.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/* SPDX-FileCopyrightText: 2016-present Kriasoft <[email protected]> */
/* SPDX-License-Identifier: MIT */

import { mapTo, mapToMany, mapToManyValues, mapToValues } from "./map";

test("mapTo()", () => {
const result = mapTo(
[
{ id: 2, name: "b" },
{ id: 1, name: "a" },
],
[1, 2],
(x) => x.id
);
expect(result).toMatchInlineSnapshot(`
Array [
Object {
"id": 1,
"name": "a",
},
Object {
"id": 2,
"name": "b",
},
]
`);
});

test("mapToMany()", () => {
const result = mapToMany(
[
{ id: 2, name: "b" },
{ id: 1, name: "a" },
{ id: 1, name: "c" },
],
[1, 2],
(x) => x.id
);
expect(result).toMatchInlineSnapshot(`
Array [
Array [
Object {
"id": 1,
"name": "a",
},
Object {
"id": 1,
"name": "c",
},
],
Array [
Object {
"id": 2,
"name": "b",
},
],
]
`);
});

test("mapToValues()", () => {
const result = mapToValues(
[
{ id: 2, name: "b" },
{ id: 1, name: "a" },
{ id: 3, name: "c" },
],
[1, 2, 3, 4],
(x) => x.id,
(x) => x?.name || null
);
expect(result).toMatchInlineSnapshot(`
Array [
"a",
"b",
"c",
null,
]
`);
});

test("mapToManyValues()", () => {
const result = mapToManyValues(
[
{ id: 2, name: "b" },
{ id: 2, name: "c" },
{ id: 1, name: "a" },
],
[1, 2],
(x) => x.id,
(x) => x?.name || null
);
expect(result).toMatchInlineSnapshot(`
Array [
Array [
"a",
],
Array [
"b",
"c",
],
]
`);
});
4 changes: 4 additions & 0 deletions api/utils/map.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
/* SPDX-FileCopyrightText: 2016-present Kriasoft <[email protected]> */
/* SPDX-License-Identifier: MIT */

// These helper functions are intended to be used in data loaders for mapping
// entity keys to entity values (db records). See `../context.ts`.
// https://github.com/graphql/dataloader

export function mapTo<R, K>(
records: ReadonlyArray<R>,
keys: ReadonlyArray<K>,
Expand Down
19 changes: 19 additions & 0 deletions api/utils/relay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* SPDX-FileCopyrightText: 2016-present Kriasoft <[email protected]> */
/* SPDX-License-Identifier: MIT */

import { fromGlobalId as parse } from "graphql-relay";

/**
* Converts (Relay) global ID into a raw database ID.
*/
export function fromGlobalId(globalId: string, expectedType: string): string {
const { id, type } = parse(globalId);

if (expectedType && type !== expectedType) {
throw new Error(
`Expected an ID of type '${expectedType}' but got '${type}'.`
);
}

return id;
}
Loading

0 comments on commit e075f3f

Please sign in to comment.