JSORM the isomorphic, framework-agnostic Javascript ORM

Installation

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

If using yarn:

$ yarn add jsorm isomorphic-fetch

If using npm:

$ npm install jsorm isomorphic-fetch

Now import it:

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

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

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
class ApplicationRecord extends JSORMBase {
  static baseUrl = "http://my-api.com"
  static apiNamespace = "/api/v1"
}
const ApplicationRecord = JSORMBase.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
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.

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
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 = jsorm.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
class Dog extends ApplicationRecord {
  // ... code ...
  @BelongsTo() person: Person[]
}

class Person extends ApplicationRecord {
  // ... code ...
  @HasMany() dogs: Dog[]
}
const hasMany = jsorm.hasMany
const belongsTo = jsorm.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
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
})