Skip to content

Extend Schema

Alex Ald edited this page May 1, 2019 · 2 revisions

What is it for?

This decorator allows you to merge additional GraphQL types and resolvers into your schema. A decorator for PostGraphile's makeExtendSchemaPlugin.

Decorator Parameters

Decorator takes a single parameter of type ExtendSchemaOptions

interface ExtendSchemaOptions {
  additionalGraphql?: any;
  fieldName: string;
  fieldType: string;
  typeName?: string;
}

Method Parameters

Visit PostGraphile's docs, for more information about the parameters.

@ExtendSchema({ fieldName: 'email', fieldType: 'String!' })
public addEmailField(
  parent: any,
  args: any,
  context: any,
  info: GraphQLResolveInfo & { graphile: GraphileHelpers<any> },
  useInfoDotGraphileInstead: GraphileHelpers,
  build: any
) {
...
}

Example

However imagine you're selling internationally, and you want to expose the price in other currencies directly from the Product type itself. This kind of functionality is well suited to being performed in Node.js (e.g. by making a REST call to a foreign exchange service over the internet) but might be a struggle from with PostgreSQL.

Note: This example was taken from the PostGraphile docs, and updated to be used with the decorators.

@SchemaType({ typeName: 'Product'})
export class ProductType {

  @ExtendSchema({ fieldName: 'priceInAuCents', fieldType: 'Int! @requires(columns: ["price_in_us_cents"])' })
  public priceInAuCents(product: any) {
    // Note that the columns are converted to fields, so the case changes
    // from `price_in_us_cents` to `priceInUsCents`
    const { priceInUsCents } = product;
    return await this.convertUsdToAud(priceInUsCents);
  }

  private async convertUsdToAud(price: number) {
    ...
  }
}

Query Example

The below is a simple example which would have been better served by Custom Query SQL Procedures; however it demonstrates using ExtendSchema with a database record, table connection, and list of database records.

Note: This example was taken from the PostGraphile docs, and updated to be used with the decorators.

@Injectable()
export class PostGraphileSchemaUpdates {

  // Since we specify `typeName` we don't have to be inside a class with `SchemaType`
  @ExtendSchema({ typeName: 'Query', fieldName: 'randomUser', fieldType: 'User' })
  public randomUser(
    _query: any,
    args: any,
    context: any,
    resolveInfo: GraphQLResolveInfo & { graphile: GraphileHelpers<any> },
    { pgSql: sql }: any
  ) {
    // Remember: resolveInfo.graphile.selectGraphQLResultFromTable is where the PostGraphile
    // look-ahead magic happens!
    const rows = await resolveInfo.graphile.selectGraphQLResultFromTable(
      sql.fragment`app_public.users`,
      (tableAlias, queryBuilder) => {
        queryBuilder.orderBy(sql.fragment`random()`);
        queryBuilder.limit(1);
      }
    );
    return rows[0];
  }
}

Mutation Example

you might want to add a custom registerUser mutation which inserts the new user into the database and also sends them an email

Note: This example was taken from the PostGraphile docs, and updated to be used with the decorators.

const CUSTOM_TYPES = gql`
  input RegisterUserInput {
    name: String!
    email: String!
    bio: String
  }

  type RegisterUserPayload {
    user: User @pgField
    query: Query
  }
`;

@Injectable()
export class PostGraphileSchemaUpdates {
  
  public async mockSendEmail(...) {
    ...
  }

  @ExtendSchema({
    typeName: 'Mutation',
    fieldName: 'registerUser(input: RegisterUserInput!)',
    fieldType: 'RegisterUserPayload',
    additionalGraphql: CUSTOM_TYPES })
  public registerUser(_query: any, args: any, resolveInfo: any, { pgSql: sql }: any) {
    const { pgClient } = context;
    // Start a sub-transaction
    await pgClient.query("SAVEPOINT graphql_mutation");
    try {
    // Our custom logic to register the user:
    const {
      rows: [user],
    } = await pgClient.query(
      `INSERT INTO app_public.users(name, email, bio) VALUES ($1, $2, $3) RETURNING *`,
      [args.input.name, args.input.email, args.input.bio]
    );
    // Now we fetch the result that the GraphQL
    // client requested, using the new user
    // account as the source of the data. You
    // should always use
    // `resolveInfo.graphile.selectGraphQLResultFromTable` if you return database
    // data from your custom field.
    const [
      row,
    ] = await resolveInfo.graphile.selectGraphQLResultFromTable(
      sql.fragment`app_public.users`,
      (tableAlias, queryBuilder) => {
        queryBuilder.where(
          sql.fragment`${tableAlias}.id = ${sql.value(user.id)}`
        );
      }
      );
      // Finally we send the email. If this
      // fails then we'll catch the error
      // and roll back the transaction, and
      // it will be as if the user never
      // registered
      await this.mockSendEmail(
        args.input.email,
        "Welcome to my site",
        `You're user ${user.id} - ` + `thanks for being awesome`
      );
      // Success! Write the user to the database.
      await pgClient.query("RELEASE SAVEPOINT graphql_mutation");
      // If the return type is a database record type, like User, then
      // you would return `row` directly. However if it's an indirect
      // interface such as a connection or mutation payload then
      // you return an object with a `data` property. You can add
      // additional properties too, that can be used by other fields
      // on the result type.
      return {
        data: row,
        query: build.$$isQuery,
      };
    } catch (e) {
      // Oh noes! If at first you don't succeed,
      // destroy all evidence you ever tried.
      await pgClient.query("ROLLBACK TO SAVEPOINT graphql_mutation");
      throw e;
    }
  }
}