GraphQL offers a popular way to query, manipulate, and subscribe to data. Interestingly GraphQL is transport protocol-agnostic, although in reality most requests are served over HTTP. But what’s stopping you from creating GraphQL endpoints in the database? This post hopefully shows that nothing does.

The article assumes you are reasonably familiar with GraphQL, if not, may I suggest you head over to graphql.org for an introduction and some really good demos.

By the way, if you are looking for an alternative way to enable GraphQL for your schema, I suggest you have a look at Oracle REST Data Services (ORDS): ORDS offers GraphQL-support as a feature, documented in Oracle® REST Data Services Developer’s Guide chapter 10 .

The Environment

An Oracle Database 23ai Free container image from Oracle’s container registry was used to write this post. The container instance ran in Podman on Oracle Linux 8 x86-64.

The GraphQL endpoint is implemented using GraphQL’ JavaScript, GraphQL’s reference implementation in JavaScript. You may have guessed it, this is possible thanks to Multilingul Engine (MLE) allowing me to run server-side JavaScript within the database.

As always, before using third-party modules in your project, make sure that your security and compliance department give you the go-ahead before starting your project!

Here is the list of the components used in the project for reference:

{
  "devDependencies": {
    "@rollup/plugin-node-resolve": "^15.2.3",
    "mle-js-oracledb": "^23.3.0",
    "prettier": "^3.2.5",
    "rollup": "^4.14.3"
  },
  "type": "module",
  "dependencies": {
    "graphql": "^16.8.1"
  }
}

Node v20.11.1 is installed and maintained via nvm. Linting doesn’t feature in this little post to keep it manageable.

Getting the necessary JavaScript modules

The code is based on GraphQL 16.8.1, the most current version at the time of writing. For some unknown reason several content delivery networks failed to provide an ECMA Script Version of the graphql module, meaning it is necessary to roll your own.

Challenge accepted.

Use your favourite IDE and install the above modules in a new directory, based on the package.json from earlier in this post. Note that versions might be out of date by the time you read this, always make sure you use the latest version of a given package to avoid potential security vulnerabilities. You are going to develop the MLE modules in your favourite IDE (on a file system) first, before they can be loaded into the database.

Creating the GraphQL schema

In the next step you are going to create the GraphQL schema, based on the HR.LOCATIONS table. It should be possible to perform the following 3 queries:

  • get location by ID (a primary key lookup)
  • get a location based on various criteria
  • get all locations

The GraphQL schema defines a location type, representing all the columns in the table. These are:

SQL> desc hr.locations
 Name                                      Null?    Type
 ----------------------------------------- -------- ----------------------------
 LOCATION_ID                               NOT NULL NUMBER(4)
 STREET_ADDRESS                                     VARCHAR2(40)
 POSTAL_CODE                                        VARCHAR2(12)
 CITY                                      NOT NULL VARCHAR2(30)
 STATE_PROVINCE                                     VARCHAR2(25)
 COUNTRY_ID                                         CHAR(2)

There’s also a list type for returning more than 1 location. Finally, the Query Type defines all 3 aforementioned queries. The resulting JavaScript code is shown in the appendix.

The following function assembles the schema and returns it. And yes, that’s a lot of code. A future article shows how to use SDL (Schema Definition Language) instead of JavaScript to create the schema.

/**
 * A function to generate a GraphQL Schema based on the Locations
 * table stored in the HR schema.
 *
 * @returns {GraphQLSchema} The generated GraphQLSchema
 */
export async function setupSchema() {

    // a lot of detail skipped

    const QueryType = new GraphQLObjectType({
        name: 'Query',
        fields: () => ({
            getLocationByID: {
                type: LocationType,
                args: {
                    id: { type: GraphQLInt },
                },
                resolve: async (root, args, context, info) => {
                    const { id } = args;

                    return await getLocationByID(id);
                },
            },
           // more detail skipped
        }),
    });

    // this is where everything is put together.
    const schema = new GraphQLSchema({
        query: QueryType,
    });

    return schema;
}

The resolver functions in the schema use the MLE JavaScript SQL Driver to fetch information from the database and return it. This is particularly convenient: the IExecuteReturn object provides the result set as an array of objects by default. These can simply be returned – no extra work needed.

