Schema

sGraph uses a schema first approach to GraphQL API development. The behavior of the API is used to mostly controlled by the type definitions and their associations in the schema.

Here is the simplest form of a type definition in a schema. This type is mapped to an underlying table in the database

type Employee @model {
    # This corresponds to the primary key of the Employee table
    Id: ID

    FirstName: String
    LastName: String
}

Architecture

sGraph is powered by Sequelize ORM, so underneath, each defined type in the schema is mapped directly to a sequelize model which is in turn mapped to a database table. Each defined type in the schema will be mapped to a corresponding table in the underlying database.

classDiagram SchemaEmployee <|.. SequelizeEmployee: Mapped class SchemaEmployee { string Id string FirstName string LastName } class SequelizeEmployee { string Id string FirstName string LastName }

Definition

sGraph schema uses a collection of directives to influence how the eventual API is generated. The generated APIs are dynamic therefore, any changes in the schema will be reflected in API when the server is restarted.

Primary Key

Every type requires at least one field marked as the @primaryKey or typed as ID and this will correspond to the primary key field of the underlying database table. The name of the type is used to map to the SQL table.

type Employee {
    Id: ID
}
type Employee {
    Id: Int @primaryKey
}

Modelling

DirectiveDescriptionArgsDefault Behavior
@modelMaps a type to a database table using the Type name. The name can be modified by the tableName argument{ tableName: String }The type name will used for the SQL Table name
@autoTimestampAutomatically creates createdAt and updatedAt fields for typesnonenow
@crudControls what CRUD API is generated for this type. In some cases, it might be helpful to disable mutations on a public API for example{ create: Boolean, read: Boolean, update: Boolean, delete: Boolean }Full CRUD is enabled by default
@autoIncrementMarks a field to auto incrementAuto increment this field
@columnRemaps a field to a column in the database{ name: String }The field name maps directly to the database columns

Example Schema

type Employee
    @model(tableName: "employees")
    @autoTimestamp
    @crud(delete: false) {
    id: ID
    logins: Int @autoIncrement
    firstName: String @column(name: "FirstName")
}

Special Scalar Types

sGraph ships with special scalar types that are automatically validated before insertion into the database and makes the schema more readable and contextual

TypeDescriptionUnderlying Type
UUIDUUIDV4UUID
EmailEmail formatted fieldString
DateTimeDatabase timestampDate, defaults to now
DateDate only (without the time component)DateTime, defaults to now
URLURL formatted fieldString
CreditCardCredit card formatted fieldString
IPV4IP address version 4String
IPV6IP address version 6String
JSONJSON format for databases that support itJSON

:::info

These scalar fields are automatically validated before insertion into the database except for the JSON type

:::

Example Schema

type Post @model(tableName: 'Posts') @autoTimestamp {
    id: UUID @primaryKey
    permalink: URL! @column(name: "perma_link")
    authorId: Email
    origin: IPV4
    content: JSON
    birthday: Date
}

Field Validations

sGraph supports all the validations supported by Sequelize. Validations are applied before insertion into the database.

DirectiveDescriptionArgs
@validate_isAlphawill only allow lettersNone
@validate_isAlphanumericwill only allow alphanumeric characters, so "_abc" will failNone
@validate_isNumericwill only allow numbersNone
@validate_isLowercasechecks for lowercaseNone
@validate_isUppercasechecks for uppercaseNone
@validate_notEmptydon't allow empty stringsNone
@validate_equalsonly allow a specific valuevalue: String
@validate_containsforce specific substringsvalue: String
@validate_lenonly allow values with a certain lengthvalue: [Int!]!
@validate_isAfteronly allow date strings after a specific datevalue: String
@validate_isBeforeonly allow date strings before a specific datevalue: String
@validate_maxonly allow values below this maxvalue: Int
@validate_ismatches this a RegExpvalue: String
@validate_notdoes not match a RegExpvalue: String

Example Schema

type Profile @model {
    id: ID @primaryKey

    # Validate post code
    postcode: String @validate_is(value: "[A-Z]{1,2}[0-9][0-9A-Z]?\s?[0-9][A-Z]{2}")

    # Title must be Mr|Mrs
    title: String @validate_contains(value: "Mr")

    #  Must be at least 18 years
    dob: String @validate_isAfter(value: "2004-01-01")

    # Address is of 20 characters long
    address: String @validate_len(value: 20)
}

Associations

sGraph supports all the types of associations supported by the Sequelize ORM and the respective parameters.

Here are the supported associations that are directly mapped to the associations supported by Sequelize. All association options supported by Sequelize are also supported.

