@grafbase/sdk
v0.24.0
Published
The Grafbase SDK
Downloads
10,922
Readme
Get Started
Adding to an existing project:
npm install @grafbase/sdk --save-dev
Initializing a new project
grafbase init --config-format typescript
Example
The configuration should define the schema, exporting the config as default
:
// g is a schema generator, config the final object to return
import { graph, config } from '@grafbase/sdk'
const g = graph.Standalone()
// types are generated with the `type` method,
// followed by the name and fields.
const profile = g.type('Profile', {
address: g.string()
})
// finally we export the default config
export default config({
graph: g
})
When grafbase dev
finds the above config from $PROJECT/grafbase/grafbase.config.ts
, it genereates the SDL to $PROJECT/.grafbase/generated/schema/schema.graphql
:
type Profile {
address: String!
}
type User @model {
name: String!
age: Int
profile: Profile
parent: User
}
The above SDL is now used when starting the dev.
Federated Graphs
A federated graph can be defined as follows:
import { graph, config } from '@grafbase/sdk'
export default config({
graph: g.Federated()
})
Types
Types are generated with the type
method:
g.type('Profile', {
address: g.string()
})
Enums
Enums can be generated with the enum
method:
g.enum('Fruits', ['Apples', 'Oranges'])
An enum can be used as a field type with the enumRef
method:
const e = g.enum('Fruits', ['Apples', 'Oranges'])
g.type('User', {
favoriteFruit: g.enumRef(e)
})
Queries and Mutations
Queries are generated with the query
method, mutations with the mutation
method:
g.query('greet', {
args: { name: g.string().optional() },
returns: g.string(),
resolver: 'hello'
})
const input = g.input('CheckoutSessionInput', { name: g.string() })
const output = g.type('CheckoutSessionOutput', { successful: g.boolean() })
g.mutation('checkout', {
args: { input: g.inputRef(input) },
returns: g.ref(output),
resolver: 'checkout'
})
Unions
Unions can be done using the union
method:
const user = g.type('User', {
name: g.string(),
age: g.int().optional()
})
const address = g.type('Address', {
street: g.string().optional()
})
g.union('UserOrAddress', { user, address })
Interfaces
Interfaces can be generated using the interface
method, and a type can be extended with an interface:
const produce = g.interface('Produce', {
name: g.string(),
quantity: g.int(),
price: g.float(),
nutrients: g.string().optional().list().optional()
})
g.type('Fruit', {
isSeedless: g.boolean().optional(),
ripenessIndicators: g.string().optional().list().optional()
}).implements(produce)
Notice how one doesn't need to type the fields to the type: they are inferred to the final SDL from the interface definition.
Field generation
Fields are generated from the g
object:
- ID:
g.id()
- String:
g.string()
- Int:
g.int()
- Float:
g.float()
- Boolean:
g.boolean()
- Date:
g.date()
- DateTime:
g.datetime()
- Email:
g.email()
- IPAddress:
g.ipAddress()
- Timestamp:
g.timestamp()
- URL:
g.url()
- JSON:
g.json()
- PhoneNumber:
g.phoneNumber()
Enum fields
// first greate an enum
const fruits = g.enumType('Fruits', ['Apples', 'Oranges'])
// then use it e.g. in a model
g.model('User', {
favoriteFruit: g.enum(fruits)
})
Reference fields
Referencing a type is with the ref
method:
const profile = g.type('Profile', {
address: g.string()
})
g.model('User', {
profile: g.ref(profile)
})
Optional fields
By default the generated fields are required. To make them optional is with the optional
method:
const user = g.type('User', {
posts: g.string().optional()
})
List fields
List fields can be done with the list
method:
const user = g.type('User', {
names: g.string().list()
})
By default, the list or list items are required. To make the items nullable, call the optional
method to the base type:
const user = g.type('User', {
names: g.string().optional().list()
})
To make the list itself optional, call the optional
method to the list type:
const user = g.type('User', {
names: g.string().list().optional()
})
Unique
Unique field can be defined to certain types of fields with the unique
method:
const user = g.type('User', {
name: g.string().unique()
})
Additional unique scope can be given as a parameter:
const user = g.type('User', {
name: g.string().unique(['email']),
email: g.string()
})
Length limit
Certain field types can have a limited length:
const user = g.type('User', {
name: g.string().length({ min: 1, max: 255 })
})
Connectors
Connectors are created through the connector interface:
import { connector } from '../../src/index'
OpenAPI
The OpenAPI connector can be created with the OpenAPI
method:
const openai = connector.OpenAPI("OpenAI",
schema:
'https://raw.githubusercontent.com/openai/openai-openapi/master/openapi.yaml'
})
const stripe = connector.OpenAPI("Stripe", {
schema:
'https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.json',
headers: (headers) => {
// used in client and introspection requests
headers.set('Authorization', `Bearer ${g.env('STRIPE_API_KEY')}`)
// used only in introspection requests
headers.introspection('foo', 'bar')
// forward headers from requests to datasource
headers.set('x-api-key', { forward: 'x-api-key' })
}
})
Connectors can be added to the schema using g.datasource()
, including an optional namespace
:
g.datasource(stripe)
g.datasource(openai)
GraphQL
The GraphQL connector can be created with the GraphQL
method:
const contentful = connector.GraphQL('Contentful', {
url: g.env('CONTENTFUL_API_URL'),
headers: (headers) => {
headers.set('Authorization', `Bearer ${g.env('CONTENTFUL_API_KEY')}`)
headers.set('Method', 'POST')
headers.set('x-api-key', { forward: 'x-api-key' })
}
})
const github = connector.GraphQL('GitHub', {
url: 'https://api.github.com/graphql'
})
Connectors can be added to the schema using g.datasource()
, including an optional namespace
:
g.datasource(contentful)
g.datasource(github)
MongoDB
The MongoDB connector can be created with the MongoDB
method:
const mongodb = connector.MongoDB('MongoDB', {
url: 'https://data.mongodb-api.com/app/data-test/endpoint/data/v1',
apiKey: 'SOME_KEY',
dataSource: 'data',
database: 'tables'
})
// Models must be added manually for this connector.
mongodb.model('User', { field: g.string() }).collection('users')
g.datasource(mongodb)
Authentication
Auth providers can be created through the auth
object.
import { auth } from '@grafbase/sdk'
OpenID
Required fields:
issuer
Optional fields:
clientId
groupsClaim
// first create the provider
const clerk = auth.OpenIDConnect({
issuer: g.env('ISSUER_URL')
})
// add it to the config with the rules
const cfg = config({
schema: g,
auth: {
providers: [clerk],
rules: (rules) => {
rules.private()
}
}
})
JWT
Required fields:
issuer
secret
Optional fields:
clientId
groupsClaim
const derp = auth.JWT({
issuer: g.env('ISSUER_URL'),
secret: g.env('JWT_SECRET')
})
JWKS
Required fields:
issuer
Optional fields:
clientId
groupsClaim
jwksEndpoint
A JWKS provider has to define either issuer
or jwksEndpoint
const derp = auth.JWKS({
issuer: g.env('ISSUER_URL')
})
Authorizer
Required fields:
name
const authorizer = auth.Authorizer({
name: 'custom-auth'
})
The name maps the name of the file including a custom authentication function. For this example, there has to be a file implementing the authentication function in grafbase/auth/custom-auth.js
.
Rule Definitions
Everywhere where one can define authentication rules, it happens through a lambda with a rules builder.
{
rules: (rules) => {
rules.private().read()
rules.groups(['admin', 'root']).delete()
}
}
Global Rules
Global rules are defined through the auth definition in the configuration.
const clerk = auth.OpenIDConnect({
issuer: g.env('ISSUER_URL')
})
const cfg = config({
schema: g,
auth: {
providers: [clerk],
rules: (rules) => {
rules.private()
}
}
})
Caching
Caching can be defined globally, per type or per field.
config({
schema: g,
cache: {
rules: [
{
types: 'Query',
maxAge: 60
},
{
types: ['GitHub', 'Strava'],
maxAge: 60,
staleWhileRevalidate: 60
},
{
types: [{ name: 'Query' }, { name: 'GitHub', fields: ['name'] }],
maxAge: 60
}
]
}
})
g.model('User', {
name: g.string().optional()
}).cache({
maxAge: 60,
staleWhileRevalidate: 60,
mutationInvalidation: 'entity'
})
g.type('User', {
name: g.string().optional()
}).cache({
maxAge: 60,
staleWhileRevalidate: 60,
mutationInvalidation: 'type'
})
g.model('User', {
name: g.string().cache({ maxAge: 60, staleWhileRevalidate: 60 })
})
Extending Types
Types can be extended with extra queries, handled with resolvers.
To extend a type that is defined in the Grafbase schema, define the type first and extend it by using the type as the parameter:
const user = g.type('User', {
name: g.string()
})
g.extend(user, {
myField: {
args: { myArg: g.string() },
returns: g.string(),
resolver: 'file'
}
})
Sometimes a type is not defined directly in the schema, but instead introspected from an external connector. In these cases passing a string as the first argument allows extending the type with custom queries. Keep in mind that in these cases it is not validated in compile-time if the type exist.
g.extend('StripeCustomer', {
myField: {
args: { myArg: g.string() },
returns: g.string(),
resolver: 'file'
}
})
Environment variables
Node's process.env
return nullable strings, which are a bit annoying to use in fields requiring non-nullable values. The schema has a helper g.env()
that throws if the variable is not set, and returns a guaranteed string.
const github = connector.GraphQL('GitHub', {
url: 'https://api.github.com/graphql',
headers: (headers) => {
headers.set('Authorization', `Bearer ${g.env('GITHUB_TOKEN')}`)
}
})
When working locally with Grafbase CLI you must set the environment variables inside grafbase/.env
.