Spraypaint the isomorphic, framework-agnostic Graphiti ORM

Installation

Installation is straightforward. Since we use fetch underneath the hood, we recommend installing alongside a fetch polyfill.

If using yarn:

$ yarn add spraypaint isomorphic-fetch

If using npm:

$ npm install spraypaint isomorphic-fetch

Now import it:

Typescript
Javascript
import {
  Model,
  SpraypaintBase,
  Attr,
  BelongsTo,
  HasMany
  // etc
} from "spraypaint"
const {
  SpraypaintBase,
  attr,
  belongsTo,
  hasMany
  // etc
} = require("spraypaint/dist/spraypaint")

…or, if you’re avoiding JS modules, spraypaint will be available as a global in the browser.

Typescript

Spraypaint supports Typescript versions >= 2.8.

By default you will need a ! after each attribute and relationship:

@Attr first_name!: string
@HasMany() positions!: Position[]

This is due to Strict Class Initialization. For the purposes of Spraypaint, we don’t need this. Remove the need for ! (as the rest of these guides do) by setting

"strictPropertyInitialization": false

in tsconfig.json.

Defining Models

Connecting to the API

Just like ActiveRecord, our models will inherit from a base class that holds connection information (ApplicationRecord, or ActiveRecord::Base in Rails < 5):

Typescript
Javascript
@Model()
class ApplicationRecord extends SpraypaintBase {
  static baseUrl = "http://my-api.com"
  static apiNamespace = "/api/v1"
}
const ApplicationRecord = SpraypaintBase.extend({
  static: {
    baseUrl: "http://my-api.com",
    apiNamespace: "/api/v1"
  }
})

All URLs follow the following pattern:

  • baseUrl + apiNamespace + jsonapiType

As you can see above, typically baseUrl and apiNamespace are set on a top-level ApplicationRecord (though any subclass can override). jsonapiType, however, is set per-model:

Typescript
Javascript
@Model()
class Person extends ApplicationRecord {
  static jsonapiType = "people"
}
const Person = ApplicationRecord.extend({
  static: {
    jsonapiType: "people"
  }
})

With the above configuration, all Person endpoints will begin http://my-api.com/api/v1/people.

TIP: Avoid CORS and use relative paths by simply setting baseUrl to ""

TIP: You can always use the endpoint option to override this pattern and set the endpoint manually.

Setting Application Name

It can be helpful to send the name of your client application in request headers. With this information, servers can keep track of which clients are hitting which APIs.

To do this:

Typescript
Javascript
@Model()
class Person extends ApplicationRecord {
  static clientApplication = "sales-backend"
}
const Person = ApplicationRecord.extend({
  static: {
    clientApplication: "sales-backend"
  }
})

Defining Attributes

ActiveRecord automatically sets attributes by introspecting database columns. We could do the same - swagger.json is our schema - but tend to agree with those who feel this aspect of ActiveRecord is a bit too “magical”. In addition, explicitly defining our attributes can be used to track which applications are using which attributes of the API.

Though this is configurable, by default we expect the API to be under_scored and attributes to be camelCased.

Typescript
Javascript
@Model()
class Person extends ApplicationRecord {
  // ... code ...
  @Attr() firstName: string
  @Attr() lastName: string
  @Attr() age: number

  get fullName() : string {
    return `${this.firstName} ${this.lastName}`
  }
}

let person = new Person({ firstName: "John" })
person.firstName // "John"
person.lastName = "Doe"
person.attributes // { firstName: "John", lastName: "Doe" }
person.fullName // "John Doe"
const attr = spraypaint.attr
const Person = ApplicationRecord.extend({
  // ... code ...
  attrs: {
    firstName: attr(),
    lastName: attr(),
    age: attr()
  },
  methods: {
    fullName: function() {
      return this.firstName + " " + this.lastName;
    }
  }
})

var person = new Person({ firstName: "John" })
person.firstName // "John"
person.lastName = "Doe"
person.attributes // { firstName: "John", lastName: "Doe" }
person.fullName() // "John Doe"

Attributes can be marked read-only, so they are never sent to the server on a write request:

Typescript
Javascript
@Attr({ persist: false }) createdAt: string
@Attr({ persist: false }) updatedAt: string
attrs: {
  createdAt: attr({ persist: false }),
  updatedAt: attr({ persist: false })
}

Defining Relationships

Just like ActiveRecord, there are HasMany, BelongsTo, and HasOne relationships:

Typescript
Javascript
@Model()
class Dog extends ApplicationRecord {
  // ... code ...
  @BelongsTo() person: Person[]
}

class Person extends ApplicationRecord {
  // ... code ...
  @HasMany() dogs: Dog[]
}
const hasMany = spraypaint.hasMany
const belongsTo = spraypaint.belongsTo

const Person = ApplicationRecord.extend({
  // ... code ...
  attrs: {
    dogs: hasMany()
  }
})

const Dog = ApplicationRecord.extend({
  // ... code ...
  attrs: {
    person: belongsTo()
  }
})

By default, we expect the relationship name to correspond to a pluralized jsonapiType on a separate Model. If your models don’t use this convention, feel free to supply it explicitly:

Typescript
Javascript
@Model()
class Dog extends ApplicationRecord {
  // ... code ...
  @BelongsTo('people') owner: Person[]
}

// alternatively, specify the class directly

class Dog extends ApplicationRecord {
  // ... code ...
  @BelongsTo(Person) owner: Person[]
}
const Dog = ApplicationRecord.extend({
  // ... code ...
  attrs: {
    owner: belongsTo('people')
  }
})

Relationships can be:

  • Assigned via constructor
  • Assigned directly
  • Automatically loaded via .includes() (see reads)
  • Saved in a single request .save({ with: 'dogs' }) (see writes)
Typescript
Javascript
let dog = new Dog({ name: "Fido" })
let person = new Person({ dogs: [dog] })
person.dogs[0].name // "Fido"

let person = new Person()
person.dogs = [dog]
person.dogs[0].name // "Fido"

// Will auto-create Dog instance
let person = new Person({ dogs: [{ name: "Scooby" }] })
person.dogs[0].name // "Scooby"

let person = (await Person.includes('dogs')).data
person.dogs // array of Dog instances from the server
  
var dog = new Dog({ name: "Fido" })
var person = new Person({ dogs: [dog] })
person.dogs[0].name // "Fido"

let person = new Person()
person.dogs = [dog]
person.dogs[0].name // "Fido"

// Will auto-create Dog instance
var person = new Person({ dogs: [{ name: "Scooby" }] })
person.dogs[0].name // "Scooby"

Person.includes('dogs').then((response) => {
  var person = response.data
  person.dogs // array of Dog instances from the server
})