The next step is to define the database API (you can find it in the appendix). It’s just 3 functions selecting from the database, each returns a row from the table or an array of rows:

  1. getLocationByID() performs a lookup based on a primary key
  2. getLocation(): lookups based on multiple critieria
  3. getAllLocations(): no surprises there 🙂

Defining the entrypoint

Now that the schema is complete an entry point is needed. A 3rd file, grapQLQuery.js allows the execution of arbitrary GraphQL queries against the schema.

import { setupSchema } from './graphQLSchema.js';
import { graphql } from 'graphql';

/**
 * Perform a generic GraphQL query
 * @param {String} queryText - the graphQL query
 * @param {object} args - arguments to the query, empty array if none
 * @returns {object} the JSON representation of the query result. Might contain errors, too
 */
export async function graphQLQuery(queryText, args) {
    const schema = await setupSchema();
    const results = await graphql({
        schema,
        source: queryText,
        variableValues: args,
    });

    return results;
}

This function can either be used directly in JavaScript, or later on wrapped into a call specification, made available to SQL and PL/SQL.

Loading the code into the database

Let’s take a step back and recap what has happened so far

  • The required 3rd party modules have been downloaded from npm
  • 3 files have been created by you:
    • the graphQL schema
    • the database API
    • the entrypoint into GraphQL

What’s next, or rather, how do you get these into the database? Remember from earlier it wasn’t possible to get an ECMAScript version of the GraphQL module from the content delivery network. Using the latest version of a module is strongly recommended however, which means you have to bundle your own code with the third party code. In the JavaScript world this is done using a bundler.

This post features rollup for no particular reason. Based on the following configuration rollup bundles everything (except mle-js-oracledb, which already exists in the database) into a single file, which you use as the module’s source. The file is named rollup.config.mjs and it defines what rollup should do:

import { nodeResolve } from '@rollup/plugin-node-resolve';

export default {
    input: 'src/graphQLQuery.js',
    output: {
        file: 'build/bundle.js',
        format: 'esm',
    },
    plugins: [
        nodeResolve({
            jsnext: true,
            exclude: ['mle-js-oracledb']
        }),
    ],
    external: [ 'mle-js-oracledb'  ],
};

Note that the rollup configuration file is well-documented. A short call to npx rollup -c creates build/bundle.js which I loaded as GRAPHQL_ENDPOINT_MODULE.

Testing

Now that the module is present you can finally test it all. The first step is to create the call specification for the MLE module. Once the call specification has been created, you can call it from anywhere you call SQL and PL/SQL.

create or replace function graphql_query(
  p_query varchar2,
  p_args json
) return json
as mle module GRAPHQL_ENDPOINT_MODULE
signature 'graphQLQuery';
/

select
  json_serialize(
    graphql_query(
      'query locByID($id: Int) { getLocationByID(id: $id) { city country_id } }',
      JSON('{id: 1000}')
    )
    pretty
  ) graphql_result
/

GRAPHQL_RESULT
--------------------------------------------------------------------------------
{
  "data" :
  {
    "getLocationByID" :
    {
      "city" : "Roma",
      "country_id" : "IT"
    }
  }
}

That’s it! The function accepts a GraphQL query and returns a JSON document containing the requested information.

Summary

This was fun! GraphQL is a popular way to interact with APIs, not quite as popular as REST, but it’s on the rise. You had to write a fair bit of code to create the GraphQL schema. A follow-up post demonstrates how to define the GraphQL schema using SDL rather than code.

Appendix

File graphQLSchema.js

import {
    GraphQLSchema,
    GraphQLObjectType,
    GraphQLString,
    GraphQLInt,
    GraphQLList,
    GraphQLError,
} from 'graphql';

import {
    getLocationByID,
    getLocation,
    getAllLocations,
} from './databaseAPI.js';

/**
 * A function to generate a GraphQL Schema based on the Locations
 * table stored in the HR schema.
 *
 * @returns {GraphQLSchema} The generated GraphQLSchema
 */