Database RelationshipDirective
One-to-One@belongsTo, @hasOne
One-to-Many@hasMany
Many-to-Many@belongsToMany

What directive to use depends on the typ of relationship between the types and the source of that relationship

One-to-One relationships

To model a one-to-one relationship, use both the @hasOne and @belongsTo directive to create single direction or bi-directional relationship between types.

@belongsTo

Using this example below, an OrderDetail references a Product, therefore it (OrderDetails) holds the key to the relationship between it and the Product.

erDiagram
    OrderDetail ||..|| Product: Product

    OrderDetail {
        int Id PK
        float Quantity
        string ProductId FK
    }

    Product {
        int Id PK
        float UnitPrice
    }

In this case an OrderDetail is said to belong to a Product because OrderDetail owns the relationship. OrderDetail is referred to as the source type and Product is the target type. The source type defines the relationship

type Customer @model {
    Id: ID
    ContactName: String
    Orders: [Order] @hasMany(onDelete: "CASCADE")
}

type Order @model {
    Id: Int @primaryKey @autoIncrement
    OrderDate: Date
    Freight: Float
    CustomerId: String
    Customer: Customer
        @belongsTo(foreignKey: "CustomerId", onDelete: "SET NULL")
}
{
    find_orderdetails(limit: 1) {
        orderdetails {
            Product {
                ProductName
            }
        }
    }
}

Options

ArgumentDescriptionOptionsDefault
foreignKeySet a custom foreignKey to used for the relationship
sourceKeyCustomize field in the source for the relationship
onDeleteThe behavior if the target model is deletedRESTRICT | CASCADE | NO ACTION | SET DEFAULT | SET NULLSET NULL
onUpdateThe behavior if the target model is updatedRESTRICT | CASCADE | NO ACTION | SET DEFAULT | SET NULLSET NULL

One-to-Many

Modelling one-to-many database relationships

erDiagram Customer ||--o{ Order: has_orders Order { string Id PK string ShipCountry string CustomerId FK } Customer { string Id PK string ContactName }
type Customer @model {
    ID: String
    ContactName: String

    Orders: [Order] @hasMany(foreignKey: 'CustomerId')
}

type Order @model {
    Id: ID
    ShipCountry: String
    CustomerId: String
}
{
    find_customers(limit: 2) {
        customers {
            Orders(limit: 5) {
                ShippedDate
            }
        }
    }
}

Many-to-Many

Modelling many-to-many relationships

erDiagram
Order ||--o{ OrderDetail: has
Product ||--o{ OrderDetail: belongs

Order {
    string Id PK
    date   OrderDate
    string ShipName
    string ShipAddress
}

OrderDetail {
    int Id PK
    string CustomerId FK
    string ProductId FK
}

Product {
    int Id PK
    string ProductName
    float UnitPrice
    int ReorderLevel
}

type Order @model {
    Id: ID
    OrderDate: Date

    Products: [Product] @belongsToMany(through: "OrderDetail")
}

type OrderDetail @model {
    Id: ID
    Quantity: Float

    # The many-to-many relationship expects these Ids to exist on this table in this exact format. i.e ModelId
    # If this not the case, the column name can be mapped instead.
    OrderId: String
    # This is an example of how to keep the schema rules mapped to the underlying database
    ProductId: String @column(name: "ProductId")

    Order: Product @belongsTo(sourceKey: "OrderId")
    Product: Product @belongsTo(sourceKey: "ProductId")
}

type Product @model {
    Id: ID
    ProductName: String
    UnitPrice: Float
}
# Find the products attached to an order with price greater than 20
{
    find_orders(limit: 1) {
        orders {
            Products(where: { UnitPrice: { gt: 20 } }) {
                UnitPrice
                ProductName
            }
        }
    }
}

:::info To successfully model a many-to-many relationship, remember to make sure that the joint table has the respective ModelId columns or map them in the schema as the schema above :::

Self Referential Relationships

Combining @belongsTo and @hasMany to model a self referential relationships.

In this example, an Employee can manage other employees but can also be managed by another employee.

erDiagram Employee |o..o| Employee: Manager Employee }o..o{ Employee: Manages Employee { int Id string FirstName int ReportsTo FK }
type Employee @model {
    Id: Int @primaryKey
    FirstName: String
    ReportsTo: Int

    Manager: Employee @belongsTo(foreignKey: "ReportsTo")
    Manages: [Employee] @hasMany(foreignKey: "ReportsTo")
}
{
    find_employee_by_pk(id: 5) {
        FirstName
        Manager {
            FirstName
        }
        Manages {
            FirstName
        }
        Manages_aggregate {
            count
        }
    }
}
Edit this page on GitHub Updated at Tue, Mar 8, 2022