Building a GraphQL Service to Expose Data in a SQL Database
- How to Create a GraphQL API
- Custom Views
- Testing Your GraphQL API
- Using Variables in a Query
- Using Aliases
- Getting Multiple Records
- Calling a Mutation
- Required Fields
- Reporting Errors
- How to Create Custom Mutations
- Security
- Creating a Test Page For Your GraphQL API
- Xbasic Helper Functions
- Javascript Helper Functions
- Videos
Description
GraphQL is a popular approach for defining APIs. Unlike REST APIs, which have multiple endpoints, a GraphQL API exposes a single endpoint that allows many different types of queries and mutations (a mutation is a method that changes data). Alpha Anywhere's built-in genie makes creating a GraphQL service (i.e. API) to expose data in a SQL database extremely easy.
How to Create a GraphQL API
Alpha Anywhere has a built-in genie that automatically generates a GraphQL API that can be used to query a SQL database, perform CRUD operations against tables in a SQL database, execute custom mutations (i.e. call custom function defined in Xbasic classes or Node APIs that you write).
To create a GraphQL API select the Web Services category in the Web Control Panel.
Then click the New button.
This will display the New Service Type dialog.
Click the Create a new definition to create a GraphQL service button. This will open the builder.
The GraphQL service builder assumes you will be creating a service to expose data in a SQL database. Therefore, you must specify the connection string to your SQL database. You will then be able to select which tables in your SQL database you want to expose in your service.
Click the Change Connection String... hyperlink and select a connection string.
Next, click the Pick Tables... hyperlink. The Select Tables to Expose dialog will be shown.
Pick the tables that you want your service to expose. The genie will automatically detect relationships between tables. For example, if you select a connection string for the sample Northwind database, the builder will automatically infer that there is a one-to-many relationship between the Customers and Orders tables, between the Orders and Orders Details table and so on. The builder will also be able to automatically infer that that there is a one-to-one relationship between the Order Details and the Product table, etc.
Custom Views
In addition to exposing the physical tables in your database you can also expose views (virtual tables) by clicking the Add a Custom View hyperlink. A custom view lets you define which columns to expose and what WHERE clause to apply. You can also specify if you want DISTINCT records. (When executing a GraphQL query against a Custom View, the user will be able to apply additional filter parameters - in addition to the WHERE clause defined for the Custom View.)
Another way in which to limit the columns you expose from a particular table is to modify the schema that Alpha Anywhere generates after you click the Save button to close the Database GraphQL Service window.
Click the Create button button to define the Custom View and return to the Database GraphQL Service window.
Once you are back at the Database GraphQL Service window, click the Save button when you are done and give your service a name. Alpha Anywhere will then automatically generate a schema for your service.
A sample of the Schema is shown below (in abbreviated form to save space).
Notice that for each table you selected, the schema specifies which type of CRUD operations are allowed, and it specifies which fields from the table are exposed. In addition, each table may have a children property which specifies the one-to-many tables that are linked to the table and a join property which specifies the one-to-one tables that are linked to the table.
{ "connection": "::Name::sqlserver_northwind", "tables": [ { "name": "Customers", "actualname": "Customers", "query": true, "insert": true, "update": true, "delete": true, "first": true, "cursor": true, "skip": true, "totalCount": true, "orderBy": true, "fields": [ { "name": "CustomerID", "type": "String" }, { "name": "CompanyName", "type": "String" }, { "name": "ContactName", "type": "String" }, // more fields... { "name": "cursor", "expression": "CustomerID", "type": "String" } ], "children": [ { "name": "Orders", "parent": "CustomerID", "child": "CustomerID" } ] }, { "name": "Employees", "actualname": "Employees", "query": true, "insert": true, "update": true, "delete": true, "first": true, "cursor": true, "skip": true, "totalCount": true, "orderBy": true, "fields": [ { "name": "EmployeeID", "type": "Double" }, { "name": "LastName", "type": "String" }, { "name": "FirstName", "type": "String" }, // more fields... { "name": "cursor", "expression": "EmployeeID", "type": "String" } ], "children": [ { "name": "Employees", "parent": "EmployeeID", "child": "ReportsTo" }, { "name": "Orders", "parent": "EmployeeID", "child": "EmployeeID" } ] }, { "name": "OrderDetails", "actualname": "Order Details", "query": true, "insert": true, "update": true, "delete": true, "first": true, "cursor": true, "skip": true, "totalCount": true, "orderBy": true, "fields": [ { "name": "OrderID", "type": "Double" }, { "name": "ProductID", "type": "Double" }, // more fields... { "name": "cursor", "expression": "OrderID+ProductID", "type": "Double" } ], "join": [ { "name": "Orders", "parent": "OrderID", "child": "OrderID" }, { "name": "Product", "parent": "ProductID", "child": "ProductID" } ] }, { "name": "Orders", "actualname": "Orders", "query": true, "insert": true, "update": true, "delete": true, "first": true, "cursor": true, "skip": true, "totalCount": true, "orderBy": true, "fields": [ { "name": "OrderID", "type": "Double" }, { "name": "CustomerID", "type": "String" }, { "name": "EmployeeID", "type": "Double" }, // more fields... { "name": "cursor", "expression": "OrderID", "type": "String" } ], "children": [ { "name": "OrderDetails", "parent": "OrderID", "child": "OrderID" } ], "join": [ { "name": "Customers", "parent": "CustomerID", "child": "CustomerID" }, { "name": "Employees", "parent": "EmployeeID", "child": "EmployeeID" } ] }, { "name": "Product", "actualname": "Product", "query": true, "insert": true, "update": true, "delete": true, "first": true, "cursor": true, "skip": true, "totalCount": true, "orderBy": true, "fields": [ { "name": "ProductID", "type": "Double" }, { "name": "ProductName", "type": "String" }, // more fields... { "name": "cursor", "expression": "ProductID", "type": "Boolean" } ], "children": [ { "name": "OrderDetails", "parent": "ProductID", "child": "ProductID" } ], "join": [ { "name": "Suppliers", "parent": "SupplierID", "child": "SupplierID" } ] } // more tables... ] }
Testing Your GraphQL API
Right-click on the service in the Web Control Panel and select Test API with GraphIQL. GraphIQL is a graphical GraphQL query tester.
You can either perform the testing using Working Preview mode (Development Server does not need to be running) or Live mode (Development server must be running and service will be published to the Live Preview folder).
If you select the Working Preview mode, a dialog will open shown the GraphIQL tester.
If you select Live mode, the browser will open, showing the GraphIQL tester page and your GraphQL API definition will be loaded.
The Document Explorer panel will be shown on the right where you can explore the Queries and Mutations defined in your GraphQL Service.
If you do not see the Document Explorer panel, click the Docs button in the toolbar.
Enter a GraphQL query and then click the Run button to execute your query.
To learn more about GraphQL syntax, visit graphql.org.
Assuming you have build a service that exposes the Customers, Orders, Order Details and Product table from the sample Northwind database, here are some example queries and the corresponding data return by the query.
Query 1 - A simple query that only returns fields from the Customer table. The query uses the GetCustomer query and it specifies the Id (CustomerID: BONAP) of the customer to fetch.
{ GetCustomer(CustomerID: BONAP) { CustomerID ContactName City Country } }
Query 2 - Builds on Query 1, but includes data from the child Orders table. The OrderID and OrderData fields from the Orders table are retrieved
{ GetCustomer(CustomerID: BONAP) { CustomerID ContactName City Country Orders{ OrderID OrderDate } } }
Result of Query 2
{ "data": { "GetCustomer": { "CustomerID": "BONAP", "ContactName": "Laurence Lebihan", "City": "Marseille", "Country": "France", "Orders": [ { "OrderID": 10331, "OrderDate": "1994-11-16 00:00:00" }, { "OrderID": 10340, "OrderDate": "1994-11-29 00:00:00" }, { "OrderID": 10362, "OrderDate": "1994-12-26 00:00:00" }, { "OrderID": 10470, "OrderDate": "1995-04-11 00:00:00" }, .......truncated
Query 3 - Builds on Query 2, but includes data from the Order Details table (which is aliased as OrderDetails). OrderDetails is a child table of the Orders table.
{ GetCustomer(CustomerID: BONAP) { CustomerID ContactName City Country Orders{ OrderID OrderDate OrderDetails{ ProductID Quantity UnitPrice Product_ProductName } } } }
Result of Query 3
{ "data": { "GetCustomer": { "CustomerID": "BONAP", "ContactName": "Laurence Lebihan", "City": "Marseille", "Country": "France", "Orders": [ { "OrderID": 10331, "OrderDate": "1994-11-16 00:00:00", "OrderDetails": [ { "ProductID": 54, "Quantity": 15, "UnitPrice": 5.9, "Product_ProductName": "Tourtière" } ] }, { "OrderID": 10340, "OrderDate": "1994-11-29 00:00:00", "OrderDetails": [ { "ProductID": 18, "Quantity": 20, "UnitPrice": 50, "Product_ProductName": "Carnarvon Tigers" }, { "ProductID": 41, "Quantity": 12, "UnitPrice": 7.7, "Product_ProductName": "Jack's New England Clam Chowder" }, { "ProductID": 43, "Quantity": 40, "UnitPrice": 36.8, "Product_ProductName": "Ipoh Coffee" } ] },
Using Variables in a Query
You can use variables, rather than hard coded literal values in your GraphQL queries. In the above samples, the CustomerID parameter is hard coded as BONAP. This example can be rewritten to use a variable for the CustomerID parameter value.
To use a variable, define your variables in Query Variables section in the GraphIQL builder.
Query Variables are defined using a JSON syntax. For example:
{ "customerId": "BOLID", "city": "London" }
To consume a query variable in the query definition, use the variable name with a $ prefix. For example:
{ GetCustomer(CustomerID: $customerId) { CustomerID ContactName City Country } }
Using Aliases
You can define an alias for any of the property names in the query result. For example in the sample Query 3 above, the Product Name is called Product_ProductName in the query result. You can change the alias to a simpler name, such as ProductName. To do this, specify an alias in the query definition. To specify an alias for a property name, prefix the property name with aliasName: where aliasName is the alias you want to use.
For example:
{ GetCustomer(CustomerID: BONAP) { CustomerID ContactName City Country Orders{ OrderID OrderDate OrderDetails{ ProductID Quantity UnitPrice ProductName:Product_ProductName } } } }
Getting Multiple Records
In the previous examples we have executed queries that returned a single record. The Alpha Anywhere genie also generates queries to return multiple records. For example, the genie generates these queries for the Customer table: GetCustomer and GetManyCustomers.
When executing a GetMany* query (e.g. GetManyCustomers, GetManyOrders, etc.), you can optionally specify that you want to return a "page" of records (by specifying a "page size" parameter -- called first. You can also specify a cursor property to indicate at which record you want to start, and a skip property which indicates how many records you want to skip - after the record with the specified cursor) . For example, to return the first page of records where the Country field is "USA" and the page size is 3 records, you would define this query shown below:
Notice that the fields to be returned by the query are specified in an object called records. Your query definition can also include a property called pageInfo, which returns information about the pages in the query result, specifically if there are more pages of data.
{ GetManyCustomers( country: USA, first:3, skip:0){ records{ CustomerID City ContactName cursor } pageInfo { hasNextPage } } }
Here is the result of this query:
{ "data": { "GetManyCustomers": { "records": [ { "CustomerID": "GREAL", "City": "Eugene", "ContactName": "Howard Snyder", "cursor": "eyJjdXJzb3IiOiJHUkVBTCJ9" }, { "CustomerID": "HUNGC", "City": "Elgin", "ContactName": "Yoshi Latimer", "cursor": "eyJjdXJzb3IiOiJIVU5HQyJ9" }, { "CustomerID": "LAZYK", "City": "Walla Walla", "ContactName": "John Steel", "cursor": "eyJjdXJzb3IiOiJMQVpZSyJ9" } ], "pageInfo": { "hasNextPage": true } } } }
Notice that in the query result pageInfo.hasNextPage property we can see that there are more pages of data. To get the second page of data we include the cursor property in the query parameters and we set the skip property to 1. (If we did not set the skip property to 1 then the last record in the previous query would become the first record in this query).
Here is the query to get the second page of records:
{ GetManyCustomers( country: USA, first: 3, skip: 1 cursor: "eyJjdXJzb3IiOiJMQVpZSyJ9" ){ records{ CustomerID City ContactName cursor } pageInfo{ hasNextPage } } }
Calling a Mutation
By default the GraphQL genie will automatically create mutations to update, insert, and delete records in each of the tables in your API definition.
In the next example, we show how to do an update on the Customers table.
mutation { UpdateCustomer( where: {CustomerID: BOLID}, set: {City: Madrid} ) { CustomerID CompanyName ContactName ContactTitle City Country } }
The syntax for the mutation includes a where clause with values for certain fields (in the above example, the record for CustomerID = BOLID will be updated) and a set clause that contains a comma delimited list of fields and corresponding values. You can use arguments in both the where clause and the set clause. You can also optionally specify fields to be returned after he update is completed. In the above example, after the update is performed, the query returns data for the CustomerID, CompanyName, ContactTitle, City and Country fields.
In addition to the automatically created mutations (such as the UpdateCustomer mutation shown above), you can also create custom mutations (using either Xbasic or NodeJS). This is discussed below.
This mutation will update multiple records (because the Set clause specified city = London and there are multiple matching records):
mutation { UpdateCustomer(where: {city: london}, set: {ContactTitle: Apprentice}) { CustomerID CompanyName ContactName ContactTitle City Country } }
Here is a mutation to insert a new record:
mutation { AddCustomer(CustomerID: ALPHA, CompanyName: "Alpha Software", ContactName: "John Smith") { CustomerID CompanyName ContactName } }
Notice that the inserted record has set three field values and after the insert is completed, the query returns the value in the CustomerID field and the CompanyName field.
In this case, the table primary key (CustomerID) is not an auto-increment field (and it is explicitly set when the new record is added). But in many cases the primary key will be an auto-increment field and you will want to know what the primary key of the inserted record is. By specifying the primary key field in the list of fields returned by the query, you can get the value of the primary key.
Required Fields
When you define a GraphQL API to perform CRUD operations on a SQL table, you can mark certain fields in your definition as required fields (i.e. non nullable). For example in the sample definition show earlier, we indicated that the Categories table exposed certain fields. If we want to indicate that the CategoryName field is a required field (so that when you use the AddCategories mutation you will be required to specify a value for this field, change the definition for the CategoryName field to use an ! character after the type name. For example:
{ "name": "CategoryName", "type": "String!" }
Reporting Errors
When you execute a mutation, by default errors are not reported unless you add a directive to your GraphQL definition.
For example, say we want the mutations that operate on the Categories table to report errors. You would need to add a new property to the definition. You can add the "nativeErrors" or "errors" property to the definition (set the property value to true or false). The nativeErrors property will report errors returned by the SQL database. The errors property will return error reported to Xbasic.
For example:
"totalCount": true, "orderBy": true, "nativeErrors": true, ....
To see the errors in the query result add the __error column to the query result specification.
You can easily set or unset the error properties for all tables in your GraphQL service definition by clicking on the Runtime Error Settings hyperlink the the builder.
This will open a builder when you can select the error setting.
How to Create Custom Mutations
Alpha Anywhere automatically adds mutations to perform update, insert and delete operations for all of the tables you select when you define the service (unless you set the corresponding query, update, insert or delete property to false in the service definition). You may also want your GraphQL service to define custom mutations -- that you write using either Xbasic or Node.
To add custom mutations to your GraphQL API create a new Xbasic class or Node API. Your Xbasic class definition or Node API will include one or more methods. Your GraphQL API will expose all of the methods defined in the Xbasic class or Node API.
In the walk-through that follows we will define a custom mutation (called parseNameXB based on a method in an Xbasic class) that will take a string of the form:
firstname lastname,streetNumber streetName,city state zip
and will return and object with these properties:
firstname lastname address { streetNumber streetName citystatezip { city state zip } }
We will then show a second custom mutation (called parseNameNode) that does the same thing, but this time based on a method defined in a Node API.
Here is a summary of the steps we will go through:
- Create an Xbasic class with a method called parseNameXB.
- Test the method in the Interactive Window
- Create a new GraphQL service and define a Custom Mutation using the Xbasic class you created
- Test the custom Mutation in the GraphIQL tester
Create an Xbasic class
At the Web Projects Control panel, select the Xbasic category. Click the New button and create a new Xbasic Class. Enter the following code in the editor:
define class customMutations::class1 function parseNamexb as customMutations::name (fullnameandaddress as c) dim text as c = fullnameandaddress dim line1 as c dim line2 as c dim line3 as c text = comma_to_crlf(text) line1 = alltrim(word(text,1, crlf())) line2 = alltrim(word(text,2, crlf())) line3 = alltrim(word(text,3, crlf())) if w_count(line1," ") = 2 then dim fname as c = word(line1,1," ") dim lname as c = word(line1,2," ") else 'account for possible middle initial dim fname as c = word(line1,1," ") dim lname as c = word(line1,-1," ") end if dim streetNumber as c = word(line2,1," ") dim street as c street = alltrim(stritran(line2,streetNumber,"")) 'remove streetnumber from line2 dim zip as c = word(line3,-1," ") dim state as c = word(line3,-2," ",1) dim city as c = alltrim(stritran(line3,zip,"")) 'remove zip from line3 city = alltrim(stritran(city,state,"")) 'remove state from city dim address as customMutations::address dim citystatezip as customMutations::citystatezip address.streetNumber = streetNumber address.street = street citystatezip.zip = zip citystatezip.city = city citystatezip.state = state address.citystatezip = citystatezip parseNamexb.firstname = fname parseNamexb.lastname = lname parseNamexb.address = address end function end class define class customMutations::name dim firstname as c dim lastname as c dim address as customMutations::address end class define class customMutations::citystatezip dim zip as c dim state as c dim city as c end class define class customMutations::address dim street as c dim streetNumber as c DIM citystatezip as customMutations::citystatezip end class
Here are some key points about the code in the method definition:
- We define four classes in the file, one of which contains the definitions of the methods we want to expose. The other three classes (customMutations::name, customMutations::citystatezip and customMutations::address) define complex types used in defining the return value of the method.
- Notice that the parseNameXB method returns a value of type customMutations::name. (It is more common to see custom Xbasic code return a value of type P, but by setting the return value to an explicit type we can be more specific about what values the method returns).
- The customMutations::name class is defined in the file. Notice that this class returns a class with 3 properties, one of which (address) is also a complex type.
- The address type is also defined in the file and one of the properties in this class is another complex type (citystatezip) which is also defined in this class.
- The method that does the work (parseNameXb) works as follows:
- Split the input parameter into 3 lines
- Extract fname and lname from line1 (taking into account that the input value might include a middle initial, in which case line1 will have 3 words)
- Extract the streetnumber and street from line2
- Extract the city, state and zip from line3
- DIM an instance of the address and citystatezip types
- Assign property values to the properties of these two classes
- Assign the return value of the method
When you are done entering the Xbasic class definitions, click the Save button on the toolbar. Alpha Anywhere will suggest a default file name, which you can accept.
Testing with the Interactive Window
To test the code in the mutation from the Interactive window, go to the Interactive window and enter these lines (pressing enter after you enter each line)
dim x as customMutations::class1 p = x.parseNamexb("fred smith, 123 main street,boston ma") ?json_generate(p) = { "firstname": "fred", "lastname": "smith", "address": { "street": "main street", "streetNumber": "123", "citystatezip": { "zip": "ma", "state": "boston", "city": "" } } }
Create a new GraphQL Service
Next you will need to add the custom mutation to your GraphQL service definition. Edit the service definition and then click on the User-defined mutations hyperlink at the bottom of the window.
This will open a dialog where you can specify the classes that define the methods you want to expose in your custom mutations:
Click the Select class hyperlink.
Click OK twice. Then, save the service definition.
Testing Custom Mutation in the GraphIQL Tester
To test the mutation in the GraphIQL tester, right-click on the service name in the Web Control Panel and select the Test with GraphIQL command.
Enter this mutation:
mutation { parseNamexb(fullnameandaddress: "Fred Smith,123 Main St,Ithaca NY 12345") { firstname lastname address { street streetNumber citystatezip { city state zip } } } }
When you run the query, you should see this result:
{ "data": { "parseNamexb": { "firstname": "Fred", "lastname": "Smith", "address": { "street": "Main St", "streetNumber": "123", "citystatezip": { "city": "Ithaca", "state": "NY", "zip": "12345" } } } } }
Security
By default, the GraphQL API that you create is not authenticated. You can add authentication to your API by clicking the Authorization hyperlink in the GraphQL Service builder.
This will open a dialog where you can specify the name of the method to call to authorize the user. You can use either Xbasic or Node to write the authorization method.
You can also specify if the arguments to the authentication method are supplied in the request header, or in the query string.
The authentication method should return an HTTP status code. For example if your method returns 200, the user is authorized. If your method returns 401, the user is denied permission to use the API.
For example, here is a Xbasic class that defines an authorize method that you can specify in the Authentication method dialog. This class must be defined in the Xbasic section of the Web Control Panel. The method authorizes the user if they specify a value of "alpha" for the apikey.
define class graphql::auth function authorize as n (apikey as c ) if apikey = "alpha" authorize = 200 else authorize = 401 end if end function end class
After creating the class, the Authentication method can be configured in the GraphQL Service:
The next time you test the API, you'll be prompted to enter an API key to continue:
The above configuration used the QueryString to pass the API key to the service. The API key can be passed on the Query String (part of the URL) or in a header:
- Arguments
- Description
- queryString
Authorization is passed to the API as a query string parameter.
- header
(Recommended) Authorization information is passed to the API as a header. The header that contains the authorization information is defined in the Header arguments property. Placeholders for arguments to the authentication method can be referenced in the Header argument value, which will pass the value of the header to the specified authentication argument when a request is made.
Creating a Test Page For Your GraphQL API
You might want your published application to include a page where users can test your GraphQL API (in the same way that you can test it at development time by right clicking on the service name in the Web Control Panel, and then selecting the Test API with GraphIQL command.
To create a page for testing your GraphQL service open the GraphQL service definition and click the Create GraphQL API test page hyperlink.
Xbasic Helper Functions
The following Xbasic helper functions are available:
- Function
- Description
- a5wcb_graphQLQueryBuilder()
Allows you to use the GraphIQL Query Tester to define a GraphQL query.
- a5_graphQL_Execute()
Allows you to execute a GraphQL query against a GraphQL service defined in Alpha Anywhere.
Javascript Helper Functions
You can also execute queries against GraphQL services directly from the browser using a function defined in the Alpha Anywhere Javascript library.
- Function
- Description
- {dialog.object}.graphQLQuery()
Executes a GraphQL query. Takes two functions as parameters to call when the query succeeds and when the query fails.
- {dialog.object}.graphQLQueryPromise()
Executes a GraphQL query. Returns a promise.
Videos
Introduction to GraphQL
Discover GraphQL and how it can be used to build custom APIs for your Alpha Anywhere applications.
Building a GraphQL Service
In this video, we show how to create a GraphQL API to expose data in a SQL database.
Testing Your Service
Once you have defined a GraphQL API, you can use the built-in tester to test your GraphQL API. In this video, we show how this is done.
Testing Your Service - Using Variables
You can use variables in your query parameters rather than using hard coded literal values. In this video, we show how this is done.
Testing Your Service - Getting Multiple Records
You can use a GetMany query to return multiple records. When you return multiple records, you can specify a page size, number of records to skip over etc. In this video, we show how this is done.
Paginated Queries
When you use a GetMany query, you can do paginated queries. In this video, we show how this is done.
Update Mutations
Using an Update mutation, you can perform Updates on any of the tables exposed in the API. In this video, we show how this is done.
Add Mutations
Using an Add mutation, you can add records to a table. In this video, we show how this is done.
Delete Mutations
Using a Delete mutation, you can delete records to a table. In this video, we show how this is done.
Executing a GraphQL Query Directly from the Browser
When you execute a GraphQL query, you can either do the query in server-side code or client-side code.
In this video, we show how to make a request to a GraphQL endpoint directly from the browser and populate a List control with the returned data.
See Also