export async function setupSchema() {
    // maps hr.locations (a relational table) to a GraphQL type
    const LocationType = new GraphQLObjectType({
        name: 'Location',
        fields: {
            location_id: {
                type: GraphQLInt,
                resolve: (root, args, context, info) => {
                    return root.LOCATION_ID;
                },
            },
            street_address: {
                type: GraphQLString,
                resolve: (root, args, context, info) => {
                    return root.STREET_ADDRESS;
                },
            },
            postal_code: {
                type: GraphQLString,
                resolve: (root, args, context, info) => {
                    return root.POSTAL_CODE;
                },
            },
            city: {
                type: GraphQLString,
                resolve: (root, args, context, info) => {
                    return root.CITY;
                },
            },
            state_province: {
                type: GraphQLString,
                resolve: (root, args, context, info) => {
                    return root.STATE_PROVINCE;
                },
            },
            country_id: {
                type: GraphQLString,
                resolve: (root, args, context, info) => {
                    return root.COUNTRY_ID;
                },
            },
        },
    });

    // if you want to get all locations back, you have to provide
    // a new type that's essentially a list of location types
    // defined previously
    const LocationsType = new GraphQLList(LocationType);

    // Entry point: you must provide at least a Query type. Mutations
    // and Subscriptions are optional. The Query type defines which
    // queries are accepted, and which parameters (if any) these
    // can take. Except for the most basic examples the Query type
    // uses custom object types.
    const QueryType = new GraphQLObjectType({
        name: 'Query',
        fields: () => ({
            getLocationByID: {
                type: LocationType,
                args: {
                    id: { type: GraphQLInt },
                    city: { type: GraphQLString },
                },
                resolve: async (root, args, context, info) => {
                    const { id } = args;

                    return await getLocationByID(id);
                },
            },
            getLocation: {
                type: LocationType,
                args: {
                    city: { type: GraphQLString },
                },
                resolve: async (root, args, context, info) => {
                    const { street, postalCode, city, stateProvince, country } =
                        args;

                    return await getLocation(
                        street,
                        postalCode,
                        city,
                        stateProvince,
                        country,
                    );
                },
            },
            getAllLocations: {
                type: LocationsType,
                resolve: async (root, args, context, info) => {
                    return await getAllLocations();
                },
            },
        }),
    });

    // this is where everything is put together.
    const schema = new GraphQLSchema({
        query: QueryType,
    });

    return schema;
}

File: databaseAPI.js

import oracledb from 'mle-js-oracledb';

/**
 * Helper function to connect to the database
 * @returns [Connection] a connection to the database
 */
async function getConn() {

    return oracledb.defaultConnection();
}

/**
 * Fetch a location from the database by ID (= primary key)
 * This function is required by the resolver in the GraphQL Query type
 * @returns {Location} the location
 */
export async function getLocationByID(id) {
    const connection = await getConn();

    const result = await connection.execute(
        `select
                location_id,
                street_address,
                postal_code,
                city,
                state_province,
                country_id
            from
                locations
            where
                location_id = :id`,
        [id],
        {
            outFormat: oracledb.OUT_FORMAT_OBJECT,
        },
    );

    const data = result.rows[0];
    return data;
}

/**
 * Fetch all locations by parameter from the database
 * This function is required by the resolver in the GraphQL Query type
 * @returns {[Location]} an array of location types
 */

export async function getLocation(
    street,
    postalCode,
    city,
    stateProvince,
    country,
) {
    const connection = await getConn();

    const result = await connection.execute(
        `select
                location_id,
                street_address,
                postal_code,
                city,
                state_province,
                country_id
            from
                locations
            where
                street = :street`,
        [street],
        {
            outFormat: oracledb.OUT_FORMAT_OBJECT,
        },
    );

    const data = result.rows;
    return data;
}

/**
 * Fetch all locations from the database
 * This function is required by the resolver in the GraphQL Query type
 * @returns {[Location]} an array of location types
 */
export async function getAllLocations() {
    const connection = await getConn();

    const result = await connection.execute(
        `select
            location_id,
            street_address,
            postal_code,
            city,
            state_province,
            country_id
        from
            locations`,
        [],
        {
            outFormat: oracledb.OUT_FORMAT_OBJECT,
        },
    );

    const data = result.rows;
    return data;
}