beast-orm
v1.1.5
Published
ORM for accessing indexedDB as a promise base api implementation.
Downloads
107
Maintainers
Readme
Beast ORM
ORM for accessing indexedDB and localStorage.
DBMS Support
- IndexedDB
- Sqlite (Will be publish in version 2.0.0)
- localstorage
Details
- The indexedDB implementation runs multiple request in a single transaction
A promise base api implementation for accessing for accessing indexedDB
Create model
A model is a representation of a database table. Feel free to place your models anywhere that can be exported
import { models } from 'beast-orm';
class User extends models.Model {
userId = models.AutoField({primaryKey:true})
username = models.CharField({maxLength: 100})
email = models.CharField({blank: true, maxLength: 100})
age = models.IntegerField()
}
Register model
Once you’ve register your data models, automatically gives you a database-abstraction API for accessing indexedDB as a promise base api implementation that lets you create, retrieve, update and delete objects. Models that belongs to the same database should be register at the same time.
import { models } from 'beast-orm';
import { User } from './models/user.js';
models.register({
databaseName: 'tutorial',
version: 1,
type: 'indexedDB',
models: [User]
})
Creating objects
An instance of that class represents a particular record in the database table
const user = await User.create({username:'kobe', email:'[email protected]'})
To add multiple records in one go
const users = [
{username:'kobe', email:'[email protected]', age: 30},
{username:'michael', email:'[email protected]', age: 30}
]
const users = await User.create(users)
Saving changes to objects
this example changes its name and updates its record in the database
const u1 = await User.get({userId:1})
u1.username = 'New name'
u1.save()
Retrieving objects
The query is not executed as soon as the function is called
Retrieving all objects
The simplest way to retrieve objects from a table is to get all of them. To do this, use the all()
User.all()
Retrieving specific objects with filters
Filter - returns objects that match the given lookup parameters.
get - return object that match the given lookup
parameters.
const user = await User.get({username:'kobe'})
console.log(user.username) // kobe
get only works with unique fields
Filter can have complex lookup parameters
const users = await User.filter({age:10}).execute()
Field lookup
- gt - greater
- gte - greater or equal
- lt - less
- lte - less or equal
- not - different
Example:
const users = await User.filter({age__gt: 10}).execute()
// sql Select * from User Where age > 10
const users = await User.filter({age__gt: 10, age__lt: 50}).execute()
// sql Select * from User Where age > 10 AND age < 50
const users = await User.filter({age: 10, age__lt: 50}, {username: 'kobe'}).execute()
// sql Select * from User Where age = 10 AND age < 50 OR username = 'kobe'
const users = await User.filter({age__not: 10}, [{age: 20},{username:'james'}]).execute()
// sql Select * from User Where not age = 10 OR (age = 20 OR username = 'james')
Saving changes to objects
To save changes to an object that’s already in the database, use save().
const user = await User.get({username:'kobe'})
user.username = 'james'
user.save()
Updating multiple objects at once
Sometimes you want to set a field to a particular value for all the objects You can do this with the update() method. For example:
User.filter({age:10}).update({age:11})
Deleting objects
The delete method, conveniently, is named delete(). This method immediately deletes the object
User.delete()
You can also delete objects in bulk. Every QuerySet has a delete() method, which deletes all members of that QuerySet.
For example, this deletes all User objects with a age 40, and returns the number of objects deleted.
User.filter({age: 40}).delete()
ArrayField
A field for storing lists of data. Most field types can be used, and you pass another field instance. You may also specify a size. ArrayField can be nested to store multi-dimensional arrays.
If you give the field a default, ensure it’s a callable such as list (for an empty default) or a callable that returns a list (such as a function). Incorrectly using default:[] creates a mutable default that is shared between all instances of ArrayField.
const { ArrayField } = models.indexedDB.fields
class ChessBoardUser extends models.Model {
board = ArrayField({
field: ArrayField({
field: models.CharField({maxLength:10, blank:true}),
size:8,
}),
size:8,
})
}
models.register({
databaseName: 'tutorial-ArrayField',
version: 1,
type: 'indexedDB',
models: [ChessBoardUser]
})
// valid
ChessBoardUser.create({
board: [
['01','02','03','04','05','06','07','08'],
['21','22','23','24','25','26','27','28'],
['31','32','33','34','35','36','37','38'],
['41','42','43','44','45','46','47','48'],
['51','52','53','54','55','56','57','58'],
['61','62','63','64','65','66','67','68'],
['71','72','73','74','75','76','77','78'],
['81','82','83','84','85','86','87','88'],
]
})
Querying ArrayField
There are a number of custom lookups and transforms for ArrayField. We will use the following example model:
const { ArrayField } = models.indexedDB.fields
class Post extends models.Model {
name = models.CharField({maxLength=200})
tags = ArrayField({field:models.CharField({maxLength:200}), blank:true})
}
contains
The contains lookup is overridden on ArrayField. The returned objects will be those where the values passed are a subset of the data. It uses the SQL operator @>. For example:
Post.create({name:'First post', tags:['thoughts', 'django']})
Post.create({name:'Second post', tags:['thoughts']})
Post.create({name:'Third post', tags:['tutorial', 'django']})
Post.filter({tags__contains:['thoughts']}).execute()
// [<Post: First post>, <Post: Second post>]
Post.filter({tags__contains:['django']}).execute()
// [<Post: First post>, <Post: Third post>]
Post.filter({tags__contains:['django', 'thoughts']}).execute()
// [<Post: First post>]
contained_by
This is the inverse of the contains lookup - the objects returned will be those where the data is a subset of the values passed. It uses the SQL operator <@. For example:
Post.create({name:'First post', tags:['thoughts', 'django']}).execute()
Post.create({name:'Second post', tags:['thoughts']}).execute()
Post.create({name:'Third post', tags:['tutorial', 'django']}).execute()
Post.filter({tags__contained_by:['thoughts', 'django']}).execute()
// <Post: First post>, <Post: Second post>]
Post.filter({tags__contained_by:['thoughts', 'django', 'tutorial']}).execute()
// [<Post: First post>, <Post: Second post>, <Post: Third post>]
overlap
Returns objects where the data shares any results with the values passed. Uses the SQL operator &&. For example:s the SQL operator <@. For example:
Post.create({name:'First post', tags:['thoughts', 'django']}).execute()
Post.create({name:'Second post', tags:['thoughts']}).execute()
Post.create({name:'Third post', tags:['tutorial', 'django']}).execute()
Post.filter({tags__overlap:['thoughts']}).execute()
// [<Post: First post>, <Post: Second post>]
Post.filter({tags__overlap:['thoughts', 'tutorial']}).execute()
// [<Post: First post>, <Post: Second post>, <Post: Third post>]
len
Returns the length of the array. The lookups available afterward are those available for IntegerField. For example:
Post.create({name:'First post', tags:['thoughts', 'django']})
Post.create({name:'Second post', tags:['thoughts']})
Post.filter(tags__len=1).execute()
// [<Post: Second post>]
Index transforms
Index transforms index into the array. Any non-negative integer can be used. There are no errors if it exceeds the size of the array. The lookups available after the transform are those from the base_field. For example:
Post.create({name:'First post', tags:['thoughts', 'django']})
Post.create({name:'Second post', tags:['thoughts']})
Post.filter({tags__0:'thoughts'}).execute()
// [<Post: First post>, <Post: Second post>]
Post.filter({tags__1__iexact:'Django'}).execute()
// [<Post: First post>]
Post.filter({tags__276:'javascript'}).execute()
// []
JSONField
Lookups implementation is different in JSONField, mainly due to the existence of key transformations. To demonstrate, we will use the following example model:
const { JsonField } = models.indexedDB.fields
class Dog extends models.Model {
name = models.CharField({maxLength:200})
data = JsonField({null: false})
}
models.register({
databaseName: 'tutorial-JsonField',
version: 1,
type: 'indexedDB',
models: [Dog]
})
Storing and querying for None
As with other fields, storing None as the field’s value will store it as SQL NULL. While not recommended, it is possible to store JSON scalar null instead of SQL NULL by using Value('null').
Whichever of the values is stored, when retrieved from the database, the Python representation of the JSON scalar null is the same as SQL NULL, i.e. None. Therefore, it can be hard to distinguish between them.
This only applies to None as the top-level value of the field. If None is inside a list or dict, it will always be interpreted as JSON null.
When querying, None value will always be interpreted as JSON null. To query for SQL NULL, use isnull:
Dog.create({name:'Max', data: null}) # SQL NULL.
// <Dog: Max>
Dog.create({name:'Archie', data:Value('null')}) # JSON null.
// <Dog: Archie>
Dog.filter({data:null}).execute()
// [<Dog: Archie>]
Dog.filter({data=Value('null')}).execute()
// [<Dog: Archie>]
Dog.filter({data__isnull:true}).execute()
// [<Dog: Max>]
Dog.filter({data__isnull:false}).execute()
// [<Dog: Archie>]
Key, index, and path transforms
To query based on a given dictionary key, use that key as the lookup name:
Dog.create({name:'Rufus', data: {
'breed': 'labrador',
'owner': {
'name': 'Bob',
'other_pets': [{
'name': 'Fishy',
}],
},
}})
Dog.create({name:'Meg', data:{'breed': 'collie', 'owner': null}})
// <Dog: Meg>
Dog.filter({data__breed:'collie'}).execute()
// [<Dog: Meg>]
Multiple keys can be chained together to form a path lookup:
Dog.objects.filter({data__owner__name:'Bob'}).execute()
// [<Dog: Rufus>]
If the key is an integer, it will be interpreted as an index transform in an array:
Dog.objects.filter({data__owner__other_pets__0__name:'Fishy'}).execute()
// [<Dog: Rufus>]
If the key you wish to query by clashes with the name of another lookup, use the contains lookup instead.
To query for missing keys, use the isnull lookup:
Dog.objects.create({name:'Shep', data:{'breed': 'collie'}})
Dog.objects.filter({data__owner__isnull:true}).execute()
// [<Dog: Shep>]
Note
The lookup examples given above implicitly use the exact lookup. Key, index, and path transforms can also be chained with: icontains, endswith, iendswith, iexact, regex, iregex, startswith, istartswith, lt, lte, gt, and gte, as well as with Containment and key lookups.
contains
The contains lookup is overridden on JSONField. The returned objects are those where the given dict of key-value pairs are all contained in the top-level of the field. For example:
Dog.create({name:'Rufus', data:{'breed': 'labrador', 'owner': 'Bob'}})
// <Dog: Rufus>
Dog.create({name:'Meg', data:{'breed': 'collie', 'owner': 'Bob'}})
// <Dog: Meg>
Dog.create({name:'Fred', data:{}})
// <Dog: Fred>
Dog.filter({data__contains:{'owner': 'Bob'}}).execute()
// [<Dog: Rufus>, <Dog: Meg>]
Dog.filter({data__contains:{'breed': 'collie'}}).execute()
// [<Dog: Meg>]
contained_by
This is the inverse of the contains lookup - the objects returned will be those where the key-value pairs on the object are a subset of those in the value passed. For example:
Dog.create({name:'Rufus', data:{'breed': 'labrador', 'owner': 'Bob'}})
Dog.create({name:'Meg', data:{'breed': 'collie', 'owner': 'Bob'}})
Dog.create({name:'Fred', data:{}})
Dog.filter({data__contained_by:{'breed': 'collie', 'owner': 'Bob'}}).execute()
// [<Dog: Meg>, <Dog: Fred>]
Dog.filter({data__contained_by:{'breed': 'collie'}}).execute()
// [<Dog: Fred>]
has_key
Returns objects where the given key is in the top-level of the data. For example:
Dog.create({name:'Rufus', data:{'breed': 'labrador'}})
// [<Dog: Rufus>]
Dog.create({name:'Meg', data:{'breed': 'collie', 'owner': 'Bob'}})
// [<Dog: Meg>]
Dog.filter({data__has_key:'owner'}).execute()
// [<Dog: Meg>]
has_keys
Returns objects where all of the given keys are in the top-level of the data. For example:
Dog.create(name:'Rufus', data:{'breed': 'labrador'})
// [<Dog: Rufus>]
Dog.create({name:'Meg', data:{'breed': 'collie', 'owner': 'Bob'}})
// [<Dog: Meg>]
Dog.filter({data__has_keys:['breed', 'owner']})
// [<Dog: Meg>]
has_any_keys
Returns objects where any of the given keys are in the top-level of the data. For example:
Dog.create({name:'Rufus', data:{'breed': 'labrador'}})
// [<Dog: Rufus>]
Dog.create({name:'Meg', data:{'owner': 'Bob'}})
// [<Dog: Meg>]
Dog.filter({data__has_any_keys:['owner', 'breed']})
// [<Dog: Rufus>, <Dog: Meg>]
Many-to-many relationships
In this example, an Article can be published in multiple Publication objects, and a Publication has multiple Article objects:
class Publication extends models.Model {
title = models.CharField({maxLength: 50})
}
class Article extends models.Model {
headline = models.CharField({maxLength: 100})
publication = models.ManyToManyField({model:Publication})
}
models.register({
databaseName:'One-to-one-relationships',
type: 'indexedDB',
version: 1,
models: [Publication, Article]
})
What follows are examples of operations that can be performed using the Python API facilities.
Create a few Publications:
const p1 = await Publication.create({title:'The Python Journal'})
const p2 = await Publication.create({title:'Science News'})
const p3 = await Publication.create({title:'Science Weekly'})
Create an Article:
const a1 = await Article.create({headline:'lets you build web apps easily'})
const a2 = await Article.create({headline:'NASA uses Python'})
Associate the Article with a Publication:
await a1.publication_add([p1, p2])
Article objects have access to their related Publication objects:
await a1.publication_all()
Article objects have access to their related Publication objects:
await p1.article_set_all()
One-to-one relationships
In this example, a Place optionally can be a Restaurant:
class Place extends models.Model {
name = models.CharField({maxLength: 50})
address = models.CharField({maxLength: 50})
}
class Restaurant extends models.Model {
place = models.OneToOneField({model:Place})
servesHotDogs = models.BooleanField({default: false})
servesPizza = models.BooleanField({default: false})
}
await models.register({
databaseName:'jest-test'+ new Date().getTime(),
type: 'indexedDB',
version: 1,
models: [Place, Restaurant]
})
What follows are examples of operations that can be performed using the Python API facilities.
Create a couple of Places:
const p1 = await Place.create({name:'Demon Dogs', address:'944 W. Fullerton'})
Create a Restaurant. Pass the “parent” object as this object’s primary key:
const r = await Restaurant.create({place:p1, servesHotDogs: false, servesPizza:false})
A Restaurant can access its place:
const r = await p1.Restaurant()
A Place can access its restaurant, if available:
const p = await await r.Place()
many-to-one relationships
class Reporter extends models.Model {
firstName = models.CharField({maxLength: 50})
lastName = models.CharField({maxLength: 50})
email = models.CharField()
}
class Article extends models.Model {
headline = models.CharField({maxLength: 50})
pubDate = models.DateField()
reporter = models.ForeignKey({model:Reporter})
}
await models.register({
databaseName:'jest-test'+ new Date().getTime(),
type: 'indexedDB',
version: 1,
models: [Reporter, Article]
})
What follows are examples of operations that can be performed using the Python API facilities.
Create a few Reporters:
const r1 = await Reporter.create({firstName: 'asdfsadf', lastName: 'asdfsd', email:'teste'})
const r2 = await Reporter.create({firstName: 'Peter', lastName: 'Maquiran', email:'teste'})
Create an Article:
const a = await Article.create({headline:"This is a test", pubDate:'', reporter:r1})
Article objects have access to their related Reporter objects:
const r1 = await a.Reporter()
Reporter objects have access to their related Article objects:
const a = await await r1.article_setAll()
Add the same article to a different article set
const a = await await r1.article_setAdd({headline:"This is a test", pubDate:''})
Reactive List
class Person extends models.Model {
username = models.CharField({})
age = models.IntegerField({blank:true})
}
models.migrate({
databaseName:'jest-test',
type: 'indexedDB',
version: 1,
models: [Person]
})
Create a reactive List that update when a transaction is committed on the database.
const PersonList = Person.ReactiveList((model)=> model.all())
const PersonAge5List = Person.ReactiveList((model)=> model.filter({age: 5}).execute())
Get the value
PersonList.value
// [<Person: Rufus>, <Person: Meg>]
unsubscribe the reactive list
PersonList.unsubscribe()
Trigger transaction
class Person extends models.Model {
username = models.CharField({})
}
models.migrate({
databaseName:'jest-test',
type: 'indexedDB',
version: 1,
models: [Person]
})
Create a callback function that fire every time a commit is made in the Person
let subscription = Person.transactionOnCommit( async () => {
console.log('commit')
})
unsubscribe
subscription.unsubscribe()
Trigger { BEFORE | AFTER } { INSERT | UPDATE | DELETE}
coming soon
LocalStorage base api implementation.
Create model
A model is a representation of a database table. Feel free to place your models anywhere that can be exported
import { models } from 'beast-orm';
class Session extends models.LocalStorage {
static username = ''
static userId = 55
static token = ''
}
Register model
Once you’ve register your data models, automatically gives you a abstraction API for accessing local storage base api implementation that lets you create, retrieve, update and delete object.
import { models } from 'beast-orm';
import { Session } from './models/user.js';
models.migrate({
databaseName: 'tutorial',
version: 1,
type: 'indexedDB',
models: [Session]
})
Creating objects or Update
Change Session and save the changes, it will create the key value to the local storage in case it doesn't exist or simple update the value
Session.username = 'kobe'
Session.userId = '1'
Session.token = 'fjif8382'
Session.save()
Deleting objects
The delete method, conveniently, is named delete(). This method immediately deletes the object and returns the number of objects deleted and a dictionary with the number of deletions per object type. Example:
Session.delete()
Languages and Tools
Credits
- This library takes inspiration from django orm
- browser-orm
- indexeddb-orm
- use-indexeddb
- DjORMgo-js
📜 License
MIT © BackdoorTech