ActiveRecord implementation for Supabase (experimental).
- CRUD
- Validations
- Relationships
belongsTo()
,hasMany()
- Named scopes
- Filtering
- Joining
- Isomorphic
A model is a class that inherits from ActiveRecord
:
import { ActiveRecord } from 'supabase-active-record'
class Person extends ActiveRecord {
// define table name and fields
static config = {
table: 'people',
fields: {
id: 'serial',
firstName: 'string',
lastName: 'string'
}
}
}
To get all records:
const people = await Person.all()
To find a single record:
const person = await Person.findBy({firstName: "Josh", lastName: "Nussbaum"})
To find by id
:
const person = await Person.find(41)
ActiveRecord.find()
and ActiveRecord.findBy()
return null
when no record is found. If you perfer to raise an NotFoundError
, use ActiveRecord.get()
or ActiveRecord.getBy()
:
try {
const person = await Person.get(41)
} catch (error) {
console.error(error.name) // RecordNotFound
}
Filters can be added by chaining .where()
calls together:
// single filter
await Person.where({lastName: 'Jobs'})
// multiple filters
await Person
.where({firstName: 'Steve', lastName: 'Jobs'})
// equivalent
await Person
.where('firstName', 'Steve')
.where('lastName', 'Jobs')
// equivalent, using operators
await Person
.where('firstName', '=', 'Steve')
.where('lastName', '=', 'Jobs')
// supported operators: =, !=, >, <, <=, >=, like, ilike
await Product
.where('price', '>', 100)
.where('name', 'like', '*shirt*')
Data can be ordered by one or more columns:
// sort by column
await Product
.all()
.order('name')
// sort by multiple columns
await Product
.all()
.order(['price', 'name'])
// ascending and descending can be specified as `asc` and `desc`
await Product
.all()
.order({price: 'desc', name: 'asc'})
To limit the number of records returned:
await Product
.all()
.limit(10) // 10 records max
(not yet working)
Multiple models can be joined together
import { ActiveRecord, belongsTo } from 'supabase-active-record'
class Product extends ActiveRecord {
static config = [
table: 'products',
fields: {
id: 'serial',
name: 'string',
category: belongsTo(Category)
}
]
}
const product = await Product
.find(1)
.join('category')
console.log(product.category) // Category { name: 'T-Shirts' }
Named scopes can be defined using static
functions:
class Product extends ActiveRecord {
static config = {
table: 'products'
}
static expensive() {
return this
.where('price', '>', 100)
.order({price: 'desc'})
.limit(3)
}
}
And then call the scope like this:
const products = await Product.expensive()
Multiple validations are supported. They are defined in config.validate
:
import { ActiveRecord, is } from 'supabase-active-record'
class Product extends ActiveRecord {
static config = {
table: 'products',
fields: {
name: 'string',
price: 'number'
},
validates: {
name: is.required()
price: is.type('number')
}
}
}
Supported validations: is.required()
, is.type()
, is.length()
, is.format()
A validation is a function that takes an object
(the record) and returns a string
(the error message).
import { ActiveRecord, is } from 'supabase-active-record'
class Product extends ActiveRecord {
static config = {
// ...
validates: {
name: record => {
if (record.name.length < 3)
return 'is too short'
}
}
}
}
To create a single record:
const {valid, errors, record} = await Product.create({name: 'Shirt'})
or using an instance:
const product = new Product()
product.name = 'Shirt'
product.price = 100
const { valid, errors } = await product.save()
To create multiple records at once:
await Product.create([
{name: 'Shirt', price: 20},
{name: 'Pants', price: 100}
])
To update a record, call record.save()
:
// get existing record
const product = await Product.find(1)
// change record
product.price++
// save record
const {valid, errors} = await product.save()
or update multiple records at once:
// update all records where price=1 to price=2
await Product
.where('price', '=', 1)
.update({price: 2})
To delete a record:
// get existing record
const product = await Product.find(1)
// delete it
await product.delete()
or delete multiple records at once:
// delete all records where price > 1000
await Product
.where('price', '>', 1000)
.delete()
Instance methods, getters and setters can be added to the model:
class Person extends ActiveRecord {
// define a getter
get fullName() {
return `${this.firstName} ${this.lastName}`
}
// define a setter
set fullName(value) {
const [firstName, lastName] = value.split(' ')
this.firstName = firstName
this.lastName = lastName
}
// define a method to update name
anonymize() {
this.firstName = 'Anonymous'
this.lastName = 'Anonymous'
}
}
const person = new Person({firstName: "Steve", lastName: "Jobs"})
console.log(person.fullName) // Steve Jobs
person.anonymize()
console.log(person.fullName) // Anonymous Anonymous
When a record is changed, record.isChanged
is set to true
. There is also record.isPersisted
which is the opposite of record.isChanged
.
const product = new Product()
console.log(product.isNewRecord) // true
console.log(product.isChanged) // true
console.log(product.isPersisted) // false (opposite of isChanged)
await product.save()
// now it's no longer a new record or dirty
console.log(product.isNewRecord) // false
console.log(product.isChanged) // false
console.log(product.isPersisted) // true
Install the npm package:
yarn add supabase-active-record
Setup the supabase client:
import { createClient } from '@supabase/supabase-js'
import { ActiveRecord } from 'supabase-active-record'
ActiveRecord.client = createClient("<your supabase url>", "<your supabase key>")